diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..47086cb --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,30 @@ +version: 2 +mergeable: + - when: pull_request.*, pull_request_review.* + filter: + # ignore 'Feedback' PR + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none + validate: + - do: description + no_empty: + enabled: true + message: "Description matter and should not be empty. Provide detail with **what** was changed, **why** it was changed, and **how** it was changed." + - do: approvals + min: + count: 1 + required: + assignees: true + + - when: pull_request.opened + name: "Remind about contributing guidelines" + validate: [ ] + pass: + - do: comment + payload: + body: > + Thanks for creating a pull request! Please, check that your pull request meets the [CONTRIBUTING](./CONTRIBUTING.md) requirements. \ No newline at end of file diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 0000000..3961720 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,30 @@ +name: Build CI + +on: + pull_request: + branches: + - main + - release + +permissions: + contents: read + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ macos-latest, ubuntu-latest, windows-latest ] + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: zulu + - name: Build & test with Gradle + uses: gradle/gradle-build-action@v2 + with: + arguments: build \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1fbeba --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +/.idea/.name +/.idea/gradle.xml +/.idea/kotlinc.xml +/.idea/misc.xml +/.idea/vcs.xml +/.idea/ + +# Ignore Gradle build output directory +build +/app/build \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..20c1ee1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Making edits + +## Basic Tips + +1. Don't use merge, only rebase (to keep a linear commit history) +2. Do not change other people's branches unless absolutely necessary +3. Recheck your commit history before creating a pull request +4. **Check you're on the right branch**, never commit directly in main + +## Rules for adding commits + +Commits are added according to conventional commits. Those +`(): `. + +The `` field must take one of these values: + +* `feat` to add new functionality +* `fix` to fix a bug in the program +* `refactor` for code refactoring, such as renaming a variable +* `test` to add tests, refactor them +* `struct` for changes related to a change in the structure of the project (BUT NOT CODE), for example, changing + folder locations +* `ci` for various ci/cd tasks + +The `` field contains the gist of the changes in the present imperative in English without the dot in +at the end, the first word is a verb with a small letter. +Examples: + +* Good: "feat: Add module for future BST implementations" +* Bad: "Added module for future BST implementations." + +## Rules for pull requests + +**Forbidden** to merge your pull request into the branch yourself. + +If you click on the green button, then **make sure** that it says `REBASE AND MERGE` + +The review takes place in the form of comments to pull requests, discussions in the team chat and personal +communication. \ No newline at end of file diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..46adcd2 --- /dev/null +++ b/DOCS.md @@ -0,0 +1,111 @@ +## Getting started + +To build the library run + +```bash + ./gradlew build +``` + +## Using Trees + +Any `Comparable` data can be stored in trees. +We also provide access to the `KeyValue` class, which allows you to store a key-value pair in the nodes of the tree. + +```kotlin +import lib.trees.AVLTree +import lib.trees.RBTree +import lib.trees.BSTree +import lib.trees.KeyValue + +val alvTree = AVLTree() // instantiate empty AVL tree +val bsTree = BSTree() // instantiate empty simple tree +val rbTree = RBTree>() // instantiate empty red-black tree with key-value +``` + +Each tree supports 3 basic operations: `add`, `contain`, `delete` and `get` (if you need to get value by key using +KeyValue) + +```kotlin +avlTree.add(42) +bsTree.add("42") +rbTree.add(KeyValue(42, "42")) +bsTree.contain("42") // returns true +avlTree.contain(1) // returns false +rbTree.get(KeyValue(42, null))?.getValue() // returns "42" +``` + +Trees' nodes can be read-only accessed by `root` property. + +```kotlin +avlTree.add(10) +avlTree.add(5) +avlTree.add(20) +avlTree.add(30) +avlTree.add(42) +// avlTree after balancing: +// 10 +// ┌─────────┴─────────┐ +// 5 30 +// ┌──┴──┐ +// 20 42 + +avlTree.root?.data // 10 +avlTree.root?.left?.data // 5 +avlTree.root?.right?.data // 30 +avlTree.root?.right?.right?.data // 42 +avlTree.root?.right?.left?.data // 20 +``` + +## Storing Trees + +`teemEight` provides `JsonRepository`, `SqlRepository` and `Neo4jRepository` to save & load trees. + +Each instance of repository is used to store exactly 1 tree type. To store different tree types several repositories can +be instantiated. +Repository must be provided with `Serialization` which describes how to serialize & deserialize any particular +type of tree. + +`bstrees` is shipped with `AVLStrategy`, `RBStrategy` and `SimpleStrategy` to serialize & deserialize AVL trees, +Red-black trees and Simple BSTs respectively. As these strategies don't know anything about the data type stored in +trees' nodes, user must provide `serializeData` and `deserializeData` functions to them. + +Different tree types can be stored in the same database (directory) by creating several repositories and passing them +same databases (directory paths). + +### Using Neo4j + +Before using this, you must have [Docker](https://www.docker.com/) (also see [docs](https://docs.docker.com/)) + +#### Before started + +1. Run docker container with `docker-compose.yml` +2. Open http://localhost:7474 +3. Create new user (default password: neo4j) +4. Change the password +5. You got this + +#### Example + +```kotlin +val username = "neo4j" +val password = "" // insert password to database here +val conf = Configuration.Builder() + .uri("bolt://localhost") + .credentials(username, password) + .build() + +fun serializeInt(data: Int) = SerializableValue(data.toString()) + +fun deserializeInt(data: SerializableValue) = data.value.toInt() + +val avlRepo = Neo4jRepo(AVLStrategy(::serializeInt, ::deserializeInt), conf) + + +val tree = AVLTree() +val randomizer = Random(42) +val lst = List(15) { randomizer.nextInt(1000) } +lst.forEach { tree.add(it) } +avlRepo.save("test", tree) +val testTree = avlRepo.loadByName("test") +println(testTree.preOrder()) // output pre-order traversal of tree +``` \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f3a0e72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,216 @@ +Apache License 2.0 +A permissive license whose main conditions require preservation of copyright and license notices. Contributors provide an express grant of patent rights. Licensed works, modifications, and larger works may be distributed under different terms and without source code. + +Permissions Conditions +Limitations +Commercial use +Distribution +Modification +Patent use +Private use +License and copyright notice +State changes +Liability +Trademark use +Warranty + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a668993 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Trees + +teemEight's project of implementation of three trees: AVL, red-black, binary + +[![Apache License](https://img.shields.io/badge/license-Apache%202.0-black.svg)](https://www.apache.org/licenses/LICENSE-2.0) + +## Authors (teemEight) + +- [absolute](https://steamcommunity.com/groups/absoluteplayer) [@roketflame](https://github.com/RoketFlame) +- [отдел продаж](https://steamcommunity.com/groups/Otedel_Prodaj) [@wokuparalyzed](https://www.github.com/wokuparalyzed) +- [and their fans](https://steamcommunity.com/groups/kazakhstansgaminggirls) [@Lesh79](https://www.github.com/Lesh79) + +## About + +It is a library that provides kotlin implementations of 3 binary search trees data +structures: [BS tree](https://en.wikipedia.org/wiki/Binary_search_tree), [AVL tree](https://en.wikipedia.org/wiki/AVL_trees), [Red-black tree](https://en.wikipedia.org/wiki/Red–black_tree). +It also provides storing BSTs in either plain `.json` files, `SQLite` +or `neo4j` databases. + +## Roadmap + +- [x] License +- [x] CI +- [x] Realized all types of trees +- [x] Added tests +- Storing with: + - [x] Neo4j + - [x] Sqlite + - [x] json +- [x] GUI + +## Building + +To build this project run + +```bash + ./gradlew build +``` + +## How to use + +If you need access to trees, see official [documentation](/DOCS.md) + +To run our tree viewer + +```bash +./gradlew run +``` + +The viewer is currently only able to open trees stored in a json file. +Create an empty .json file and open it in the application, enter any name for the tree +and it will be created. You can drag and drop tree nodes, add and remove values, +save the state of the tree for later loading. If there are available trees in the .json file, they will be in the +dropdown list. + +## Feedback + +If you have any feedback, please reach out to us at Issues + +## About creators + +We're a full stack developers... (( \ +18-20 y.o SPBU SE +
+ + +## 🛠 Skills + +2006 ELO Faciet, 1300 MMR, 1400 ELO CHESS, +21k TROPHIES BrAWL STARs, 5BC \ +[](https://i.imgur.com/TFDL3rB.jpeg) + +## 🔗 Links (Источники вдохновления) + +[![gradle](https://img.shields.io/badge/gradle-FFFFFF?style=for-the-badge&logo=gradle&logoColor=black&)](https://gradle.org/) \ +[](https://youtu.be/6Cv2kmgX0So?t=30) \ +[](https://kotlinlang.org/) \ +[](https://www.youtube.com/watch?v=_CTod1hk-bc) \ +[](https://i.imgur.com/rgGO1Oc.png) + + +
+ diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..ab3e20b --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.compose) +} + +dependencies { + implementation(libs.gson) + implementation(libs.kotlinx.serialization.json) + implementation(compose.desktop.currentOs) + implementation(compose.material3) + implementation(project(":lib")) +} + +compose.desktop { + application { + mainClass = "MainKt" + } +} diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt new file mode 100644 index 0000000..42e49de --- /dev/null +++ b/app/src/main/kotlin/Main.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import app.app +import java.awt.Dimension + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Compose for Desktop", + state = rememberWindowState( + size = DpSize(700.dp, 700.dp), + position = WindowPosition(alignment = Alignment.Center) + ) + + ) { + window.minimumSize = Dimension(700, 700) + app(window) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/Editor.kt b/app/src/main/kotlin/app/Editor.kt new file mode 100644 index 0000000..db17834 --- /dev/null +++ b/app/src/main/kotlin/app/Editor.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package app + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import trees.nodes.AbstractNode + +@Composable +fun > Editor( + editorController: EditorController, + onGoHome: () -> Unit, +) { + + LaunchedEffect(Unit) { + withContext(Dispatchers.Default) { + editorController.initTree() + } + } + + val coroutineScope = rememberCoroutineScope { Dispatchers.Default } + + + Row { + Menu( + onAdd = { key, value -> coroutineScope.launch { editorController.add(key, value) } }, + onDelete = { coroutineScope.launch { editorController.delete(it) } }, + onContains = { coroutineScope.launch { editorController.contains(it) } }, + onSave = { coroutineScope.launch { editorController.saveTree() } }, + onGoHome = onGoHome, + ) + Box( + modifier = Modifier + .fillMaxSize() + .background(color = Color(240, 240, 240)) + .padding(20.dp) + ) { + MaterialTheme { + EditorScreen(editorController) + } + + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Menu( + onAdd: (Int, String) -> Unit, + onDelete: (Int) -> Unit, + onContains: (Int) -> Unit, + onSave: () -> Unit, + onGoHome: () -> Unit +) { + var keyString by remember { mutableStateOf("") } + var valueString by remember { mutableStateOf("") } + // Размещаем поля ввода в вертикальном столбце + Column( + Modifier.padding(16.dp).width(260.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + TextField( + value = keyString, + onValueChange = { + if (it.isEmpty() || it == "-" || it.toIntOrNull() != null) { + keyString = it + } + }, + label = { Text("Key") }, + modifier = Modifier.fillMaxWidth(), + ) + + TextField( + value = valueString, + onValueChange = { valueString = it }, + label = { Text("Value") }, + modifier = Modifier.fillMaxWidth(), + ) + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + Button(onClick = { + if (keyString.isNotEmpty()) onAdd(keyString.toInt(), valueString) + keyString = "" + valueString = "" + }) { + Icon(Icons.Default.Add, contentDescription = "Add") + } + Button(onClick = { + if (keyString.isNotEmpty()) onContains(keyString.toInt()) + keyString = "" + valueString = "" + }) { + Icon(Icons.Default.Search, contentDescription = "Contains") + } + Button(onClick = { + if (keyString.isNotEmpty()) onDelete(keyString.toInt()) + keyString = "" + valueString = "" + }) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + } + Row( + Modifier.fillMaxWidth().padding(horizontal = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) + { + Button( + onClick = onSave, + modifier = Modifier.weight(2f) + ) { + Text( + text = "Save" + ) + } + Button( + onClick = onGoHome, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Home, contentDescription = "Go back") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/EditorController.kt b/app/src/main/kotlin/app/EditorController.kt new file mode 100644 index 0000000..1ff01dc --- /dev/null +++ b/app/src/main/kotlin/app/EditorController.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package app + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import app.graph.DrawableNode +import repository.Repository +import trees.AbstractTree +import trees.KeyValue +import trees.nodes.AbstractNode +import trees.nodes.Color +import trees.nodes.RBNode + + +class EditorController>( + private val tree: AbstractTree?, + private val repository: Repository>?, + private val name: String, +) { + + var drawableRoot: DrawableNode? by mutableStateOf(toDrawableNode(tree?.root)) + private set + + fun initTree() { + drawableRoot = tree?.root?.let { toDrawableNode(it, savePosition = true) } + } + + fun saveTree() { + fun saveCoordinates(node: NodeType, drawableNode: DrawableNode?) { + if (drawableNode == null) { + return + } + node.left?.let { saveCoordinates(it, drawableNode.left) } + node.data.x = drawableNode.x + node.data.y = drawableNode.y + node.right?.let { saveCoordinates(it, drawableNode.right) } + } + + tree?.root?.let { saveCoordinates(it, drawableRoot) } + if (tree != null) { + repository?.save(name, tree) + } + } + + fun add(key: Int, value: String) { + tree?.add(NodeDataGUI(KeyValue(key, value))) + drawableRoot = tree?.root?.let { toDrawableNode(it) } + } + + fun delete(key: Int) { + tree?.delete(NodeDataGUI(KeyValue(key, ""))) + drawableRoot = tree?.root?.let { toDrawableNode(it) } + } + + fun contains(key: Int) { + val res = tree?.contains(NodeDataGUI(KeyValue(key, ""))) + } + + private fun toDrawableNode(root: NodeType?, savePosition: Boolean = false): DrawableNode? { + if (root == null) { + return null + } + + val drawRoot = DrawableNode(root.data.data.key, root.data.data.value) + + fun calculateCoordinates( + node: NodeType, + drawableNode: DrawableNode, + offsetX: Int, + curH: Int, + ): Int { + var resX = offsetX + node.left?.let { left -> + drawableNode.left = DrawableNode(left.data.data.key, left.data.data.value).also { drawLeft -> + resX = calculateCoordinates(left, drawLeft, offsetX, curH + 1) + 1 + } + } + + drawableNode.x = if (savePosition) node.data.x else ((60.dp * 2 / 3) * resX) + drawableNode.y = if (savePosition) node.data.y else ((60.dp * 5 / 4) * curH) + if (node is RBNode<*>) { + drawableNode.color = when (node.color) { + Color.RED -> androidx.compose.ui.graphics.Color.Red + Color.BLACK -> androidx.compose.ui.graphics.Color.Black + } + } + + node.right?.let { right -> + drawableNode.right = DrawableNode(right.data.data.key, right.data.data.value).also { drawRight -> + resX = calculateCoordinates(right, drawRight, resX + 1, curH + 1) + } + } + + return resX + } + calculateCoordinates(root, drawRoot, 0, 0) + return drawRoot + } + + fun dragNode(node: DrawableNode, dragAmount: DpOffset) { + (node).let { + node.x += dragAmount.x + node.y += dragAmount.y + } + } +} diff --git a/app/src/main/kotlin/app/EditorField.kt b/app/src/main/kotlin/app/EditorField.kt new file mode 100644 index 0000000..c78fe3d --- /dev/null +++ b/app/src/main/kotlin/app/EditorField.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package app + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.graph.TreeGraph +import trees.nodes.AbstractNode + +@Composable +fun > EditorScreen( + editorController: EditorController +) { + val viewModel = remember { editorController } + Column(verticalArrangement = Arrangement.spacedBy(20.dp)) { + Surface( + modifier = Modifier.fillMaxSize().weight(1f), + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surface + ) { + viewModel.drawableRoot?.let { + TreeGraph(it, 60.dp, viewModel::dragNode) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/MainActivity.kt b/app/src/main/kotlin/app/MainActivity.kt new file mode 100644 index 0000000..acffd42 --- /dev/null +++ b/app/src/main/kotlin/app/MainActivity.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package app + +import androidx.compose.runtime.* +import androidx.compose.ui.awt.ComposeWindow +import com.google.gson.JsonSyntaxException +import repository.serialization.TypeOfTree +import java.io.File + + +enum class States { + OPENING_TREE, DRAW_TREE +} + + +enum class TypeOfDatabase { + Neo4j, Json, SQL +} + +@Composable +fun app(window: ComposeWindow) { + val typesOfDatabase = remember { + mapOf( + "SQL" to TypeOfDatabase.SQL, "Neo4j" to TypeOfDatabase.Neo4j, ".json file" to TypeOfDatabase.Json + ) + } + + val typesOfTrees = remember { + mapOf( + "AVL" to TypeOfTree.AVL_TREE, + "Red-black" to TypeOfTree.RB_TREE, + "Binary" to TypeOfTree.BS_TREE, + ) + } + + val listOfTrees = typesOfTrees.keys.toList() + val listOfDatabase = typesOfDatabase.keys.toList() + var listNames by remember { mutableStateOf(listOf("")) } + + val typeOfTree = remember { mutableStateOf(TypeOfTree.BS_TREE) } + val stringTypeOfDatabaseState = remember { mutableStateOf("") } + val pathState = remember { mutableStateOf("") } + val typeOfDatabaseState = remember { mutableStateOf(typesOfDatabase[stringTypeOfDatabaseState.value]) } + + val state = remember { mutableStateOf(States.OPENING_TREE) } + val openingController by remember { mutableStateOf(OpeningController()) } + val fileState = remember { mutableStateOf(null) } + + val nameState = remember { mutableStateOf("") } + + fun loadTrees() { + if (fileState.value?.name?.endsWith(".json") == true) { + try { + openingController.loadDatabase( + typeOfDatabaseState.value, typeOfTree.value, fileState.value?.parent, fileState.value?.name + ) + listNames = openingController.getNamesOfTrees() + } catch (_: JsonSyntaxException) { + fileState.value = null + } + } else { + fileState.value = null + } + } + + fun resetStates() { + fileState.value = null + nameState.value = "" + typeOfDatabaseState.value = null + stringTypeOfDatabaseState.value = "" + typeOfTree.value = null + pathState.value = "" + } + + when (state.value) { + States.OPENING_TREE -> { + window.setSize(700, 700) + OpenTree( + listOfDatabase = listOfDatabase, + listOfNames = listNames, + listOfTypes = listOfTrees, + + typeOfDatabaseState = typeOfDatabaseState, + file = fileState, + + onTypeOfDatabaseChanged = { newType -> + resetStates() + stringTypeOfDatabaseState.value = newType + typeOfDatabaseState.value = typesOfDatabase[stringTypeOfDatabaseState.value] + }, + onPathChanged = { newPath -> pathState.value = newPath }, + onFilePicked = { + fileState.value = openingController.openFileDialog( + window, "Load a file", listOf(".json"), allowMultiSelection = false + ) + loadTrees() + }, + onNameChanged = { newName -> nameState.value = newName }, + onTypeChanged = { newType -> + typeOfTree.value = typesOfTrees[newType] + loadTrees() + }, + onLoadTree = { + state.value = States.DRAW_TREE + typeOfTree.value?.let { openingController.loadTree(it, nameState.value) } + }, + onLoadDatabase = { + openingController.loadDatabase( + typeOfDatabaseState.value, typeOfTree.value, pathState.value, fileState.value?.name + ) + }, + isEnabled = (fileState.value != null && nameState.value != "") + ) + } + + else -> { + window.setSize(1080, 800) + Editor( + editorController = EditorController( + openingController.tree, + openingController.repository, + nameState.value + ), + onGoHome = { + resetStates() + state.value = States.OPENING_TREE + } + ) + } + } +} diff --git a/app/src/main/kotlin/app/NodeDataGUI.kt b/app/src/main/kotlin/app/NodeDataGUI.kt new file mode 100644 index 0000000..d9e7784 --- /dev/null +++ b/app/src/main/kotlin/app/NodeDataGUI.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package app + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.google.gson.Gson +import repository.serialization.SerializableValue +import trees.KeyValue + +class NodeDataGUI( + val data: KeyValue, + var x: Dp = 0.dp, + var y: Dp = 0.dp, +) : Comparable { + override fun compareTo(other: NodeDataGUI) = data.compareTo(other.data) + + override fun toString() = "key=${data.key} value=${data.value}\nx=$x y=$y" + + companion object { + @JvmStatic + fun serialize(data: NodeDataGUI) = SerializableValue(Gson().toJson(data)) + + @JvmStatic + fun deserialize(data: SerializableValue): NodeDataGUI = Gson().fromJson(data.value, NodeDataGUI::class.java) + + } +} + diff --git a/app/src/main/kotlin/app/OpeningController.kt b/app/src/main/kotlin/app/OpeningController.kt new file mode 100644 index 0000000..b208135 --- /dev/null +++ b/app/src/main/kotlin/app/OpeningController.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package app + +import androidx.compose.ui.awt.ComposeWindow +import repository.JsonRepository +import repository.Repository +import repository.serialization.TypeOfTree +import repository.serialization.strategies.AVLStrategy +import repository.serialization.strategies.BSStrategy +import repository.serialization.strategies.RBStrategy +import repository.serialization.strategies.Serialization +import trees.AVLTree +import trees.AbstractTree +import trees.BSTree +import trees.RBTree +import trees.nodes.AbstractNode +import java.awt.FileDialog +import java.io.File + +class OpeningController< + NodeType : AbstractNode, + TreeType : AbstractTree, + SerializationType : Serialization> { + + var repository: Repository>? = null + var tree: AbstractTree? = null + fun openFileDialog( + window: ComposeWindow, title: String, allowedExtensions: List, allowMultiSelection: Boolean = true + ): File? { + val fileDialog = FileDialog(window, title, FileDialog.LOAD) + fileDialog.isMultipleMode = allowMultiSelection + fileDialog.isVisible = true + fileDialog.setFilenameFilter { _, name -> name.endsWith(".jpg") } + return fileDialog.files.firstOrNull() + } + + fun loadDatabase( + typeOfDatabase: TypeOfDatabase?, + typeOfTree: TypeOfTree?, + dirPath: String?, + filename: String?, + ) { + if (dirPath == null || filename == null || typeOfDatabase == null || typeOfTree == null) + return + val strategy = getStrategy(typeOfTree) + repository = when (typeOfDatabase) { + TypeOfDatabase.Json -> JsonRepository( + strategy, + dirPath, + filename + ) as Repository> +// TypeOfDatabase.Neo4j -> Neo4jRepo(strategy, dirPath) +// TypeOfDatabase.SQL -> SQLRepository(strategy, dirPath) + else -> JsonRepository( + strategy, + dirPath, + filename + ) as Repository> + } + } + + fun getNamesOfTrees(): List { + return repository?.getNames() ?: listOf() + } + + fun loadTree(typeOfTree: TypeOfTree, name: String) { + tree = repository?.loadByName(name) as AbstractTree? + if (tree == null) { + tree = when (typeOfTree) { + TypeOfTree.RB_TREE -> RBTree() as AbstractTree + TypeOfTree.AVL_TREE -> AVLTree() as AbstractTree + TypeOfTree.BS_TREE -> BSTree() as AbstractTree + } + } + } + + fun getStrategy(typeOfTree: TypeOfTree): SerializationType { + val strategy = when (typeOfTree) { + TypeOfTree.AVL_TREE -> AVLStrategy(NodeDataGUI::serialize, NodeDataGUI::deserialize) + TypeOfTree.BS_TREE -> BSStrategy(NodeDataGUI::serialize, NodeDataGUI::deserialize) + TypeOfTree.RB_TREE -> RBStrategy(NodeDataGUI::serialize, NodeDataGUI::deserialize) + } + return strategy as SerializationType + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/OpeningScreen.kt b/app/src/main/kotlin/app/OpeningScreen.kt new file mode 100644 index 0000000..253b8d1 --- /dev/null +++ b/app/src/main/kotlin/app/OpeningScreen.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package app + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import java.io.File + + +@Composable +fun OpenTree( + listOfDatabase: List, + listOfTypes: List, + listOfNames: List, + + typeOfDatabaseState: State, + file: State, + + onTypeChanged: (String) -> Unit, + onTypeOfDatabaseChanged: (String) -> Unit, + onFilePicked: () -> Unit, + onPathChanged: (String) -> Unit, + onNameChanged: (String) -> Unit, + onLoadTree: () -> Unit, + onLoadDatabase: () -> Unit, + + isEnabled: Boolean = false, +) { + MaterialTheme { + Scaffold(topBar = { + TopAppBar(title = { Text("Tree viewer by teemEight") }) + }) { + Column(Modifier.padding(10.dp)) { + DropDownTextFiled("Type of database", listOfDatabase, onTypeOfDatabaseChanged) + DropDownTextFiled("Type of tree", listOfTypes, onTypeChanged) + when (typeOfDatabaseState.value) { + TypeOfDatabase.Json -> FilePicker(onFilePicked, file) + TypeOfDatabase.Neo4j -> PathToStorage(onPathChanged, onLoadDatabase) + TypeOfDatabase.SQL -> PathToStorage(onPathChanged, onLoadDatabase) + else -> {} + } + if (typeOfDatabaseState.value != null) { + + DropDownTextFiled("Name of tree", listOfNames, onNameChanged, false) + Button( + onClick = onLoadTree, + modifier = Modifier.padding(horizontal = 10.dp), + enabled = isEnabled, + ) { + Text("Let's go!") + } + } + } + } + } +} + + +@Composable +fun FilePicker( + onFilePicked: () -> Unit, + file: State, +) { + Row( + modifier = Modifier.padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onFilePicked + ) { + Text( + text = "Choose File", + ) + } + Spacer(modifier = Modifier.width(8.dp)) + file.value?.let { + Text(file.value?.name ?: "") + } + } +} + +@Composable +fun PathToStorage( + onPathChanged: (String) -> Unit, + onLoadDatabase: () -> Unit, +) { + var path by remember { mutableStateOf("") } + Column(Modifier.padding(horizontal = 10.dp)) { + OutlinedTextField( + value = path, + singleLine = true, + onValueChange = { + onPathChanged(it) + path = it + }, + label = { Text(text = "Path to database") }, + modifier = Modifier.fillMaxWidth() + ) + Button( + onClick = onLoadDatabase + ) { + Text("Load") + } + } +} + +@Composable +fun DropDownTextFiled( + title: String, + listOfValues: List, + onValueChanged: (String) -> Unit, + readOnly: Boolean = true, +) { + var mExpanded by remember { mutableStateOf(false) } + var mTextFieldSize by remember { mutableStateOf(Size.Zero) } + var mSelectedText by remember { mutableStateOf("") } + + val icon = if (mExpanded) Icons.Filled.KeyboardArrowUp + else Icons.Filled.KeyboardArrowDown + + Column(Modifier.padding(horizontal = 10.dp)) { + OutlinedTextField(value = mSelectedText, + singleLine = true, + readOnly = readOnly, + onValueChange = { newName -> + mSelectedText = newName + onValueChanged(newName) + }, + modifier = Modifier.fillMaxWidth().onGloballyPositioned { coordinates -> + mTextFieldSize = coordinates.size.toSize() + }, + label = { + Text( + text = title, + ) + }, + trailingIcon = { + Icon(icon, "contentDescription", Modifier.clickable { mExpanded = !mExpanded }) + }) + DropdownMenu( + expanded = mExpanded, + onDismissRequest = { mExpanded = false }, + modifier = Modifier.width(with(LocalDensity.current) { mTextFieldSize.width.toDp() }) + ) { + listOfValues.forEach { label -> + DropdownMenuItem(onClick = { + mSelectedText = label + onValueChanged(label) + mExpanded = false + }) { + Text(text = label) + } + } + } + } +} diff --git a/app/src/main/kotlin/app/graph/DrawableNode.kt b/app/src/main/kotlin/app/graph/DrawableNode.kt new file mode 100644 index 0000000..7a5023f --- /dev/null +++ b/app/src/main/kotlin/app/graph/DrawableNode.kt @@ -0,0 +1,45 @@ +/* + MIT License + +Copyright (c) 2023-present Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +package app.graph + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +class DrawableNode( + val key: Int, + var value: String, + var left: DrawableNode? = null, + var right: DrawableNode? = null, + var color: Color? = null, + y: Dp = 0.dp, + x: Dp = 0.dp, +) { + var x by mutableStateOf(x) + var y by mutableStateOf(y) +} diff --git a/app/src/main/kotlin/app/graph/GraphLine.kt b/app/src/main/kotlin/app/graph/GraphLine.kt new file mode 100644 index 0000000..3d06eee --- /dev/null +++ b/app/src/main/kotlin/app/graph/GraphLine.kt @@ -0,0 +1,59 @@ +/* + MIT License + +Copyright (c) 2023-present Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +package app.graph + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp + +@Composable +fun GraphLine( + modifier: Modifier = Modifier, + start: DrawableNode, + end: DrawableNode, + nodeSize: Dp, + sDragProvider: () -> Offset, + sScaleProvider: () -> ScreenZoom +) { + Canvas(modifier = modifier.fillMaxSize()) { + val drag = sDragProvider() + val scale = sScaleProvider() + drawLine( + start = Offset( + ((start.x + nodeSize / 2).toPx() + drag.x) * scale.scale + scale.offset.x, + ((start.y + nodeSize / 2).toPx() + drag.y) * scale.scale + scale.offset.y, + ), + end = Offset( + ((end.x + nodeSize / 2).toPx() + drag.x) * scale.scale + scale.offset.x, + ((end.y + nodeSize / 2).toPx() + drag.y) * scale.scale + scale.offset.y, + ), + strokeWidth = 1.5f * scale.scale, + color = Color.Black + ) + } +} diff --git a/app/src/main/kotlin/app/graph/GraphNode.kt b/app/src/main/kotlin/app/graph/GraphNode.kt new file mode 100644 index 0000000..6344ae7 --- /dev/null +++ b/app/src/main/kotlin/app/graph/GraphNode.kt @@ -0,0 +1,138 @@ +/* + MIT License + +Copyright (c) 2023-present Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +package app.graph + +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.* +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import kotlin.math.roundToInt + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun GraphNode( + modifier: Modifier = Modifier, + node: DrawableNode, + nodeSize: Dp, + onNodeDrag: (DrawableNode, DpOffset) -> Unit, + sDragProvider: () -> Offset, + sScaleProvider: () -> ScreenZoom +) { + TooltipArea( + modifier = modifier.zIndex(5f) + .layout { measurable: Measurable, _: Constraints -> + val placeable = measurable.measure( + Constraints.fixed( + (nodeSize * sScaleProvider().scale).roundToPx(), + (nodeSize * sScaleProvider().scale).roundToPx() + ) + ) + layout(placeable.width, placeable.height) { + val drag = sDragProvider() + val scale = sScaleProvider() + placeable.placeRelative( + ((node.x.toPx() + drag.x) * scale.scale + scale.offset.x).roundToInt(), + ((node.y.toPx() + drag.y) * scale.scale + scale.offset.y).roundToInt(), + ) + } + }, + tooltip = { + Surface( + shape = MaterialTheme.shapes.medium, + ) { + Text( + text = "Key: ${node.key}\nValue: ${node.value}", + modifier = Modifier.padding(15.dp) + ) + } + }, + tooltipPlacement = TooltipPlacement.CursorPoint( + offset = DpOffset(0.dp, (-100).dp) + ), + delayMillis = 600, + ) { + Box(modifier = modifier + .fillMaxSize() + .background( + color = node.color ?: MaterialTheme.colorScheme.primary, + shape = CircleShape + ) + .pointerInput(node) { + detectDragGestures { change, offset -> + change.consume() + val scale = sScaleProvider().scale + onNodeDrag( + node, + DpOffset( + offset.x.toDp() / scale, + offset.y.toDp() / scale + ), + ) + } + } + ) { + NodeText( + modifier = Modifier.align(Alignment.Center), + text = node.key.toString(), + scaleProvider = { sScaleProvider().scale } + ) + } + } +} + +@Composable +fun NodeText( + modifier: Modifier = Modifier, + text: String, + scaleProvider: () -> Float, +) { + val scale = scaleProvider() + Text( + modifier = modifier, + text = if (text.length > 4) text.substring(0, 5) + ".." else text, + color = MaterialTheme.colorScheme.onPrimary, + style = TextStyle( + fontSize = MaterialTheme.typography.bodyMedium.fontSize * scale, + lineHeight = MaterialTheme.typography.bodyMedium.lineHeight * scale + ) + ) +} diff --git a/app/src/main/kotlin/app/graph/GraphState.kt b/app/src/main/kotlin/app/graph/GraphState.kt new file mode 100644 index 0000000..b8e5e37 --- /dev/null +++ b/app/src/main/kotlin/app/graph/GraphState.kt @@ -0,0 +1,76 @@ +/* + MIT License + +Copyright (c) 2023-present Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +package app.graph + +import androidx.compose.runtime.* +import androidx.compose.ui.geometry.Offset +import kotlin.math.max +import kotlin.math.min + +class GraphState { + var screenZoom by mutableStateOf(ScreenZoom(1f, Offset(0f, 0f))) + private set + + var screenDrag by mutableStateOf(Offset(0f, 0f)) + private set + + fun handleScroll(scrollDelta: Offset, scrollPosition: Offset) { + screenDrag += Offset(-scrollDelta.x / screenZoom.scale * 25, 0f) + + val prevScale = screenZoom.scale + val newScale = min( + max( + screenZoom.scale - scrollDelta.y / 20, + 0.1f + ), 2f + ) + val relScale = newScale / prevScale + + screenZoom = ScreenZoom( + scale = newScale, + offset = Offset( + screenZoom.offset.x * relScale + scrollPosition.x * (1 - relScale), + screenZoom.offset.y * relScale + scrollPosition.y * (1 - relScale) + ) + ) + } + + fun handleScreenDrag(dragAmount: Offset) { + screenDrag += Offset( + dragAmount.x / screenZoom.scale, + dragAmount.y / screenZoom.scale + ) + } + + + fun resetGraphView(dragX: Float = 0f, dragY: Float = 0f) { + screenDrag = Offset(dragX, dragY) + screenZoom = ScreenZoom(1f, Offset(0f, 0f)) + } + +} + +@Composable +fun rememberGraphState() = remember { GraphState() } diff --git a/app/src/main/kotlin/app/graph/ScreenZoom.kt b/app/src/main/kotlin/app/graph/ScreenZoom.kt new file mode 100644 index 0000000..bfc888f --- /dev/null +++ b/app/src/main/kotlin/app/graph/ScreenZoom.kt @@ -0,0 +1,32 @@ +/* + MIT License + +Copyright (c) 2023-present Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +package app.graph + +import androidx.compose.ui.geometry.Offset + +data class ScreenZoom( + val scale: Float, + val offset: Offset +) \ No newline at end of file diff --git a/app/src/main/kotlin/app/graph/TreeGraph.kt b/app/src/main/kotlin/app/graph/TreeGraph.kt new file mode 100644 index 0000000..070d1ae --- /dev/null +++ b/app/src/main/kotlin/app/graph/TreeGraph.kt @@ -0,0 +1,138 @@ +/* + MIT License + +Copyright (c) 2023-present Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +package app.graph + +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun TreeGraph( + root: DrawableNode, + nodeSize: Dp, + onNodeDrag: (DrawableNode, DpOffset) -> Unit, + graphState: GraphState = rememberGraphState() +) { + val currentDensity = LocalDensity.current + fun centerGraph(viewWidth: Int) { + currentDensity.run { + graphState.resetGraphView( + dragX = viewWidth / 2 - (nodeSize / 2 + root.x).toPx(), + dragY = nodeSize.toPx() + ) + } + } + + var graphViewWidth = 0 + Box(modifier = Modifier + .fillMaxSize() + .onPointerEvent(PointerEventType.Scroll) { + graphState.handleScroll( + it.changes.first().scrollDelta, + it.changes.first().position + ) + } + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + graphState.handleScreenDrag(dragAmount) + } + } + .onSizeChanged { + graphViewWidth = it.width + } + ) { + LaunchedEffect(Unit) { + centerGraph(graphViewWidth) + } + + drawTree( + node = root, + nodeSize = nodeSize, + onNodeDrag = onNodeDrag, + sDragProvider = { graphState.screenDrag }, + sScaleProvider = { graphState.screenZoom } + ) + + TextButton( + modifier = Modifier.align(Alignment.TopEnd).padding(10.dp), + onClick = { centerGraph(graphViewWidth) } + ) { + Text("Reset view") + } + } +} + +@Composable +fun drawTree( + node: DrawableNode?, + parent: DrawableNode? = null, + nodeSize: Dp, + onNodeDrag: (DrawableNode, DpOffset) -> Unit, + sDragProvider: () -> Offset, + sScaleProvider: () -> ScreenZoom +) { + node?.let { + parent?.let { parent -> + GraphLine( + start = parent, + end = node, + nodeSize = nodeSize, + sDragProvider = sDragProvider, + sScaleProvider = sScaleProvider + ) + } + + drawTree(node.left, node, nodeSize, onNodeDrag, sDragProvider, sScaleProvider) + drawTree(node.right, node, nodeSize, onNodeDrag, sDragProvider, sScaleProvider) + + GraphNode( + node = node, + nodeSize = nodeSize, + onNodeDrag = onNodeDrag, + sDragProvider = sDragProvider, + sScaleProvider = sScaleProvider + ) + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d3ecf21 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.kotlin.jvm).apply(false) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4e4f4be --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.9' + +services: + neo4j: + image: neo4j:latest + container_name: neo4j + ports: + - "7474:7474" + - "7687:7687" \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..11f6173 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,10 @@ +# +# Copyright (c) 2023 teemEight +# SPDX-License-Identifier: Apache-2.0 +# +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..8e7990c --- /dev/null +++ b/gradlew @@ -0,0 +1,233 @@ +#!/bin/sh + +# +# Copyright (c) 2023 teemEight +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 0000000..b1a91f2 --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Kotlin application project to get you started. + * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle + * User Manual available at https://docs.gradle.org/8.0.2/userguide/building_java_projects.html + */ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.noarg) + `java-library` +} + +dependencies { + implementation(libs.kotlinx.serialization.json) + implementation(libs.gson) + + implementation(libs.exposed.core) + implementation(libs.exposed.dao) + implementation(libs.exposed.jdbc) + + implementation(libs.neo4j.ogm.core) + implementation(libs.neo4j.ogm.bolt) + testImplementation(kotlin("test")) +} + +noArg { + annotation("org.neo4j.ogm.annotation.NodeEntity") + annotation("org.neo4j.ogm.annotation.RelationshipEntity") +} + +tasks.test { +} diff --git a/lib/src/main/kotlin/repository/JsonRepository.kt b/lib/src/main/kotlin/repository/JsonRepository.kt new file mode 100644 index 0000000..52e7cb0 --- /dev/null +++ b/lib/src/main/kotlin/repository/JsonRepository.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import repository.jsonEntities.JsonNode +import repository.jsonEntities.JsonTree +import repository.serialization.SerializableNode +import repository.serialization.SerializableTree +import repository.serialization.strategies.Serialization +import trees.AbstractTree +import trees.nodes.AbstractNode +import java.io.File +import java.io.FileNotFoundException + +//creates a new JsonRepository object with the given strategy and dirPath. strategy +class JsonRepository, NodeType : AbstractNode, TreeType : AbstractTree> + ( + //serialization strategy for working with trees and nodes. + strategy: Serialization, + //path to the directory where tree files will be stored in JSON format. + private val dirPath: String, private val filename: String +) : Repository(strategy) { + + private val typeToken = object : TypeToken>() {}.type + + + //function to convert JsonNode to SerializableNode. + private fun JsonNode.toSerializableNode(): SerializableNode { + return SerializableNode( + data, metadata, left?.toSerializableNode(), right?.toSerializableNode() + ) + } + + //a function to deserialize a JsonNode into a tree node + private fun JsonNode.deserialize(parent: NodeType? = null): NodeType? { + val node = strategy.createNode(this.toSerializableNode()) + node?.parent = parent + node?.left = left?.deserialize(node) + node?.right = right?.deserialize(node) + return node + } + + //function to convert SerializableNode to JsonNode. + private fun SerializableNode.toJsonNode(): JsonNode { + return JsonNode( + data, metadata, left?.toJsonNode(), right?.toJsonNode() + ) + } + + //function to convert SerializableTree to JsonTree + private fun SerializableTree.toJsonTree(): JsonTree { + return JsonTree( + name, typeOfTree, root?.toJsonNode() + ) + } + + //method for getting a list of tree names. + override fun getNames(): List { + try { + File(dirPath, filename).run { + val names = Gson().fromJson>(readText(), typeToken) + ?.filter { it.typeOfTree == strategy.typeOfTree }?.map { it.name } + return names ?: listOf() + } + } catch (_: FileNotFoundException) { + return listOf() + } + } + + //method to load a tree by name + override fun loadByName(name: String): TreeType? { + val json = try { + File(dirPath, filename).readText() + } catch (_: FileNotFoundException) { + return null + } + + val jsonTree = Gson().fromJson>(json, typeToken) + ?.firstOrNull { it.name == name && it.typeOfTree == strategy.typeOfTree } ?: return null + return strategy.createTree().apply { + root = jsonTree.root?.deserialize() + } + } + + //method to save tree by name + override fun save(name: String, tree: TreeType) { + val jsonTree = tree.toSerializableTree(name).toJsonTree() + + deleteByName(name) + + File(dirPath).mkdirs() + File(dirPath, filename).run { + createNewFile() + var trees = Gson().fromJson>(readText(), typeToken) + if (trees == null) { + trees = mutableListOf() + } + trees.add(jsonTree) + writeText(Gson().toJson(trees)) + } + } + + //a method for deleting a tree by name + override fun deleteByName(name: String) { + try { + File(dirPath, filename).run { + val trees = Gson().fromJson>(readText(), typeToken) ?: mutableListOf() + trees.removeIf { it.name == name && it.typeOfTree == strategy.typeOfTree } + writeText(Gson().toJson(trees)) + } + } catch (_: FileNotFoundException) { + } + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/Neo4jRepository.kt b/lib/src/main/kotlin/repository/Neo4jRepository.kt new file mode 100644 index 0000000..5c4c066 --- /dev/null +++ b/lib/src/main/kotlin/repository/Neo4jRepository.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository + +import org.neo4j.ogm.config.Configuration +import org.neo4j.ogm.cypher.ComparisonOperator +import org.neo4j.ogm.cypher.Filter +import org.neo4j.ogm.cypher.Filters +import org.neo4j.ogm.session.SessionFactory +import repository.neo4jEntities.Neo4jNodeEntity +import repository.neo4jEntities.Neo4jTreeEntity +import repository.serialization.SerializableNode +import repository.serialization.SerializableTree +import repository.serialization.strategies.Serialization +import trees.AbstractTree +import trees.nodes.AbstractNode + +class Neo4jRepo, + NodeType : AbstractNode, + TreeType : AbstractTree>( + strategy: Serialization, + configuration: Configuration +) : Repository(strategy) { + private val sessionFactory = SessionFactory(configuration, "repository") + private val session = sessionFactory.openSession() + + //converts Neo4jNodeEntity to SerializableNode. + private fun Neo4jNodeEntity.toSerializableNode(): SerializableNode { + return SerializableNode( + data, + metadata, + left?.toSerializableNode(), + right?.toSerializableNode(), + ) + } + + //converts Neo4jNodeEntity to a node + private fun Neo4jNodeEntity.deserialize(parent: NodeType? = null): NodeType? { + val node = strategy.createNode(this.toSerializableNode()) + node?.parent = parent + node?.left = left?.deserialize(node) + node?.right = right?.deserialize(node) + return node + } + + //converts SerializableTree to Neo4jTreeEntity. + private fun SerializableNode.toEntity(): Neo4jNodeEntity { + return Neo4jNodeEntity( + data, + metadata, + left?.toEntity(), + right?.toEntity(), + ) + } + + //converts Neo4jTreeEntity to SerializableTree. + private fun Neo4jTreeEntity.toTree(): SerializableTree { + return SerializableTree( + name, + typeOfTree, + root?.toSerializableNode(), + ) + } + + private fun SerializableTree.toEntity(): Neo4jTreeEntity { + return Neo4jTreeEntity( + name, + typeOfTree, + root?.toEntity(), + ) + } + + //saves the tree with the specified name to the database. + override fun save(name: String, tree: TreeType) { + deleteByName(name) + val entityTree = tree.toSerializableTree(name).toEntity() + session.save(entityTree) + } + + //is used to get a list of trees from the database that match the specified filtering options. + private fun findByVerboseName(name: String) = session.loadAll( + Neo4jTreeEntity::class.java, + Filters().and( + Filter("name", ComparisonOperator.EQUALS, name) + ).and( + Filter("typeOfTree", ComparisonOperator.EQUALS, strategy.typeOfTree) + ), + -1 + ) + + //loads the tree with the specified name from the database. + override fun loadByName(name: String): TreeType { + val tree = findByVerboseName(name).singleOrNull() + val result = strategy.createTree().apply { + root = tree?.root?.deserialize() + } + return result + } + + //removes the tree with the specified name from the database. + override fun deleteByName(name: String) { + session.query( + "MATCH toDelete=(" + + "t:Tree {typeOfTree: \$typeOfTree, name : \$name}" + + ")-[*0..]->() DETACH DELETE toDelete", + mapOf("typeOfTree" to strategy.typeOfTree, "name" to name) + ) + } + + //returns a list of the names of all saved trees in the database. + override fun getNames(): List = session.loadAll( + Neo4jTreeEntity::class.java, + Filter("typeOfTree", ComparisonOperator.EQUALS, strategy.typeOfTree), + 0 + ).map(Neo4jTreeEntity::name) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/Repository.kt b/lib/src/main/kotlin/repository/Repository.kt new file mode 100644 index 0000000..8601a85 --- /dev/null +++ b/lib/src/main/kotlin/repository/Repository.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository + +import repository.serialization.SerializableNode +import repository.serialization.SerializableTree +import repository.serialization.strategies.Serialization +import trees.AbstractTree +import trees.nodes.AbstractNode + +abstract class Repository, + NodeType : AbstractNode, + TreeType : AbstractTree>( + protected val strategy: Serialization +) { + //An extension function that converts an instance of a NodeType to a serializable + //representation of a SerializableNode using the serialization strategy + protected fun NodeType.toSerializableNode(): SerializableNode { + return SerializableNode( + strategy.serializeValue(this.data), + strategy.serializeMetadata(this), + left?.toSerializableNode(), + right?.toSerializableNode(), + ) + } + + //An extension function that converts a TreeType instance to a serializable + // representation of a SerializableTree using the serialization strategy. + // The name of the tree is given by the name parameter + protected fun TreeType.toSerializableTree(name: String): SerializableTree { + return SerializableTree( + name = name, + typeOfTree = strategy.typeOfTree, + root = this.root?.toSerializableNode(), + ) + } + + //an abstract method that saves an instance of tree in the repository under the given name + abstract fun save(name: String, tree: TreeType) + + //abstract method that loads a tree instance from the repository given name. + // If no tree with the specified name is found, the method should return null. + abstract fun loadByName(name: String): TreeType? + + //an abstract method that removes a tree instance from the repository at the given name + abstract fun deleteByName(name: String) + + //abstract method that returns a list of the names of all trees stored in the repository. + // If there are no trees in the repository, the method should return an empty list + abstract fun getNames(): List +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/SQLRepository.kt b/lib/src/main/kotlin/repository/SQLRepository.kt new file mode 100644 index 0000000..07f9943 --- /dev/null +++ b/lib/src/main/kotlin/repository/SQLRepository.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository + +import NodesTable +import SQLNodeEntity +import SQLTreeEntity +import TreesTable +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.transactions.transaction +import repository.serialization.Metadata +import repository.serialization.SerializableNode +import repository.serialization.SerializableTree +import repository.serialization.SerializableValue +import repository.serialization.strategies.Serialization +import trees.AbstractTree +import trees.nodes.AbstractNode + +class SQLRepository, + NodeType : AbstractNode, + TreeType : AbstractTree>( + strategy: Serialization, + private val db: Database +) : Repository(strategy) { + + private val typeOfTree = strategy.typeOfTree.toString() + + //Initializes the database tables for storing trees and nodes. + init { + transaction(db) { + SchemaUtils.create(TreesTable) + SchemaUtils.create(NodesTable) + } + } + + //Converts an instance of SQLNodeEntity to a SerializableNode object using the serialization strategy. + private fun SQLNodeEntity.toSerializableNode(): SerializableNode { + return SerializableNode( + SerializableValue(data), + Metadata(metadata), + left?.toSerializableNode(), + right?.toSerializableNode(), + ) + } + + //Deserializes an instance of SQLNodeEntity to a NodeType object using the serialization strategy. + private fun SQLNodeEntity.deserialize(parent: NodeType? = null): NodeType? { + val node = strategy.createNode(this.toSerializableNode()) + node?.parent = parent + node?.left = left?.deserialize(node) + node?.right = right?.deserialize(node) + return node + } + + //Converts a SerializableNode object to an instance of SQLNodeEntity using the serialization strategy. + private fun SerializableNode.toEntity(tree: SQLTreeEntity): SQLNodeEntity = SQLNodeEntity.new { + this@new.data = this@toEntity.data.value + this@new.metadata = this@toEntity.metadata.value + this@new.left = this@toEntity.left?.toEntity(tree) + this@new.right = this@toEntity.right?.toEntity(tree) + this.tree = tree + } + + //Converts a SerializableTree object to an instance of SQLTreeEntity. + private fun SerializableTree.toEntity(): SQLTreeEntity { + return SQLTreeEntity.new { + this.name = this@toEntity.name + this.typeOfTree = this@SQLRepository.typeOfTree + } + } + + //Saves a TreeType object to the database with a given name by deleting any existing tree with the same name and creating a new one. + override fun save(name: String, tree: TreeType): Unit = transaction(db) { + deleteByName(name) + val entityTree = tree.toSerializableTree(name).toEntity() + entityTree.root = tree.root?.toSerializableNode()?.toEntity(entityTree) + } + + //Loads a TreeType object from the database by name, if it exists. + override fun loadByName(name: String): TreeType? = transaction(db) { + SQLTreeEntity.find( + TreesTable.typeOfTree eq typeOfTree and (TreesTable.name eq name) + ).firstOrNull()?.let { + strategy.createTree().apply { root = it.root?.deserialize() } + } + } + + //Deletes a tree from the database by name. + override fun deleteByName(name: String): Unit = transaction(db) { + val treeId = SQLTreeEntity.find( + TreesTable.typeOfTree eq strategy.typeOfTree.toString() and (TreesTable.name eq name) + ).firstOrNull()?.id?.value + SQLTreeEntity.find( + TreesTable.id eq treeId + ).firstOrNull()?.delete() + NodesTable.deleteWhere { + tree eq treeId + } + } + + //Returns a list of names of all the trees in the database. + override fun getNames(): List = transaction(db) { + SQLTreeEntity.find(TreesTable.typeOfTree eq typeOfTree).map(SQLTreeEntity::name) + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/jsonEntities/JsonNode.kt b/lib/src/main/kotlin/repository/jsonEntities/JsonNode.kt new file mode 100644 index 0000000..fd10028 --- /dev/null +++ b/lib/src/main/kotlin/repository/jsonEntities/JsonNode.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.jsonEntities + +import kotlinx.serialization.Serializable +import repository.serialization.Metadata +import repository.serialization.SerializableValue + +//A class that represents a tree node in JSON format. Contains +@Serializable +data class JsonNode( + //Node value in serializable format; + val data: SerializableValue, + //Node metadata, including node height, balancing factor, + //and other information needed to work with the tree; + val metadata: Metadata, + //Link to the left child node. + val left: JsonNode?, + //Link to the right child node. + val right: JsonNode? +) \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/jsonEntities/JsonTree.kt b/lib/src/main/kotlin/repository/jsonEntities/JsonTree.kt new file mode 100644 index 0000000..3540f62 --- /dev/null +++ b/lib/src/main/kotlin/repository/jsonEntities/JsonTree.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.jsonEntities + +import kotlinx.serialization.Serializable +import repository.serialization.TypeOfTree + +//Describes the "repository.jsonEntities.JsonTree" object - this is the tree +// that will be serialized and stored in JSON format +@Serializable +data class JsonTree( + //a string that contains the name of the tree. + val name: String, + //contains a tree type, which can be one of the "TypeOfTree" enums. + val typeOfTree: TypeOfTree, + //This is a reference to the root node of the tree, which can either be a "repository.jsonEntities.JsonNode" object + val root: JsonNode? +) \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/neo4jEntities/Neo4jNodeEntity.kt b/lib/src/main/kotlin/repository/neo4jEntities/Neo4jNodeEntity.kt new file mode 100644 index 0000000..d7636ef --- /dev/null +++ b/lib/src/main/kotlin/repository/neo4jEntities/Neo4jNodeEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.neo4jEntities + +import org.neo4j.ogm.annotation.* +import repository.serialization.Metadata +import repository.serialization.SerializableValue + +@NodeEntity("Node") +class Neo4jNodeEntity( + + //serializable node value stored in the database as a string. + @Property("data") + var data: SerializableValue, + //node metadata stored in the database as a string. + @Property("metadata") + var metadata: Metadata, + //reference to the left child node in the tree structure. + @Relationship(type = "LEFT", direction = Relationship.Direction.OUTGOING) + var left: Neo4jNodeEntity? = null, + //reference to the right child node in the tree structure. + @Relationship(type = "RIGHT", direction = Relationship.Direction.OUTGOING) + var right: Neo4jNodeEntity? = null, +) { + //the node ID generated by the database on save. + @Id + @GeneratedValue + var id: Long? = null +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/neo4jEntities/Neo4jTreeEntity.kt b/lib/src/main/kotlin/repository/neo4jEntities/Neo4jTreeEntity.kt new file mode 100644 index 0000000..da90a7a --- /dev/null +++ b/lib/src/main/kotlin/repository/neo4jEntities/Neo4jTreeEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.neo4jEntities + +import org.neo4j.ogm.annotation.* +import repository.serialization.TypeOfTree + +@NodeEntity("Tree") +class Neo4jTreeEntity( + //a class property that stores the name of the tree. + @Property("name") + var name: String, + //a class property that stores the tree type. + @Property("typeOfTree") + var typeOfTree: TypeOfTree, + //a class property that stores a reference to the root node of the tree. + @Relationship(type = "ROOT", direction = Relationship.Direction.OUTGOING) + var root: Neo4jNodeEntity?, +) { + //a class property that stores the tree ID in the Neo4j database. + @Id + @GeneratedValue + var id: Long? = null + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/serialization/Serializable.kt b/lib/src/main/kotlin/repository/serialization/Serializable.kt new file mode 100644 index 0000000..89d048f --- /dev/null +++ b/lib/src/main/kotlin/repository/serialization/Serializable.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + * SPDX-License-Identifier: MIT + */ + +package repository.serialization + +import kotlinx.serialization.Serializable + +@Serializable +enum class TypeOfTree { + BS_TREE, + RB_TREE, + AVL_TREE +} + +@Serializable +class SerializableNode( + val data: SerializableValue, + val metadata: Metadata, + val left: SerializableNode? = null, + val right: SerializableNode? = null, +) + +@Serializable +class SerializableTree( + val name: String, + val typeOfTree: TypeOfTree, + val root: SerializableNode?, +) + +@Serializable +@JvmInline +value class Metadata(val value: String) + +@Serializable +@JvmInline +value class SerializableValue(val value: String) \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/serialization/strategies/AVLStrategy.kt b/lib/src/main/kotlin/repository/serialization/strategies/AVLStrategy.kt new file mode 100644 index 0000000..fb4e127 --- /dev/null +++ b/lib/src/main/kotlin/repository/serialization/strategies/AVLStrategy.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.serialization.strategies + +import repository.serialization.Metadata +import repository.serialization.SerializableNode +import repository.serialization.SerializableValue +import repository.serialization.TypeOfTree +import trees.AVLTree +import trees.nodes.AVLNode + +class AVLStrategy>( + serializeData: (T) -> SerializableValue, + deserializeData: (SerializableValue) -> T +) : Serialization, AVLTree, Int>(serializeData, deserializeData) { + override val typeOfTree: TypeOfTree = TypeOfTree.AVL_TREE + + //method that creates an AVL tree node from the passed SerializableNode, which was obtained as a result of serialization. + override fun createNode(node: SerializableNode?): AVLNode? = node?.let { + AVLNode( + data = deserializeValue(node.data), + left = createNode(node.left), + right = createNode(node.right), + height = deserializeMetadata(node.metadata), + ) + } + + //method that deserializes the node's metadata (in this case, the node's height). Returns the deserialized metadata. + override fun deserializeMetadata(metadata: Metadata) = metadata.value.toInt() + + //method that serializes the node's metadata (in this case, the node's height). Returns the serialized metadata. + override fun serializeMetadata(node: AVLNode) = Metadata(node.height.toString()) + + //method that creates an AVL tree. Returns a new AVL tree of type AVLTree. + override fun createTree() = AVLTree() +} diff --git a/lib/src/main/kotlin/repository/serialization/strategies/BSStrategy.kt b/lib/src/main/kotlin/repository/serialization/strategies/BSStrategy.kt new file mode 100644 index 0000000..46664e4 --- /dev/null +++ b/lib/src/main/kotlin/repository/serialization/strategies/BSStrategy.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.serialization.strategies + +import repository.serialization.Metadata +import repository.serialization.SerializableNode +import repository.serialization.SerializableValue +import repository.serialization.TypeOfTree +import trees.BSTree +import trees.nodes.BSNode + +class BSStrategy>( + serializeData: (T) -> SerializableValue, deserializeData: (SerializableValue) -> T +) : Serialization, BSTree, Int>(serializeData, deserializeData) { + override val typeOfTree: TypeOfTree = TypeOfTree.BS_TREE + + //method that creates an AVL tree node from the passed SerializableNode, which was obtained as a result of serialization. + override fun createNode(node: SerializableNode?): BSNode? = node?.let { + BSNode( + data = deserializeValue(node.data), + left = createNode(node.left), + right = createNode(node.right), + ) + } + + //method that deserializes the node's metadata (in this case, the node's height). Returns the deserialized metadata. + override fun deserializeMetadata(metadata: Metadata) = metadata.value.toInt() + + //method that serializes the node's metadata (in this case, the node's height). Returns the serialized metadata. + override fun serializeMetadata(node: BSNode) = Metadata("") + + //method that creates an AVL tree. Returns a new tree of type BSTree. + override fun createTree() = BSTree() +} diff --git a/lib/src/main/kotlin/repository/serialization/strategies/RBStrategy.kt b/lib/src/main/kotlin/repository/serialization/strategies/RBStrategy.kt new file mode 100644 index 0000000..d306e56 --- /dev/null +++ b/lib/src/main/kotlin/repository/serialization/strategies/RBStrategy.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.serialization.strategies + +import repository.serialization.Metadata +import repository.serialization.SerializableNode +import repository.serialization.SerializableValue +import repository.serialization.TypeOfTree +import trees.RBTree +import trees.nodes.Color +import trees.nodes.RBNode + +class RBStrategy>( + serializeData: (T) -> SerializableValue, + deserializeData: (SerializableValue) -> T +) : Serialization, RBTree, Color>(serializeData, deserializeData) { + override val typeOfTree: TypeOfTree = TypeOfTree.RB_TREE + + //method that creates an AVL tree node from the passed SerializableNode, which was obtained as a result of serialization. + override fun createNode(node: SerializableNode?): RBNode? = node?.let { + RBNode( + data = deserializeValue(node.data), + color = deserializeMetadata(node.metadata), + left = createNode(node.left), + right = createNode(node.right), + ) + } + + //method that deserializes the node's metadata (in this case, the node's height). Returns the deserialized metadata. + override fun deserializeMetadata(metadata: Metadata): Color { + return when (metadata.value) { + "RED" -> Color.RED + "BLACK" -> Color.BLACK + else -> throw IllegalArgumentException("Can't deserialize metadata $metadata") + } + } + + //method that serializes the node's metadata (in this case, the node's height). Returns the serialized metadata. + override fun serializeMetadata(node: RBNode): Metadata { + return Metadata( + when (node.color) { + Color.RED -> "RED" + else -> "BLACK" + } + ) + } + + //method that creates an AVL tree. Returns a new tree of type RBTree. + override fun createTree() = RBTree() +} diff --git a/lib/src/main/kotlin/repository/serialization/strategies/Serialization.kt b/lib/src/main/kotlin/repository/serialization/strategies/Serialization.kt new file mode 100644 index 0000000..769db59 --- /dev/null +++ b/lib/src/main/kotlin/repository/serialization/strategies/Serialization.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package repository.serialization.strategies + +import repository.serialization.Metadata +import repository.serialization.SerializableNode +import repository.serialization.SerializableValue +import repository.serialization.TypeOfTree +import trees.AbstractTree +import trees.nodes.AbstractNode + +abstract class Serialization, + NodeType : AbstractNode, + TreeType : AbstractTree, + M>( + val serializeValue: (T) -> SerializableValue, + val deserializeValue: (SerializableValue) -> T +) { + abstract val typeOfTree: TypeOfTree + + //abstract method that creates an AVL tree node from the passed SerializableNode, which was obtained as a result of serialization. + abstract fun createNode(node: SerializableNode?): NodeType? + + //abstract method that deserializes the node's metadata (in this case, the node's height). Returns the deserialized metadata. + abstract fun deserializeMetadata(metadata: Metadata): M + + //abstract method that serializes the node's metadata (in this case, the node's height). Returns the serialized metadata. + abstract fun serializeMetadata(node: NodeType): Metadata + + //abstract method that creates an AVL tree. Returns a new tree of type Tree. + abstract fun createTree(): TreeType +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/sqliteEntities/SQLNodeEntity.kt b/lib/src/main/kotlin/repository/sqliteEntities/SQLNodeEntity.kt new file mode 100644 index 0000000..49741ab --- /dev/null +++ b/lib/src/main/kotlin/repository/sqliteEntities/SQLNodeEntity.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption + +class SQLNodeEntity(id: EntityID) : IntEntity(id) { + //companion object of a class that inherits from IntEntityClass + //and defines the base class for the entity. + companion object : IntEntityClass(NodesTable) + + //a variable responsible for storing node data in the database. + var data by NodesTable.data + + //a variable responsible for storing the node's metadata in the database. + var metadata by NodesTable.metadata + + //a variable responsible for storing a link to the left node of the tree in the database. + var left by SQLNodeEntity optionalReferencedOn NodesTable.left + + //a variable responsible for storing a link to the right node of the tree in the database. + var right by SQLNodeEntity optionalReferencedOn NodesTable.right + + //variable responsible for saving the link to the tree in the database. + var tree by SQLTreeEntity referencedOn NodesTable.tree +} + +internal object NodesTable : IntIdTable("nodes") { + //table column containing node data. + val data = text("data") + + //a table column containing node metadata. + val metadata = text("metadata") + + //table column containing a link to the left node of the tree. + // It may be undefined if there is no left node. + val left = reference("left_id", NodesTable).nullable() + + //table column containing a link to the right node of the tree. + //It may be undefined if the right node is missing. + val right = reference("right_id", NodesTable).nullable() + + //a table column containing a link to the tree. + // When a tree is removed from the database, all nodes associated with it are also removed. + val tree = reference("tree_id", TreesTable, onDelete = ReferenceOption.CASCADE) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/repository/sqliteEntities/SQLTreeEntity.kt b/lib/src/main/kotlin/repository/sqliteEntities/SQLTreeEntity.kt new file mode 100644 index 0000000..10a1ce2 --- /dev/null +++ b/lib/src/main/kotlin/repository/sqliteEntities/SQLTreeEntity.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable + +class SQLTreeEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(TreesTable) + + //tree name + var name by TreesTable.name + + //tree type + var typeOfTree by TreesTable.typeOfTree + + //root node of the tree, refers to a node in the node table + var root by SQLNodeEntity optionalReferencedOn TreesTable.root +} + +//an object containing metadata for the trees table in the database. +internal object TreesTable : IntIdTable("trees") { + //tree name + val name = text("name") + + //tree type + val typeOfTree = text("type") + + //root node of the tree, refers to a node in the node table + val root = reference("root_node_id", NodesTable).nullable() + + //a table initializer that sets a unique index by tree name and type. + init { + uniqueIndex(name, typeOfTree) + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/AVLTree.kt b/lib/src/main/kotlin/trees/AVLTree.kt new file mode 100644 index 0000000..895ce94 --- /dev/null +++ b/lib/src/main/kotlin/trees/AVLTree.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + +import trees.nodes.AVLNode + +class AVLTree> : AbstractTree>() { + //adds a new node to the AVL tree. + override fun add(data: T) { + root = balancedAdd(root, AVLNode(data)) + root?.updateHeight() + root?.parent = null + } + + //checks if the given value is contained in the AVL tree. + override fun contains(data: T): Boolean { + return (contains(root, AVLNode(data)) != null) + } + + //removes the node with the given value from the AVL tree. + override fun delete(data: T) { + root = balancedDelete(root, AVLNode(data)) + root?.updateHeight() + root?.parent = null + } + + //method that balances the tree starting from the given node. + override fun balance(initNode: AVLNode?): AVLNode? { + if (initNode == null) { + return null + } + initNode.updateHeight() + val balanceFactor = initNode.balanceFactor() + + if (balanceFactor > 1) { + if ((initNode.right?.balanceFactor() ?: 0) < 0) { + initNode.right = initNode.right?.let { rotateRight(it) } + initNode.right?.updateHeight() + } + val node = rotateLeft(initNode) + updateChildrenHeight(node) + node?.updateHeight() + return node + } else if (balanceFactor < -1) { + if ((initNode.left?.balanceFactor() ?: 0) > 0) { + initNode.left = initNode.left?.let { rotateLeft(it) } + initNode.left?.updateHeight() + } + val node = rotateRight(initNode) + updateChildrenHeight(node) + node?.updateHeight() + return node + } + return initNode + } + + //method that returns the value of the node with the given value. + fun get(data: T): T? { + return contains(root, AVLNode(data))?.data + } + + //helper method of the AVLTree class that updates the height of the passed node's child nodes. + private fun updateChildrenHeight(node: AVLNode?) { + node?.left?.updateHeight() + node?.right?.updateHeight() + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/AbstractTree.kt b/lib/src/main/kotlin/trees/AbstractTree.kt new file mode 100644 index 0000000..5f337cb --- /dev/null +++ b/lib/src/main/kotlin/trees/AbstractTree.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + +import trees.interfaces.Tree +import trees.nodes.AbstractNode + +abstract class AbstractTree, NodeType : AbstractNode> : Tree { + var root: NodeType? = null + internal set + + //This function is called after a node has been inserted or removed from the tree, + // and is used to balance the tree if necessary. + // The default implementation simply returns the starting node without performing any balancing. + protected open fun balance(initNode: NodeType?): NodeType? { + return initNode + } + + //This function is used to add a node to a tree while ensuring that the tree remains balanced. + //It recursively traverses the tree until it finds the correct position for the new node, + // and then calls balance to balance the tree. + protected fun balancedAdd(initNode: NodeType?, node: NodeType): NodeType? { + + if (initNode == null) { + return node + } + + if (initNode < node) { + initNode.right = balancedAdd(initNode.right, node) + initNode.right?.parent = initNode + } else if (initNode > node) { + initNode.left = balancedAdd(initNode.left, node) + initNode.left?.parent = initNode + } + return balance(initNode) + } + + //This function is used to remove a node from a tree while ensuring that the tree remains balanced. + // It recursively traverses the tree until it finds the node to be removed, and then calls + protected fun balancedDelete(initNode: NodeType?, node: NodeType): NodeType? { + if (initNode == null) { + return null + } + if (initNode < node) { + initNode.right = balancedDelete(initNode.right, node) + initNode.right?.parent = initNode + } else if (initNode > node) { + initNode.left = balancedDelete(initNode.left, node) + initNode.left?.parent = initNode + } else { + if ((initNode.left == null) || (initNode.right == null)) { + return initNode.left ?: initNode.right + } else { + initNode.right?.let { + val tmp = getMinimal(it) + initNode.data = tmp.data + initNode.right = balancedDelete(initNode.right, tmp) + initNode.right?.parent = initNode + } + } + } + return balance(initNode) + } + + //This function is used to check if a node matches with the same value as + protected fun contains(initNode: NodeType?, node: NodeType): NodeType? { + if (initNode == null) { + return null + } + + return if (initNode < node) { + contains(initNode.right, node) + } else if (initNode > node) { + contains(initNode.left, node) + } else { + initNode + } + } + + //This function returns the node with the smallest value in the subtree rooted at the specified node. + protected fun getMinimal(node: NodeType): NodeType { + var minNode = node + while (true) { + minNode = minNode.left ?: break + } + return minNode + } + + //This function returns the node with the largest value in the subtree rooted at the specified node. + protected fun getMaximal(node: NodeType): NodeType { + var maxNode = node + while (true) { + maxNode = maxNode.left ?: break + } + return maxNode + } + + //This function performs a left rotation on the specified node, changing its position to its right child node. + protected fun rotateLeft(node: NodeType): NodeType? { + val rightChild = node.right + val secondSubtree = rightChild?.left + rightChild?.left = node + rightChild?.left?.parent = rightChild + node.right = secondSubtree + node.right?.parent = node + return rightChild + } + + //This function performs a right rotation for the specified node, changing its position to its left child. + protected fun rotateRight(node: NodeType): NodeType? { + val leftChild = node.left + val secondSubtree = leftChild?.right + leftChild?.right = node + leftChild?.right?.parent = leftChild + node.left = secondSubtree + node.left?.parent = node + return leftChild + } + + //This function replaces the specified child node of a node with a new child node + protected fun replaceChild(child: NodeType, newChild: NodeType?): NodeType? { + if (child == root) { + root = newChild + newChild?.parent = null + return newChild + } + if (child.parent?.left == child) { + child.parent?.left = newChild + } else if (child.parent?.right == child) { + child.parent?.right = newChild + } + newChild?.parent = child.parent + return newChild + } + + //This function returns a preview of the tree as a list of nodes. + // It uses the walk internal function to recursively traverse the tree in advance + // and add each node to the list. + fun preOrder(): List { + val result = mutableListOf() + fun walk(node: NodeType, lst: MutableList) { + lst.add(node) + node.left?.let { walk(it, lst) } + node.right?.let { walk(it, lst) } + } + if (root == null) return result + root?.let { walk(it, result) } + return result + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/BSTree.kt b/lib/src/main/kotlin/trees/BSTree.kt new file mode 100644 index 0000000..177b5a0 --- /dev/null +++ b/lib/src/main/kotlin/trees/BSTree.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + +import trees.nodes.BSNode + +class BSTree> : AbstractTree>() { + + //method that adds a new node + override fun add(data: T) { + root = balancedAdd(root, BSNode(data)) + } + + //method that checks if the given value is contained + override fun contains(data: T): Boolean { + return (contains(root, BSNode(data)) != null) + } + + //method that removes a node with a given value + override fun delete(data: T) { + root = balancedDelete(root, BSNode(data)) + root?.parent = null + } + + //method that returns the value of the node with the given value. + fun get(data: T): T? { + return contains(root, BSNode(data))?.data + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/KeyValue.kt b/lib/src/main/kotlin/trees/KeyValue.kt new file mode 100644 index 0000000..bf996d2 --- /dev/null +++ b/lib/src/main/kotlin/trees/KeyValue.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + +class KeyValue, V>( + val key: K, + var value: V, +) : Comparable> { + + //Compares KeyValue objects based on their keys. + override fun compareTo(other: KeyValue): Int { + return key.compareTo(other.key) + } + + //Checks if a KeyValue object is equal to another object. + //Two objects are considered equal if their keys are equal. + override fun equals(other: Any?): Boolean { + if (other is KeyValue<*, *>) { + return key == other.key + } + return false + } + + //Returns the string representation of the KeyValue object in "key: value" format. + override fun toString(): String { + return "$key: $value" + } + + //Returns the hash code of the KeyValue object based on the hash code of the key. + override fun hashCode(): Int { + return key.hashCode() + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/RBTree.kt b/lib/src/main/kotlin/trees/RBTree.kt new file mode 100644 index 0000000..132f736 --- /dev/null +++ b/lib/src/main/kotlin/trees/RBTree.kt @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + +import trees.nodes.Color +import trees.nodes.RBNode + +class RBTree> : AbstractTree>() { + //This function adds a new node containing the provided data to the tree while ensuring + //that it maintains the Red-Black tree properties. + //It first calls balancedAdd to add the node, + //and then balanceAfterAdd to perform any necessary rotations and color changes. + //It then sets the root's parent to null and the root's color to black. + override fun add(data: T) { + val node = RBNode(data) + root = balancedAdd(root, node) + root = balanceAfterAdd(node, root) + root?.parent = null + root?.color = Color.BLACK + } + + //This function deletes a node containing the provided data from the tree while ensuring + //that it maintains the Red-Black tree properties. If the node has no children, it simply deletes it. + // If it has one child, it replaces it with its child. If it has two children, + // it replaces it with the next node in the in-order traversal (i.e., the node with the smallest data value in its right subtree), + // and then deletes that node. It then calls balanceAfterDelete to perform any necessary rotations and color changes. + override fun delete(data: T) { + + val node = contains(root, RBNode(data)) ?: return + val next: RBNode + + if ((node.left == null) && (node.right == null)) { + if (node == root) { + root = null + } else { + if (node.color == Color.RED) { + // delete red node without child + replaceChild(node, null) + } else { + // delete black node without children + root = balanceAfterDelete(node) + replaceChild(node, null) + } + } + return + } + + if ((node.left == null) || (node.right == null)) { + // delete for black node with one red child + replaceChild(node, node.right ?: node.left)?.color = Color.BLACK + } else { + // delete node with two children + next = getMinimal(node.right ?: error("Node must have right child")) + node.data = next.data.also { next.data = node.data } + if (next.color == Color.RED) { + // delete red node without child + replaceChild(next, null) + } else { + if (next.right == null) { + root = balanceAfterDelete(next) + replaceChild(next, null) + } else { + // delete for black node with one child + replaceChild(next, next.right)?.color = Color.BLACK // next.right must be red and not null + } + } + } + } + + //method that checks if the given value is contained + override fun contains(data: T): Boolean { + return (contains(root, RBNode(data)) != null) + } + + //This function performs any necessary rotations and color changes to ensure that the tree maintains + //the Red-Black tree properties after a node has been deleted from the tree. It takes as input the node + //that was just deleted, and returns the new root node of the tree. + private fun balanceAfterDelete(node: RBNode?): RBNode? { + var newRoot = root + var current = node + + while ((current != newRoot) && (current?.color == Color.BLACK)) { + if (current == current.parent?.left) { + var brother = current.parent?.right + if (isRed(brother)) { + brother?.color = Color.BLACK + current.parent?.color = Color.RED + newRoot = clearRotateLeft(current.parent, newRoot) + brother = current.parent?.right + } + if (isBlack(brother?.left) && (isBlack(brother?.right))) { + brother?.color = Color.RED + current = current.parent + } else { + if (isBlack(brother?.right)) { + brother?.left?.color = Color.BLACK + brother?.color = Color.RED + newRoot = clearRotateRight(brother, newRoot) + brother = current.parent?.right + } + brother?.color = current.parent?.color ?: Color.BLACK + current.parent?.color = Color.BLACK + brother?.right?.color = Color.BLACK + newRoot = clearRotateLeft(current.parent, newRoot) + current = newRoot + } + } else { + var brother = current.parent?.left + if (isRed(brother)) { + brother?.color = Color.BLACK + current.parent?.color = Color.RED + newRoot = clearRotateRight(current.parent, newRoot) + brother = current.parent?.left + } + if ((isBlack(brother?.right)) && (isBlack(brother?.left))) { + brother?.color = Color.RED + current = current.parent + } else { + if (isBlack(brother?.left)) { + brother?.right?.color = Color.BLACK + brother?.color = Color.RED + newRoot = clearRotateLeft(brother, newRoot) + brother = current.parent?.left + } + brother?.color = current.parent?.color ?: Color.BLACK + current.parent?.color = Color.BLACK + brother?.left?.color = Color.BLACK + newRoot = clearRotateRight(current.parent, newRoot) + current = newRoot + } + } + } + current?.color = Color.BLACK + return newRoot + } + + //This function performs any necessary rotations and color changes to ensure that + //the tree maintains the Red-Black tree properties after a node has been added to the tree. + //It takes as input the node that was just ad + private fun balanceAfterAdd(initNode: RBNode?, subRoot: RBNode?): RBNode? { + if (initNode?.parent == null) return subRoot + var newRoot = subRoot + var current = initNode + while (current?.parent?.color == Color.RED) { + if (current.parent == current.parent?.parent?.left) { + if (current.parent?.parent?.right?.color == Color.RED) { + current.parent?.color = Color.BLACK + current.parent?.parent?.right?.color = Color.BLACK + current.parent?.parent?.color = Color.RED + current = current.parent?.parent + } else { + if (current == current.parent?.right) { + current = current.parent + newRoot = current?.let { clearRotateLeft(it, newRoot) } + } + current?.parent?.color = Color.BLACK + current?.parent?.parent?.color = Color.RED + newRoot = current?.parent?.parent?.let { clearRotateRight(it, newRoot) } + + } + } else { + if (current.parent?.parent?.left?.color == Color.RED) { + current.parent?.color = Color.BLACK + current.parent?.parent?.left?.color = Color.BLACK + current.parent?.parent?.color = Color.RED + current = current.parent?.parent + } else { + if (current == current.parent?.left) { + current = current.parent + newRoot = current?.let { clearRotateRight(it, newRoot) } + } + current?.parent?.color = Color.BLACK + current?.parent?.parent?.color = Color.RED + newRoot = current?.parent?.parent?.let { clearRotateLeft(it, newRoot) } + + } + } + } + newRoot?.color = Color.BLACK + return newRoot ?: root + } + + //method that returns the value of the node with the given value. + fun get(data: T): T? { + return contains(root, RBNode(data))?.data + } + + //This function performs a left rotation on the subtree rooted at the provided node, and returns the new root node of the subtree. + private fun clearRotateLeft(node: RBNode?, initRoot: RBNode?): RBNode? { + if (node?.right == null) return null + + var newRoot = initRoot + val parent = node.parent + val subTree = rotateLeft(node) + + if (parent == null) { + subTree?.parent = null + newRoot = subTree + } else if (parent.left == node) { + parent.left = subTree + subTree?.parent = parent + } else { + parent.right = subTree + subTree?.parent = parent + } + + return newRoot + } + + //This function performs a right rotation on the subtree rooted at the provided node, and returns the new root node of the subtree. + private fun clearRotateRight(node: RBNode?, root: RBNode?): RBNode? { + if (node?.left == null) return null + + var newRoot = root + val parent = node.parent + val subTree = rotateRight(node) + + if (parent == null) { + newRoot = subTree + } else if (parent.left == node) { + parent.left = subTree + } else { + parent.right = subTree + } + subTree?.parent = parent + return newRoot + } + + companion object { + //This function returns a Boolean indicating whether the provided node is black. If the node is null, it returns true. + @JvmStatic + internal fun > isBlack(node: RBNode?): Boolean { + return ((node == null) || (node.color == Color.BLACK)) + } + + //This function returns a Boolean indicating whether the provided node is red. If the node is null, it returns false. + @JvmStatic + internal fun > isRed(node: RBNode?): Boolean { + return node?.color == Color.RED + } + } +} diff --git a/lib/src/main/kotlin/trees/interfaces/Node.kt b/lib/src/main/kotlin/trees/interfaces/Node.kt new file mode 100644 index 0000000..bd2bf00 --- /dev/null +++ b/lib/src/main/kotlin/trees/interfaces/Node.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees.interfaces + +interface Node, Subtype : Node> : Comparable> { + val data: T + val left: Subtype? + val right: Subtype? + val parent: Subtype? +} diff --git a/lib/src/main/kotlin/trees/interfaces/Tree.kt b/lib/src/main/kotlin/trees/interfaces/Tree.kt new file mode 100644 index 0000000..8edd59b --- /dev/null +++ b/lib/src/main/kotlin/trees/interfaces/Tree.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees.interfaces + +interface Tree> { + fun add(data: T) + fun contains(data: T): Boolean + fun delete(data: T) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/nodes/AVLNode.kt b/lib/src/main/kotlin/trees/nodes/AVLNode.kt new file mode 100644 index 0000000..671a18c --- /dev/null +++ b/lib/src/main/kotlin/trees/nodes/AVLNode.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees.nodes + +class AVLNode>( + //stores the data value of the current node + override var data: T, + //Stores the height of the tree + internal var height: Int = 1, + //stores the left subtree of the current node + override var left: AVLNode? = null, + //stores the right subtree of the current node + override var right: AVLNode? = null, + //stores the parent node of the current node + override var parent: AVLNode? = null, +) : AbstractNode>() { + + //Updates the node's height value based on the height of its left and right subtrees. +// Gets the height of the left subtree and right subtree, +// then sets the height of the node to 1 + the maximum height of the left and right subtrees. +// This function is used to maintain balance in the tree. + internal fun updateHeight() { + val leftNode = left + val rightNode = right + val leftHeight = leftNode?.height ?: 0 + val rightHeight = rightNode?.height ?: 0 + height = 1 + maxOf(leftHeight, rightHeight) + } + + //Calculates the balance factor of a node, +//which is the difference between the height of the right subtree and the height of the left subtree. +// The function is used to determine if the tree needs to be rebalanced. +//If the balance factor is greater than 1 or less than -1, then the tree needs to be rebalanced. + internal fun balanceFactor(): Int { + val leftNode = left + val rightNode = right + return (rightNode?.height ?: 0) - (leftNode?.height ?: 0) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/nodes/AbstractNode.kt b/lib/src/main/kotlin/trees/nodes/AbstractNode.kt new file mode 100644 index 0000000..a8e07dd --- /dev/null +++ b/lib/src/main/kotlin/trees/nodes/AbstractNode.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees.nodes + +import trees.interfaces.Node + +abstract class AbstractNode, Subtype : AbstractNode> : Node { + //stores the data value of the current node + abstract override var data: T + internal set + + //stores the left subtree of the current node + abstract override var left: Subtype? + internal set + + //stores the right subtree of the current node + abstract override var right: Subtype? + internal set + + //stores the parent node of the current node + abstract override var parent: Subtype? + internal set + + //method to compare nodes based on their data values + override fun compareTo(other: Node): Int { + return data.compareTo(other.data) + } + + //method to get the hash code of the current node + override fun hashCode(): Int { + return data.hashCode() + } + + //method for comparing the current node with another object, + // checking for equality of the value of the data property + override fun equals(other: Any?): Boolean { + if (other is AbstractNode<*, *>) { + return data == other.data + } + return false + } + + //returns a string representation of the node in data format. + override fun toString(): String { + return "$data" + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/nodes/BSNode.kt b/lib/src/main/kotlin/trees/nodes/BSNode.kt new file mode 100644 index 0000000..76ca5f9 --- /dev/null +++ b/lib/src/main/kotlin/trees/nodes/BSNode.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees.nodes + +class BSNode>( + //stores the data value of the current node + override var data: T, + //stores the left subtree of the current node + override var left: BSNode? = null, + //stores the right subtree of the current node + override var right: BSNode? = null, + //stores the parent node of the current node + override var parent: BSNode? = null, +) : AbstractNode>() \ No newline at end of file diff --git a/lib/src/main/kotlin/trees/nodes/RBNode.kt b/lib/src/main/kotlin/trees/nodes/RBNode.kt new file mode 100644 index 0000000..01c04db --- /dev/null +++ b/lib/src/main/kotlin/trees/nodes/RBNode.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees.nodes + +//enum that defines two colors: RED and BLACK +enum class Color { + RED, BLACK +} + +class RBNode>( + //stores the data value of the current node + override var data: T, + //node color, default is set to RED + var color: Color = Color.RED, + //stores the left subtree of the current node + override var left: RBNode? = null, + //stores the right subtree of the current node + override var right: RBNode? = null, + //stores the parent node of the current node + override var parent: RBNode? = null, +) : AbstractNode>() { + + //an overridden toString function that returns a string + //representation of a node containing its color and data. + override fun toString(): String { + return "$color - $data" + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/trees/AVLTreeTest.kt b/lib/src/test/kotlin/trees/AVLTreeTest.kt new file mode 100644 index 0000000..6674792 --- /dev/null +++ b/lib/src/test/kotlin/trees/AVLTreeTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + +import trees.nodes.AVLNode +import kotlin.random.Random +import kotlin.test.* + + +class AVLTreeTest { + companion object { + const val seed = 42 + } + + private lateinit var tree: AVLTree + private lateinit var values: Array + private val randomizer = Random(seed) + + @BeforeTest + fun init() { + values = Array(1000) { randomizer.nextInt(10000) } + tree = AVLTree() + } + + @Test + fun `check invariant while adding`() { + for (value in values) { + tree.add(value) + assertTrue(InvariantChecker.checkHeightAVL(tree.root), "Failed invariant, incorrect height, value: $value") + assertTrue(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data, value: $value") + assertTrue( + InvariantChecker.checkLinksToParent(tree.root), + "Failed invariant, incorrect parent's link, value: $value" + ) + } + } + + @Test + fun `check invariant while deleting`() { + for (value in values) { + tree.add(value) + } + values.shuffle(randomizer) + for (value in values) { + tree.delete(value) + assertTrue(InvariantChecker.checkHeightAVL(tree.root), "Failed invariant, incorrect height, value: $value") + assertTrue(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data, value: $value") + assertTrue( + InvariantChecker.checkLinksToParent(tree.root), + "Failed invariant, incorrect parent's link, value: $value" + ) + } + } + + @Test + fun `special incorrect test`() { + tree.add(10) + tree.add(15) + tree.add(5) + tree.root?.left?.left = AVLNode(100) + tree.root?.left?.left?.left = AVLNode(150) + assertFalse(InvariantChecker.checkHeightAVL(tree.root)) + assertFalse(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data") + assertFalse(InvariantChecker.checkLinksToParent(tree.root), "Failed invariant, incorrect parent's link") + } + + @Test + fun `check deleting from empty tree without exceptions`() { + values.forEach { tree.delete(it) } + assertNull(tree.root, "Root should be null") + } + + + @Test + fun `check for the presence of elements`() { + for (value in values) { + tree.add(value) + assertTrue(tree.contains(value)) + } + } + + @Test + fun `check using KeyValue in data`() { + val newTree = AVLTree>() + val keyValue = values.map { KeyValue(it.toString(), it) } + for (stringIntKeyValue in keyValue) { + newTree.add(stringIntKeyValue) + assertEquals( + stringIntKeyValue.value, + newTree.get(KeyValue(stringIntKeyValue.key, 0))?.value, + "Values should be equals" + ) + } + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/trees/BSTreeTest.kt b/lib/src/test/kotlin/trees/BSTreeTest.kt new file mode 100644 index 0000000..7d4857e --- /dev/null +++ b/lib/src/test/kotlin/trees/BSTreeTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + +import trees.nodes.BSNode +import kotlin.random.Random +import kotlin.test.* + + +class BSTreeTest { + companion object { + const val seed = 42 + } + + private lateinit var tree: BSTree + private lateinit var values: Array + private val randomizer = Random(seed) + + @BeforeTest + fun init() { + values = Array(1000) { randomizer.nextInt(10000) } + tree = BSTree() + } + + @Test + fun `check invariant while adding`() { + for (value in values) { + tree.add(value) + assertTrue(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data, value: $value") + assertTrue( + InvariantChecker.checkLinksToParent(tree.root), + "Failed invariant, incorrect parent's link, value: $value" + ) + } + } + + @Test + fun `check invariant while deleting`() { + for (value in values) { + tree.add(value) + } + values.shuffle(randomizer) + for (value in values) { + tree.delete(value) + assertTrue(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data, value: $value") + assertTrue( + InvariantChecker.checkLinksToParent(tree.root), + "Failed invariant, incorrect parent's link, value: $value" + ) + } + } + + @Test + fun `special incorrect test`() { + tree.add(10) + tree.root?.left = BSNode(15) + tree.root?.right = BSNode(5) + assertFalse(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data") + assertFalse(InvariantChecker.checkLinksToParent(tree.root), "Failed invariant, incorrect parent's link") + } + + @Test + fun `check for the presence of elements`() { + for (value in values) { + tree.add(value) + assertTrue(tree.contains(value)) + } + } + + @Test + fun `check deleting from empty tree without exceptions`() { + values.forEach { tree.delete(it) } + assertNull(tree.root, "Root should be null") + } + + @Test + fun `check using KeyValue in data`() { + val newTree = BSTree>() + val keyValue = values.map { KeyValue(it.toString(), it) } + for (stringIntKeyValue in keyValue) { + newTree.add(stringIntKeyValue) + assertEquals( + stringIntKeyValue.value, + newTree.get(KeyValue(stringIntKeyValue.key, 0))?.value, + "Values should be equals" + ) + } + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/trees/InvariantChecker.kt b/lib/src/test/kotlin/trees/InvariantChecker.kt new file mode 100644 index 0000000..3d623a6 --- /dev/null +++ b/lib/src/test/kotlin/trees/InvariantChecker.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package trees + +import trees.interfaces.Node +import trees.nodes.AVLNode +import trees.nodes.Color +import trees.nodes.RBNode +import kotlin.math.abs + +object InvariantChecker { + fun , NodeType : Node> checkLinksToParent(node: Node?): Boolean { + if (node == null) return true + var result = true + result = result && ((node.left?.parent == node) || (node.left == null)) + result = result && ((node.right?.parent == node) || (node.right == null)) + if (result) { + return checkLinksToParent(node.left) && checkLinksToParent(node.right) + } + return false + } + + fun , NodeType : Node> checkDataInNodes(node: Node?): Boolean { + if (node == null) return true + var result = true + val leftChild = node.left + val rightChild = node.right + result = result && ((leftChild?.data == null) || (leftChild.data < node.data)) + result = result && ((rightChild?.data == null) || (rightChild.data > node.data)) + if (result) { + return checkDataInNodes(node.left) && checkDataInNodes(node.right) + } + return false + } + + fun > checkHeightAVL(node: AVLNode?): Boolean { + if (node == null) return true + var result = true + val rightHeight = node.right?.height ?: 0 + val leftHeight = node.left?.height ?: 0 + result = result && (maxOf(rightHeight, leftHeight) + 1 == node.height) + result = result && (abs(node.balanceFactor()) <= 1) + if (result) { + return checkHeightAVL(node.right) && checkHeightAVL(node.left) + } + return false + } + + fun > checkBlackHeight(node: RBNode?): Boolean { + fun > getBlackHeightRB(node: RBNode?): Pair { + if (node == null) return Pair(true, 1) + val leftHeight = getBlackHeightRB(node.left) + val rightHeight = getBlackHeightRB(node.right) + if (leftHeight.second != rightHeight.second || !leftHeight.first || !rightHeight.first) { + return Pair(false, rightHeight.second + (if (node.color == Color.BLACK) 1 else 0)) + } + return if (node.color == Color.BLACK) + Pair(true, rightHeight.second + 1) + else Pair(true, rightHeight.second) + } + return getBlackHeightRB(node).first + } + + fun > checkRedParent(node: RBNode?): Boolean { + if (node == null) return true + if (RBTree.isRed(node)) { + if ((RBTree.isRed(node.left)) or (RBTree.isRed(node.right))) + return false + } + return checkRedParent(node.left) && checkRedParent(node.right) + } +} diff --git a/lib/src/test/kotlin/trees/RBTreeTest.kt b/lib/src/test/kotlin/trees/RBTreeTest.kt new file mode 100644 index 0000000..31f3f2a --- /dev/null +++ b/lib/src/test/kotlin/trees/RBTreeTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +package trees + + +import trees.nodes.Color +import trees.nodes.RBNode +import kotlin.random.Random +import kotlin.test.* + +class RBTreeTest { + companion object { + const val seed = 42 + } + + private lateinit var tree: RBTree + private lateinit var values: Array + private val randomizer = Random(seed) + + @BeforeTest + fun init() { + values = Array(1000) { randomizer.nextInt(10000) } + tree = RBTree() + } + + @Test + fun `check invariant while adding`() { + for (value in values) { + tree.add(value) + assertTrue( + InvariantChecker.checkBlackHeight(tree.root), + "Failed invariant, incorrect black height, value: $value" + ) + assertTrue( + InvariantChecker.checkRedParent(tree.root), + "Failed invariant, incorrect color for node, value: $value" + ) + assertTrue(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data, value: $value") + assertTrue( + InvariantChecker.checkLinksToParent(tree.root), + "Failed invariant, incorrect parent's link, value: $value" + ) + } + } + + @Test + fun `check invariant while deleting`() { + for (value in values) { + tree.add(value) + } + values.shuffle(randomizer) + for (value in values) { + tree.delete(value) + assertTrue( + InvariantChecker.checkBlackHeight(tree.root), + "Failed invariant, incorrect black height, value: $value" + ) + assertTrue( + InvariantChecker.checkRedParent(tree.root), + "Failed invariant, incorrect color for node, value: $value" + ) + assertTrue(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data, value: $value") + assertTrue( + InvariantChecker.checkLinksToParent(tree.root), + "Failed invariant, incorrect parent's link, value: $value" + ) + } + } + + @Test + fun `special incorrect test`() { + tree.add(10) + tree.root?.left = RBNode(15) + tree.root?.right = RBNode(5) + tree.root?.right?.color = Color.BLACK + tree.root?.color = Color.RED + assertFalse(InvariantChecker.checkBlackHeight(tree.root), "Failed invariant, incorrect black height") + assertFalse(InvariantChecker.checkRedParent(tree.root), "Failed invariant, incorrect color for node") + assertFalse(InvariantChecker.checkDataInNodes(tree.root), "Failed invariant, incorrect data") + assertFalse(InvariantChecker.checkLinksToParent(tree.root), "Failed invariant, incorrect parent's link") + } + + @Test + fun `check for the presence of elements`() { + for (value in values) { + tree.add(value) + assertTrue(tree.contains(value)) + } + } + + @Test + fun `check deleting from empty tree without exceptions`() { + values.forEach { tree.delete(it) } + assertNull(tree.root, "Root should be null") + } + + @Test + fun `check using KeyValue in data`() { + val newTree = RBTree>() + val keyValue = values.map { KeyValue(it.toString(), it) } + for (stringIntKeyValue in keyValue) { + newTree.add(stringIntKeyValue) + assertEquals( + stringIntKeyValue.value, + newTree.get(KeyValue(stringIntKeyValue.key, 0))?.value, + "Values should be equals" + ) + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..47bb24a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 teemEight + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = "trees-8" +include("lib") +include("app") + +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + } + + versionCatalogs { + create("libs") { + version("kotlin", "1.8.20") + plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").versionRef("kotlin") + plugin("kotlin-serialization", "org.jetbrains.kotlin.plugin.serialization").versionRef("kotlin") + plugin("kotlin-noarg", "org.jetbrains.kotlin.plugin.noarg").versionRef("kotlin") + + plugin("compose", "org.jetbrains.compose").version("1.4.0") + + library("kotlinx-serialization-json", "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") + library("gson", "com.google.code.gson:gson:2.10.1") + + version("exposed", "0.41.1") + library("exposed-core", "org.jetbrains.exposed", "exposed-core").versionRef("exposed") + library("exposed-dao", "org.jetbrains.exposed", "exposed-dao").versionRef("exposed") + library("exposed-jdbc", "org.jetbrains.exposed", "exposed-jdbc").versionRef("exposed") + + version("neo4j-ogm", "4.0.5") + library("neo4j-ogm-core", "org.neo4j", "neo4j-ogm-core").versionRef("neo4j-ogm") + library("neo4j-ogm-bolt", "org.neo4j", "neo4j-ogm-bolt-driver").versionRef("neo4j-ogm") + + } + } +}