001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.examples.tutorials.bsp;
018
019import java.io.File;
020import java.util.Deque;
021import java.util.HashMap;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.Map;
025
026import javax.xml.parsers.DocumentBuilder;
027import javax.xml.parsers.DocumentBuilderFactory;
028import javax.xml.parsers.ParserConfigurationException;
029import javax.xml.transform.OutputKeys;
030import javax.xml.transform.Transformer;
031import javax.xml.transform.TransformerException;
032import javax.xml.transform.TransformerFactory;
033import javax.xml.transform.dom.DOMSource;
034import javax.xml.transform.stream.StreamResult;
035
036import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
037import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
038import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
039import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
040import org.apache.commons.geometry.euclidean.twod.Bounds2D;
041import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
042import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
043import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
044import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D.RegionNode2D;
045import org.apache.commons.geometry.euclidean.twod.Vector2D;
046import org.apache.commons.geometry.euclidean.twod.path.LinePath;
047import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
048import org.apache.commons.numbers.core.Precision;
049import org.w3c.dom.Document;
050import org.w3c.dom.Element;
051
052/** Class for writing SVG visualizations of 2D BSP trees.
053 */
054public class BSPTreeSVGWriter {
055
056    /** SVG XML namespace. */
057    private static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";
058
059    /** SVG version. */
060    private static final String SVG_VERSION = "1.1";
061
062    /** Property key used to set the indent level of the output xml. */
063    private static final String INDENT_AMOUNT_KEY = "{http://xml.apache.org/xslt}indent-amount";
064
065    /** Indent amount for the output xml. */
066    private static final int INDENT_AMOUNT = 4;
067
068    /** String containing default node name characters. */
069    private static final String DEFAULT_NODE_NAMES = "abcdefghijklmnopqrstuvwxyz";
070
071    /** Id used for the geometry area clip path. */
072    private static final String GEOMETRY_AREA_CLIP_PATH_ID = "geometry-area";
073
074    /** Path string command to move to a point. */
075    private static final char PATH_MOVE_TO = 'M';
076
077    /** Path string command to draw a line to a point. */
078    private static final char PATH_LINE_TO = 'L';
079
080    /** Space character. */
081    private static final char SPACE = ' ';
082
083    /** Name of the SVG "rect" element. */
084    private static final String RECT_ELEMENT = "rect";
085
086    /** Name of the SVG "path" element. */
087    private static final String PATH_ELEMENT = "path";
088
089    /** Name of the "class" attribute. */
090    private static final String CLASS_ATTR = "class";
091
092    /** Name of the "width" attribute. */
093    private static final String WIDTH_ATTR = "width";
094
095    /** Name of the "height" attribute. */
096    private static final String HEIGHT_ATTR = "height";
097
098    /** Name of the "x" attribute. */
099    private static final String X_ATTR = "x";
100
101    /** Name of the "y" attribute. */
102    private static final String Y_ATTR = "y";
103
104    /** CSS style string for the generated SVG. */
105    private static final String STYLE =
106        "text { font-size: 14px; } " +
107        ".node-name { text-anchor: middle; font-family: \"Courier New\", Courier, monospace; } " +
108        ".geometry-border { fill: none; stroke: gray; stroke-width: 1; } " +
109        ".arrow { fill: none; stroke: blue; stroke-width: 1; } " +
110        ".cut { fill: none; stroke: blue; stroke-width: 1; stroke-dasharray: 5,3; } " +
111        ".region-boundary { stroke: orange; stroke-width: 2; } " +
112        ".inside { fill: #aaa; opacity: 0.2; } " +
113        ".tree-path { fill: none; stroke: gray; stroke-width: 1; } " +
114        ".inside-node { font-weight: bold; }";
115
116    /** The width of the SVG. */
117    private static final int WIDTH = 750;
118
119    /** The height of the SVG. */
120    private static final int HEIGHT = 375;
121
122    /** The margin used in the SVG. */
123    private static final int MARGIN = 5;
124
125    /** Amount of the overall width of the SVG to use for the geometry area. */
126    private static final double GEOMETRY_AREA_WIDTH_FACTOR = 0.5;
127
128    /** Amount of the overall width of the SVG to use for the tree structure area. */
129    private static final double TREE_AREA_WIDTH_FACTOR = 1.0 - GEOMETRY_AREA_WIDTH_FACTOR;
130
131    /** Angle that arrow heads on lines make with the direction of the line. */
132    private static final double ARROW_ANGLE = 0.8 * Math.PI;
133
134    /** Length of arrow head lines. */
135    private static final double ARROW_LENGTH = 8;
136
137    /** Distance between levels of the tree in the tree structure display. */
138    private static final double TREE_VERTICAL_SPACING = 45;
139
140    /** Line end margin used in the lines between nodes in the tree structure display. */
141    private static final double TREE_LINE_MARGIN = 10;
142
143    /** Precision context used for floating point comparisons. */
144    private static final Precision.DoubleEquivalence PRECISION = Precision.doubleEquivalenceOfEpsilon(1e-6);
145
146    /** Geometry bounds; only geometry within these bounds is rendered. */
147    private final Bounds2D bounds;
148
149    /** Factor determining how much of the available horizontal width for a node should be used to
150     * offset it from its parent.
151     */
152    private double treeParentOffsetFactor = 0.25;
153
154    /** Minimum horizontal offset for tree nodes from their parents. */
155    private double treeParentXOffsetMin = 0;
156
157    /** Construct a new instance that will render regions within the given bounds.
158     * @param bounds bounds used to determine what output
159     */
160    public BSPTreeSVGWriter(final Bounds2D bounds) {
161        this.bounds = bounds;
162    }
163
164    /** Set the offset factor determining how much of the available horizontal width for
165     * a node should be used to offset it from its parent.
166     * @param treeParentOffsetFactor offset factor
167     */
168    public void setTreeParentOffsetFactor(final double treeParentOffsetFactor) {
169        this.treeParentOffsetFactor = treeParentOffsetFactor;
170    }
171
172    /** Set the minimum horizontal offset for tree nodes from their parents.
173     * @param treeParentXOffsetMin minimum offset
174     */
175    public void setTreeParentXOffsetMin(final double treeParentXOffsetMin) {
176        this.treeParentXOffsetMin = treeParentXOffsetMin;
177    }
178
179    /** Write an SVG visualization of the given BSP tree. Default names are assigned to the tree nodes.
180     * @param tree tree to output
181     * @param file path of the svg file to write
182     */
183    public void write(final RegionBSPTree2D tree, final File file) {
184        final Deque<RegionNode2D> nodeQueue = new LinkedList<>();
185        nodeQueue.add(tree.getRoot());
186
187        final Map<RegionNode2D, String> nodeNames = new HashMap<>();
188
189        final String names = DEFAULT_NODE_NAMES;
190        RegionNode2D node;
191        for (int i = 0; i < names.length() && !nodeQueue.isEmpty(); ++i) {
192            node = nodeQueue.removeFirst();
193
194            nodeNames.put(node, names.substring(i, i + 1));
195
196            if (node.isInternal()) {
197                nodeQueue.add(node.getMinus());
198                nodeQueue.add(node.getPlus());
199            }
200        }
201
202        write(tree, nodeNames, file);
203    }
204
205    /** Write an SVG visualization of the given BSP tree.
206     * @param tree tree to output
207     * @param nodeNames map of node instances to the names that should be used for them in the svg
208     * @param file path of the svg file to write
209     */
210    public void write(final RegionBSPTree2D tree, final Map<RegionNode2D, String> nodeNames, final File file) {
211        try {
212            final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
213            final DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
214
215            final Document doc = docBuilder.newDocument();
216
217            // create the svg element
218            final Element root = svgElement("svg", doc);
219            doc.appendChild(root);
220
221            root.setAttribute("version", SVG_VERSION);
222            root.setAttribute(WIDTH_ATTR, String.valueOf(WIDTH));
223            root.setAttribute(HEIGHT_ATTR, String.valueOf(HEIGHT));
224
225            // add a defs element for later use
226            final Element defs = svgElement("defs", doc);
227            root.appendChild(defs);
228
229            // add a style element
230            final Element style = svgElement("style", doc);
231            root.appendChild(style);
232            style.setTextContent(STYLE);
233
234            // write the tree
235            writeTreeGeometryArea(tree, nodeNames, root, defs, doc);
236            writeTreeStructureArea(tree, nodeNames, root, doc);
237
238            // output to the target file
239            final TransformerFactory transformerFactory = TransformerFactory.newInstance();
240            final Transformer transformer = transformerFactory.newTransformer();
241            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
242            transformer.setOutputProperty(INDENT_AMOUNT_KEY, String.valueOf(INDENT_AMOUNT));
243
244            final DOMSource source = new DOMSource(doc);
245            final StreamResult target = new StreamResult(file);
246
247            transformer.transform(source, target);
248
249        } catch (final ParserConfigurationException | TransformerException e) {
250            // throw as a runtime exception for convenience
251            throw new RuntimeException("Failed to create SVG", e);
252        }
253    }
254
255    /** Write the svg area containing the visual representation of the tree geometry.
256     * @param tree tree to write
257     * @param nodeNames map of nodes to the names that should be used to identify them
258     * @param root root svg element
259     * @param defs svg defs element
260     * @param doc xml document
261     */
262    private void writeTreeGeometryArea(final RegionBSPTree2D tree, final Map<RegionNode2D, String> nodeNames,
263            final Element root, final Element defs, final Document doc) {
264        final double geometrySvgX = MARGIN;
265        final double geometrySvgY = MARGIN;
266        final double geometrySvgWidth = (GEOMETRY_AREA_WIDTH_FACTOR * WIDTH) - (2 * MARGIN);
267        final double geometrySvgHeight = HEIGHT - (2 * MARGIN);
268
269        defineClipRect(GEOMETRY_AREA_CLIP_PATH_ID,
270                geometrySvgX, geometrySvgY,
271                geometrySvgWidth, geometrySvgHeight,
272                defs, doc);
273
274        // create the box containing the 2D content
275        final Element geometryGroup = svgElement("g", doc);
276        root.appendChild(geometryGroup);
277        geometryGroup.setAttribute(CLASS_ATTR, "geometry");
278        geometryGroup.setAttribute("clip-path", "url(#" + GEOMETRY_AREA_CLIP_PATH_ID + ")");
279
280        // set up the transform so we can write geometry elements with their natural coordinates
281        final AffineTransformMatrix2D transform = computeGeometryTransform(bounds, geometrySvgX, geometrySvgY,
282                geometrySvgWidth, geometrySvgHeight);
283
284        // add the tree geometry
285        tree.accept(new TreeGeometryVisitor(transform, nodeNames, geometryGroup, doc));
286
287        // create a box outlining the geometry area
288        final Element border = svgElement(RECT_ELEMENT, doc);
289        border.setAttribute(CLASS_ATTR, "geometry-border");
290        floatAttr(border, X_ATTR, geometrySvgX);
291        floatAttr(border, Y_ATTR, geometrySvgY);
292        floatAttr(border, WIDTH_ATTR, geometrySvgWidth);
293        floatAttr(border, HEIGHT_ATTR, geometrySvgHeight);
294
295        root.appendChild(border);
296
297    }
298
299    /** Write the svg area containing the visual representation of the tree structure.
300     * @param tree tree to write
301     * @param nodeNames map of nodes to the names that should be used to identify them
302     * @param root svg root element
303     * @param doc xml document
304     */
305    private void writeTreeStructureArea(final RegionBSPTree2D tree, final Map<RegionNode2D, String> nodeNames,
306            final Element root, final Document doc) {
307        final Element treeGroup = svgElement("g", doc);
308        root.appendChild(treeGroup);
309        treeGroup.setAttribute(CLASS_ATTR, "tree");
310
311        final double offsetX = ((1 - TREE_AREA_WIDTH_FACTOR) * WIDTH) + MARGIN;
312        final double offsetY = MARGIN;
313
314        final double svgWidth = (TREE_AREA_WIDTH_FACTOR * WIDTH) - (2 * MARGIN);
315        final double svgHeight = HEIGHT - (2 * MARGIN);
316
317        treeGroup.setAttribute("transform", "translate(" + offsetX + " " + offsetY + ")");
318
319        tree.accept(new TreeStructureVisitor(tree.height(), svgWidth, svgHeight, nodeNames, treeGroup, doc));
320    }
321
322    /** Compute the transform required to convert from the geometry coordinate system given in {@code bounds}
323     * to the one defined by the given svg coordinates. The y-axis from the geometry is oriented to point
324     * upwards in the svg.
325     * @param bounds the bounds of the geometry
326     * @param svgX x coordinate in the upper-left corner of the svg target box
327     * @param svgY y coordinate in the upper-left corner of the svg target box
328     * @param svgWidth width of the svg target box
329     * @param svgHeight height of the svg target box
330     * @return a transform converting from geometry space to svg space
331     */
332    private static AffineTransformMatrix2D computeGeometryTransform(final Bounds2D bounds,
333            final double svgX, final double svgY, final double svgWidth, final double svgHeight) {
334
335        final Vector2D boundsDiagonal = bounds.getDiagonal();
336
337        return AffineTransformMatrix2D
338            .createTranslation(bounds.getMin().negate())
339            .scale(svgWidth / boundsDiagonal.getX(), -svgHeight / boundsDiagonal.getY())
340            .translate(svgX, svgY + svgHeight);
341    }
342
343    /** Define an SVG clipping rectangle.
344     * @param id id of the clipping rectangle to add
345     * @param x x coordinate of the clipping rectangle
346     * @param y y coordinate of the clipping rectangle
347     * @param svgWidth width of the clipping rectangle
348     * @param svgHeight height of the clipping rectangle
349     * @param defs svg "defs" element
350     * @param doc xml document
351     */
352    private void defineClipRect(final String id, final double x, final double y,
353            final double svgWidth, final double svgHeight, final Element defs, final Document doc) {
354
355        final Element clipPath = svgElement("clipPath", doc);
356        clipPath.setAttribute("id", id);
357
358        defs.appendChild(clipPath);
359
360        final Element rect = svgElement(RECT_ELEMENT, doc);
361        floatAttr(rect, X_ATTR, x);
362        floatAttr(rect, Y_ATTR, y);
363        floatAttr(rect, WIDTH_ATTR, svgWidth);
364        floatAttr(rect, HEIGHT_ATTR, svgHeight);
365
366        clipPath.appendChild(rect);
367    }
368
369    /** Convenience method for setting a floating-point attribute on an element.
370     * @param element element to set the attribute on
371     * @param name name of the attribute to set
372     * @param value value of the attribute to set
373     */
374    private static void floatAttr(final Element element, final String name, final double value) {
375        element.setAttribute(name, String.valueOf(value));
376    }
377
378    /** Convenience method for creating an element from the svg namespace.
379     * @param name the name of the element
380     * @param doc document to create the element in
381     * @return the element from the svg namespace
382     */
383    private static Element svgElement(final String name, final Document doc) {
384        return doc.createElementNS(SVG_NAMESPACE, name);
385    }
386
387    /** Base class for BSP tree visitors that output SVG content.
388     */
389    private abstract class AbstractSVGTreeVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {
390
391        /** Map of nodes to the names that should be used to identify them. */
392        private final Map<RegionNode2D, String> nodeNames;
393
394        /** Parent SVG element to append new elements to. */
395        private final Element parent;
396
397        /** Xml document. */
398        private final Document doc;
399
400        /** Number of nodes visited so far. */
401        private int count = 0;
402
403        /** Construct a new instance.
404         * @param nodeNames map of nodes ot the names that should be used to identify them
405         * @param parent parent SVG element
406         * @param doc xml document
407         */
408        AbstractSVGTreeVisitor(final Map<RegionNode2D, String> nodeNames, final Element parent, final Document doc) {
409            this.nodeNames = nodeNames;
410            this.parent = parent;
411            this.doc = doc;
412        }
413
414        /** {@inheritDoc} */
415        @Override
416        public Order visitOrder(final RegionNode2D internalNode) {
417            return Order.NODE_MINUS_PLUS;
418        }
419
420        /** {@inheritDoc} */
421        @Override
422        public Result visit(final RegionNode2D node) {
423            ++count;
424
425            final String name = (nodeNames != null && nodeNames.containsKey(node)) ?
426                    nodeNames.get(node) :
427                    String.valueOf(count);
428
429            visitNode(name, node);
430
431            return Result.CONTINUE;
432        }
433
434        /** Create a new svg element with the given name and append it to the parent node.
435         * @param name name of the element
436         * @return the created element, already appended to the parent
437         */
438        protected Element createChild(final String name) {
439            final Element child = createElement(name);
440            parent.appendChild(child);
441
442            return child;
443        }
444
445        /** Create an svg element with the given name. The element is <em>not</em> appended to the
446         * parent node.
447         * @param name name of the element
448         * @return the created element
449         */
450        protected Element createElement(final String name) {
451            return svgElement(name, doc);
452        }
453
454        /** Create an SVG text element containing the name of a tree node. The returned element is
455         * <em>not</em> appended to the parent.
456         * @param name name of the tree node
457         * @param svgPt location to place the text
458         * @return text element containing the given node name
459         */
460        protected Element createNodeNameElement(final String name, final Vector2D svgPt) {
461            final Element text = createElement("text");
462            text.setAttribute(CLASS_ATTR, "node-name");
463            text.setAttribute("dominant-baseline", "middle");
464            floatAttr(text, X_ATTR, svgPt.getX());
465            floatAttr(text, Y_ATTR, svgPt.getY());
466            text.setTextContent(name);
467
468            return text;
469        }
470
471        /** Create a path element representing a line from {@code svgStart} to {@code svgEnd}.
472         * @param className class name to place on the element
473         * @param svgStart start point
474         * @param svgEnd end point
475         * @return path element
476         */
477        protected Element createPathElement(final String className, final Vector2D svgStart, final Vector2D svgEnd) {
478            final Element path = createElement(PATH_ELEMENT);
479            path.setAttribute(CLASS_ATTR, className);
480
481            final StringBuilder pathStr = new StringBuilder();
482            pathStr.append(PATH_MOVE_TO)
483                .append(pointString(svgStart))
484                .append(SPACE)
485                .append(PATH_LINE_TO)
486                .append(pointString(svgEnd));
487
488            path.setAttribute("d", pathStr.toString());
489
490            return path;
491        }
492
493        /** Create a string containing the coordinates of the given point in the format used by the SVG
494         * {@code path} element.
495         * @param pt point to represent as an SVG string
496         * @return SVG string representation of the point
497         */
498        protected String pointString(final Vector2D pt) {
499            return pt.getX() + " " + pt.getY();
500        }
501
502        /** Visit a node in the tree.
503         * @param name the name for the node in the visualization
504         * @param node the node being visited
505         */
506        protected abstract void visitNode(String name, RegionNode2D node);
507    }
508
509    /** BSP tree visitor that outputs SVG representing the tree geometry.
510     */
511    private final class TreeGeometryVisitor extends AbstractSVGTreeVisitor {
512
513        /** The geometry bounds as a region instance. */
514        private final Parallelogram boundsRegion;
515
516        /** Transform converting from geometry space to SVG space. */
517        private final AffineTransformMatrix2D transform;
518
519        /** Group element containing the tree geometry paths. */
520        private final Element pathGroup;
521
522        /** Group element containing the tree node labels. */
523        private final Element labelGroup;
524
525        /** Construct a new instance for generating SVG geometry content.
526         * @param transform transform converting from geometry space to SVG space
527         * @param nodeNames map of nodes to the names that should be used to identify them
528         * @param parent parent SVG element
529         * @param doc xml document
530         */
531        TreeGeometryVisitor(final AffineTransformMatrix2D transform, final Map<RegionNode2D, String> nodeNames,
532                final Element parent, final Document doc) {
533            super(nodeNames, parent, doc);
534
535            this.boundsRegion = bounds.toRegion(PRECISION);
536
537            this.transform = transform;
538
539            // place the label group after the geometry group so the labels appear on top
540            this.pathGroup = svgElement("g", doc);
541            pathGroup.setAttribute(CLASS_ATTR, "paths");
542            parent.appendChild(pathGroup);
543
544            this.labelGroup = svgElement("g", doc);
545            labelGroup.setAttribute(CLASS_ATTR, "labels");
546            parent.appendChild(labelGroup);
547        }
548
549        /** {@inheritDoc} */
550        @Override
551        protected void visitNode(final String name, final RegionNode2D node) {
552            if (node.isLeaf()) {
553                visitLeafNode(name, node);
554            } else {
555                visitInternalNode(name, node);
556            }
557        }
558
559        /** Visit a leaf node.
560         * @param name name of the node
561         * @param node the leaf node
562         */
563        private void visitLeafNode(final String name, final RegionNode2D node) {
564            final RegionBSPTree2D tree = node.getNodeRegion().toTree();
565            tree.intersection(boundsRegion.toTree());
566
567            final Vector2D svgCentroid = toSvgSpace(tree.getCentroid());
568
569            labelGroup.appendChild(createNodeNameElement(name, svgCentroid));
570
571            if (node.isInside()) {
572                for (final LinePath linePath : tree.getBoundaryPaths()) {
573                    final Element path = createElement(PATH_ELEMENT);
574                    pathGroup.appendChild(path);
575                    path.setAttribute(CLASS_ATTR, "inside");
576
577                    final StringBuilder sb = new StringBuilder();
578
579                    for (final Vector2D pt : linePath.getVertexSequence()) {
580                        if (sb.length() < 1) {
581                            sb.append(PATH_MOVE_TO);
582                        } else {
583                            sb.append(SPACE)
584                                .append(PATH_LINE_TO);
585                        }
586
587                        sb.append(pointString(toSvgSpace(pt)));
588                    }
589
590                    path.setAttribute("d", sb.toString());
591                }
592            }
593        }
594
595        /** Visit an internal node.
596         * @param name name of the node
597         * @param node the internal node
598         */
599        private void visitInternalNode(final String name, final RegionNode2D node) {
600            final LineConvexSubset trimmedCut = boundsRegion.trim(node.getCut());
601
602            final Vector2D svgStart = toSvgSpace(trimmedCut.getStartPoint());
603            final Vector2D svgEnd = toSvgSpace(trimmedCut.getEndPoint());
604
605            final Vector2D svgMid = svgStart.lerp(svgEnd, 0.5);
606
607            labelGroup.appendChild(createNodeNameElement(name, svgMid));
608
609            pathGroup.appendChild(createPathElement("cut", svgStart, svgEnd));
610
611            final String arrowPathString = createCutArrowPathString(svgStart, svgEnd);
612            if (arrowPathString != null) {
613                final Element arrowPath = createElement(PATH_ELEMENT);
614                pathGroup.appendChild(arrowPath);
615                arrowPath.setAttribute(CLASS_ATTR, "arrow");
616                arrowPath.setAttribute("d", arrowPathString);
617            }
618
619            final RegionCutBoundary<Vector2D> boundary = node.getCutBoundary();
620            if (boundary != null) {
621                addRegionBoundaries(boundary.getInsideFacing());
622                addRegionBoundaries(boundary.getOutsideFacing());
623            }
624        }
625
626        /** Add path elements for the given list of region boundaries.
627         * @param boundaries boundaries to add path elements for
628         */
629        private void addRegionBoundaries(final List<HyperplaneConvexSubset<Vector2D>> boundaries) {
630            LineConvexSubset trimmed;
631            for (final HyperplaneConvexSubset<Vector2D> boundary : boundaries) {
632                trimmed = boundsRegion.trim(boundary);
633
634                if (trimmed != null) {
635                    pathGroup.appendChild(createPathElement(
636                            "region-boundary",
637                            toSvgSpace(trimmed.getStartPoint()),
638                            toSvgSpace(trimmed.getEndPoint())));
639                }
640            }
641        }
642
643        /** Create an SVG path string defining an arrow head for the line segment that extends from
644         * {@code svgStart} to {@code svgEnd}.
645         * @param svgStart line segment start point
646         * @param svgEnd line segment end point
647         * @return an SVG path string defining an arrow head for the line segment
648         */
649        private String createCutArrowPathString(final Vector2D svgStart, final Vector2D svgEnd) {
650            final Vector2D dir = svgStart.vectorTo(svgEnd);
651            if (!dir.eq(Vector2D.ZERO, PRECISION)) {
652
653                final double az = Math.atan2(dir.getY(), dir.getX());
654                final Vector2D upperArrowPt = PolarCoordinates
655                        .toCartesian(ARROW_LENGTH, az + ARROW_ANGLE)
656                        .add(svgEnd);
657
658                final Vector2D lowerArrowPt = PolarCoordinates
659                        .toCartesian(ARROW_LENGTH, az - ARROW_ANGLE)
660                        .add(svgEnd);
661
662                final StringBuilder sb = new StringBuilder();
663                sb.append(PATH_MOVE_TO)
664                    .append(pointString(upperArrowPt))
665                    .append(SPACE)
666                    .append(PATH_LINE_TO)
667                    .append(pointString(svgEnd))
668                    .append(SPACE)
669                    .append(PATH_LINE_TO)
670                    .append(pointString(lowerArrowPt));
671
672                return sb.toString();
673            }
674
675            return null;
676        }
677
678        /** Convert the given point in geometry space to SVG space.
679         * @param pt point in geometry space to convert
680         * @return point in SVG space
681         */
682        private Vector2D toSvgSpace(final Vector2D pt) {
683            return transform.apply(pt);
684        }
685    }
686
687    /** BSP tree visitor that outputs SVG representing the tree structure.
688     */
689    private final class TreeStructureVisitor extends AbstractSVGTreeVisitor {
690
691        /** Top of the content area. */
692        private final double svgTop;
693
694        /** Width of the content area. */
695        private final double svgWidth;
696
697        /** Map of nodes to their rendered locations in the content area. */
698        private final Map<RegionNode2D, Vector2D> nodeLocations = new HashMap<>();
699
700        /** Construct a new instance for rendering a representation of the structure of a BSP tree.
701         * @param treeNodeHeight the height of the BSP tree
702         * @param svgWidth available SVG width for the content
703         * @param svgHeight available SVG height for the content
704         * @param nodeNames map of nodes to the names that should be used to identify them
705         * @param parent parent SVG element
706         * @param doc xml document
707         */
708        TreeStructureVisitor(final int treeNodeHeight, final double svgWidth, final double svgHeight,
709                final Map<RegionNode2D, String> nodeNames, final Element parent, final Document doc) {
710            super(nodeNames, parent, doc);
711
712            final double requiredSvgHeight = treeNodeHeight * TREE_VERTICAL_SPACING;
713            final double svgMid = 0.5 * svgHeight;
714
715            this.svgTop = svgMid - (0.5 * requiredSvgHeight);
716            this.svgWidth = svgWidth;
717        }
718
719        /** {@inheritDoc} */
720        @Override
721        protected void visitNode(final String name, final RegionNode2D node) {
722            final Vector2D loc = getNodeLocation(node);
723            nodeLocations.put(node, loc);
724
725            final Element nodeGroup = createChild("g");
726            nodeGroup.setAttribute(CLASS_ATTR, getNodeClassNames(name, node));
727
728            nodeGroup.appendChild(createNodeNameElement(name, loc));
729
730            final Vector2D parentLoc = nodeLocations.get(node.getParent());
731            if (parentLoc != null) {
732                final Vector2D offset = loc.vectorTo(parentLoc).withNorm(TREE_LINE_MARGIN);
733                nodeGroup.appendChild(createPathElement("tree-path", loc.add(offset), parentLoc.subtract(offset)));
734            }
735        }
736
737        /** Get a string containing the class names that should be used for the given node.
738         * @param name name of the node
739         * @param node the node
740         * @return a string containing the class names that should be used for the given node
741         */
742        private String getNodeClassNames(final String name, final RegionNode2D node) {
743            final StringBuilder sb = new StringBuilder();
744            sb.append("node-").append(name);
745
746            if (node.isLeaf()) {
747                sb.append(SPACE)
748                    .append(node.isInside() ? "inside-node" : "outside-node");
749            }
750
751            return sb.toString();
752        }
753
754        /** Get the SVG location where the visual representation of the given node should be rendered.
755         * @param node the node to determine the location for
756         * @return the SVG location where the node should be rendered
757         */
758        private Vector2D getNodeLocation(final RegionNode2D node) {
759            // find the node parent
760            final RegionNode2D parent = node.getParent();
761            final Vector2D parentLoc = nodeLocations.get(parent);
762            if (parentLoc == null) {
763                // this is the root node
764                return Vector2D.of(
765                        0.5 * svgWidth,
766                        svgTop);
767            } else {
768                // align with the parent
769                double parentXOffset = Math.max(
770                        treeParentOffsetFactor * (svgWidth / (1 << parent.depth())),
771                        treeParentXOffsetMin);
772                if (node.isMinus()) {
773                    parentXOffset = -parentXOffset;
774                }
775
776                return Vector2D.of(
777                            parentLoc.getX() + parentXOffset,
778                            parentLoc.getY() + TREE_VERTICAL_SPACING
779                        );
780            }
781        }
782    }
783}