1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.examples.tutorials.bsp;
18
19 import java.io.File;
20 import java.util.Deque;
21 import java.util.HashMap;
22 import java.util.LinkedList;
23 import java.util.List;
24 import java.util.Map;
25
26 import javax.xml.parsers.DocumentBuilder;
27 import javax.xml.parsers.DocumentBuilderFactory;
28 import javax.xml.parsers.ParserConfigurationException;
29 import javax.xml.transform.OutputKeys;
30 import javax.xml.transform.Transformer;
31 import javax.xml.transform.TransformerException;
32 import javax.xml.transform.TransformerFactory;
33 import javax.xml.transform.dom.DOMSource;
34 import javax.xml.transform.stream.StreamResult;
35
36 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
37 import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
38 import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
39 import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
40 import org.apache.commons.geometry.euclidean.twod.Bounds2D;
41 import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
42 import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
43 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
44 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D.RegionNode2D;
45 import org.apache.commons.geometry.euclidean.twod.Vector2D;
46 import org.apache.commons.geometry.euclidean.twod.path.LinePath;
47 import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
48 import org.apache.commons.numbers.core.Precision;
49 import org.w3c.dom.Document;
50 import org.w3c.dom.Element;
51
52
53
54 public class BSPTreeSVGWriter {
55
56
57 private static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";
58
59
60 private static final String SVG_VERSION = "1.1";
61
62
63 private static final String INDENT_AMOUNT_KEY = "{http://xml.apache.org/xslt}indent-amount";
64
65
66 private static final int INDENT_AMOUNT = 4;
67
68
69 private static final String DEFAULT_NODE_NAMES = "abcdefghijklmnopqrstuvwxyz";
70
71
72 private static final String GEOMETRY_AREA_CLIP_PATH_ID = "geometry-area";
73
74
75 private static final char PATH_MOVE_TO = 'M';
76
77
78 private static final char PATH_LINE_TO = 'L';
79
80
81 private static final char SPACE = ' ';
82
83
84 private static final String RECT_ELEMENT = "rect";
85
86
87 private static final String PATH_ELEMENT = "path";
88
89
90 private static final String CLASS_ATTR = "class";
91
92
93 private static final String WIDTH_ATTR = "width";
94
95
96 private static final String HEIGHT_ATTR = "height";
97
98
99 private static final String X_ATTR = "x";
100
101
102 private static final String Y_ATTR = "y";
103
104
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
117 private static final int WIDTH = 750;
118
119
120 private static final int HEIGHT = 375;
121
122
123 private static final int MARGIN = 5;
124
125
126 private static final double GEOMETRY_AREA_WIDTH_FACTOR = 0.5;
127
128
129 private static final double TREE_AREA_WIDTH_FACTOR = 1.0 - GEOMETRY_AREA_WIDTH_FACTOR;
130
131
132 private static final double ARROW_ANGLE = 0.8 * Math.PI;
133
134
135 private static final double ARROW_LENGTH = 8;
136
137
138 private static final double TREE_VERTICAL_SPACING = 45;
139
140
141 private static final double TREE_LINE_MARGIN = 10;
142
143
144 private static final Precision.DoubleEquivalence PRECISION = Precision.doubleEquivalenceOfEpsilon(1e-6);
145
146
147 private final Bounds2D bounds;
148
149
150
151
152 private double treeParentOffsetFactor = 0.25;
153
154
155 private double treeParentXOffsetMin = 0;
156
157
158
159
160 public BSPTreeSVGWriter(final Bounds2D bounds) {
161 this.bounds = bounds;
162 }
163
164
165
166
167
168 public void setTreeParentOffsetFactor(final double treeParentOffsetFactor) {
169 this.treeParentOffsetFactor = treeParentOffsetFactor;
170 }
171
172
173
174
175 public void setTreeParentXOffsetMin(final double treeParentXOffsetMin) {
176 this.treeParentXOffsetMin = treeParentXOffsetMin;
177 }
178
179
180
181
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
206
207
208
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
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
226 final Element defs = svgElement("defs", doc);
227 root.appendChild(defs);
228
229
230 final Element style = svgElement("style", doc);
231 root.appendChild(style);
232 style.setTextContent(STYLE);
233
234
235 writeTreeGeometryArea(tree, nodeNames, root, defs, doc);
236 writeTreeStructureArea(tree, nodeNames, root, doc);
237
238
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
251 throw new RuntimeException("Failed to create SVG", e);
252 }
253 }
254
255
256
257
258
259
260
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
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
281 final AffineTransformMatrix2D transform = computeGeometryTransform(bounds, geometrySvgX, geometrySvgY,
282 geometrySvgWidth, geometrySvgHeight);
283
284
285 tree.accept(new TreeGeometryVisitor(transform, nodeNames, geometryGroup, doc));
286
287
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
300
301
302
303
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
323
324
325
326
327
328
329
330
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
344
345
346
347
348
349
350
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
370
371
372
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
379
380
381
382
383 private static Element svgElement(final String name, final Document doc) {
384 return doc.createElementNS(SVG_NAMESPACE, name);
385 }
386
387
388
389 private abstract class AbstractSVGTreeVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {
390
391
392 private final Map<RegionNode2D, String> nodeNames;
393
394
395 private final Element parent;
396
397
398 private final Document doc;
399
400
401 private int count = 0;
402
403
404
405
406
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
415 @Override
416 public Order visitOrder(final RegionNode2D internalNode) {
417 return Order.NODE_MINUS_PLUS;
418 }
419
420
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
435
436
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
446
447
448
449
450 protected Element createElement(final String name) {
451 return svgElement(name, doc);
452 }
453
454
455
456
457
458
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
472
473
474
475
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
494
495
496
497
498 protected String pointString(final Vector2D pt) {
499 return pt.getX() + " " + pt.getY();
500 }
501
502
503
504
505
506 protected abstract void visitNode(String name, RegionNode2D node);
507 }
508
509
510
511 private final class TreeGeometryVisitor extends AbstractSVGTreeVisitor {
512
513
514 private final Parallelogram boundsRegion;
515
516
517 private final AffineTransformMatrix2D transform;
518
519
520 private final Element pathGroup;
521
522
523 private final Element labelGroup;
524
525
526
527
528
529
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
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
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
560
561
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
596
597
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
627
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
644
645
646
647
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
679
680
681
682 private Vector2D toSvgSpace(final Vector2D pt) {
683 return transform.apply(pt);
684 }
685 }
686
687
688
689 private final class TreeStructureVisitor extends AbstractSVGTreeVisitor {
690
691
692 private final double svgTop;
693
694
695 private final double svgWidth;
696
697
698 private final Map<RegionNode2D, Vector2D> nodeLocations = new HashMap<>();
699
700
701
702
703
704
705
706
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
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
738
739
740
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
755
756
757
758 private Vector2D getNodeLocation(final RegionNode2D node) {
759
760 final RegionNode2D parent = node.getParent();
761 final Vector2D parentLoc = nodeLocations.get(parent);
762 if (parentLoc == null) {
763
764 return Vector2D.of(
765 0.5 * svgWidth,
766 svgTop);
767 } else {
768
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 }