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.core.precision.DoublePrecisionContext;
40 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
41 import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
42 import org.apache.commons.geometry.euclidean.twod.Bounds2D;
43 import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
44 import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
45 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
46 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D.RegionNode2D;
47 import org.apache.commons.geometry.euclidean.twod.Vector2D;
48 import org.apache.commons.geometry.euclidean.twod.path.LinePath;
49 import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
50 import org.w3c.dom.Document;
51 import org.w3c.dom.Element;
52
53
54
55 public class BSPTreeSVGWriter {
56
57
58 private static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";
59
60
61 private static final String SVG_VERSION = "1.1";
62
63
64 private static final String INDENT_AMOUNT_KEY = "{http://xml.apache.org/xslt}indent-amount";
65
66
67 private static final int INDENT_AMOUNT = 4;
68
69
70 private static final String DEFAULT_NODE_NAMES = "abcdefghijklmnopqrstuvwxyz";
71
72
73 private static final String GEOMETRY_AREA_CLIP_PATH_ID = "geometry-area";
74
75
76 private static final char PATH_MOVE_TO = 'M';
77
78
79 private static final char PATH_LINE_TO = 'L';
80
81
82 private static final char SPACE = ' ';
83
84
85 private static final String RECT_ELEMENT = "rect";
86
87
88 private static final String PATH_ELEMENT = "path";
89
90
91 private static final String CLASS_ATTR = "class";
92
93
94 private static final String WIDTH_ATTR = "width";
95
96
97 private static final String HEIGHT_ATTR = "height";
98
99
100 private static final String X_ATTR = "x";
101
102
103 private static final String Y_ATTR = "y";
104
105
106 private static final String STYLE =
107 "text { font-size: 14px; } " +
108 ".node-name { text-anchor: middle; font-family: \"Courier New\", Courier, monospace; } " +
109 ".geometry-border { fill: none; stroke: gray; stroke-width: 1; } " +
110 ".arrow { fill: none; stroke: blue; stroke-width: 1; } " +
111 ".cut { fill: none; stroke: blue; stroke-width: 1; stroke-dasharray: 5,3; } " +
112 ".region-boundary { stroke: orange; stroke-width: 2; } " +
113 ".inside { fill: #aaa; opacity: 0.2; } " +
114 ".tree-path { fill: none; stroke: gray; stroke-width: 1; } " +
115 ".inside-node { font-weight: bold; }";
116
117
118 private final Bounds2D bounds;
119
120
121 private int width = 750;
122
123
124 private int height = 375;
125
126
127 private int margin = 5;
128
129
130 private double geometryAreaWidthFactor = 0.5;
131
132
133 private double treeAreaWidthFactor = 1.0 - geometryAreaWidthFactor;
134
135
136 private double arrowAngle = 0.8 * Math.PI;
137
138
139 private double arrowLength = 8;
140
141
142 private double treeVerticalSpacing = 45;
143
144
145 private double treeLineMargin = 10;
146
147
148
149
150 private double treeParentOffsetFactor = 0.25;
151
152
153 private double treeParentXOffsetMin = 0;
154
155
156 private DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
157
158
159
160
161 public BSPTreeSVGWriter(final Bounds2D bounds) {
162 this.bounds = bounds;
163 }
164
165
166
167
168
169 public void setTreeParentOffsetFactor(final double treeParentOffsetFactor) {
170 this.treeParentOffsetFactor = treeParentOffsetFactor;
171 }
172
173
174
175
176 public void setTreeParentXOffsetMin(final double treeParentXOffsetMin) {
177 this.treeParentXOffsetMin = treeParentXOffsetMin;
178 }
179
180
181
182
183
184 public void write(final RegionBSPTree2D tree, final File file) {
185 final Deque<RegionNode2D> nodeQueue = new LinkedList<>();
186 nodeQueue.add(tree.getRoot());
187
188 final Map<RegionNode2D, String> nodeNames = new HashMap<>();
189
190 final String names = DEFAULT_NODE_NAMES;
191 RegionNode2D node;
192 for (int i = 0; i < names.length() && !nodeQueue.isEmpty(); ++i) {
193 node = nodeQueue.removeFirst();
194
195 nodeNames.put(node, names.substring(i, i + 1));
196
197 if (node.isInternal()) {
198 nodeQueue.add(node.getMinus());
199 nodeQueue.add(node.getPlus());
200 }
201 }
202
203 write(tree, nodeNames, file);
204 }
205
206
207
208
209
210
211 public void write(final RegionBSPTree2D tree, final Map<RegionNode2D, String> nodeNames, final File file) {
212 try {
213 final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
214 final DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
215
216 final Document doc = docBuilder.newDocument();
217
218
219 final Element root = svgElement("svg", doc);
220 doc.appendChild(root);
221
222 root.setAttribute("version", SVG_VERSION);
223 root.setAttribute(WIDTH_ATTR, String.valueOf(width));
224 root.setAttribute(HEIGHT_ATTR, String.valueOf(height));
225
226
227 final Element defs = svgElement("defs", doc);
228 root.appendChild(defs);
229
230
231 final Element style = svgElement("style", doc);
232 root.appendChild(style);
233 style.setTextContent(STYLE);
234
235
236 writeTreeGeometryArea(tree, nodeNames, root, defs, doc);
237 writeTreeStructureArea(tree, nodeNames, root, doc);
238
239
240 final TransformerFactory transformerFactory = TransformerFactory.newInstance();
241 final Transformer transformer = transformerFactory.newTransformer();
242 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
243 transformer.setOutputProperty(INDENT_AMOUNT_KEY, String.valueOf(INDENT_AMOUNT));
244
245 final DOMSource source = new DOMSource(doc);
246 final StreamResult target = new StreamResult(file);
247
248 transformer.transform(source, target);
249
250 } catch (ParserConfigurationException | TransformerException e) {
251
252 throw new RuntimeException("Failed to create SVG", e);
253 }
254 }
255
256
257
258
259
260
261
262
263 private void writeTreeGeometryArea(final RegionBSPTree2D tree, final Map<RegionNode2D, String> nodeNames,
264 final Element root, final Element defs, final Document doc) {
265 final double geometrySvgX = margin;
266 final double geometrySvgY = margin;
267 final double geometrySvgWidth = (geometryAreaWidthFactor * width) - (2 * margin);
268 final double geometrySvgHeight = height - (2 * margin);
269
270 defineClipRect(GEOMETRY_AREA_CLIP_PATH_ID,
271 geometrySvgX, geometrySvgY,
272 geometrySvgWidth, geometrySvgHeight,
273 defs, doc);
274
275
276 final Element geometryGroup = svgElement("g", doc);
277 root.appendChild(geometryGroup);
278 geometryGroup.setAttribute(CLASS_ATTR, "geometry");
279 geometryGroup.setAttribute("clip-path", "url(#" + GEOMETRY_AREA_CLIP_PATH_ID + ")");
280
281
282 final AffineTransformMatrix2D transform = computeGeometryTransform(bounds, geometrySvgX, geometrySvgY,
283 geometrySvgWidth, geometrySvgHeight);
284
285
286 tree.accept(new TreeGeometryVisitor(transform, nodeNames, geometryGroup, doc));
287
288
289 final Element border = svgElement(RECT_ELEMENT, doc);
290 border.setAttribute(CLASS_ATTR, "geometry-border");
291 floatAttr(border, X_ATTR, geometrySvgX);
292 floatAttr(border, Y_ATTR, geometrySvgY);
293 floatAttr(border, WIDTH_ATTR, geometrySvgWidth);
294 floatAttr(border, HEIGHT_ATTR, geometrySvgHeight);
295
296 root.appendChild(border);
297
298 }
299
300
301
302
303
304
305
306 private void writeTreeStructureArea(final RegionBSPTree2D tree, final Map<RegionNode2D, String> nodeNames,
307 final Element root, final Document doc) {
308 final Element treeGroup = svgElement("g", doc);
309 root.appendChild(treeGroup);
310 treeGroup.setAttribute(CLASS_ATTR, "tree");
311
312 final double offsetX = ((1 - treeAreaWidthFactor) * width) + margin;
313 final double offsetY = margin;
314
315 final double svgWidth = (treeAreaWidthFactor * width) - (2 * margin);
316 final double svgHeight = height - (2 * margin);
317
318 treeGroup.setAttribute("transform", "translate(" + offsetX + " " + offsetY + ")");
319
320 tree.accept(new TreeStructureVisitor(tree.height(), svgWidth, svgHeight, nodeNames, treeGroup, doc));
321 }
322
323
324
325
326
327
328
329
330
331
332
333 private static AffineTransformMatrix2D computeGeometryTransform(final Bounds2D bounds,
334 final double svgX, final double svgY, final double svgWidth, final double svgHeight) {
335
336 final Vector2D boundsDiagonal = bounds.getDiagonal();
337
338 return AffineTransformMatrix2D
339 .createTranslation(bounds.getMin().negate())
340 .scale(svgWidth / boundsDiagonal.getX(), -svgHeight / boundsDiagonal.getY())
341 .translate(svgX, svgY + svgHeight);
342 }
343
344
345
346
347
348
349
350
351
352
353 private void defineClipRect(final String id, final double x, final double y,
354 final double svgWidth, final double svgHeight, final Element defs, final Document doc) {
355
356 final Element clipPath = svgElement("clipPath", doc);
357 clipPath.setAttribute("id", id);
358
359 defs.appendChild(clipPath);
360
361 final Element rect = svgElement(RECT_ELEMENT, doc);
362 floatAttr(rect, X_ATTR, x);
363 floatAttr(rect, Y_ATTR, y);
364 floatAttr(rect, WIDTH_ATTR, svgWidth);
365 floatAttr(rect, HEIGHT_ATTR, svgHeight);
366
367 clipPath.appendChild(rect);
368 }
369
370
371
372
373
374
375 private static void floatAttr(final Element element, final String name, final double value) {
376 element.setAttribute(name, String.valueOf(value));
377 }
378
379
380
381
382
383
384 private static Element svgElement(final String name, final Document doc) {
385 return doc.createElementNS(SVG_NAMESPACE, name);
386 }
387
388
389
390 private abstract class AbstractSVGTreeVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {
391
392
393 private final Map<RegionNode2D, String> nodeNames;
394
395
396 private final Element parent;
397
398
399 private final Document doc;
400
401
402 private int count = 0;
403
404
405
406
407
408
409 AbstractSVGTreeVisitor(final Map<RegionNode2D, String> nodeNames, final Element parent, final Document doc) {
410 this.nodeNames = nodeNames;
411 this.parent = parent;
412 this.doc = doc;
413 }
414
415
416 @Override
417 public Order visitOrder(final RegionNode2D internalNode) {
418 return Order.NODE_MINUS_PLUS;
419 }
420
421
422 @Override
423 public Result visit(final RegionNode2D node) {
424 ++count;
425
426 final String name = (nodeNames != null && nodeNames.containsKey(node)) ?
427 nodeNames.get(node) :
428 String.valueOf(count);
429
430 visitNode(name, node);
431
432 return Result.CONTINUE;
433 }
434
435
436
437
438
439 protected Element createChild(final String name) {
440 final Element child = createElement(name);
441 parent.appendChild(child);
442
443 return child;
444 }
445
446
447
448
449
450
451 protected Element createElement(final String name) {
452 return svgElement(name, doc);
453 }
454
455
456
457
458
459
460
461 protected Element createNodeNameElement(final String name, final Vector2D svgPt) {
462 final Element text = createElement("text");
463 text.setAttribute(CLASS_ATTR, "node-name");
464 text.setAttribute("dominant-baseline", "middle");
465 floatAttr(text, X_ATTR, svgPt.getX());
466 floatAttr(text, Y_ATTR, svgPt.getY());
467 text.setTextContent(name);
468
469 return text;
470 }
471
472
473
474
475
476
477
478 protected Element createPathElement(final String className, final Vector2D svgStart, Vector2D svgEnd) {
479 final Element path = createElement(PATH_ELEMENT);
480 path.setAttribute(CLASS_ATTR, className);
481
482 final StringBuilder pathStr = new StringBuilder();
483 pathStr.append(PATH_MOVE_TO)
484 .append(pointString(svgStart))
485 .append(SPACE)
486 .append(PATH_LINE_TO)
487 .append(pointString(svgEnd));
488
489 path.setAttribute("d", pathStr.toString());
490
491 return path;
492 }
493
494
495
496
497
498
499 protected String pointString(final Vector2D pt) {
500 return pt.getX() + " " + pt.getY();
501 }
502
503
504
505
506
507 protected abstract void visitNode(String name, RegionNode2D node);
508 }
509
510
511
512 private final class TreeGeometryVisitor extends AbstractSVGTreeVisitor {
513
514
515 private final Parallelogram boundsRegion;
516
517
518 private final AffineTransformMatrix2D transform;
519
520
521 private final Element pathGroup;
522
523
524 private final Element labelGroup;
525
526
527
528
529
530
531
532 TreeGeometryVisitor(final AffineTransformMatrix2D transform, final Map<RegionNode2D, String> nodeNames,
533 final Element parent, final Document doc) {
534 super(nodeNames, parent, doc);
535
536 this.boundsRegion = bounds.toRegion(precision);
537
538 this.transform = transform;
539
540
541 this.pathGroup = svgElement("g", doc);
542 pathGroup.setAttribute(CLASS_ATTR, "paths");
543 parent.appendChild(pathGroup);
544
545 this.labelGroup = svgElement("g", doc);
546 labelGroup.setAttribute(CLASS_ATTR, "labels");
547 parent.appendChild(labelGroup);
548 }
549
550
551 @Override
552 protected void visitNode(final String name, final RegionNode2D node) {
553 if (node.isLeaf()) {
554 visitLeafNode(name, node);
555 } else {
556 visitInternalNode(name, node);
557 }
558 }
559
560
561
562
563
564 private void visitLeafNode(final String name, final RegionNode2D node) {
565 final RegionBSPTree2D tree = node.getNodeRegion().toTree();
566 tree.intersection(boundsRegion.toTree());
567
568 final Vector2D svgCentroid = toSvgSpace(tree.getCentroid());
569
570 labelGroup.appendChild(createNodeNameElement(name, svgCentroid));
571
572 if (node.isInside()) {
573 for (LinePath linePath : tree.getBoundaryPaths()) {
574 final Element path = createElement(PATH_ELEMENT);
575 pathGroup.appendChild(path);
576 path.setAttribute(CLASS_ATTR, "inside");
577
578 StringBuilder sb = new StringBuilder();
579
580 for (final Vector2D pt : linePath.getVertexSequence()) {
581 if (sb.length() < 1) {
582 sb.append(PATH_MOVE_TO);
583 } else {
584 sb.append(SPACE)
585 .append(PATH_LINE_TO);
586 }
587
588 sb.append(pointString(toSvgSpace(pt)));
589 }
590
591 path.setAttribute("d", sb.toString());
592 }
593 }
594 }
595
596
597
598
599
600 private void visitInternalNode(final String name, final RegionNode2D node) {
601 final LineConvexSubset trimmedCut = boundsRegion.trim(node.getCut());
602
603 final Vector2D svgStart = toSvgSpace(trimmedCut.getStartPoint());
604 final Vector2D svgEnd = toSvgSpace(trimmedCut.getEndPoint());
605
606 final Vector2D svgMid = svgStart.lerp(svgEnd, 0.5);
607
608 labelGroup.appendChild(createNodeNameElement(name, svgMid));
609
610 pathGroup.appendChild(createPathElement("cut", svgStart, svgEnd));
611
612 final String arrowPathString = createCutArrowPathString(svgStart, svgEnd);
613 if (arrowPathString != null) {
614 final Element arrowPath = createElement(PATH_ELEMENT);
615 pathGroup.appendChild(arrowPath);
616 arrowPath.setAttribute(CLASS_ATTR, "arrow");
617 arrowPath.setAttribute("d", arrowPathString);
618 }
619
620 final RegionCutBoundary<Vector2D> boundary = node.getCutBoundary();
621 if (boundary != null) {
622 addRegionBoundaries(boundary.getInsideFacing());
623 addRegionBoundaries(boundary.getOutsideFacing());
624 }
625 }
626
627
628
629
630 private void addRegionBoundaries(final List<HyperplaneConvexSubset<Vector2D>> boundaries) {
631 LineConvexSubset trimmed;
632 for (final HyperplaneConvexSubset<Vector2D> boundary : boundaries) {
633 trimmed = boundsRegion.trim(boundary);
634
635 if (trimmed != null) {
636 pathGroup.appendChild(createPathElement(
637 "region-boundary",
638 toSvgSpace(trimmed.getStartPoint()),
639 toSvgSpace(trimmed.getEndPoint())));
640 }
641 }
642 }
643
644
645
646
647
648
649
650 private String createCutArrowPathString(final Vector2D svgStart, final Vector2D svgEnd) {
651 final Vector2D dir = svgStart.vectorTo(svgEnd);
652 if (!dir.eq(Vector2D.ZERO, precision)) {
653
654 final double az = Math.atan2(dir.getY(), dir.getX());
655 final Vector2D upperArrowPt = PolarCoordinates
656 .toCartesian(arrowLength, az + arrowAngle)
657 .add(svgEnd);
658
659 final Vector2D lowerArrowPt = PolarCoordinates
660 .toCartesian(arrowLength, az - arrowAngle)
661 .add(svgEnd);
662
663 final StringBuilder sb = new StringBuilder();
664 sb.append(PATH_MOVE_TO)
665 .append(pointString(upperArrowPt))
666 .append(SPACE)
667 .append(PATH_LINE_TO)
668 .append(pointString(svgEnd))
669 .append(SPACE)
670 .append(PATH_LINE_TO)
671 .append(pointString(lowerArrowPt));
672
673 return sb.toString();
674 }
675
676 return null;
677 }
678
679
680
681
682
683 private Vector2D toSvgSpace(final Vector2D pt) {
684 return transform.apply(pt);
685 }
686 }
687
688
689
690 private final class TreeStructureVisitor extends AbstractSVGTreeVisitor {
691
692
693 private final double svgTop;
694
695
696 private final double svgWidth;
697
698
699 private final Map<RegionNode2D, Vector2D> nodeLocations = new HashMap<>();;
700
701
702
703
704
705
706
707
708
709 TreeStructureVisitor(final int treeNodeHeight, final double svgWidth, final double svgHeight,
710 final Map<RegionNode2D, String> nodeNames, final Element parent, final Document doc) {
711 super(nodeNames, parent, doc);
712
713 final double requiredSvgHeight = treeNodeHeight * treeVerticalSpacing;
714 final double svgMid = 0.5 * svgHeight;
715
716 this.svgTop = svgMid - (0.5 * requiredSvgHeight);
717 this.svgWidth = svgWidth;
718 }
719
720
721 @Override
722 protected void visitNode(final String name, final RegionNode2D node) {
723 final Vector2D loc = getNodeLocation(node);
724 nodeLocations.put(node, loc);
725
726 final Element nodeGroup = createChild("g");
727 nodeGroup.setAttribute(CLASS_ATTR, getNodeClassNames(name, node));
728
729 nodeGroup.appendChild(createNodeNameElement(name, loc));
730
731 final Vector2D parentLoc = nodeLocations.get(node.getParent());
732 if (parentLoc != null) {
733 final Vector2D offset = loc.vectorTo(parentLoc).withNorm(treeLineMargin);
734 nodeGroup.appendChild(createPathElement("tree-path", loc.add(offset), parentLoc.subtract(offset)));
735 }
736 }
737
738
739
740
741
742
743 private String getNodeClassNames(final String name, final RegionNode2D node) {
744 final StringBuilder sb = new StringBuilder();
745 sb.append("node-" + name);
746
747 if (node.isLeaf()) {
748 sb.append(SPACE)
749 .append(node.isInside() ? "inside-node" : "outside-node");
750 }
751
752 return sb.toString();
753 }
754
755
756
757
758
759 private Vector2D getNodeLocation(final RegionNode2D node) {
760
761 final RegionNode2D parent = node.getParent();
762 final Vector2D parentLoc = nodeLocations.get(parent);
763 if (parentLoc == null) {
764
765 return Vector2D.of(
766 0.5 * svgWidth,
767 svgTop);
768 } else {
769
770 double parentXOffset = Math.max(
771 treeParentOffsetFactor * (svgWidth / (1 << parent.depth())),
772 treeParentXOffsetMin);
773 if (node.isMinus()) {
774 parentXOffset = -parentXOffset;
775 }
776
777 return Vector2D.of(
778 parentLoc.getX() + parentXOffset,
779 parentLoc.getY() + treeVerticalSpacing
780 );
781 }
782 }
783 }
784 }