From e7bc78260d4b00cba177530154f09b24cef1278c Mon Sep 17 00:00:00 2001 From: Behrang Saeedzadeh Date: Fri, 20 Oct 2023 18:39:33 +1100 Subject: [PATCH] Added demo --- .github/workflows/gradle-publish.yml | 45 --------- .github/workflows/gradle.yml | 30 +++--- build.gradle | 2 +- .../algorithm/tree/DimensionCalculator.java | 63 +++++++++++++ .../behrang/algorithm/tree/TreeTraversal.java | 15 +++ .../algorithm/tree/WalkerAlgorithm.java | 33 +------ .../algorithm/tree/demo/DemoFrame.java | 24 +++++ .../algorithm/tree/demo/SampleTree.java | 60 ++++++++++++ .../algorithm/tree/demo/TreePanel.java | 91 +++++++++++++++++++ .../tree/DimensionCalculatorTest.java | 32 +++++++ .../algorithm/tree/WalkerAlgorithmTest.java | 6 +- 11 files changed, 303 insertions(+), 98 deletions(-) delete mode 100644 .github/workflows/gradle-publish.yml create mode 100644 src/main/java/org/behrang/algorithm/tree/DimensionCalculator.java create mode 100644 src/main/java/org/behrang/algorithm/tree/TreeTraversal.java create mode 100644 src/main/java/org/behrang/algorithm/tree/demo/DemoFrame.java create mode 100644 src/main/java/org/behrang/algorithm/tree/demo/SampleTree.java create mode 100644 src/main/java/org/behrang/algorithm/tree/demo/TreePanel.java create mode 100644 src/test/java/org/behrang/algorithm/tree/DimensionCalculatorTest.java diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml deleted file mode 100644 index 0bc2be0..0000000 --- a/.github/workflows/gradle-publish.yml +++ /dev/null @@ -1,45 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle - -name: Gradle Package - -on: - release: - types: [created] - -jobs: - build: - - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - server-id: github # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Build with Gradle - uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 - with: - arguments: build - - # The USERNAME and TOKEN need to correspond to the credentials environment variables used in - # the publishing section of your build.gradle - - name: Publish to GitHub Packages - uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 - with: - arguments: publish - env: - USERNAME: ${{ github.actor }} - TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c76da97..e89bfe6 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,15 +1,7 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle - -name: Java CI with Gradle +name: CI on: push: - branches: [ "master" ] pull_request: branches: [ "master" ] @@ -20,13 +12,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - name: Build with Gradle - uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 - with: - arguments: build + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 + with: + arguments: build \ No newline at end of file diff --git a/build.gradle b/build.gradle index d2c8286..12cca5b 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ dependencies { } group = 'org.behrang.algorithm' -version = '1.0.0' +version = '1.0-SNAPSHOT' description = 'John Q Walker Node Positioning Algorithm' java { diff --git a/src/main/java/org/behrang/algorithm/tree/DimensionCalculator.java b/src/main/java/org/behrang/algorithm/tree/DimensionCalculator.java new file mode 100644 index 0000000..357728c --- /dev/null +++ b/src/main/java/org/behrang/algorithm/tree/DimensionCalculator.java @@ -0,0 +1,63 @@ +package org.behrang.algorithm.tree; + +import java.awt.*; +import java.awt.geom.Dimension2D; +import java.util.concurrent.atomic.AtomicReference; + +public class DimensionCalculator { + public static Dimension2D calculateTreeDimension(Node root) { + final AtomicReference minX = new AtomicReference<>(Double.MAX_VALUE); + final AtomicReference minY = new AtomicReference<>(Double.MAX_VALUE); + final AtomicReference maxX = new AtomicReference<>(Double.MIN_VALUE); + final AtomicReference maxY = new AtomicReference<>(Double.MIN_VALUE); + + TreeTraversal.preorder(root, n -> { + if (n.getX() < minX.get()) { + minX.set(n.getX()); + } + + if (n.getY() < minY.get()) { + minY.set(n.getY()); + } + + double x2 = n.getX() + n.getWidth(); + if (x2 > maxX.get()) { + maxX.set(x2); + } + + double y2 = n.getY() + n.getHeight(); + if (y2 > maxY.get()) { + maxY.set(y2); + } + }); + + double width = (maxX.get() - minX.get()); + double height = (maxY.get() - minY.get()); + + Dimension dim = new Dimension(); + dim.setSize(width, height); + + return dim; + } + + public static Dimension2D calculateMaxNodeDimension(Node root) { + final AtomicReference width = new AtomicReference<>(Double.MIN_VALUE); + final AtomicReference height = new AtomicReference<>(Double.MIN_VALUE); + + TreeTraversal.preorder(root, n -> { + if (width.get() < n.getWidth()) { + width.set(n.getWidth()); + } + + if (height.get() < n.getHeight()) { + height.set(n.getHeight()); + } + }); + + Dimension dim = new Dimension(); + dim.setSize(width.get(), height.get()); + + return dim; + } + +} diff --git a/src/main/java/org/behrang/algorithm/tree/TreeTraversal.java b/src/main/java/org/behrang/algorithm/tree/TreeTraversal.java new file mode 100644 index 0000000..849677e --- /dev/null +++ b/src/main/java/org/behrang/algorithm/tree/TreeTraversal.java @@ -0,0 +1,15 @@ +package org.behrang.algorithm.tree; + +import org.behrang.algorithm.tree.Node; + +import java.util.function.Consumer; + +public class TreeTraversal { + public static void preorder(Node root, Consumer> consumer) { + consumer.accept(root); + + root.getChildren().forEach(ch -> { + preorder(ch, consumer); + }); + } +} diff --git a/src/main/java/org/behrang/algorithm/tree/WalkerAlgorithm.java b/src/main/java/org/behrang/algorithm/tree/WalkerAlgorithm.java index d189055..68c3da2 100755 --- a/src/main/java/org/behrang/algorithm/tree/WalkerAlgorithm.java +++ b/src/main/java/org/behrang/algorithm/tree/WalkerAlgorithm.java @@ -6,38 +6,7 @@ public class WalkerAlgorithm { private static final long MAX_DEPTH = Long.MAX_VALUE; - - /** - * During the operation of the algorithm, we walk the tree two times. - *

- * Whenever we visit a new node {@code N} at a level {@code L}, we store it in this map. - *

- * Then whenever we need to look up a node's neighboring node to the left, we can consult this map. - *

- * For example, consider the following tree: - *

-     *     A
-     * ┌───┼───┐
-     * B   C   D
-     *     │   │
-     *     E   F
-     * 
- *

- * As you can see, node {@code A} is at level 0, nodes {@code B}, {@code C}, {@code D} are at level 1, and nodes - * {@code E} and {@code F} are at level 2. - *

- * When we arrive at node {@code F}, we can consult to look up its left neighbor {@code E} using this table: - *

{@code
-     *      // leftNeighbor will resolve to node "E"
-     *      var leftNeighbor = previousNodeAtLevel.get(2);
-     * }
- *

- * Similarly, when we arrive at node {@code C}: - *

{@code
-     *      // leftNeighbor will resolve to node "B"
-     *      var leftNeighbor = previousNodeAtLevel.get(1);
-     * }
- */ + private final Map> previousNodeAtLevel; private final double siblingSeparation; diff --git a/src/main/java/org/behrang/algorithm/tree/demo/DemoFrame.java b/src/main/java/org/behrang/algorithm/tree/demo/DemoFrame.java new file mode 100644 index 0000000..3bdcc2b --- /dev/null +++ b/src/main/java/org/behrang/algorithm/tree/demo/DemoFrame.java @@ -0,0 +1,24 @@ +package org.behrang.algorithm.tree.demo; + +import javax.swing.*; +import java.awt.*; + +public class DemoFrame extends JFrame { + + private JPanel mainPanel; + + public DemoFrame() { + mainPanel = new TreePanel<>(SampleTree.newUniformInstance(64, 64)); + add(mainPanel, BorderLayout.CENTER); + } + + public static void main(String[] args) { + DemoFrame demoFrame = new DemoFrame(); + demoFrame.setTitle("Walker Algorithm Demo"); + demoFrame.setSize(1024, 768); + demoFrame.setResizable(false); + demoFrame.setLocationByPlatform(true); + demoFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + demoFrame.setVisible(true); + } +} diff --git a/src/main/java/org/behrang/algorithm/tree/demo/SampleTree.java b/src/main/java/org/behrang/algorithm/tree/demo/SampleTree.java new file mode 100644 index 0000000..89a786e --- /dev/null +++ b/src/main/java/org/behrang/algorithm/tree/demo/SampleTree.java @@ -0,0 +1,60 @@ +package org.behrang.algorithm.tree.demo; + +import org.behrang.algorithm.tree.Node; + +public final class SampleTree { + + private SampleTree() { + } + + public static Node newUniformInstance() { + return newUniformInstance(2, 2); + } + + public static Node newUniformInstance(double nodeWidth, double nodeHeight) { + Node a, b, c, d, e, f, g, h, i, j, k, l, m, n, o; + + o = node("O", nodeWidth, nodeHeight); + e = node("E", nodeWidth, nodeHeight); + f = node("F", nodeWidth, nodeHeight); + n = node("N", nodeWidth, nodeHeight); + link(o, e, f, n); + + a = node("A", nodeWidth, nodeHeight); + d = node("D", nodeWidth, nodeHeight); + link(e, a, d); + + b = node("B", nodeWidth, nodeHeight); + c = node("C", nodeWidth, nodeHeight); + link(d, b, c); + + g = node("G", nodeWidth, nodeHeight); + m = node("M", nodeWidth, nodeHeight); + link(n, g, m); + + h = node("H", nodeWidth, nodeHeight); + i = node("I", nodeWidth, nodeHeight); + j = node("J", nodeWidth, nodeHeight); + k = node("K", nodeWidth, nodeHeight); + l = node("L", nodeWidth, nodeHeight); + link(m, h, i, j, k, l); + + return o; + } + + static Node node(String value, double nodeWidth, double nodeHeight) { + Node node = new Node<>(value); + node.setWidth(nodeWidth); + node.setHeight(nodeHeight); + + return node; + } + + @SafeVarargs + static void link(Node parent, Node... children) { + for (var child : children) { + parent.getChildren().add(child); + child.setParent(parent); + } + } +} diff --git a/src/main/java/org/behrang/algorithm/tree/demo/TreePanel.java b/src/main/java/org/behrang/algorithm/tree/demo/TreePanel.java new file mode 100644 index 0000000..6735aca --- /dev/null +++ b/src/main/java/org/behrang/algorithm/tree/demo/TreePanel.java @@ -0,0 +1,91 @@ +package org.behrang.algorithm.tree.demo; + +import org.behrang.algorithm.tree.Node; +import org.behrang.algorithm.tree.TreeTraversal; +import org.behrang.algorithm.tree.WalkerAlgorithm; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; + +import static org.behrang.algorithm.tree.DimensionCalculator.calculateMaxNodeDimension; + +public class TreePanel extends JPanel { + + private Node root; + + public TreePanel(Node root) { + setLayout(null); + setRoot(root); + } + + public void setRoot(Node root) { + this.root = root; + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (root == null) { + return; + } + + var dim = calculateMaxNodeDimension(root); + + var walker = new WalkerAlgorithm( + dim.getWidth(), + dim.getWidth(), + 20, + 20, + dim.getHeight() * 2 + ); + + walker.position(root); + + var g2 = (Graphics2D) g; + TreeTraversal.preorder(root, n -> { + drawNode(g2, n); + }); + } + + void drawNode(Graphics2D g, Node node) { + double labelMarginX = 12.0; + double labelMarginY = 1.0; + + g.setColor(Color.BLACK); + Rectangle2D rect = new Rectangle2D.Double( + node.getX(), + node.getY(), + node.getWidth(), + node.getHeight() + ); + g.draw(rect); + + String label = node.getValue().toString(); + + g.setColor(Color.DARK_GRAY); + Font font = new Font(Font.SERIF, Font.ITALIC, 24); + g.setFont(font); + + FontMetrics fontMetrics = g.getFontMetrics(font); + Rectangle2D labelBounds = fontMetrics.getStringBounds(label, g); + + g.drawString(label, + (float) (node.getX() + labelMarginX), + (float) (node.getY() + labelMarginY + labelBounds.getHeight()) + ); + + if (node.hasParent()) { + Node parent = node.getParent(); + double x0 = parent.getX() + parent.getWidth() / 2; + double y0 = parent.getY() + parent.getHeight(); + double x1 = node.getX() + node.getWidth() / 2; + double y1 = node.getY(); + Line2D line2D = new Line2D.Double(x0, y0, x1, y1); + g.draw(line2D); + } + } +} diff --git a/src/test/java/org/behrang/algorithm/tree/DimensionCalculatorTest.java b/src/test/java/org/behrang/algorithm/tree/DimensionCalculatorTest.java new file mode 100644 index 0000000..4dcc93d --- /dev/null +++ b/src/test/java/org/behrang/algorithm/tree/DimensionCalculatorTest.java @@ -0,0 +1,32 @@ +package org.behrang.algorithm.tree; + +import org.behrang.algorithm.tree.demo.SampleTree; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DimensionCalculatorTest { + @Test + void testCalculateDimensions() { + var root = SampleTree.newUniformInstance(); + var algorithm = new WalkerAlgorithm(4, 4, 0, 0, 4); + + algorithm.position(root); + + var dim = DimensionCalculator.calculateTreeDimension(root); + assertEquals(41, dim.getWidth()); + assertEquals(14, dim.getHeight()); + } + + @Test + void testCalculateMaxNodeDimension() { + var root = SampleTree.newUniformInstance(); + var algorithm = new WalkerAlgorithm(4, 4, 0, 0, 4); + + algorithm.position(root); + + var dim = DimensionCalculator.calculateMaxNodeDimension(root); + assertEquals(2, dim.getWidth()); + assertEquals(2, dim.getHeight()); + } +} \ No newline at end of file diff --git a/src/test/java/org/behrang/algorithm/tree/WalkerAlgorithmTest.java b/src/test/java/org/behrang/algorithm/tree/WalkerAlgorithmTest.java index 7cc35dd..d3c2a6e 100755 --- a/src/test/java/org/behrang/algorithm/tree/WalkerAlgorithmTest.java +++ b/src/test/java/org/behrang/algorithm/tree/WalkerAlgorithmTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.awt.geom.Dimension2D; + import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -19,7 +21,7 @@ void beforeEach() { 4, 0, 0, - 0 + 4 ); o = node("O"); @@ -96,6 +98,8 @@ void testGetLeftmostDescendant() { @Test void testPosition() { algorithm.position(o); + Dimension2D dimension2D = DimensionCalculator.calculateTreeDimension(o); + System.out.println(dimension2D); assertNode("O", 13.5, o);