diff --git a/.github/.keep b/.github/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 00000000..67b8290b --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,41 @@ +version: 2 +mergeable: + - when: pull_request.* + name: "Description check" + + filter: # to ignore Feedback branch + - 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. + + - when: pull_request.*, pull_request_review.* + name: 'Approval check' + + filter: # to ignore Feedback branch + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none + + branches-ignore: + - main + validate: + - do: approvals + min: + count: 2 + required: + requested_reviewers: true + message: All requested reviewers must approve changes before merging. + limit: + users: [ 'qrutyy', 'spisladqo', 'kar1mgh' ] \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..81322b15 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Build & Run JUnit5 tests + +on: + workflow_dispatch: + pull_request: + +jobs: + build: + if: github.actor!= 'github-classroom[bot]' # to ignore Feedback branch + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: zulu + + - name: Build & Test by Gradle + run: ./gradlew build \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8ab266ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,219 @@ +# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,macos,windows,linux,gradle,kotlin +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,macos,windows,linux,gradle,kotlin + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Kotlin ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/intellij+all,macos,windows,linux,gradle,kotlin diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..67fe4143 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# 💪 Interested in contributing? + +Good boy! Please read the few sections below to understand how to implement new features. + +> Be positive! Even if your changes don't get merged in WUDU, please don't be too sad, you will always be able to run workflows directly from your fork! +> Meow + +## 🤝 Accepted contributions + +The following contributions are accepted: + + + + + + + + + + + + + + + + + + + + + + + + +
SectionChangesAdditionsNotes
🧩 Features💢💢 +
    +
  • New features are allowed, but must be optional and backward compatible
  • +
+
🧪 Tests💢✅ + +
    +
  • Everything that makes WUDU more stable is welcomed!
  • +
+
🗃️ Repository +
    +
  • Workflows, license, readmes, etc. usually don't need to be edited
  • +
+
+ +**Legend** +* ✅: Contributions welcomed! +* 💢: Contributions are welcomed, but must be discussed first +* ❌: Only maintainers can manage these files \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..4a3a5801 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,869 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +--- + +All files in the package model.algorithms.clustering.implementation are provided under the following license: + + 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 + +Copyright 2021-2022 JetBrains s.r.o. + +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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3a64e44d --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# WUDU + +#### - Weighted Unweighted Directed Undirected (or like a Vodoo doll ;) ) + +Application that lets you create, save, visualise, analyse and modify 4 different types of graphs + + +## Features + +- Support 4 types of graphs +- Use all in all - 9 algorithms (SCC, bridges, shortest paths, cycles, layout, communities, etc. see [wiki]) +- Store graphs anywhere you want: SQLite, Neo4J, JSON (WIP) (see [wiki]) +- Drag, zoom, replace nodes + +![alt text][main_screen_image] + +## Usage + +Firstly - clone the last release/version of our app. Since our app uses Gradle build system - you can use these commands: + +| Command | Description | +|----------------------------------------|-----------------------------------------------------------------------| +| `./gradlew run` | Runs the application | +| `./gradlew assemble` | Builds without tests | +| `./gradlew test` | Runs the unit and integration tests | + +## Contributing + +If you have found a bug, or want to propose some useful feature for our project, please firstly read our [Contribution Rules][contribute_rules_url] and +do the following: +1. Fork the Project +2. Create your Feature Branch (git checkout -b feat/my-feature) +3. Commit your Changes (git commit -m 'add some feature'), but don't forget to keep the commit style. (We use [Conventional Commits]) +4. Push to the Branch (git push origin feat/my-feature) +5. Open a Pull Request + +## License + +Distributed under the [GPL-3.0 License][repo_license_url]. + +## Authors + +- [Gavrilenko Mike](https://github.com/qrutyy) +- [Shakirov Karim](https://github.com/kar1mgh) +- [Vlasenco Daniel](https://github.com/spisladqo) +_______________________________ + +[*Java gnomik*][java_gnomik_url] + +[wiki]: https://github.com/spbu-coding-2023/graphs-graph-2/wiki +[Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/ +[repo_license_url]: https://github.com/spbu-coding-2023/graphs-graph-2/blob/main/LICENSE.md +[contribute_rules_url]: https://github.com/spbu-coding-2023/graphs-graph-2/blob/main/CONTRIBUTING.md + +[java_gnomik_url]: https://ibb.co/54hJVd2 +[main_screen_image]: https://github.com/spbu-coding-2023/graphs-graph-2/assets/64466788/9c708a11-dc6e-4cf9-a848-44f21dec7a37 diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..b7aba023 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,36 @@ +val kotlinxCoroutinesVersion: String by project +val neo4jDriverVersion: String by project +val composeVersion: String by project +val junitVersion: String by project + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.compose) +} + +dependencies { + implementation(compose.desktop.currentOs) + testImplementation("org.jetbrains.compose.ui:ui-test-junit4:$composeVersion") + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$kotlinxCoroutinesVersion") + + implementation("org.xerial:sqlite-jdbc:3.41.2.2") + implementation("org.slf4j:slf4j-api:1.7.36") + implementation("ch.qos.logback:logback-classic:1.4.12") + testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") + testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + + implementation("org.neo4j.driver:neo4j-java-driver:$neo4jDriverVersion") +} + +compose.desktop { + application { + mainClass = "MainKt" + } +} + +tasks.test { + useJUnitPlatform() +} diff --git a/app/database/my_graph_database.db b/app/database/my_graph_database.db new file mode 100644 index 00000000..ba458403 Binary files /dev/null and b/app/database/my_graph_database.db differ diff --git a/app/src/main/kotlin/Constants.kt b/app/src/main/kotlin/Constants.kt new file mode 100644 index 00000000..1b66486a --- /dev/null +++ b/app/src/main/kotlin/Constants.kt @@ -0,0 +1,3 @@ +const val SQLITE = "SQLite" +const val NEO4J = "Neo4j" +const val JSON = "JSON" diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt new file mode 100644 index 00000000..e20fff28 --- /dev/null +++ b/app/src/main/kotlin/Main.kt @@ -0,0 +1,28 @@ +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +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 java.awt.Dimension +import view.components.dialogWindows.SelectInitDialogWindow + +@Composable +@Preview +private fun App() { + MyAppTheme { + SelectInitDialogWindow().GraphInitDialogWindow(true) + } +} + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "WUDU", + state = rememberWindowState(position = WindowPosition(alignment = Alignment.Center)), + ) { + window.minimumSize = Dimension(1200, 700) + App() + } +} diff --git a/app/src/main/kotlin/MyAppTheme.kt b/app/src/main/kotlin/MyAppTheme.kt new file mode 100644 index 00000000..7260ea20 --- /dev/null +++ b/app/src/main/kotlin/MyAppTheme.kt @@ -0,0 +1,37 @@ +import androidx.compose.material.MaterialTheme +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +data class ColorPalette( + val primary: Color, + val secondary: Color, + val background: Color, + val surface: Color, + val secondaryVariant: Color, +) +// Make theme switchable +@Composable +fun MyAppTheme(content: @Composable () -> Unit) { + val mycolors = ColorPalette( + primary = Color(0xFF8AAAC6), + secondary = Color(0xFFAECCE4), + background = Color(0xFFE5F3FD), + secondaryVariant = Color(0xFFF5FBFF), + surface = Color.White, + ) + + fun ColorPalette.toMaterialColors() = lightColors( + primary = primary, + secondary = secondary, + background = background, + secondaryVariant = secondaryVariant, + surface = surface, + ) + + MaterialTheme( + colors = mycolors.toMaterialColors() + ) { + content() + } +} diff --git a/app/src/main/kotlin/model/algorithms/BridgesFinder.kt b/app/src/main/kotlin/model/algorithms/BridgesFinder.kt new file mode 100644 index 00000000..fe68d98a --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/BridgesFinder.kt @@ -0,0 +1,63 @@ +package model.algorithms + +import model.graphs.UndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import kotlin.math.min + +class BridgesFinder { + fun findBridges(graph: UndirectedGraph): List> { + val vertices = graph.getVertices() + val bridges = mutableListOf>() + + val graphSize = vertices.size + + val discoveryTime = MutableList(graphSize) { -1 } + val minDiscoveryTime = MutableList(graphSize) { -1 } + + val visitedList = MutableList(graphSize) { false } + val parentList = MutableList(graphSize) { -1 } + + var iterationCount = 0 + + fun doDFSToFindBridgesFromVertex(vertex: Vertex) { + visitedList[vertex.id] = true + + iterationCount++ + discoveryTime[vertex.id] = iterationCount + minDiscoveryTime[vertex.id] = iterationCount + + for (neighbour in graph.getNeighbours(vertex)) { + if (neighbour.id == parentList[vertex.id]) continue + + if (visitedList[neighbour.id]) { + minDiscoveryTime[vertex.id] = + min(minDiscoveryTime[vertex.id], discoveryTime[neighbour.id]) + + continue + } + + parentList[neighbour.id] = vertex.id + + doDFSToFindBridgesFromVertex(neighbour) + + minDiscoveryTime[vertex.id] = + min(minDiscoveryTime[vertex.id], minDiscoveryTime[neighbour.id]) + + if (minDiscoveryTime[neighbour.id] > discoveryTime[vertex.id]) { + val bridgeFound = graph.getEdge(vertex, neighbour) + bridges.add(bridgeFound) + } + } + } + + for (v in vertices) { + if (visitedList[v.id]) continue + + doDFSToFindBridgesFromVertex(v) + } + + return bridges + } +} diff --git a/app/src/main/kotlin/model/algorithms/CyclesFinder.kt b/app/src/main/kotlin/model/algorithms/CyclesFinder.kt new file mode 100644 index 00000000..50a2fa70 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/CyclesFinder.kt @@ -0,0 +1,110 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex + +class CyclesFinder { + fun findCycles(graph: DirectedGraph, srcVertex: Vertex): Set, Vertex>>> { + if (graph.getOutgoingEdges(srcVertex).isEmpty()) return emptySet() + + val SCCFinder = SCCFinder() + val sccs = SCCFinder.findSCC(graph) + val vertexSCC = sccs.find { srcVertex in it } + ?: throw NoSuchElementException("Vertex (${srcVertex.id}, ${srcVertex.data}) isn't in any of the SCCs") + + if (vertexSCC.size == 1) return emptySet() + + // create SCC subgraph + val subGraph = DirectedGraph() + + // map to restore original vertices + val verticesCopiesMap = mutableMapOf, Vertex>() + + for (originalVertex in vertexSCC) { + val vertexCopy = subGraph.addVertex(originalVertex.data) + verticesCopiesMap[vertexCopy] = originalVertex + } + + for (edge in graph.getEdges()) { + if (edge.vertex1 in vertexSCC && edge.vertex2 in vertexSCC) { + val vertex1copy = verticesCopiesMap.filterValues { it == edge.vertex1 }.keys.toList()[0] + val vertex2copy = verticesCopiesMap.filterValues { it == edge.vertex2 }.keys.toList()[0] + + subGraph.addEdge(vertex1copy, vertex2copy) + } + } + + val copyOfSrcVertex = verticesCopiesMap.filterValues { it == srcVertex }.keys.toList()[0] + + val blockedSet = mutableSetOf>() + val blockedMap = mutableMapOf, MutableSet>>() + val stack = ArrayDeque>() + val verticesCycles = mutableSetOf>>() + + fun DFSToFindCycles(currentVertex: Vertex): Boolean { + var cycleIsFound = false + + stack.addLast(currentVertex) + blockedSet.add(currentVertex) + + for (neighbour in subGraph.getNeighbours(currentVertex)) { + if (neighbour == copyOfSrcVertex) { + // cycle is found + stack.addLast(copyOfSrcVertex) + + val cycleOfVertices = mutableListOf>() + cycleOfVertices.addAll(stack) + verticesCycles.add(cycleOfVertices) + + stack.removeLast() + + cycleIsFound = true + } else if (neighbour !in blockedSet) { + // next iteration + cycleIsFound = DFSToFindCycles(neighbour) || cycleIsFound + } + } + + fun unblock(vertex: Vertex) { + blockedSet.remove(vertex) + blockedMap[vertex]?.forEach { unblock(it) } + blockedMap.remove(vertex) + } + + if (cycleIsFound) unblock(currentVertex) + else { + for (neighbour in subGraph.getNeighbours(currentVertex)) { + blockedMap[neighbour]?.add(currentVertex) + ?: blockedMap.put(neighbour, mutableSetOf(currentVertex)) + } + } + + stack.removeLast() + + return cycleIsFound + } + + DFSToFindCycles(copyOfSrcVertex) + + val cycles = mutableSetOf, Vertex>>>() + for (verticesCycle in verticesCycles) { + val originalVerticesCycle = mutableListOf>() + for (vertex in verticesCycle) originalVerticesCycle += verticesCopiesMap[vertex] + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the vertices map") + + val cycle = mutableListOf, Vertex>>() + + for (i in 0..originalVerticesCycle.size - 2) { + val v1 = originalVerticesCycle[i] + val v2 = originalVerticesCycle[i + 1] + + cycle.add(graph.getEdge(v1, v2) to v2) + } + + cycles.add(cycle) + } + + return cycles + } +} diff --git a/app/src/main/kotlin/model/algorithms/KeyVerticesFinder.kt b/app/src/main/kotlin/model/algorithms/KeyVerticesFinder.kt new file mode 100644 index 00000000..a73bfa4a --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/KeyVerticesFinder.kt @@ -0,0 +1,146 @@ +package model.algorithms + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import java.util.* +import java.util.ArrayDeque +import kotlin.math.roundToInt + +class KeyVerticesFinder { + /** + * For every vertex, calculates normalized closeness centrality, based on which the key vertices are picked. + * Formula was taken from "Efficient Top-k Closeness Centrality Search" by Paul W. Olsen et al., + * yet an easier algorithm for traversal was chosen. + */ + fun findKeyVertices(graph: Graph): Set>? { + val vertices = graph.getVertices() + val graphSize = vertices.size + + val distanceMap = graph.getWeightMap() + if (graph.hasNegativeEdges()) return null + + val centralityMap = mutableMapOf, Double>() + + for (currVertex in vertices) { + val currSumOfDistances = calcSumOfDistancesFromVertex(graph, currVertex, distanceMap, graphSize) + + val reachableNum = findReachableVerticesNumFromVertex(graph, currVertex, graphSize) + + val currCentrality = calcCentralityOfVertex(currSumOfDistances, reachableNum, graphSize) + centralityMap[currVertex] = currCentrality + } + + val keyVertices = pickMostKeyVertices(centralityMap, graphSize) + + return keyVertices + } + + /** + * Uses modified Dijkstra's algorithm to calculate the sum of all weights (distances) + * of shortest paths from source vertex to every other reachable one + */ + private fun calcSumOfDistancesFromVertex( + graph: Graph, + srcVertex: Vertex, + distanceMap: Map, Int>, + graphSize: Int + ): Int { + val POS_INF = 100_000_000 // to infinity and beyond + + val visited = Array(graphSize) { false } + + val distances = Array(graphSize) { POS_INF } + distances[srcVertex.id] = 0 + + // stores pairs of vertices and total distances to them, sorted by distances ascending + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }) + priorityQueue.add(srcVertex to distances[srcVertex.id]) + + while (priorityQueue.isNotEmpty()) { + val (currVertex, currDistance) = priorityQueue.poll() + + if (visited[currVertex.id]) continue + + visited[currVertex.id] = true + + val neighbours = graph.getNeighbours(currVertex) + for (neighbour in neighbours) { + val edgeToNeighbour = graph.getEdge(currVertex, neighbour) + + val edgeToNeighbourDistance = distanceMap[edgeToNeighbour] ?: POS_INF + + val totalDistanceToNeighbour = currDistance + edgeToNeighbourDistance + + if (totalDistanceToNeighbour < distances[neighbour.id]) { + distances[neighbour.id] = totalDistanceToNeighbour + priorityQueue.add(neighbour to totalDistanceToNeighbour) + } + } + } + + for (i in distances.indices) { + if (distances[i] == POS_INF) distances[i] = 0 + } + + val sum = distances.sum() + + return sum + } + + private fun findReachableVerticesNumFromVertex(graph: Graph, vertex: Vertex, graphSize: Int): Int { + var reachableVerticesNum = 0 + + val verticesToVisit = ArrayDeque>() + verticesToVisit.add(vertex) + + val visited = Array(graphSize) { false } + + while (verticesToVisit.isNotEmpty()) { + val vertexToVisit = verticesToVisit.poll() + + if (visited[vertexToVisit.id]) continue + + for (neighbour in graph.getNeighbours(vertexToVisit)) { + verticesToVisit.add(neighbour) + } + + visited[vertexToVisit.id] = true + verticesToVisit.add(vertexToVisit) + + reachableVerticesNum++ + } + + return reachableVerticesNum + } + + private fun calcCentralityOfVertex(sumOfDistances: Int, reachableNum: Int, graphSize: Int): Double { + if (sumOfDistances == 0) return 0.0 + + val centrality = + ((reachableNum - 1) * (reachableNum - 1)) / ((graphSize - 1) * sumOfDistances).toDouble() + + return centrality + } + + private fun pickMostKeyVertices(centralityMap: Map, Double>, graphSize: Int): Set> { + val keyVertices = mutableSetOf>() + + val percent = 0.2 + val keyVerticesNum = (graphSize * percent).roundToInt() // rounds up + + var currKeyVerticesNum = 0 + + val vertexCentralityPairs = centralityMap.toList() + val vertexCentralityPairsSorted = vertexCentralityPairs.sortedByDescending { it.second } + + for ((vertex, _) in vertexCentralityPairsSorted) { + if (currKeyVerticesNum >= keyVerticesNum) break + + keyVertices.add(vertex) + currKeyVerticesNum++ + } + + return keyVertices + } +} diff --git a/app/src/main/kotlin/model/algorithms/MinSpanningTreeFinder.kt b/app/src/main/kotlin/model/algorithms/MinSpanningTreeFinder.kt new file mode 100644 index 00000000..50887c84 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/MinSpanningTreeFinder.kt @@ -0,0 +1,53 @@ +package model.algorithms + +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge + +class MinSpanningTreeFinder { + fun findMinSpanningTree(graph: WeightedUndirectedGraph): List> { + val graphSize = graph.getVertices().size + + // set each vertex parent to be itself and each vertex rank to 0 + val parentIdList = Array(graphSize) { i: Int -> i } + val rankList = Array(graphSize) { 0 } + + fun findRootIdByVertexId(vId: Int): Int { + if (parentIdList[vId] == vId) return vId + + parentIdList[vId] = findRootIdByVertexId(parentIdList[vId]) + + return parentIdList[vId] + } + + fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { + val rootId1 = findRootIdByVertexId(vId1) + val rootId2 = findRootIdByVertexId(vId2) + + if (rootId1 == rootId2) return + + if (rankList[rootId1] < rankList[rootId2]) { + parentIdList[rootId1] = rootId2 + } else { + parentIdList[rootId2] = rootId1 + if (rankList[rootId1] == rankList[rootId2]) rankList[rootId1]++ + } + } + + val edgeWeightPairs = graph.getWeightMap().toList() + val sortedEdgeWeightPairs = edgeWeightPairs.sortedBy { it.second } + + val chosenEdges = mutableListOf>() + + for ((edge, _) in sortedEdgeWeightPairs) { + val id1 = edge.vertex1.id + val id2 = edge.vertex2.id + + if (findRootIdByVertexId(id1) != findRootIdByVertexId(id2)) { + uniteTwoTreesByVerticesIds(id1, id2) + chosenEdges.add(edge) + } + } + + return chosenEdges + } +} diff --git a/app/src/main/kotlin/model/algorithms/SCCFinder.kt b/app/src/main/kotlin/model/algorithms/SCCFinder.kt new file mode 100644 index 00000000..0c4b3cf1 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/SCCFinder.kt @@ -0,0 +1,64 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Vertex + +class SCCFinder { + // SCC - Strongly Connected Components (by Kosaraju) + fun findSCC(graph: DirectedGraph): Set>> { + val visited = mutableMapOf, Boolean>().withDefault { false } + val stack = ArrayDeque>() + val component = arrayListOf>() + val SCCs: MutableSet>> = mutableSetOf() + + fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { + visited[srcVertex] = true + componentList.add(srcVertex) + graph.getNeighbours(srcVertex).forEach { vertex2 -> + if (visited[vertex2] != true) { + auxiliaryDFS(vertex2, componentList) + } + } + stack.add(srcVertex) + } + + graph.getVertices().forEach { vertex -> + if (visited[vertex] != true) auxiliaryDFS(vertex, component) + } + + val reversedEdgesMap = reverseEdgesMap(graph) + visited.clear() + component.clear() + + fun reverseDFS(vertex: Vertex, componentList: MutableSet>) { + visited[vertex] = true + componentList.add(vertex) + reversedEdgesMap[vertex]?.forEach { vertex2 -> + if (visited[vertex2] != true) { + reverseDFS(vertex2, componentList) + } + } + } + + while (stack.isNotEmpty()) { + val vertex = stack.removeLast() + if (visited[vertex] != true) { + val currentComponent = mutableSetOf>() + reverseDFS(vertex, currentComponent) + SCCs.add(currentComponent) + } + } + + return SCCs + } + + private fun reverseEdgesMap(graph: DirectedGraph): Map, MutableSet>> { + val reversedEdgesMap = mutableMapOf, MutableSet>>() + graph.getVertices().forEach { reversedEdgesMap[it] = mutableSetOf() } + graph.getEdges().forEach { edge -> + reversedEdgesMap[edge.vertex2]?.add(edge.vertex1) + } + + return reversedEdgesMap + } +} diff --git a/app/src/main/kotlin/model/algorithms/ShortestPathFinder.kt b/app/src/main/kotlin/model/algorithms/ShortestPathFinder.kt new file mode 100644 index 00000000..28f4bdff --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/ShortestPathFinder.kt @@ -0,0 +1,180 @@ +package model.algorithms + +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import java.util.* + +class ShortestPathFinder { + fun findShortestPath(graph: Graph, srcVertex: Vertex, destVertex: Vertex): + List, Vertex>>? { + if (graph.hasNegativeEdges()) { + // there is no shortest path in undirected graphs with negative edges + if (graph is WeightedUndirectedGraph) return null + else return findShortestPathFordBellman(graph as WeightedDirectedGraph, srcVertex, destVertex) + } + + if (graph is WeightedDirectedGraph) return findShortestPathDijkstra(graph, srcVertex, destVertex) + if (graph is WeightedUndirectedGraph) return findShortestPathDijkstra(graph, srcVertex, destVertex) + + // if graph isn't weighted + return null + } + + private fun findShortestPathDijkstra( + graph: WeightedDirectedGraph, + srcVertex: Vertex, + destVertex: Vertex + ): List, Vertex>>? { + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } + val visited = mutableSetOf, Int>>() + + if (srcVertex == destVertex) return emptyList() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (currentVertex, currentDistance) = priorityQueue.poll() + if (visited.add(currentVertex to currentDistance)) { + graph.getNeighbours(currentVertex).forEach { adjacent -> + val currentEdge = graph.getEdge(currentVertex, adjacent) // Ensure correct edge direction + + val totalDist = currentDistance + graph.getWeight(currentEdge) + + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = currentVertex // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, Vertex>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + ?: return null // path doesn't exist + + val currentEdge = graph.getEdge(predecessor, currentVertex) + path.add(currentEdge to currentVertex) + + currentVertex = predecessor + } + return path.reversed() + } + + private fun findShortestPathDijkstra( + graph: WeightedUndirectedGraph, + srcVertex: Vertex, + destVertex: Vertex + ): List, Vertex>>? { + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } + val visited = mutableSetOf, Int>>() + + if (srcVertex == destVertex) return emptyList() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (currentVertex, currentDistance) = priorityQueue.poll() + if (visited.add(currentVertex to currentDistance)) { + graph.getNeighbours(currentVertex).forEach { adjacent -> + val currentEdge = graph.getEdge(currentVertex, adjacent) // Ensure correct edge direction + + val totalDist = currentDistance + graph.getWeight(currentEdge) + + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = currentVertex // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, Vertex>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + ?: return null // path doesn't exist + + val currentEdge = graph.getEdge(predecessor, currentVertex) + path.add(currentEdge to currentVertex) + + currentVertex = predecessor + } + return path.reversed() + } + + // returns null if path doesn't exist + private fun findShortestPathFordBellman( + graph: WeightedDirectedGraph, + srcVertex: Vertex, + destVertex: Vertex + ): List, Vertex>>? { + val NEG_INF = -1000000 + + val vertices = graph.getVertices() + val edges = graph.getEdges() + + val distance = MutableList(vertices.size) { Int.MAX_VALUE } + val predecessor = MutableList?>(vertices.size) { null } + + distance[srcVertex.id] = 0 + + for (i in 0..vertices.size - 1) { + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + graph.getWeight(edge)) { + // distance will equal negative infinity if there is negative cycle + distance[v2.id] = maxOf(distance[v1.id] + graph.getWeight(edge), NEG_INF) + + predecessor[v2.id] = v1 + } + } + } + + // check for negative cycles, determine if path to destVertex exists + for (i in 0..vertices.size - 1) { + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + graph.getWeight(edge)) { + distance[v2.id] = NEG_INF + + } + } + } + + // there is a negative cycle on the way, so path doesn't exist + if (distance[destVertex.id] == NEG_INF) return null + + if (srcVertex == destVertex) return emptyList() + + // reconstruct the path from srcVertex to destVertex + val path: MutableList, Vertex>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val currentPredecessor = predecessor[currentVertex.id] + ?: return null // path doesn't exist + + path.add(graph.getEdge(currentPredecessor, currentVertex) to currentVertex) + + currentVertex = currentPredecessor + } + + return path.reversed() + } +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt b/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt new file mode 100644 index 00000000..2312d723 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt @@ -0,0 +1,68 @@ +package model.algorithms.clustering + +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import model.algorithms.clustering.implementation.Link +import model.algorithms.clustering.implementation.getPartition + +class CommunitiesFinder { + fun findCommunities(graph: Graph): Set>> { + if (graph.getVertices().size == 1) return setOf(setOf(graph.getVertices()[0])) + + val links = convertToAPIFormat(graph) + + val result = getPartition(links) + + return convertResult(result, graph) + } + + private fun convertToAPIFormat(graph: Graph): List { + val links = mutableListOf() + + for (edge in graph.getEdges()) { + val id1 = edge.vertex1.id + val id2 = edge.vertex2.id + + val weight: Double = + when (graph) { + is WeightedUndirectedGraph -> graph.getWeight(edge).toDouble() + is WeightedDirectedGraph -> graph.getWeight(edge).toDouble() + else -> 1.0 + } + + links += EdgeLink(id1, id2, weight) + } + + return links + } + + private fun convertResult(resultMap: Map, graph: Graph): Set>> { + val vertices = graph.getVertices() + val communities = mutableSetOf>>() + + var currentCommunityId = 0 + var currentCommunity = mutableSetOf>() + + for (vertexId in resultMap.keys) { + if (resultMap[vertexId] == currentCommunityId) currentCommunity += vertices[vertexId] + else { + communities += currentCommunity + + currentCommunityId++ + currentCommunity = mutableSetOf() + + currentCommunity += vertices[vertexId] + } + } + + return communities + } + + inner class EdgeLink(private val vertex1Id: Int, private val vertex2Id: Int, val weight: Double) : Link { + override fun source() = vertex1Id + override fun target() = vertex2Id + override fun weight() = weight + } +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/API.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/API.kt new file mode 100644 index 00000000..5f6230eb --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/API.kt @@ -0,0 +1,33 @@ +/** + * 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. + */ + +/** + * This file was changed compared to the source (one public method was deleted) + */ + +package model.algorithms.clustering.implementation + +/** + * Runs Louvain algorithm to approximate the best modularity partition and returns a corresponding mapping of nodes to communities. + * If depth > 0 then algorithm tries to split large communities into smaller ones depth times recursively. + * + * @param depth Number of attempts to split large communities + * @return Map: nodeIndex -> communityIndex + */ +fun getPartition(links: List, depth: Int = 0): Map { + val louvain = Louvain(links) + louvain.optimizeModularity(depth) + return louvain.resultingCommunities() +} + diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Community.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Community.kt new file mode 100644 index 00000000..22e8ef09 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Community.kt @@ -0,0 +1,100 @@ +/** + * 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. + */ + +package model.algorithms.clustering.implementation + +import kotlin.math.pow +import kotlin.math.sqrt + +internal fun Community(nodeIndex: Int, graph: List): Community { + val node = graph[nodeIndex] + return Community(node.selfLoopsWeight, node.outDegree, mutableSetOf(nodeIndex)) +} + +internal class Community( + private var selfLoopsWeight: Double = 0.0, + private var outLinksWeight: Double = 0.0, + val nodes: MutableSet = mutableSetOf() +) { + + private fun totalWeightsSum() = selfLoopsWeight + outLinksWeight + + fun addNode(index: Int, nodes: List) { + val node = nodes[index] + node.incidentLinks.forEach { link -> + if (link.to in this.nodes) { + selfLoopsWeight += 2 * link.weight + outLinksWeight -= link.weight + } else { + outLinksWeight += link.weight + } + } + selfLoopsWeight += node.selfLoopsWeight + this.nodes.add(index) + } + + fun removeNode(index: Int, nodes: List): Boolean { + val node = nodes[index] + node.incidentLinks.forEach { link -> + if (link.to in this.nodes) { + selfLoopsWeight -= 2 * link.weight + outLinksWeight += link.weight + } else { + outLinksWeight -= link.weight + } + } + this.nodes.remove(index) + selfLoopsWeight -= node.selfLoopsWeight + return this.nodes.size == 0 + } + + fun modularityChangeIfNodeAdded(node: Node, graphWeight: Double): Double = + (1 / graphWeight) * (weightsToNode(node) - totalWeightsSum() * node.degree() / (2 * graphWeight)) + + private fun weightsToNode(node: Node): Double = node.incidentLinks.filter { it.to in nodes }.sumOf { it.weight } + + fun computeModularity(graphWeight: Double): Double = (selfLoopsWeight / (2 * graphWeight)) - (totalWeightsSum() / (2 * graphWeight)).pow(2) + + fun toLouvainNode(nodes: List): Node { + val newIndex = nodes[this.nodes.first()].community + val consumedNodes = this.nodes.flatMap { nodes[it].originalNodes }.toSet() + var newSelfLoopsWeight = 0.0 + + val incidentLinksMap = mutableMapOf() + this.nodes.forEach { nodeIndex -> + newSelfLoopsWeight += nodes[nodeIndex].selfLoopsWeight + nodes[nodeIndex].incidentLinks.forEach { link -> + val toNewNode = nodes[link.to].community + if (toNewNode != newIndex) { + if (toNewNode in incidentLinksMap) { + incidentLinksMap[toNewNode] = incidentLinksMap[toNewNode]!! + link.weight + } else { + incidentLinksMap[toNewNode] = link.weight + } + } else { + newSelfLoopsWeight += link.weight + } + } + } + val links = incidentLinksMap.map { InternalLink(it.key, it.value) } + + return Node(newIndex, consumedNodes, links, newSelfLoopsWeight) + } + + /** + * If communities size is less than sqrt(2 * graphWeight) then merging it with another one will always increase modularity. + * Hence, if community size is greater than sqrt(2 * graphWeight), it might actually consist of several smaller communities. + */ + fun overResolutionLimit(graphWeight: Double): Boolean = selfLoopsWeight >= sqrt(2 * graphWeight) +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Link.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Link.kt new file mode 100644 index 00000000..284c5971 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Link.kt @@ -0,0 +1,21 @@ +/** + * 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. + */ + +package model.algorithms.clustering.implementation + +interface Link { + fun source(): Int + fun target(): Int + fun weight(): Double +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Louvain.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Louvain.kt new file mode 100644 index 00000000..973df394 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Louvain.kt @@ -0,0 +1,214 @@ +/** + * 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. + */ + +package model.algorithms.clustering.implementation + +/** + * Class that encapsulates the Louvain algorithm. + */ +internal class Louvain( + private val links: List +) { + private var communities: MutableMap + private var nodes: List = emptyList() + private var graphWeight: Double + private val originalNodesNumber: Int + + init { + buildNodesFromLinks() + originalNodesNumber = nodes.size + communities = nodes.withIndex().associate { it.index to Community(it.index, nodes) }.toMutableMap() + graphWeight = computeGraphWeight() + } + + private fun buildNodesFromLinks() { + val nodeIndices = links.flatMap { listOf(it.source(), it.target()) }.distinct().sorted() + val mutableNodes = nodeIndices + .withIndex() + .associateBy({ it.value }, { MutableNode(it.index, setOf(it.value)) }) + links.forEach { link -> + if (link.source() == link.target()) { + mutableNodes[link.source()]!!.selfLoopsWeight += 2 * link.weight() + } else { + val newSource = mutableNodes[link.source()]!!.community + val newTarget = mutableNodes[link.target()]!!.community + mutableNodes[link.source()]!!.incidentLinks.add(InternalLink(newTarget, link.weight())) + mutableNodes[link.target()]!!.incidentLinks.add(InternalLink(newSource, link.weight())) + } + } + nodes = mutableNodes.values.map { it.toNode() } + } + + private fun computeGraphWeight() = + nodes.sumOf { n -> n.incidentLinks.sumOf { l -> l.weight } + n.selfLoopsWeight } / 2 + + private fun aggregateCommunities() { + // re-index communities in nodes + communities.values.withIndex().forEach { (newIndex, community) -> + community.nodes.forEach { nodeIndex -> + nodes[nodeIndex].community = newIndex + } + } + + val newNodes = communities.values.map { it.toLouvainNode(nodes) } + val newCommunities = + newNodes.withIndex().associateBy({ it.index }, { Community(it.index, nodes) }).toMutableMap() + + nodes = newNodes + communities = newCommunities + } + + private fun moveNode(nodeIndex: Int, node: Node, toCommunityIndex: Int) { + val from = communities[node.community]!! + if (from.removeNode(nodeIndex, nodes)) { + communities.remove(node.community) + } + node.community = toCommunityIndex + communities[toCommunityIndex]!!.addNode(nodeIndex, nodes) + } + + private fun computeCostOfMovingOut(index: Int, node: Node): Double { + val theCommunity = communities[node.community]!! + theCommunity.removeNode(index, nodes) + val cost = theCommunity.modularityChangeIfNodeAdded(node, graphWeight) + theCommunity.addNode(index, nodes) + return cost + } + + /** + * Step I of the algorithm: + * For each node i evaluate the gain in modularity if node i is moved to the community of one of its neighbors j. + * Then move node i in the community for which the modularity gain is the largest, but only if this gain is positive. + * This process is applied to all nodes until no further improvement can be achieved, completing Step I. + * @see optimizeModularity + */ + private fun findLocalMaxModularityPartition() { + var repeat = true + while (repeat) { + repeat = false + for ((i, node) in nodes.withIndex()) { + var bestCommunity = node.community + var maxDeltaM = 0.0 + val costOfMovingOut = computeCostOfMovingOut(i, node) + for (communityIndex in node.neighbourCommunities(nodes)) { + if (communityIndex == node.community) { + continue + } + val toCommunity = communities[communityIndex]!! + val deltaM = toCommunity.modularityChangeIfNodeAdded(node, graphWeight) - costOfMovingOut + if (deltaM > maxDeltaM) { + bestCommunity = communityIndex + maxDeltaM = deltaM + } + } + if (bestCommunity != node.community) { + moveNode(i, node, bestCommunity) + repeat = true + } + } + } + } + + fun computeModularity() = communities.values.sumOf { it.computeModularity(graphWeight) } + + fun optimizeModularity(depth: Int = 0) { + var bestModularity = computeModularity() + var bestCommunities = communities + var bestNodes = nodes + do { + val from = communities.size + findLocalMaxModularityPartition() + aggregateCommunities() + val newModularity = computeModularity() + if (newModularity > bestModularity) { + bestModularity = newModularity + bestCommunities = communities + bestNodes = nodes + } + } while (communities.size != from) + communities = bestCommunities + nodes = bestNodes + if (communities.size != 1 && depth != 0) { + refine(depth) + } + } + + fun resultingCommunities(): Map { + val communitiesMap = mutableMapOf() + communities.forEach { (communityIndex, community) -> + community.nodes.forEach { nodeIndex -> + val node = nodes[nodeIndex] + node.originalNodes.forEach { + communitiesMap[it] = communityIndex + } + } + } + return communitiesMap + } + + fun assignCommunities(communitiesMap: Map) { + communities.clear() + buildNodesFromLinks() + buildCommunitiesFromMap(communitiesMap) + } + + private fun buildCommunitiesFromMap(communitiesMap: Map) { + // create all necessary communities + for (entry in communitiesMap) { + val communityIndex = entry.value + if (communityIndex !in communities.keys) { + communities[communityIndex] = Community() + } + } + + val nodeIndicesMap = communitiesMap.keys.sorted().withIndex().associateBy({ it.value }, { it.index }) + + // distribute the nodes among communities + for (entry in communitiesMap) { + val nodeIndex = nodeIndicesMap[entry.key]!! + val communityIndex = entry.value + + nodes[nodeIndex].community = -1 + communities[communityIndex]!!.addNode(nodeIndex, nodes) + nodes[nodeIndex].community = communityIndex + } + } + + private fun refine(depth: Int = 0) { + var communitiesMap = resultingCommunities() + var resultingCommunitiesNumber = communitiesMap.values.distinct().size + links + .filter { communitiesMap[it.source()] == communitiesMap[it.target()] } + .groupBy({ communitiesMap[it.source()]!! }, { it }) + .filter { communities[it.key]!!.overResolutionLimit(graphWeight) } + .forEach { (communityIndex, links) -> + val thisLouvain = Louvain(links) + thisLouvain.optimizeModularity(depth - 1) + val thisMap = thisLouvain.resultingCommunities() + val reindex = reIndexMap(thisMap, communityIndex, resultingCommunitiesNumber) + communitiesMap = communitiesMap + reindex + resultingCommunitiesNumber = communitiesMap.values.distinct().size + } + assignCommunities(communitiesMap) + } + + private fun reIndexMap(theMap: Map, saveIndex: Int, startFrom: Int) = theMap + .mapValues { (_, communityIndex) -> + if (communityIndex == 0) { + saveIndex + } else { + communityIndex + startFrom - 1 + } + } +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Node.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Node.kt new file mode 100644 index 00000000..a7ec080d --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Node.kt @@ -0,0 +1,55 @@ +/** + * 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. + */ + +package model.algorithms.clustering.implementation + +/** + * Simple link representation used inside Node and Louvain algorithm. + */ +internal class InternalLink( + val to: Int, + val weight: Double +) + +/** + * @param community Index of a community to which the node has been assigned. + * @param selfLoopsWeight Weight of all links that start and end at originalNodes making up this node * 2. + */ +internal sealed class BaseNode( + var community: Int, + val originalNodes: Set, + open val incidentLinks: List, + open val selfLoopsWeight: Double +) { + fun neighbourCommunities(nodes: List) = incidentLinks.map { nodes[it.to].community }.distinct().filter { it != community } +} + +internal class Node( + community: Int, + originalNodes: Set, + incidentLinks: List, + selfLoopsWeight: Double = 0.0 +) : BaseNode(community, originalNodes, incidentLinks, selfLoopsWeight) { + val outDegree = incidentLinks.sumOf { it.weight } + fun degree() = outDegree + selfLoopsWeight +} + +internal class MutableNode( + community: Int, + originalNodes: Set, + override val incidentLinks: MutableList = mutableListOf(), + override var selfLoopsWeight: Double = 0.0 +) : BaseNode(community, originalNodes, incidentLinks, selfLoopsWeight) { + fun toNode(): Node = Node(community, originalNodes, incidentLinks) +} diff --git a/app/src/main/kotlin/model/graphs/DirectedGraph.kt b/app/src/main/kotlin/model/graphs/DirectedGraph.kt new file mode 100644 index 00000000..206f35e0 --- /dev/null +++ b/app/src/main/kotlin/model/graphs/DirectedGraph.kt @@ -0,0 +1,58 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import kotlin.NoSuchElementException +import kotlin.collections.ArrayDeque +import kotlin.collections.ArrayList + +open class DirectedGraph : Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + + if (vertex1 !in vertices || vertex2 !in vertices) + throw IllegalArgumentException( + "One of the vertices (${vertex1.id}, ${vertex1.data}) and " + + "(${vertex2.id}, ${vertex2.data}) isn't in the graph" + ) + + // Don't do anything if the edge is already in the graph + if (vertex2 in getNeighbours(vertex1)) return getEdge(vertex1, vertex2) + + val newEdge = Edge(vertex1, vertex2) + edges.add(newEdge) + + outgoingEdgesMap[vertex1]?.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + + return newEdge + } + + override fun removeEdge(edgeToRemove: Edge): Edge { + if (edgeToRemove !in edges) throw NoSuchElementException( + "Edge between vertices (${edgeToRemove.vertex1.id}, ${edgeToRemove.vertex1.data}) and " + + "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) isn't in the graph" + ) + + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 + + edges.remove(edgeToRemove) + + outgoingEdgesMap[vertex1]?.remove(edgeToRemove) + adjacencyMap[vertex1]?.remove(vertex2) + + return edgeToRemove + } + + override fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { + val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } + ?: throw NoSuchElementException( + "No edge between vertices (${vertex1.id}, ${vertex1.data}) and (${vertex2.id}, ${vertex2.data})" + ) + + return edge + } +} diff --git a/app/src/main/kotlin/model/graphs/UndirectedGraph.kt b/app/src/main/kotlin/model/graphs/UndirectedGraph.kt new file mode 100644 index 00000000..47a5d361 --- /dev/null +++ b/app/src/main/kotlin/model/graphs/UndirectedGraph.kt @@ -0,0 +1,64 @@ +package model.graphs + +import kotlin.math.min +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex + +open class UndirectedGraph : Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself") + + if (vertex1 !in vertices || vertex2 !in vertices) + throw IllegalArgumentException( + "One of vertices (${vertex1.id}, ${vertex1.data}) and " + + "(${vertex2.id}, ${vertex2.data}) isn't in the graph" + ) + + // Don't do anything if the edge is already in the graph + if (vertex2 in getNeighbours(vertex1)) return getEdge(vertex1, vertex2) + + val newEdge = Edge(vertex1, vertex2) + edges.add(newEdge) + + outgoingEdgesMap[vertex1]?.add(newEdge) + outgoingEdgesMap[vertex2]?.add(newEdge) + + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) + + return newEdge + } + + override fun removeEdge(edgeToRemove: Edge): Edge { + if (edgeToRemove !in edges) + throw NoSuchElementException( + "Edge between vertices (${edgeToRemove.vertex1.id}, ${edgeToRemove.vertex1.data}) and " + + "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) isn't in the graph" + ) + + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 + + edges.remove(edgeToRemove) + + outgoingEdgesMap[vertex1]?.remove(edgeToRemove) + outgoingEdgesMap[vertex2]?.remove(edgeToRemove) + + adjacencyMap[vertex1]?.remove(vertex2) + adjacencyMap[vertex2]?.remove(vertex1) + + return edgeToRemove + } + + override fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { + val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } + ?: outgoingEdgesMap[vertex2]?.find { it.isIncident(vertex1) } + ?: throw NoSuchElementException( + "No edge between vertices (${vertex1.id}, ${vertex1.data}) and (${vertex2.id}, ${vertex2.data})" + ) + + return edge + } +} diff --git a/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt new file mode 100644 index 00000000..11e46262 --- /dev/null +++ b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt @@ -0,0 +1,47 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import java.util.* +import kotlin.NoSuchElementException + +class WeightedDirectedGraph : DirectedGraph() { + private val weightMap: MutableMap, Int> = mutableMapOf() + + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): Edge { + val newEdge = super.addEdge(vertex1, vertex2) + + weightMap[newEdge] = weight + + return newEdge + } + + // In case weight is not passed, set it to default value = 1 + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + + override fun removeEdge(edgeToRemove: Edge): Edge { + val removedEdge = super.removeEdge(edgeToRemove) + + weightMap.remove(edgeToRemove) + + return removedEdge + } + + fun getWeight(edge: Edge): Int { + val weight = weightMap[edge] + ?: throw NoSuchElementException( + "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data}) " + + "and (${edge.vertex2.id}, ${edge.vertex2.data})" + ) + + return weight + } + + override fun getWeightMap() = weightMap.toMutableMap() + + override fun hasNegativeEdges(): Boolean { + val edgeWeighs = getWeightMap().values + + return edgeWeighs.any { it < 0 } + } +} diff --git a/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt new file mode 100644 index 00000000..a1d6bb27 --- /dev/null +++ b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt @@ -0,0 +1,48 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import java.util.* +import kotlin.NoSuchElementException + +class WeightedUndirectedGraph : UndirectedGraph() { + private val weightMap: MutableMap, Int> = mutableMapOf() + + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): Edge { + val newEdge = super.addEdge(vertex1, vertex2) + + weightMap[newEdge] = weight + + return newEdge + } + + + // In case weight is not passed, set it to default value = 1 + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + + override fun removeEdge(edgeToRemove: Edge): Edge { + val removedEdge = super.removeEdge(edgeToRemove) + + weightMap.remove(edgeToRemove) + + return removedEdge + } + + fun getWeight(edge: Edge): Int { + val weight = weightMap[edge] + ?: throw NoSuchElementException( + "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data}) " + + "and (${edge.vertex2.id}, ${edge.vertex2.data})" + ) + + return weight + } + + override fun getWeightMap() = weightMap.toMutableMap() + + override fun hasNegativeEdges(): Boolean { + val edgeWeighs = getWeightMap().values + + return edgeWeighs.any { it < 0 } + } +} diff --git a/app/src/main/kotlin/model/graphs/abstractGraph/Edge.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Edge.kt new file mode 100644 index 00000000..d8344eef --- /dev/null +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Edge.kt @@ -0,0 +1,5 @@ +package model.graphs.abstractGraph + +class Edge(val vertex1: Vertex, val vertex2: Vertex) { + fun isIncident(vertex: Vertex) = (vertex1 == vertex || vertex2 == vertex) +} diff --git a/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt new file mode 100644 index 00000000..c3e1fb0c --- /dev/null +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt @@ -0,0 +1,93 @@ +package model.graphs.abstractGraph + +import kotlin.NoSuchElementException +import kotlin.collections.ArrayList + +abstract class Graph { + protected val vertices: ArrayList> = arrayListOf() + protected val edges: MutableSet> = mutableSetOf() + + protected val adjacencyMap: MutableMap, MutableSet>> = mutableMapOf() + protected val outgoingEdgesMap: MutableMap, MutableSet>> = mutableMapOf() + + private var nextId = 0 + + fun addVertex(data: D): Vertex { + val newVertex = Vertex(nextId++, data) + + outgoingEdgesMap[newVertex] = mutableSetOf() + adjacencyMap[newVertex] = mutableSetOf() + + vertices.add(newVertex) + + return newVertex + } + + fun removeVertex(vertexToRemove: Vertex): Vertex { + nextId-- + + val adjacentVertices = getNeighbours(vertexToRemove) + for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToRemove) + + // iterator is used because an element can't be removed in a for loop + val iterator = edges.iterator() + while (iterator.hasNext()) { + val edge = iterator.next() + if (edge.isIncident(vertexToRemove)) { + iterator.remove() + + val incidentVertex = if (edge.vertex1 == vertexToRemove) edge.vertex2 else edge.vertex1 + outgoingEdgesMap[incidentVertex]?.remove(edge) + adjacencyMap[incidentVertex]?.remove(vertexToRemove) + } + } + + val lastAddedVertex = vertices[nextId] + lastAddedVertex.id = vertexToRemove.id + vertices[vertexToRemove.id] = lastAddedVertex + + vertices.remove(vertexToRemove) + adjacencyMap.remove(vertexToRemove) + outgoingEdgesMap.remove(vertexToRemove) + + return vertexToRemove + } + + abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge + + abstract fun removeEdge(edgeToRemove: Edge): Edge + + fun getEdges() = edges.toList() + + fun getVertices() = vertices.toList() + + /** + * In unweighted graph, returns a map with every edge as a key and 1 as a value + * In a weighted graph, returns copy of weightMap property + */ + open fun getWeightMap(): MutableMap, Int> { + val weightMap = mutableMapOf, Int>() + + for (edge in edges) weightMap[edge] = 1 + + return weightMap + } + + fun getNeighbours(vertex: Vertex): List> { + val neighbours = adjacencyMap[vertex] + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") + + return neighbours.toList() + } + + fun getOutgoingEdges(vertex: Vertex): List> { + val outgoingEdges = outgoingEdgesMap[vertex] + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the outgoing edges map.") + + return outgoingEdges.toList() + } + + abstract fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge + + open fun hasNegativeEdges() = false +} diff --git a/app/src/main/kotlin/model/graphs/abstractGraph/Vertex.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Vertex.kt new file mode 100644 index 00000000..d8770fe7 --- /dev/null +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Vertex.kt @@ -0,0 +1,3 @@ +package model.graphs.abstractGraph + +class Vertex(var id: Int, val data: D) diff --git a/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt b/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt new file mode 100644 index 00000000..d5485e9e --- /dev/null +++ b/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt @@ -0,0 +1,183 @@ +package model.io.neo4j + +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase +import org.neo4j.driver.Record +import java.io.Closeable + +const val DIR_LABEL = "POINTS_TO" +const val UNDIR_LABEL = "CONNECTED_TO" + +class Neo4jRepository(uri: String, user: String, password: String) : Closeable { + private val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + private val session = driver.session() + + fun getGraphNames(): List { + val result = session.executeRead { tx -> + tx.run( + "MATCH (v) " + + "RETURN " + + "distinct labels(v) AS label" + ).list() + } + + val names = mutableListOf() + for (record in result) { + val name = record["label"].toString().drop(2).dropLast(2) + names.add(name) + } + + return names + } + + fun saveOrReplaceGraph(graph: Graph, name: String, isDirected: Boolean, isWeighted: Boolean) { + clearGraph(name) + + val vertices = graph.getVertices() + val edges = graph.getEdges() + + val weightMap = if (isWeighted) graph.getWeightMap() else null + val edgeLabel = if (isDirected) DIR_LABEL else UNDIR_LABEL + + session.executeWrite { tx -> + for (vertex in vertices) { + val id = vertex.id + + val data = vertex.data.toString() + + tx.run( + "CREATE (:$name {id:$id, data:$data}) " + ) + } + + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + val id1 = v1.id + val id2 = v2.id + + val data1 = v1.data.toString() + val data2 = v2.data.toString() + + val weight = weightMap?.get(edge) + + tx.run( + "MATCH (v:$name {id:$id1, data:$data1}) " + + "MATCH (u:$name {id:$id2, data:$data2}) " + + "CREATE (v)-[:$edgeLabel {weight:$weight}]->(u) " + ) + } + } + } + + private fun clearGraph(name: String) { + session.executeWrite { tx -> + tx.run( + "MATCH (v:$name)-[e]->(u:$name) " + + "DELETE v, e, u " + ) + } + } + + fun loadGraph(name: String): Triple, Boolean, Boolean> { + val graphContents = readGraphContents(name) + + val isDirected = hasDirection(graphContents[0]) + val isWeighted = hasWeight(graphContents[0]) + + val graph = initializeGraph(isWeighted, isDirected) + + val graphSize = getStoredGraphSize(name) + + val wasVertexLoaded = MutableList(graphSize) { false } + val addedVerticesList = MutableList(graphSize) { Vertex(-1, "-1") } + + for (content in graphContents) { + val id1 = content["id1"].asInt() + val data1 = content["data1"].toString() + + val id2 = content["id2"].asInt() + val data2 = content["data2"].toString() + + val weightString = content["weight"].toString() + val edgeWeight = if (weightString != "NULL") weightString.toInt() else null + + if (!wasVertexLoaded[id1]) { + wasVertexLoaded[id1] = true + addedVerticesList[id1] = graph.addVertex(data1) + } + if (!wasVertexLoaded[id2]) { + wasVertexLoaded[id2] = true + addedVerticesList[id2] = graph.addVertex(data2) + } + + val v1 = addedVerticesList[id1] + val v2 = addedVerticesList[id2] + + if (edgeWeight == null) { + graph.addEdge(v1, v2) + } else if (isDirected) { + graph as WeightedDirectedGraph + graph.addEdge(v1, v2, edgeWeight) + } else { + graph as WeightedUndirectedGraph + graph.addEdge(v1, v2, edgeWeight) + } + } + + return Triple(graph, isWeighted, isDirected) + } + + private fun initializeGraph(isWeighted: Boolean, isDirected: Boolean): Graph { + return if (isWeighted && isDirected) WeightedDirectedGraph() + else if (isWeighted) WeightedUndirectedGraph() + else if (isDirected) DirectedGraph() + else UndirectedGraph() + } + + private fun readGraphContents(name: String) = session.executeRead { tx -> + tx.run( + "MATCH (v:$name)-[e]->(u:$name) " + + "RETURN " + + "v.id AS id1, " + + "v.data AS data1, " + + "u.id AS id2, " + + "u.data AS data2, " + + "e.weight AS weight, " + + "type(e) AS relationType " + ).list() + } + + private fun getStoredGraphSize(name: String): Int { + val result = session.executeRead { tx -> + tx.run( + "MATCH (v:$name) " + + "RETURN count(v) AS size " + ).list() + } + + val size = result[0]["size"].asInt() + + return size + } + + private fun hasWeight(graphRecord: Record): Boolean { + return graphRecord["weight"].toString() != "NULL" + } + + private fun hasDirection(graphRecord: Record): Boolean { + return graphRecord["relationType"].asString() == DIR_LABEL + } + + override fun close() { + session.close() + driver.close() + } +} diff --git a/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt b/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt new file mode 100644 index 00000000..8de1f654 --- /dev/null +++ b/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt @@ -0,0 +1,87 @@ +package model.io.neo4j + +import model.graphs.abstractGraph.Graph +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase + +object Neo4jRepositoryHandler { + var neo4jRepo: Neo4jRepository? = null + private set + + var isRepoInit = false + private set + + fun initRepo(uri: String, user: String, password: String): Boolean { + if (isRepoInit) return true + if (!checkIfCredentialsAreValid(uri, user, password)) return false + + neo4jRepo = Neo4jRepository(uri, user, password) + isRepoInit = true + + return true + } + + fun getNames(): List? { + if (!isRepoInit) return null + + val names = neo4jRepo?.getGraphNames() + + return names + } + + fun saveOrReplace(graph: Graph, name: String, isDirected: Boolean, isWeighted: Boolean): Boolean { + if (!isRepoInit || !isValidNeo4jName(name)) return false + + neo4jRepo?.saveOrReplaceGraph(graph, name, isDirected, isWeighted) + + return true + } + + fun loadGraph(name: String): Triple, Boolean, Boolean>? { + if (!isRepoInit || !isValidNeo4jName(name)) return null + + val graphWithInfo = neo4jRepo?.loadGraph(name) ?: return null + val graph = graphWithInfo.first + val isWeighed = graphWithInfo.second + val isDirected = graphWithInfo.third + + return Triple(graph, isWeighed, isDirected) + } + + fun isValidNeo4jName(name: String): Boolean { + if (name.isEmpty()) return false + for (i in name.indices) { + val isValidChar = when (name[i].code) { + 45 -> { // - + if (i!= 0) true else false + } + in 48..57 -> { // 0-9 + if (i != 0) true else false + } + in 65..90 -> true // A-Z + 95 -> true // _ + in 97..122 -> true // a-z + else -> false + } + + if (isValidChar) continue else return false + } + + return true + } + + private fun checkIfCredentialsAreValid(uri: String, user: String, password: String): Boolean { + try { + val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + driver.session().executeWrite { tx -> + tx.run { + "MATCH (v) RETURN v LIMIT 1" + } + } + } catch (e: Exception) { + return false + } + + return true + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt new file mode 100644 index 00000000..730c604f --- /dev/null +++ b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt @@ -0,0 +1,290 @@ +package model.io.sql + +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.* +import model.graphs.abstractGraph.Graph +import view.MainScreen +import view.components.dialogWindows.ErrorWindow +import viewmodel.MainScreenViewModel +import viewmodel.graph.GraphViewModel +import viewmodel.graph.GraphViewModelFactory +import viewmodel.graph.getGraphVMParameter +import java.io.File +import java.sql.* +import kotlin.system.exitProcess + +object SQLDatabaseModule { + private const val DB_DIRECTORY = "database" + private const val DB_NAME = "my_graph_database.db" + private const val QUERY_NAME = "queries.txt" + private const val QUERY_DIRECTORY = "app/src/main/kotlin/model/io/sql/" + private val DB_URL = "jdbc:sqlite:${File(DB_DIRECTORY, DB_NAME).absolutePath}" + + private fun readQueriesFromFile(): String { + val fileContent = File(QUERY_DIRECTORY, QUERY_NAME).readText() + return fileContent.trim() // Trim any leading/trailing whitespace + } + + val insertQueries = readQueriesFromFile() + + init { + createDatabaseDirectory() + createTables() + } + + private fun createDatabaseDirectory() { + val dbDir = File(DB_DIRECTORY) + if (!dbDir.exists()) { + dbDir.mkdirs() + } + } + + private fun getConnection(): Connection { + return DriverManager.getConnection(DB_URL) + } + + private fun createTables() { + val createTableSQL = insertQueries.split(":")[7].trimIndent() + + val connection = getConnection() + connection.use { conn -> + conn.createStatement().use { statement -> + statement.executeUpdate(createTableSQL) + } + } + } + + fun importGraph( + graphId: Int, currentGraphSetup: Pair, String>? + ): Pair, String>? { + var currentGS = currentGraphSetup + val connection = SQLDatabaseModule.getConnection() + connection.use { + val selectGraphSQL = SQLDatabaseModule.insertQueries.split(":")[6] + it.prepareStatement(selectGraphSQL).use { statement -> + statement.setInt(1, graphId) + val resultSet = statement.executeQuery() + + if (resultSet.next()) { + currentGS = SQLDatabaseModule.importGraphInfo(graphId) + } else { + currentGS = null + throw SQLException("Graph with ID $graphId not found.") + } + } + } + return currentGS + } + + + fun insertGraph(graphVM: GraphViewModel, graphName: String, graphType: String) { + val insertGraphSQL = insertQueries.split(":")[0] + val insertEdgesSQL = insertQueries.split(":")[1] + val insertVerticesSQL = insertQueries.split(":")[2] + + getConnection().use { connection -> + connection.autoCommit = false + try { + var graphId: Int + connection.prepareStatement(insertGraphSQL, Statement.RETURN_GENERATED_KEYS).use { statement -> + statement.setString(1, graphName) + statement.setString(2, graphType.split(" ")[0].replace("Graph", "")) + statement.setString(3, graphType.split(" ")[1]) + statement.executeUpdate() + val generatedKeys = statement.generatedKeys + if (generatedKeys.next()) { + graphId = generatedKeys.getInt(1) + } else { + throw SQLException("Failed to insert graph, no ID obtained.") + } + } + + graphVM.graph.getEdges().forEach { edge -> + connection.prepareStatement(insertEdgesSQL).use { statement -> + statement.setInt(1, graphId) + statement.setInt(2, edge.vertex1.id) + statement.setInt(3, edge.vertex2.id) + graphVM.graph.getWeightMap()[edge]?.let { weight -> + statement.setInt(4, weight) + } ?: statement.setNull(4, Types.INTEGER) + statement.executeUpdate() + } + } + + graphVM.graph.getVertices().forEach { vertex -> + connection.prepareStatement(insertVerticesSQL).use { statement -> + statement.setInt(1, graphId) + statement.setString(2, vertex.data.toString()) + statement.executeUpdate() + } + } + + connection.commit() + } catch (ex: SQLException) { + connection.rollback() + throw ex + } finally { + connection.autoCommit = true + } + } + } + + fun getGraphNames(graphNames: MutableState>>): String? { + val selectNamesSQL = insertQueries.split(":")[3] + var showErrorMessage = false + var errorMessage = "" + graphNames.value = arrayListOf() + + try { + getConnection().use { connection -> + connection.prepareStatement(selectNamesSQL).use { statement -> + val resultSet = statement.executeQuery() + while (resultSet.next()) { + graphNames.value.add(Pair(resultSet.getInt("id"), resultSet.getString("name"))) + } + } + } + } catch (e: SQLException) { + showErrorMessage = true + errorMessage = e.message.toString() + } + if (showErrorMessage) { + return errorMessage + } + return null + } + + fun updateImportedGraphVM( + graph: Graph, + graphId: Int, + graphVMState: MutableState?> + ): GraphViewModel? { + val selectGraphSQL = insertQueries.split(":")[6] + val selectVerticesSQL = insertQueries.split(":")[4] + val selectEdgesSQL = insertQueries.split(":")[5] + try { + getConnection().use { connection -> + connection.prepareStatement(selectGraphSQL).use { statement -> + statement.setInt(1, graphId) + val resultSet = statement.executeQuery() + if (resultSet.next()) { + val currentGraphSetup = importGraphInfo(graphId) + val graphVMType = currentGraphSetup.first.second.toString() + + "Graph" + " " + currentGraphSetup.first.first.toString() + + val graphVM = GraphViewModel( + graph, + mutableStateOf(false), + mutableStateOf(false), + graphVMType, + currentGraphSetup.first.second.toString().contains("Directed"), + currentGraphSetup.first.second.toString().contains("Weighted") + ) + + // Fetch vertices + connection.prepareStatement(selectVerticesSQL).use { vertexStatement -> + vertexStatement.setInt(1, graphId) + val vertexResultSet = vertexStatement.executeQuery() + while (vertexResultSet.next()) { + val vertexData = vertexResultSet.getString("data") + graphVM.addVertex(vertexData) + } + } + + // Fetch edges + connection.prepareStatement(selectEdgesSQL).use { edgeStatement -> + edgeStatement.setInt(1, graphId) + val edgeResultSet = edgeStatement.executeQuery() + while (edgeResultSet.next()) { + val vertex1Id = edgeResultSet.getInt("vertex1_id") + val vertex2Id = edgeResultSet.getInt("vertex2_id") + val weight = edgeResultSet.getInt("weight") + + graphVM.addEdge(vertex1Id, vertex2Id, weight) + } + } + graphVMState.value = graphVM + + } else { + throw SQLException("Graph with ID $graphId not found.") + } + } + } + } catch (e: SQLException) { + e.printStackTrace() + } + return graphVMState.value + } + + + private fun importGraphInfo(graphId: Int): Pair, String> { + val selectGraphSQL = insertQueries.split(":")[6] + var graphStructure: Int? + var graphWeight: Int? + var storedValueType: Int? + val graphName: String? + getConnection().use { connection -> + connection.prepareStatement(selectGraphSQL).use { statement -> + statement.setInt(1, graphId) + val resultSet = statement.executeQuery() + graphName = resultSet.getString("name") + val graphType = resultSet.getString("graph_type") + + graphStructure = if (graphType.contains("Undirected")) 0 else 1 + graphWeight = if (graphType.contains("Weighted")) 0 else 1 + + storedValueType = + if (resultSet.getString("stored_value_type") == "Int") 0 + else if (resultSet.getString("stored_value_type") == "UInt") 1 else 2 + } + } + return Pair(graphWeight?.let { + storedValueType?.let { it1 -> + graphStructure?.let { it2 -> + getGraphVMParameter(it1, it2, it) + } + } + } ?: throw NoSuchElementException("No info found about graph with ID = $graphId"), + graphName ?: throw NoSuchElementException("Graph with ID = $graphId has no name")) + } + + fun deleteGraph(graphId: Int) { + val deleteGraphSQL = insertQueries.split(":")[8] + val deleteGraphEdgesSQL = insertQueries.split(":")[9] + val deleteGraphVerticesSQL = insertQueries.split(":")[10] + try { + getConnection().use { connection -> + connection.prepareStatement(deleteGraphSQL).use { graphstatement -> + graphstatement.setInt(1, graphId) + graphstatement.executeUpdate() + } + connection.prepareStatement(deleteGraphEdgesSQL).use { edgeStatement -> + edgeStatement.setInt(1, graphId) + edgeStatement.executeUpdate() + } + connection.prepareStatement(deleteGraphVerticesSQL).use { vertexStatement -> + vertexStatement.setInt(1, graphId) + vertexStatement.executeUpdate() + } + } + } catch (e: SQLException) { + e.printStackTrace() + } + } + + fun renameGraph(graphId: Int, newGraphName: String) { + val renameGraphSQL = insertQueries.split(":")[11] + try { + getConnection().use { connection -> + connection.prepareStatement(renameGraphSQL).use { graphstatement -> + graphstatement.setString(1, newGraphName) + graphstatement.setInt(2, graphId) + graphstatement.executeUpdate() + } + } + } catch (e: SQLException) { + e.printStackTrace() + } + } +} diff --git a/app/src/main/kotlin/model/io/sql/queries.txt b/app/src/main/kotlin/model/io/sql/queries.txt new file mode 100644 index 00000000..570452de --- /dev/null +++ b/app/src/main/kotlin/model/io/sql/queries.txt @@ -0,0 +1,60 @@ +-- Insert graph query +INSERT INTO graphs(name, graph_type, stored_value_type) VALUES (?, ?, ?): + +-- Insert edges query +INSERT INTO edges(graph_id, vertex1_id, vertex2_id, weight) VALUES (?, ?, ?, ?): + +-- Insert vertices query +INSERT INTO vertices(graph_id, data) VALUES (?, ?): + +-- Select names query +SELECT id, name FROM graphs: + +-- Select vertices query +SELECT id, data FROM vertices WHERE graph_id = ?: + +-- Select edges query +SELECT id, vertex1_id, vertex2_id, weight FROM edges WHERE graph_id = ?: + +-- Select graph query +SELECT id, name, graph_type, stored_value_type FROM graphs WHERE id = ?: + +-- Create all graph tables query +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS graphs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + graph_type TEXT NOT NULL, + stored_value_type TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS edges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + graph_id INTEGER NOT NULL, + vertex1_id INTEGER NOT NULL, + vertex2_id INTEGER NOT NULL, + weight INTEGER, + FOREIGN KEY (graph_id) REFERENCES graphs(id), + FOREIGN KEY (vertex1_id) REFERENCES vertices(id), + FOREIGN KEY (vertex2_id) REFERENCES vertices(id) +); + +CREATE TABLE IF NOT EXISTS vertices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + graph_id INTEGER NOT NULL, + data TEXT, + FOREIGN KEY (graph_id) REFERENCES graphs(id) +);: + +-- Delete from graphs table +DELETE FROM graphs WHERE id = ?: + +-- Delete from edges table +DELETE FROM edges WHERE graph_id = ?: + +-- Delete from vertices table +DELETE FROM vertices WHERE graph_id = ?: + +-- Rename graph in graph table +UPDATE graphs SET name = ? WHERE id = ?: diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt new file mode 100644 index 00000000..48bea7d0 --- /dev/null +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -0,0 +1,34 @@ +package view + +import MyAppTheme +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import view.graph.GraphView +import view.tabScreen.TabHandler +import view.components.FAQBox +import view.components.ToolBox +import viewmodel.MainScreenViewModel + +@Composable +fun MainScreen(viewmodel: MainScreenViewModel) { + MyAppTheme { + // state for hover effect + val interactionSource = remember { MutableInteractionSource() } + val scale = remember { mutableStateOf(1f) } + + Row { + TabHandler(viewmodel) + Surface(modifier = Modifier.fillMaxSize()) { + GraphView(viewmodel.graphViewModel, scale) + } + } + // Hoverable box over the existing Surface + FAQBox(interactionSource, viewmodel.graphType) + ToolBox(viewmodel.graphViewModel, scale) + } +} diff --git a/app/src/main/kotlin/view/components/FAQBox.kt b/app/src/main/kotlin/view/components/FAQBox.kt new file mode 100644 index 00000000..c0404007 --- /dev/null +++ b/app/src/main/kotlin/view/components/FAQBox.kt @@ -0,0 +1,98 @@ +package view.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun FAQBox(interactionSource: MutableInteractionSource, currentGraphType: String) { + var isHovered by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .testTag("FAQBox"), + contentAlignment = Alignment.TopEnd + ) { + + val iconSize = 40.dp + val paddingSize = 5.dp + + val textBoxHeight = 58.dp + val textBoxWidth = 230.dp + + if (isHovered) { + Surface( + color = Color.White, + shape = RoundedCornerShape(15.dp), + border = BorderStroke(3.dp, MaterialTheme.colors.secondary), + modifier = + Modifier + .height(textBoxHeight + paddingSize) + .width(textBoxWidth + paddingSize) + .padding(paddingSize) + .testTag("FAQBoxHovered") + ) { + Text( + text = currentGraphType, + fontSize = 16.sp, + color = Color.Black, + textAlign = TextAlign.Center, + modifier = Modifier.testTag("HoveredText") + ) + } + } + + Surface( + color = Color.Transparent, + modifier = + Modifier + .width(iconSize + paddingSize) + .height(iconSize + paddingSize) + .padding(paddingSize) + .background(Color.Transparent) + .hoverable(interactionSource = interactionSource) + .testTag("FAQBoxNotHovered") + ) { + if (!isHovered) { + Image( + painterResource("drawable/question.svg"), + contentDescription = "Question Mark Icon", + modifier = Modifier.size(iconSize + paddingSize), + alignment = Alignment.TopEnd + ) + } + } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is HoverInteraction.Enter -> { + isHovered = true + } + + is HoverInteraction.Exit -> { + isHovered = false + } + } + } + } + } +} diff --git a/app/src/main/kotlin/view/components/RadioColumn.kt b/app/src/main/kotlin/view/components/RadioColumn.kt new file mode 100644 index 00000000..fe67ef48 --- /dev/null +++ b/app/src/main/kotlin/view/components/RadioColumn.kt @@ -0,0 +1,54 @@ +package view.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun RadioColumn( + selectText: String, + currentDataIndex: MutableState, + radioOptions: List, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text(selectText) + Spacer(modifier = Modifier.height(8.dp)) + Column { + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(vertical = 4.dp) + .fillMaxWidth() + .clickable { currentDataIndex.value = index } + ) { + RadioButton( + selected = currentDataIndex.value == index, + onClick = { currentDataIndex.value = index }, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) + ) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = if (currentDataIndex.value == index) Color.Black else Color.Gray + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/view/components/RunAlgoButton.kt b/app/src/main/kotlin/view/components/RunAlgoButton.kt new file mode 100644 index 00000000..c897b743 --- /dev/null +++ b/app/src/main/kotlin/view/components/RunAlgoButton.kt @@ -0,0 +1,40 @@ +package view.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import view.components.dialogWindows.ErrorWindow + +@Composable +fun RunAlgoButton(errorText: String, algoToRun: () -> Boolean) { + var showErrorWindow by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = { + if (!algoToRun()) { + showErrorWindow = true + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Run algorithm") + } + } + } + if (showErrorWindow) { + ErrorWindow(errorText) { showErrorWindow = false } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/components/ToolBox.kt b/app/src/main/kotlin/view/components/ToolBox.kt new file mode 100644 index 00000000..2ed364a8 --- /dev/null +++ b/app/src/main/kotlin/view/components/ToolBox.kt @@ -0,0 +1,55 @@ +package view.components + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import viewmodel.graph.GraphViewModel + +const val SCALE_FACTOR: Float = 1.1f + +@Composable +fun ToolBox(graphVM: GraphViewModel, currentScale: MutableState) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Bottom + ) { + FloatingActionButton( + onClick = { + currentScale.value = (SCALE_FACTOR * currentScale.value).coerceIn(0.7f, 1.9f) + }, + modifier = Modifier.padding(horizontal = 11.dp) + ) { + Text("+") + } + Spacer(modifier = Modifier.height(8.dp)) + FloatingActionButton(onClick = { + currentScale.value = (currentScale.value / SCALE_FACTOR).coerceIn(0.7f, 1.9f) + }, + modifier = Modifier.padding(horizontal = 11.dp) + ) { + Text("-") + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + modifier = Modifier + .width(80.dp) + .height(50.dp) + .clip(shape = RoundedCornerShape(25.dp)), + onClick = graphVM::clearGraph + ) { + Text("Clear") + } + } + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/CreateGraphDialogWindow.kt new file mode 100644 index 00000000..e88a9faf --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/CreateGraphDialogWindow.kt @@ -0,0 +1,97 @@ +package view.components.dialogWindows + +import MyAppTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import view.components.RadioColumn +import viewmodel.graph.GraphViewModelFactory +import viewmodel.graph.createGraphFromTypesIndices + +@Composable +fun CreateGraphDialogWindow(viewModel: GraphViewModelFactory) { + var closeDialog by remember { mutableStateOf(false) } + val selectedStoredDataIndex = remember { mutableStateOf(0) } + val selectedOrientationIndex = remember { mutableStateOf(0) } + val selectedWeightinessIndex = remember { mutableStateOf(0) } + var createGraphClicked by remember { mutableStateOf(false) } + + MyAppTheme { + if (!closeDialog) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = Modifier + .background(Color.White) + .padding(16.dp) + .widthIn(min = 200.dp, max = 700.dp) + .wrapContentHeight() + ) { + Text( + "Create Graph", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Row(modifier = Modifier.wrapContentHeight().fillMaxWidth()) { + RadioColumn( + "Select stored data:", + selectedStoredDataIndex, + listOf("Integer", "UInteger", "String"), + Modifier.weight(1f).padding(horizontal = 8.dp) + ) + RadioColumn( + "Select the orientation:", + selectedOrientationIndex, + listOf("Undirected", "Directed"), + Modifier.weight(1f).padding(horizontal = 8.dp) + ) + RadioColumn( + "Select the weightiness:", + selectedWeightinessIndex, + listOf("Unweighted", "Weighted"), + Modifier.weight(1f).padding(horizontal = 8.dp) + ) + } + Row( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.wrapContentSize(), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + closeDialog = true + createGraphClicked = true + } + ) { + Text("Apply", color = Color.White) + } + } + } + } + } + if (createGraphClicked) { + createGraphFromTypesIndices( + viewModel, + selectedStoredDataIndex.value, + selectedOrientationIndex.value, + selectedWeightinessIndex.value + ) + } + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/EditDBWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/EditDBWindow.kt new file mode 100644 index 00000000..d42694ec --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/EditDBWindow.kt @@ -0,0 +1,175 @@ +package view.components.dialogWindows + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import model.io.sql.SQLDatabaseModule +import view.tabScreen.DatabaseTypes + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun EditDBWindow(DBType: DatabaseTypes, onDismiss: () -> Unit) { + var showDialog by remember { mutableStateOf(false) } + val graphNamesSQL = remember { mutableStateOf(arrayListOf>()) } + var expanded by remember { mutableStateOf(false) } + var selectedGraphName by remember { mutableStateOf("") } + var selectedGraphID by remember { mutableStateOf(0)} + var updateGraphNames by remember { mutableStateOf(false) } + var graphNameToReplaceWith by remember { mutableStateOf("")} + + if (DBType == DatabaseTypes.SQLite) { + val errorMessage = SQLDatabaseModule.getGraphNames(graphNamesSQL) + if (errorMessage != null) ErrorWindow(errorMessage) {} + + if (graphNamesSQL.value.isNotEmpty()) showDialog = true + else ErrorWindow("Database doesn't have any Graphs", {}) + } else if (DBType == DatabaseTypes.NEO4J) { + ErrorWindow("Sorry! This feature will be implemented in future release") {} + // TODO + } else { + ErrorWindow("Sorry! This feature will be implemented in future release") {} + // TODO + } + + + if (showDialog) { + Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = false, usePlatformDefaultWidth = false)) { + Column( + modifier = Modifier.background(Color.White).padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(450.dp).height(180.dp) + ) { + Text( + "Edit database", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + TextField( + value = selectedGraphName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + graphNamesSQL.value.forEach { db -> + DropdownMenuItem( + modifier = Modifier, + onClick = { + selectedGraphName = db.second + selectedGraphID = db.first + expanded = false + } + ) { + Text(text = db.second) + } + } + } + } + } + Row( + modifier = Modifier + .padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 10.dp) + .fillMaxWidth() + .height(150.dp), + horizontalArrangement = Arrangement.End, + ) { + TextField( + value = graphNameToReplaceWith, + onValueChange = { newValue -> + graphNameToReplaceWith = newValue + }, + modifier = Modifier + .width(200.dp) + .height(50.dp) + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Graph name", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + Spacer(modifier = Modifier.width(15.dp)) + + Button( + modifier = Modifier + .width(50.dp) + .height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + SQLDatabaseModule.deleteGraph(selectedGraphID) + updateGraphNames = true + selectedGraphName = "" + } + ) { + Icon(Icons.Default.Delete, contentDescription = "Delete", tint = Color.White) + } + + Spacer(modifier = Modifier.width(15.dp)) + + Button( + modifier = Modifier + .width(50.dp) + .height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + SQLDatabaseModule.renameGraph(selectedGraphID, graphNameToReplaceWith) + updateGraphNames = true + selectedGraphName = "" + graphNameToReplaceWith = "" + } + ) { + Icon(Icons.Default.Edit, contentDescription = "Rename", tint = Color.White) + } + } + } + } + } + if (updateGraphNames) { + val sqlErrorMessage = SQLDatabaseModule.getGraphNames(graphNamesSQL) + if (sqlErrorMessage != null) ErrorWindow(sqlErrorMessage) {} + updateGraphNames = false + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/ErrorWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/ErrorWindow.kt new file mode 100644 index 00000000..544af9de --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/ErrorWindow.kt @@ -0,0 +1,48 @@ +package view.components.dialogWindows + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun ErrorWindow(message: String, onDismiss: () -> Unit) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + modifier = Modifier.size(300.dp, 180.dp), + shape = MaterialTheme.shapes.medium, + elevation = 24.dp + ) { + Row(modifier = Modifier.height(100.dp).fillMaxWidth().padding(16.dp)) { + Column(modifier = Modifier, horizontalAlignment = Alignment.Start) { + Text(text = "Error", style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold) + } + } + Row(modifier = Modifier.fillMaxWidth().padding(top = 32.dp)) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween + ) { + + Text(text = message) + Spacer(modifier = Modifier.height(30.dp)) + Button(onClick = { onDismiss() }, modifier = Modifier.align(Alignment.End).width(100.dp)) { + Text("ok") + } + } + } + } + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/ImportGraphDialogWindow.kt new file mode 100644 index 00000000..ea228742 --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/ImportGraphDialogWindow.kt @@ -0,0 +1,107 @@ +package view.components.dialogWindows + +import JSON +import MyAppTheme +import NEO4J +import SQLITE +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog + +@Composable +fun ImportGraphDialogWindow() { + var selectedDatabase by remember { mutableStateOf("") } + var importGraphClicked by remember { mutableStateOf(false) } + + val fontSize = 16.sp + val buttonColor = MaterialTheme.colors.secondary + + MyAppTheme { + if (!importGraphClicked) { + Dialog( + onDismissRequest = {} + ) { + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(300.dp).height(290.dp) + ) { + Text( + "Import from...", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + Button( + modifier = + Modifier.height(60.dp).width(250.dp), + colors = ButtonDefaults.buttonColors(buttonColor), + onClick = { + selectedDatabase = SQLITE + importGraphClicked = true + } + ) { + Text(SQLITE, color = Color.White, fontSize = fontSize) + } + } + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + Button( + modifier = + Modifier.height(60.dp).width(250.dp), + colors = ButtonDefaults.buttonColors(buttonColor), + onClick = { + selectedDatabase = NEO4J + importGraphClicked = true + } + ) { + Text(NEO4J, color = Color.White, fontSize = fontSize) + } + } + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + Button( + modifier = + Modifier.height(60.dp).width(250.dp), + colors = ButtonDefaults.buttonColors(buttonColor), + onClick = { + selectedDatabase = JSON + importGraphClicked = true + } + ) { + Text(JSON, color = Color.White, fontSize = fontSize) + } + } + } + } + } + if (importGraphClicked) { + when (selectedDatabase) { + SQLITE -> SQLiteImportGraphDialogWindow() + NEO4J -> Neo4jImportGraphDialogWindow { importGraphClicked = false } + } + } + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/Neo4jImportGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/Neo4jImportGraphDialogWindow.kt new file mode 100644 index 00000000..e8d98bc1 --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/Neo4jImportGraphDialogWindow.kt @@ -0,0 +1,108 @@ +package view.components.dialogWindows + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import model.io.neo4j.Neo4jRepositoryHandler + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun Neo4jImportGraphDialogWindow(onDismiss: () -> Unit) { + var expanded by remember { mutableStateOf(false) } + var importFromDBRequired by remember { mutableStateOf(false) } + var selectedGraphName by remember { mutableStateOf("") } + val graphNames = Neo4jRepositoryHandler.getNames() + + if (!Neo4jRepositoryHandler.isRepoInit) { + Neo4jLoginDialog { onDismiss() } + } + else { + Dialog( + onDismissRequest = { + onDismiss() + }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = Modifier.background(Color.White) + .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Select the graph:", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + TextField( + value = selectedGraphName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + graphNames?.forEach { name -> + DropdownMenuItem( + onClick = { + selectedGraphName = name + expanded = false + } + ) { + Text(text = name) + } + } + } + } + } + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + importFromDBRequired = true + expanded = false + onDismiss() + } + ) { + Text("Import", color = Color.White) + } + } + } + } + + if (importFromDBRequired) { +// return SQLDatabaseModule.importGraph(selectedGraphID.value) + Neo4jRepositoryHandler.loadGraph(selectedGraphName) + } + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/Neo4jLoginDialog.kt b/app/src/main/kotlin/view/components/dialogWindows/Neo4jLoginDialog.kt new file mode 100644 index 00000000..03f4ba10 --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/Neo4jLoginDialog.kt @@ -0,0 +1,154 @@ +package view.components.dialogWindows + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +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 androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.delay +import model.io.neo4j.Neo4jRepositoryHandler + +@Composable +fun Neo4jLoginDialog(onDismiss: () -> Unit) { + var uriInput by remember { mutableStateOf("") } + var userInput by remember { mutableStateOf("") } + var passwordInput by remember { mutableStateOf("") } + + var errorMessage by remember { mutableStateOf("") } + var isLoginSuccessful by remember { mutableStateOf(false) } + + val mainFontSize = 20.sp + val secondaryFontSize = 15.sp + + Dialog( + onDismissRequest = onDismiss + ) { + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(360.dp) + ) { + Text( + text = "Please login to Neo4j AuraDB to use Neo4j:", + fontSize = mainFontSize + ) + + val height = 60.dp + + Spacer(modifier = Modifier.height(15.dp)) + + TextField( + value = uriInput, + onValueChange = { + uriInput = it + errorMessage = "" + }, + modifier = Modifier.fillMaxWidth().height(height), + label = { + Text( + "Uri", + style = MaterialTheme.typography.body1.copy(fontSize = secondaryFontSize), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = Color.White + ) + ) + TextField( + value = userInput, + onValueChange = { + userInput = it + errorMessage = "" + }, + modifier = Modifier.fillMaxWidth().height(height), + label = { + Text( + "Name", + style = MaterialTheme.typography.body1.copy(fontSize = secondaryFontSize), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = Color.White + ) + ) + TextField( + value = passwordInput, + onValueChange = { + passwordInput = it + errorMessage = "" + }, + modifier = Modifier.fillMaxWidth().height(height), + label = { + Text( + "Password", + style = MaterialTheme.typography.body1.copy(fontSize = secondaryFontSize), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = Color.White + ) + ) + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(30.dp)) + + Button( + modifier = Modifier + .width(250.dp) + .height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + if (Neo4jRepositoryHandler.initRepo(uriInput, userInput, passwordInput)) { + isLoginSuccessful = true + } else { + errorMessage = "Sorry, but this input is invalid." + } + } + ) { + Text("Login", color = Color.White) + } + + if (errorMessage.isNotBlank()) { + Text( + text = errorMessage, + color = Color.Red, + modifier = Modifier.padding(top = 8.dp) + ) + + uriInput = "" + userInput = "" + passwordInput = "" + } + if (isLoginSuccessful) { + Text( + text = "Logged in successfully!", + color = MaterialTheme.colors.primary, + modifier = Modifier.padding(top = 8.dp) + ) + LaunchedEffect(Unit) { + + delay(1500) + onDismiss() + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/SQLiteImportGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/SQLiteImportGraphDialogWindow.kt new file mode 100644 index 00000000..0e94ba80 --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/SQLiteImportGraphDialogWindow.kt @@ -0,0 +1,108 @@ +package view.components.dialogWindows + +import model.io.sql.SQLDatabaseModule +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import viewmodel.importGraphAndRender + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SQLiteImportGraphDialogWindow() { + var selectedGraphID by remember { mutableStateOf(0) } + var closeDialog by remember { mutableStateOf(false) } + var expanded by remember { mutableStateOf(false) } + var importFromDBRequired by remember { mutableStateOf(false) } + var selectedGraphName by remember { mutableStateOf("") } + val graphs = remember { mutableStateOf(arrayListOf>()) } + + val errorMessage = SQLDatabaseModule.getGraphNames(graphs) + if (errorMessage != null) ErrorWindow(errorMessage) {} + + if (!closeDialog) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = Modifier.background(Color.White) + .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp) + ) { + Text( + "Select the graph:", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + TextField( + value = selectedGraphName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + graphs.value.forEach { graphName -> + // TODO: fix its layout + DropdownMenuItem( + onClick = { + selectedGraphName = graphName.second + selectedGraphID = graphName.first + expanded = false + } + ) { + Text(text = graphName.second) + } + } + } + } + } + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + importFromDBRequired = true + expanded = false + closeDialog = true + } + ) { + Text("Import", color = Color.White) + } + } + } + } + } + if (importFromDBRequired) { + return importGraphAndRender(selectedGraphID) + } +} diff --git a/app/src/main/kotlin/view/components/dialogWindows/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/SelectInitDialogWindow.kt new file mode 100644 index 00000000..3b58df99 --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/SelectInitDialogWindow.kt @@ -0,0 +1,90 @@ +package view.components.dialogWindows + +import MyAppTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import viewmodel.graph.GraphViewModelFactory + + +class SelectInitDialogWindow { + private var showGraphDialog by mutableStateOf(false) + private var showCreateGraphDialog by mutableStateOf(false) + private var showImportTab by mutableStateOf(false) + + @Composable + fun GraphInitDialogWindow( + showDialog: Boolean, + ) { + MyAppTheme { + DisposableEffect(Unit) { + if (showDialog) { + showGraphDialog = true + } + + onDispose { showGraphDialog = false } + } + + if (showGraphDialog) { + Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) { + Column( + modifier = Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) + ) { + Text( + "Welcome to WUDU!", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Text("Please select how to initialize the graph") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + Button(modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.secondary), + onClick = { + showGraphDialog = false + showCreateGraphDialog = true + }) { + Text("Create", color = Color.White) + } + + Button(modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + showGraphDialog = false + showImportTab = true + } + ) { + Text("Import", color = Color.White) + } + } + } + } + } + + if (showCreateGraphDialog) { + CreateGraphDialogWindow(GraphViewModelFactory) + } + + if (showImportTab) { + ImportGraphDialogWindow() + } + } + } +} diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt new file mode 100644 index 00000000..13d811af --- /dev/null +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -0,0 +1,71 @@ +package view.graph + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.zIndex +import viewmodel.graph.EdgeViewModel + +const val MAX_EDGE_STROKE_WIDTH = 12f +const val MIN_EDGE_STROKE_WIDTH = 4f + +@Composable +fun EdgeView(viewModel: EdgeViewModel, scale: Float) { + + val (firstVertexCenterX, firstVertexCenterY) = viewModel.calculateFirstVertexCenter(scale) + val (secondVertexCenterX, secondVertexCenterY) = viewModel.calculateSecondVertexCenter(scale) + + val arrowPoints = viewModel.calculateArrowPoints(scale) + + val highlightColor by remember { derivedStateOf { viewModel.highlightColor } } + val edgeColor = highlightColor.value + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + drawLine( + color = edgeColor, + strokeWidth = (5f * scale).coerceIn(MIN_EDGE_STROKE_WIDTH, MAX_EDGE_STROKE_WIDTH), + start = + Offset( + firstVertexCenterX.toPx(), + firstVertexCenterY.toPx() + ), + end = + Offset( + secondVertexCenterX.toPx(), + secondVertexCenterY.toPx() + ), + ) + + if (arrowPoints.isNotEmpty()) { + val trianglePath = Path() + + // these points represent the vertices of a triangle + trianglePath.moveTo( + arrowPoints[0].first.toPx(), + arrowPoints[0].second.toPx(), + ) + trianglePath.lineTo( + arrowPoints[1].first.toPx(), + arrowPoints[1].second.toPx() + ) + trianglePath.lineTo( + arrowPoints[2].first.toPx(), + arrowPoints[2].second.toPx() + ) + trianglePath.close() + + drawPath( + path = trianglePath, + color = edgeColor, + style = Fill + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/graph/GraphView.kt b/app/src/main/kotlin/view/graph/GraphView.kt new file mode 100644 index 00000000..856f6466 --- /dev/null +++ b/app/src/main/kotlin/view/graph/GraphView.kt @@ -0,0 +1,59 @@ +package view.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.gestures.panBy +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import viewmodel.graph.GraphViewModel + +@Composable +fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState) { + var offset by remember { mutableStateOf(Offset.Zero) } + val coroutineScope = rememberCoroutineScope { Dispatchers.Default } + + val updateRequired by remember { derivedStateOf { viewModel.updateIsRequired } } + + val transformationState = rememberTransformableState { zoomChange, offsetChange, _ -> + currentScaleState.value *= zoomChange + offset += offsetChange + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .pointerInput(Unit) { + detectTransformGestures { _, pan, _, _ -> + coroutineScope.launch { + // update the transformation state using the gesture values + transformationState.panBy(pan) + } + } + } + .graphicsLayer( + scaleX = currentScaleState.value, + scaleY = currentScaleState.value, + translationX = offset.x, + translationY = offset.y + ) + ) { + if (updateRequired.value) { + viewModel.randomize(740.0, 650.0) + viewModel.applyForceDirectedLayout(740.0, 650.0, 0.1, 8.0, 1.2) + } + viewModel.verticesVM.forEach { v -> VertexView(v, currentScaleState.value) } + viewModel.edgesVM.forEach { e -> EdgeView(e, currentScaleState.value) } + + viewModel.updateIsRequired.value = false + } +} diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt new file mode 100644 index 00000000..98d4dee8 --- /dev/null +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -0,0 +1,79 @@ +package view.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import viewmodel.graph.VertexViewModel + +val MAX_VERTEX_RADIUS = 35.dp +val MIN_VERTEX_RADIUS = 7.dp + +@Composable +fun VertexView(viewModel: VertexViewModel, scale: Float) { + val coroutineScope = rememberCoroutineScope { Dispatchers.Default } + + val adjustedX = viewModel.x.value + val adjustedY = viewModel.y.value + val adjustedRadius = (viewModel.radius * scale).coerceIn(MIN_VERTEX_RADIUS, MAX_VERTEX_RADIUS) + + val highlightColor by remember { derivedStateOf { viewModel.highlightColor } } + val borderColor = highlightColor.value + + Box( + modifier = Modifier + .offset { IntOffset(adjustedX.roundToPx(), adjustedY.roundToPx()) } + .size(adjustedRadius * 2) + .border(5.dp, borderColor, CircleShape) + .background( + if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, + shape = CircleShape + ) + .clip(CircleShape) + .pointerInput(Unit) { + coroutineScope.launch { + detectDragGestures { change, dragAmount -> + viewModel.onDrag( + DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()) + ) + change.consume() + } + } + detectTapGestures( + onTap = { viewModel.switchSelection() } + ) + }, + contentAlignment = Alignment.Center + ) { + if (viewModel.dataVisible.value) { + Text(modifier = Modifier.align(Alignment.Center), text = viewModel.getVertexData) + } + if (viewModel.idVisible.value) { + Text( + modifier = Modifier.align(Alignment.Center).zIndex(3f), + text = viewModel.getVertexID.toString(), + color = Color.Black, + style = MaterialTheme.typography.body1.copy(fontSize = 20.sp) + ) + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt new file mode 100644 index 00000000..7d430115 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -0,0 +1,307 @@ +package view.tabScreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.delay +import model.io.neo4j.Neo4jRepositoryHandler +import model.io.sql.SQLDatabaseModule +import view.components.dialogWindows.* +import viewmodel.graph.GraphViewModel +import java.awt.FileDialog +import java.awt.Frame + +enum class DatabaseTypes { + SQLite, NEO4J, JSON +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun FileControlTab(graphVM: GraphViewModel) { + var showSaveDialog by remember { mutableStateOf(false) } + var showLoadDialog by remember { mutableStateOf(false) } + var graphName by remember { mutableStateOf("") } + var showErrorWindow by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + var showEditDialog by remember { mutableStateOf(false) } + var showNeo4jDialog by remember { mutableStateOf(false) } + + var selectedDatabase by remember { mutableStateOf(DatabaseTypes.SQLite) } + + Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { + Row(modifier = Modifier.height(0.dp)) {} + + val rowHeight = 75.dp + val fieldHeight = 70.dp + + val borderPadding = 10.dp + val horizontalGap = 20.dp + + val tabWidth = 360.dp + val fieldWidth = (tabWidth / 2) - horizontalGap + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(borderPadding) + ) { + Column(modifier = Modifier.width(tabWidth).fillMaxHeight(), Arrangement.Center) { + TextField( + value = graphName, + onValueChange = { newValue -> + graphName = newValue + }, + modifier = Modifier + .fillMaxWidth() + .height(fieldHeight) + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Graph name", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.width(fieldWidth).fillMaxHeight() + ) { + TextField( + value = selectedDatabase.toString(), + onValueChange = { graphName = it }, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + DatabaseTypes.entries.forEach { db -> + DropdownMenuItem( + modifier = Modifier, + onClick = { + selectedDatabase = db + expanded = false + } + ) { + Text(db.toString()) + } + } + } + } + + if (selectedDatabase == DatabaseTypes.JSON) { + val fileDialog = FileDialog(null as Frame?, "Select File to Open") + fileDialog.mode = FileDialog.LOAD + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { + fileDialog.isVisible = true + }, + colors = ButtonDefaults.buttonColors(Color.White) + ) { + Text("Select File") + } + } + } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.width(fieldWidth).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { + showSaveDialog = true + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Save") + } + } + Column(modifier = Modifier.width(fieldWidth).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { + showLoadDialog = true + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Load") + } + } + } + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { + showEditDialog = true + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Edit DB") + } + } + } + } + + if (showSaveDialog) { + if (selectedDatabase == DatabaseTypes.SQLite) { + val existingGraphNamesSQL = remember { mutableStateOf(arrayListOf>()) } + val sqlErrorMessage = SQLDatabaseModule.getGraphNames(existingGraphNamesSQL) + if (sqlErrorMessage != null) ErrorWindow(sqlErrorMessage) {} + + if (existingGraphNamesSQL.value.any { it.second == graphName }) { + showErrorWindow = true + errorMessage = "Graph with name: $graphName already exists" + graphName = "" + showSaveDialog = false + } else { + SQLDatabaseModule.insertGraph(graphVM, graphName, graphVM.graphType) + Dialog( + onDismissRequest = { + showSaveDialog = false + graphName = "" + } + ) { + Column( + modifier = Modifier + .background(Color.White) + .padding(16.dp) + .width(250.dp) + .height(30.dp) + ) { + Text("Graph '$graphName' saved successfully!") + } + } + + // Automatically dismiss the dialog after 3 seconds + LaunchedEffect(Unit) { + delay(3000) + showSaveDialog = false + } + } + } else if (selectedDatabase == DatabaseTypes.NEO4J) { + if (!Neo4jRepositoryHandler.isRepoInit) { + showSaveDialog = false + showNeo4jDialog = true + } else if (!Neo4jRepositoryHandler.isValidNeo4jName(graphName)) { + showSaveDialog = false + showErrorWindow = true + errorMessage = "$graphName is an invalid name." + graphName = "" + } else { + Neo4jRepositoryHandler.saveOrReplace(graphVM.graph, graphName, graphVM.isDirected, graphVM.isWeighted) + + Dialog( + onDismissRequest = { + showSaveDialog = false + } + ) { + Column( + modifier = Modifier + .background(Color.White) + .padding(16.dp) + .width(300.dp) + .height(100.dp) + ) { + Text("Graph '$graphName' saved successfully!") + } + } + + // Automatically dismiss the dialog after 3 seconds + LaunchedEffect(Unit) { + delay(3000) + showSaveDialog = false + } + } + } + } + + if (showLoadDialog) { + when (selectedDatabase) { + DatabaseTypes.SQLite -> SQLiteImportGraphDialogWindow() + DatabaseTypes.NEO4J -> Neo4jImportGraphDialogWindow { showLoadDialog = false } + DatabaseTypes.JSON -> TODO() + } + } + + if (showEditDialog) { + EditDBWindow(selectedDatabase) { showEditDialog = false } + } + + if (showNeo4jDialog) { + Neo4jLoginDialog { showNeo4jDialog = false } + } + + if (showErrorWindow) { + ErrorWindow( + errorMessage + ) { + showErrorWindow = false + errorMessage = "" + } + } +} + +// TODO +private fun isValidNeo4jName(name: String): Boolean { + if (name.isEmpty()) return false + for (i in name.indices) { + val isValidChar = when (name[i].code) { + 45 -> { // - + if (i!= 0) true else false + } + in 48..57 -> { // 0-9 + if (i != 0) true else false + } + in 65..90 -> true // A-Z + 95 -> true // _ + in 97..122 -> true // a-z + else -> false + } + + if (isValidChar) continue else return false + } + + return true +} diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt new file mode 100644 index 00000000..1e36e13a --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -0,0 +1,264 @@ +package view.tabScreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import view.components.dialogWindows.ErrorWindow +import viewmodel.graph.GraphViewModel + +@Composable +fun GeneralTab(graphVM: GraphViewModel) { + var showVertexAddDialog by remember { mutableStateOf(false) } + var vertexData by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf("") } + var connectVertexId by remember { mutableStateOf("") } + var firstVertexId by remember { mutableStateOf("") } + var secondVertexId by remember { mutableStateOf("") } + var secondVertexData by remember { mutableStateOf("") } + var showErrorWindow by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { + Row(modifier = Modifier.height(0.dp)) {} + + Row( + modifier = Modifier.height(75.dp).padding(10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Column(modifier = Modifier.width(200.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = vertexData, + onValueChange = { vertexData = it }, + modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Vertex data", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ), + ) + } + Column(modifier = Modifier.width(120.dp).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(70.dp), + onClick = { if (vertexData.isNotEmpty()) showVertexAddDialog = true }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("add") + } + } + } + + Row( + modifier = Modifier.height(75.dp).padding(10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Column(modifier = Modifier.width(100.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = firstVertexId, + onValueChange = { firstVertexId = it }, + modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 12.sp), + label = { + Text( + "1 vertex ID", + style = MaterialTheme.typography.body1.copy(fontSize = 12.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + + Column(modifier = Modifier.width(100.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = secondVertexId, + onValueChange = { secondVertexId = it }, + modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 12.sp), + label = { + Text( + "2 vertex ID", + style = MaterialTheme.typography.body1.copy(fontSize = 12.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + + Column(modifier = Modifier.width(110.dp).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(70.dp), + onClick = { + if (graphVM.graph.getVertices() + .any { it.id == firstVertexId.toInt() } && graphVM.graph.getVertices() + .any { it.id == secondVertexId.toInt() } + ) { + graphVM.addEdge(firstVertexId.toInt(), secondVertexId.toInt()) + + graphVM.updateIsRequired.value = true + secondVertexId = "" + firstVertexId = "" + } else { + showErrorWindow = true + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("add") + } + } + } + + Row( + modifier = Modifier.fillMaxWidth().padding(5.dp).clickable { + graphVM.showVerticesID.value = !graphVM.showVerticesID.value + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = graphVM.showVerticesID.value, + onCheckedChange = { graphVM.showVerticesID.value = it }, + colors = + CheckboxDefaults.colors( + checkedColor = MaterialTheme.colors.primary, + uncheckedColor = MaterialTheme.colors.secondary + ) + ) + Text( + text = "Show ID", + modifier = Modifier.padding(start = 10.dp, bottom = 3.dp).align(Alignment.CenterVertically) + ) + + } + } + + if (showVertexAddDialog) { + Dialog( + onDismissRequest = { + showVertexAddDialog = false + } + ) { + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(180.dp) + ) { + if (graphVM.verticesVM.isEmpty()) { + Text("Input data of second vertex to create and connect with") + + TextField( + value = secondVertexData, + onValueChange = { enteredValue -> + errorMessage = "" + secondVertexData = enteredValue + }, + modifier = Modifier.fillMaxWidth().height(60.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent) + ) + + if (errorMessage.isNotBlank()) { + Text(errorMessage, color = Color.Red, modifier = Modifier.padding(top = 8.dp)) + } + + Button( + modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(vertical = 16.dp), + onClick = { + secondVertexData = secondVertexData.replace("\n", "") + + if (secondVertexData.isBlank()) { + errorMessage = "Please enter data to store" + } else { + val firstId = graphVM.addVertex(vertexData) + val secondId = graphVM.addVertex(secondVertexData) + graphVM.addEdge(firstId, secondId) + + graphVM.updateIsRequired.value = true + + showVertexAddDialog = false + errorMessage = "" + vertexData = "" + secondVertexData = "" + } + } + ) { + Text("Connect") + } + + } else { + Text("Input ID of vertex to connect with:") + + TextField( + value = connectVertexId, + onValueChange = { newValue -> + errorMessage = "" + connectVertexId = newValue + }, + modifier = Modifier.fillMaxWidth().height(60.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent) + ) + + if (errorMessage.isNotBlank()) { + Text(errorMessage, color = Color.Red, modifier = Modifier.padding(top = 8.dp)) + } + + Button( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + onClick = { + connectVertexId = connectVertexId.replace("\n", "") + + if (connectVertexId.isBlank()) { + errorMessage = "Please enter an ID" + } else if (connectVertexId.toIntOrNull() == null) { + errorMessage = "ID must be an integer" + } else if (!connectVertexId.all { char -> char.isDigit() }) { + errorMessage = "ID should be a numeric" + } else if (!graphVM.checkVertexById(connectVertexId.toInt())) { + errorMessage = "There isn't a Vertex with such ID" + } else { + val firstId = graphVM.addVertex(vertexData) + graphVM.addEdge(firstId, connectVertexId.toInt()) + + graphVM.updateIsRequired.value = true + + showVertexAddDialog = false + errorMessage = "" + connectVertexId = "" + vertexData = "" + } + } + ) { + Text("Connect") + } + } + } + } + } + if (showErrorWindow) { + ErrorWindow("No such Vertex") { showErrorWindow = false } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt new file mode 100644 index 00000000..82983543 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt @@ -0,0 +1,59 @@ +package view.tabScreen + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Tab +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SelectTabRow( + currentPageState: PagerState, + index: Int, + title: String +) { + val coroutineScope = rememberCoroutineScope { Dispatchers.Default } + + Tab( + selected = currentPageState.currentPage == index, + onClick = { coroutineScope.launch { currentPageState.animateScrollToPage(index) } }, + modifier = Modifier, + content = { + Box( + modifier = + Modifier.background( + if (currentPageState.currentPage == index) MaterialTheme.colors.primary + else Color.Transparent + ) + .padding(10.dp) + .height(30.dp) + .width(120.dp) + .align(Alignment.CenterHorizontally), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + textAlign = TextAlign.Center, + color = if (currentPageState.currentPage == index) MaterialTheme.colors.surface else Color.Black + // Set text color for selected and unselected tabs + ) + } + } + ) +} diff --git a/app/src/main/kotlin/view/tabScreen/TabHandler.kt b/app/src/main/kotlin/view/tabScreen/TabHandler.kt new file mode 100644 index 00000000..8a15da53 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/TabHandler.kt @@ -0,0 +1,71 @@ +package view.tabScreen + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import view.tabScreen.analyzeTab.AnalyzeTab +import viewmodel.MainScreenViewModel + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TabHandler(viewmodel: MainScreenViewModel) { + Column( + modifier = Modifier + .width(360.dp) + .background(color = MaterialTheme.colors.surface) + .fillMaxHeight() + .clip(shape = RoundedCornerShape(10.dp)) + ) { + val pageState = rememberPagerState(initialPage = 0, pageCount = { 3 }) + val tabs = listOf("General", "Analyze", "Save & Load") + + TabRow( + selectedTabIndex = pageState.currentPage, + contentColor = MaterialTheme.colors.surface, + backgroundColor = MaterialTheme.colors.secondary, + divider = {}, // to remove divider between + indicator = { tabPositions -> + TabRowDefaults.Indicator( + modifier = + Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), + height = 0.dp + ) + }, + modifier = Modifier.height(50.dp) + ) { + tabs.forEachIndexed { index, title -> + SelectTabRow(pageState, index, title) + } + } + + // Content corresponding to the selected tab + HorizontalPager(state = pageState, userScrollEnabled = true) { + Column( + modifier = + Modifier.width(360.dp) + .background(MaterialTheme.colors.background) + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + when (pageState.currentPage) { + 0 -> GeneralTab(viewmodel.graphViewModel) + 1 -> AnalyzeTab(viewmodel.graphViewModel) + 2 -> FileControlTab(viewmodel.graphViewModel) + } + } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt new file mode 100644 index 00000000..fc863f6e --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt @@ -0,0 +1,97 @@ +package view.tabScreen.analyzeTab + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import view.tabScreen.analyzeTab.algorithmsUI.* +import viewmodel.graph.Algorithm +import viewmodel.graph.GraphViewModel +import viewmodel.graph.getAlgorithmDisplayName + +val rowHeight = 75.dp +val borderPadding = 10.dp +val horizontalGap = 20.dp + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AnalyzeTab(graphVM: GraphViewModel) { + val algorithms = graphVM.getAvailableAlgorithms() + var selectedAlgorithm by remember { mutableStateOf(algorithms.first()) } + + Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { + Row(modifier = Modifier.height(0.dp)) {} + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column( + modifier = Modifier.width(95.dp).fillMaxHeight(), + Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Algorithm:", + textAlign = TextAlign.Center, + modifier = Modifier.padding(start = 7.dp) + ) + } + Column(modifier = Modifier.width(225.dp).fillMaxHeight(), Arrangement.Center) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.width(225.dp).fillMaxHeight() + ) { + TextField( + value = getAlgorithmDisplayName(selectedAlgorithm), + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + algorithms.forEach { algorithm -> + DropdownMenuItem( + modifier = Modifier, + onClick = { + selectedAlgorithm = algorithm + expanded = false + } + ) { + Text(text = getAlgorithmDisplayName(algorithm)) + } + } + } + } + } + } + + when (selectedAlgorithm) { + Algorithm.LAYOUT -> { LayoutUI(graphVM) } + Algorithm.FIND_COMMUNITIES -> { CommunitiesUI(graphVM) } + Algorithm.FIND_KEY_VERTICES -> { KeyVerticesUI(graphVM) } + Algorithm.FIND_SHORTEST_PATH -> { ShortestPathUI(graphVM) } + Algorithm.FIND_CYCLES -> { CyclesUI(graphVM) } + Algorithm.FIND_BRIDGES -> { BridgesUI(graphVM) } + Algorithm.FIND_SCCS -> { SCCUI(graphVM) } + Algorithm.MIN_SPANNING_TREE -> { MinSpanningTreeUI(graphVM) } + } + } +} + diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt new file mode 100644 index 00000000..66f7b904 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt @@ -0,0 +1,10 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.runtime.Composable +import view.components.RunAlgoButton +import viewmodel.graph.GraphViewModel + +@Composable +fun BridgesUI(graphVM: GraphViewModel) { + RunAlgoButton("No bridges were found") { graphVM.findBridges() } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt new file mode 100644 index 00000000..ed3a5e28 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt @@ -0,0 +1,10 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.runtime.Composable +import view.components.RunAlgoButton +import viewmodel.graph.GraphViewModel + +@Composable +fun CommunitiesUI(graphVM: GraphViewModel) { + RunAlgoButton("No communities were found") { graphVM.findCommunities() } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt new file mode 100644 index 00000000..69b20479 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt @@ -0,0 +1,108 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import view.components.dialogWindows.ErrorWindow +import viewmodel.graph.GraphViewModel + +@Composable +fun CyclesUI(graphVM: GraphViewModel) { + var vertexId by remember { mutableStateOf("") } + var showErrorWindow by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + TextField( + value = vertexId, + onValueChange = { vertexId = it }, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Vertex ID", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + } + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = { + if (vertexId.isEmpty()) { + errorMessage = "Enter vertex's ID" + showErrorWindow = true + } + else if (!vertexId.all { char -> char.isDigit() }) { + errorMessage = "ID should be a number" + showErrorWindow = true + } + else if (vertexId.toInt() > graphVM.graph.getVertices().size - 1) { + errorMessage = "No vertex with ID $vertexId" + showErrorWindow = true + } + else if (!graphVM.findCycles(vertexId.toInt())) { + errorMessage = "No cycles were found" + showErrorWindow = true + } + else { + graphVM.highlighNextCycle() + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Run algorithm") + } + } + } + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = { + if (!graphVM.highlighNextCycle()) { + errorMessage = "Please run algorithm first" + showErrorWindow = true + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Highlight next cycle") + } + } + } + + if (showErrorWindow) { + ErrorWindow(errorMessage) { showErrorWindow = false } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt new file mode 100644 index 00000000..149a4f66 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt @@ -0,0 +1,10 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.runtime.Composable +import view.components.RunAlgoButton +import viewmodel.graph.GraphViewModel + +@Composable +fun KeyVerticesUI(graphVM: GraphViewModel) { + RunAlgoButton("No key vertices were found") { graphVM.findKeyVertices() } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt new file mode 100644 index 00000000..90deaf88 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt @@ -0,0 +1,94 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + + +@Composable +fun LayoutUI(graphVM: GraphViewModel) { + var sliderValue1 by remember { mutableStateOf(0f) } + var sliderValue2 by remember { mutableStateOf(0f) } + var sliderValue3 by remember { mutableStateOf(0f) } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text("Analyze Tab", style = MaterialTheme.typography.h6, modifier = Modifier.padding(bottom = 16.dp)) + + SliderWithValue( + value = sliderValue1, + onValueChange = { sliderValue1 = it }, + label = "Slider 1", + valueRange = 0f..2f, + coefficient = 1f + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SliderWithValue( + value = sliderValue2, + onValueChange = { sliderValue2 = it }, + label = "Slider 2", + valueRange = 1f..30f, + coefficient = 1f + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SliderWithValue( + value = sliderValue3, + onValueChange = { sliderValue3 = it }, + label = "Slider 3", + valueRange = 0.5f..10f, + coefficient = 1f + ) + Button( + modifier = Modifier.fillMaxWidth().height(50.dp), + onClick = {}, +// onClick = { graphVM.applyForceDirectedLayout(740.0, 650.0, sliderValue1.toDouble(), sliderValue2.toDouble(), sliderValue3.toDouble())}) + ) { + Text("Apply layout settings") + } + + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + modifier = Modifier.fillMaxWidth().height(50.dp), + onClick = {} +// onClick = { graphVM.randomize(740.0, 650.0) }) { + ) { + Text("Randomize") + } + + } +} + +@Composable +fun SliderWithValue( + value: Float, + onValueChange: (Float) -> Unit, + label: String, + valueRange: ClosedFloatingPointRange, + coefficient: Float +) { + Column { + Text("$label: ${"%.2f".format(value * coefficient)}", style = MaterialTheme.typography.body1) + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = ((valueRange.endInclusive - valueRange.start) / 1000).toInt(), + modifier = Modifier.fillMaxWidth() + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt new file mode 100644 index 00000000..acad8c9c --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt @@ -0,0 +1,10 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.runtime.Composable +import view.components.RunAlgoButton +import viewmodel.graph.GraphViewModel + +@Composable +fun MinSpanningTreeUI(graphVM: GraphViewModel) { + RunAlgoButton("No min spanning tree was found") { graphVM.findMinSpanningTree() } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt new file mode 100644 index 00000000..785a1079 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt @@ -0,0 +1,10 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.runtime.Composable +import view.components.RunAlgoButton +import viewmodel.graph.GraphViewModel + +@Composable +fun SCCUI(graphVM: GraphViewModel) { + RunAlgoButton("No SCCs were found") { graphVM.findSCCs() } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt new file mode 100644 index 00000000..c130ae2c --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt @@ -0,0 +1,118 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import view.components.dialogWindows.ErrorWindow +import viewmodel.graph.GraphViewModel + +@Composable +fun ShortestPathUI(graphVM: GraphViewModel) { + var sourceVertexId by remember { mutableStateOf("") } + var destVertexId by remember { mutableStateOf("") } + var showErrorWindow by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.width(160.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = sourceVertexId, + onValueChange = { sourceVertexId = it }, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Source ID", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray, + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + Column(modifier = Modifier.width(160.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = destVertexId, + onValueChange = { destVertexId = it }, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Destination ID", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + } + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = { + if (sourceVertexId.isEmpty() || destVertexId.isEmpty()) { + errorMessage = "Enter vertices' IDs" + showErrorWindow = true + } + else if ( + !sourceVertexId.all { char -> char.isDigit() } || + !destVertexId.all { char -> char.isDigit() } + ) { + errorMessage = "ID should be a number" + showErrorWindow = true + } + else if (sourceVertexId == destVertexId) { + errorMessage = "Vertices' IDs should be different" + showErrorWindow = true + } + else if ( + sourceVertexId.toInt() > graphVM.graph.getVertices().size - 1 || + destVertexId.toInt() > graphVM.graph.getVertices().size - 1 + ) { + errorMessage = "No vertex with such ID" + showErrorWindow = true + } + else if (!graphVM.findShortestPath(sourceVertexId.toInt(), destVertexId.toInt())) { + errorMessage = "Shortest path doesn't exist" + showErrorWindow = true + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Run algorithm") + } + } + } + + if (showErrorWindow) { + ErrorWindow(errorMessage) { showErrorWindow = false } + } +} diff --git a/app/src/main/kotlin/viewmodel/FastFourierTransform.kt b/app/src/main/kotlin/viewmodel/FastFourierTransform.kt new file mode 100644 index 00000000..84d39077 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/FastFourierTransform.kt @@ -0,0 +1,235 @@ +package viewmodel + +/** + * Copyright (C) 2021 José Alexandre Nalon + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/. + */ + +/************************************************************************************************** + * Fast Fourier Transform -- Kotlin Version + * This version implements Cooley-Tukey algorithm for composite-length sequences. + * + * José Alexandre Nalon + ************************************************************************************************** + * This program can be compiled by issuing the command: + * + * $ kotlinc anyfft.kt + * + * It will generate a file named 'FftKt.class' in the same directory. It can be run by issuing the + * command: + * + * $ kotlin AnyfftKt + * + **************************************************************************************************/ + +/************************************************************************************************** + * Include necessary libraries: + **************************************************************************************************/ +import kotlin.math.* // Math functions; + + +/************************************************************************************************** + * Mini-library to deal with complex numbers. + **************************************************************************************************/ +class Complex(val r: Double, val i: Double) { + + // Constructor: + constructor() : this(0.0, 0.0) {} + + // Add the argument to this, giving the result as a new complex number: + operator fun plus(c: Complex): Complex { + return Complex(r + c.r, i + c.i) + } + + // Subtract the argument from this, giving the result as a new complex number: + operator fun minus(c: Complex): Complex { + return Complex(r - c.r, i - c.i) + } + + // Multiply the argument with this, giving the result as a new complex number: + operator fun times(c: Complex): Complex { + return Complex(r * c.r - i * c.i, r * c.i + i * c.r) + } + + // Multiply with an scalar, giving the reulst as a new complex number: + operator fun times(a: Double): Complex { + return Complex(a * r, a * i) + } + + // Divide this by the argument, giving the result as a new complex number: + operator fun div(a: Double): Complex { + return Complex(r / a, i / a) + } + +} + +// Complex exponential of an angle: +fun Cexp(a: Double): Complex { + return Complex(cos(a), sin(a)) +} + + +/************************************************************************************************** + * Auxiliary Function: complexShow + * Pretty printing of an array of complex numbers, used to inspect results. + * + * Parameters: + * x + * A vector of complex numbers, according to the definition above; + **************************************************************************************************/ +fun complexShow(x: Array) { + for (i in 0..x.size - 1) + println("( " + x[i].r + ", " + x[i].i + ")") +} + + +/************************************************************************************************** + * Auxiliary Function: timeIt + * Measure execution time through repeated calls to a (Fast) Fourier Transform function. + * + * Parameters: + * f + * Function to be called, with the given prototype. The first complex vector is the input + * vector, the second complex vector is the result of the computation, and the integer is the + * number of elements in the vector; + * size + * Number of elements in the vector on which the transform will be applied; + * repeat + * Number of times the function will be called. + * + * Returns: + * The average execution time for that function with a vector of the given size. + **************************************************************************************************/ +fun timeIt(f: (x: Array) -> Array, size: Int, repeat: Int): Double { + val x = Array(size) { i -> Complex(i.toDouble(), 0.0) } + val start = System.currentTimeMillis() + for (j in 1..repeat) { + f(x) + } + return (System.currentTimeMillis() - start).toDouble() / (1000 * repeat).toDouble() +} + + +/************************************************************************************************** + * Function: directFT + * Discrete Fourier Transform directly from the definition, an algorithm that has O(N^2) + * complexity. + * + * Parameters: + * x + * The vector of which the DFT will be computed. Given the nature of the implementation, there + * is no restriction on the size of the vector, although it will almost always be called with a + * power of two size to give a fair comparison; + * + * Returns: + * A complex-number vector of the same size, with the coefficients of the DFT. + **************************************************************************************************/ +fun directFT(x: Array): Array { + // Implement the direct DFT computation + // This is usually O(N^2) but can be acceptable for small N + val N = x.size + val X = Array(N) { Complex(0.0, 0.0) } + for (k in 0 until N) { + for (n in 0 until N) { + val theta = -2.0 * PI * k * n / N + X[k] = X[k] + x[n] * Cexp(theta) + } + } + return X +} + +/************************************************************************************************** + * Function: factor + * Smallest prime factor of a given number. If the argument is prime itself, then it is the + * return value. + * + * Parameters: + * n + * Number to be inspected. + * + * Returns: + * The smallest prime factor, or the number itself if it is already a prime. + **************************************************************************************************/ +fun factor(n: Int): Int { + val rn = ceil(sqrt(n.toDouble())).toInt() // Search up to the square root of the number; + for (i in 2..rn) { + if (n % i == 0) return i // If remainder is zero, a factor is found; + } + return n +} + + +/************************************************************************************************** + * Function: recursiveFFT + * Fast Fourier Transform using a recursive decimation in time algorithm. This has smaller + * complexity than the direct FT, though the exact value is difficult to compute. + * + * Parameters: + * x + * The vector of which the FFT will be computed. Its length must be a composite number, or else + * the computation will be defered to the direct FT, and there will be no efficiency gain. + * + * Returns: + * A complex-number vector of the same size, with the coefficients of the DFT. + **************************************************************************************************/ +fun recursiveFFT(x: Array): Array { + val N = x.size + + val N1 = factor(N) // Smallest prime factor of length + if (N == N1) { + return directFT(x) // Direct transform if length is prime + } else { + val N2 = N / N1 // Decompose in two factors, N1 being prime + + val X = Array(N) { Complex(0.0, 0.0) } // Allocate memory for computation + + val W = Cexp(-2 * PI / N.toDouble()) // Twiddle factor + var Wj = Complex(1.0, 0.0) + for (j in 0 until N1) { // Compute subsequences of size N2 + val xj = Array(N2) { n -> x[n * N1 + j] } // Create the subsequence + val Xj = recursiveFFT(xj) // Compute the DFT of the subsequence + var Wkj = Complex(1.0, 0.0) + for (k in 0 until N) { + X[k] = X[k] + Xj[k % N2] * Wkj // Recombine results + Wkj = Wkj * Wj // Update twiddle factors + } + Wj = Wj * W + } + return X + } +} + + +///************************************************************************************************** +// * Main function: +// **************************************************************************************************/ +//fun main() { +// +// val sizes = listOf(2*3, 2*2*3, 2*3*3, 2*3*5, 2*2*3*3, 2*2*5*5, 2*3*5*7, 2*2*3*3*5*5) +// val repeat: Int = 500 // Number of executions to compute average time; +// +// // Start by printing the table with time comparisons: +// println("+---------+---------+---------+---------+") +// println("| N | N^2 | Direct | Recurs. |") +// println("+---------+---------+---------+---------+") +// +// // Try it with vectors with the given sizes: +// for (n in sizes) { +// +// // Compute the average execution time: +// var dtime = timeIt(::directFT, n, repeat) +// var rtime = timeIt(::recursiveFFT, n, repeat) +// +// // Print the results: +// val results = "| %7d | %7d | %7.4f | %7.4f |".format(n, n*n, dtime, rtime) +// println(results); +// } +// +// println("+---------+---------+---------+---------+") +// +//} \ No newline at end of file diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt new file mode 100644 index 00000000..9ff3acec --- /dev/null +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -0,0 +1,37 @@ +package viewmodel + +import androidx.compose.runtime.mutableStateOf +import model.graphs.abstractGraph.Graph +import viewmodel.graph.GraphViewModel + +class MainScreenViewModel( + graph: Graph, + dataType: String, + existingGraphViewModel: GraphViewModel? = null, +) { + private val showVerticesData = mutableStateOf(false) + private val showVerticesIds = mutableStateOf(false) + val graphType = simplifyGraphString(graph.toString()) + "\nData type: " + dataType + + private fun simplifyGraphString(graphString: String): String { + return graphString.substringAfterLast('.').substringBefore('@') + } + + private fun setDirectionState(currentGraphType: String): Boolean { + return currentGraphType.contains("Directed") + } + + private fun setWeightinessState(currentGraphType: String): Boolean { + return currentGraphType.contains("Weighted") + } + + var graphViewModel: GraphViewModel = existingGraphViewModel + ?: GraphViewModel( + graph, + showVerticesIds, + showVerticesData, + graphType, + setDirectionState(graph.toString()), + setWeightinessState(graph.toString()) + ) +} diff --git a/app/src/main/kotlin/viewmodel/SQLiteViewModel.kt b/app/src/main/kotlin/viewmodel/SQLiteViewModel.kt new file mode 100644 index 00000000..5126420c --- /dev/null +++ b/app/src/main/kotlin/viewmodel/SQLiteViewModel.kt @@ -0,0 +1,66 @@ +package viewmodel + +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.* +import model.graphs.abstractGraph.Graph +import model.io.sql.SQLDatabaseModule +import view.MainScreen +import view.components.dialogWindows.ErrorWindow +import viewmodel.graph.GraphViewModel +import viewmodel.graph.GraphViewModelFactory +import java.sql.SQLException +import kotlin.system.exitProcess + +@Suppress("UNCHECKED_CAST") +@Composable +fun importGraphAndRender(graphId: Int) { + val graphVMState = remember { mutableStateOf?>(null) } + var showErrorMessage by remember { mutableStateOf(false) } + var updateIsRequired by remember { mutableStateOf(false) } + var currentGraphSetup: Pair, String>? = null + + try { + currentGraphSetup = SQLDatabaseModule.importGraph(graphId, currentGraphSetup) + if (currentGraphSetup == null) showErrorMessage = true + + // Execute side-effect to create graph object + GraphViewModelFactory.createGraphObject( + currentGraphSetup?.first?.first as GraphViewModelFactory.GraphType, + currentGraphSetup?.first?.second as GraphViewModelFactory.GraphStructure, + currentGraphSetup?.first?.third as GraphViewModelFactory.Weight, + graphId, + graphVMState as MutableState>?> + ) + updateIsRequired = true + + } catch (e: SQLException) { + e.printStackTrace() + showErrorMessage = true + } + + if (updateIsRequired) return importGraphUI(showErrorMessage, graphVMState, graphId) +} + +@Composable +fun importGraphUI( + showErrorMessage: Boolean, + graphVMState: MutableState?>, + graphId: Int +) { + if (showErrorMessage) { + ErrorWindow("Graph with ID $graphId not found.") {} + } + if (graphVMState.value != null) { + graphVMState.value?.updateIsRequired?.value = true + + MainScreen( + MainScreenViewModel( + graphVMState.value?.graph as Graph, + graphVMState.value?.graphType as String, + graphVMState.value + ) + ) + + } else CircularProgressIndicator() +} \ No newline at end of file diff --git a/app/src/main/kotlin/viewmodel/TFDPLayout.kt b/app/src/main/kotlin/viewmodel/TFDPLayout.kt new file mode 100644 index 00000000..6e756c6f --- /dev/null +++ b/app/src/main/kotlin/viewmodel/TFDPLayout.kt @@ -0,0 +1,170 @@ +package viewmodel + +import androidx.compose.ui.unit.dp +import viewmodel.graph.VertexViewModel +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.random.Random + +object TFDPLayout { + + private fun fft2D(input: Array>): Array> { + val rows = input.size + val cols = input[0].size + val output = Array(rows) { Array(cols) { Complex(0.0, 0.0) } } + + // Transform each row + for (i in 0 until rows) { + output[i] = recursiveFFT(input[i]) + } + + // Transpose the output matrix + val transposedOutput = Array(cols) { Array(rows) { Complex(0.0, 0.0) } } + for (i in 0 until rows) { + for (j in 0 until cols) { + transposedOutput[j][i] = output[i][j] + } + } + + // Transform each column + for (j in 0 until cols) { + val column = Array(rows) { i -> transposedOutput[j][i] } + val transformedCol = recursiveFFT(column) + for (i in 0 until rows) { + output[i][j] = transformedCol[i] + } + } + + return output + } + + private fun ifft2D(input: Array>): Array> { + val rows = input.size + val cols = input[0].size + val output = Array(rows) { Array(cols) { Complex() } } + + // Transform each row + for (i in 0 until rows) { + output[i] = recursiveFFT(input[i]) + } + + // Transpose the output matrix + val transposedOutput = Array(cols) { Array(rows) { Complex() } } + for (i in 0 until rows) { + for (j in 0 until cols) { + transposedOutput[j][i] = output[i][j] + } + } + + // Transform each column + for (j in 0 until cols) { + val column = Array(rows) { i -> transposedOutput[j][i] } + val transformedCol = recursiveFFT(column) + for (i in 0 until rows) { + output[i][j] = transformedCol[i] + } + } + + // Divide by the total number of elements to complete the inverse transformation + val totalElements = rows * cols + for (i in 0 until rows) { + for (j in 0 until cols) { + output[i][j] = output[i][j] / totalElements.toDouble() + } + } + + return output + } + + /** + * longRangeAttractionConstant - strength of attractive force (long-range) - B + * nearAttractionConstant - strength of attractive t-force (near) - A + * repulsiveConstant - extent and magnitude of the repulsive t-force that + controls the longest distance of neighbors in the layout - Y + **/ + + fun place( + width: Double, + height: Double, + vertices: Collection>, + gridSize: Int = 128, + longRangeAttractionConstant: Double, + nearAttractionConstant: Double, + repulsiveConstant: Double + ) { + val forces = Array(vertices.size) { Pair(0.0, 0.0) } + val grid = Array(gridSize) { Array(gridSize) { Complex(0.0, 0.0) } } + val deltaX = width / gridSize + val deltaY = height / gridSize + + // Assign particles to the grid + vertices.forEach { vertex -> + val i = (vertex.x.value / deltaX.dp).toInt().coerceIn(0, gridSize - 1) + val j = (vertex.y.value / deltaY.dp).toInt().coerceIn(0, gridSize - 1) + grid[i][j] = grid[i][j].plus(Complex(1.0, 0.0)) // Add particle mass + } + + // Compute potential using FFT + val potential = fft2D(grid) + // Apply the Green's function in frequency domain + for (i in 0 until gridSize) { + for (j in 0 until gridSize) { + val kx = if (i <= gridSize / 2) i else i - gridSize + val ky = if (j <= gridSize / 2) j else j - gridSize + val kSquared = (kx * kx + ky * ky).toDouble() + if (kSquared != 0.0) { + potential[i][j] = potential[i][j].div(kSquared) + } + } + } + val potentialRealSpace = ifft2D(potential) + + // Compute forces from potential + val forceX = Array(gridSize) { Array(gridSize) { 0.0 } } + val forceY = Array(gridSize) { Array(gridSize) { 0.0 } } + for (i in 0 until gridSize) { + for (j in 0 until gridSize) { + val right = potentialRealSpace[(i + 1) % gridSize][j].r - potentialRealSpace[i][j].r + val up = potentialRealSpace[i][(j + 1) % gridSize].r - potentialRealSpace[i][j].r + + val distance = sqrt(deltaX * deltaX + deltaY * deltaY) + + val repulsion = (distance) / (1 + distance * distance).pow(repulsiveConstant) + + forceX[i][j] -= right / deltaX / distance * repulsion + forceY[i][j] -= up / deltaY / distance * repulsion + + val attraction = + longRangeAttractionConstant * (distance + ((nearAttractionConstant * distance) / (1 + distance * distance))) + + forceX[i][j] -= right / deltaX / distance * attraction + forceY[i][j] -= up / deltaY / distance * attraction + } + } + + // Interpolate forces back to vertices + vertices.forEachIndexed { index, vertex -> + val i = (vertex.x.value / deltaX.dp).toInt().coerceIn(0, gridSize - 1) + val j = (vertex.y.value / deltaY.dp).toInt().coerceIn(0, gridSize - 1) + val fx = forceX[i][j] + val fy = forceY[i][j] + forces[index] = Pair(fx, fy) + } + + // Update positions + vertices.forEachIndexed { index, vertex -> + vertex.x.value += forces[index].first.dp + vertex.y.value += forces[index].second.dp + } + } + + fun randomize(width: Double, height: Double, vertices: Collection>) { + vertices.forEach { vertex -> + val randomX = Random.nextDouble(0.0, width * 1.5 - 360.0 - vertex.radius.value * 2).toFloat().dp + val randomY = Random.nextDouble(0.0, height * 1.5 - vertex.radius.value * 2).toFloat().dp + + vertex.x.value = randomX + vertex.y.value = randomY + } + } +} diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt new file mode 100644 index 00000000..7ce68446 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -0,0 +1,78 @@ +package viewmodel.graph + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import kotlin.math.sqrt + +const val ARROW_SIZE = 20f +const val ARROW_DEPTH = 2.5f +const val SQRT_3 = 1.732f + +class EdgeViewModel( + private val firstVertex: VertexViewModel, + private val secondVertex: VertexViewModel, + private val isDirected: Boolean +) { + + fun isDirected() = isDirected + + private val vertexRadius: Dp + get() = firstVertex.radius + + var highlightColor = mutableStateOf(Color.LightGray) + + internal fun calculateFirstVertexCenter(scale: Float): Pair { + val x = firstVertex.x.value + vertexRadius * scale + val y = firstVertex.y.value + vertexRadius * scale + + return Pair(x, y) + } + + internal fun calculateSecondVertexCenter(scale: Float): Pair { + val x = secondVertex.x.value + vertexRadius * scale + val y = secondVertex.y.value + vertexRadius * scale + + return Pair(x, y) + } + + internal fun calculateArrowPoints(scale: Float): List> { + if (!isDirected) return listOf() + + val firstVertexCenterX = calculateFirstVertexCenter(scale).first + val firstVertexCenterY = calculateFirstVertexCenter(scale).second + + val secondVertexCenterX = calculateSecondVertexCenter(scale).first + val secondVertexCenterY = calculateSecondVertexCenter(scale).second + + val vectorX = secondVertexCenterX - firstVertexCenterX + val vectorY = secondVertexCenterY - firstVertexCenterY + + val len = sqrt(vectorX.value * vectorX.value + vectorY.value * vectorY.value) + val normedVectorX = vectorX / len + val normedVectorY = vectorY / len + + // rotate normed vector by Pi/6 + val aX = normedVectorX * SQRT_3 / 2 - normedVectorY * 1 / 2 + val aY = normedVectorX * 1 / 2 + normedVectorY * SQRT_3 / 2 + + // rotate normed vector by negative Pi/6 + val bX = normedVectorX * SQRT_3 / 2 + normedVectorY * 1 / 2 + val bY = -normedVectorX * 1 / 2 + normedVectorY * SQRT_3 / 2 + + val arrowEndPointX = secondVertexCenterX - normedVectorX * (vertexRadius.value - ARROW_DEPTH) * scale + val arrowEndPointY = secondVertexCenterY - normedVectorY * (vertexRadius.value - ARROW_DEPTH) * scale + + val arrowLeftPointX = arrowEndPointX - aX * ARROW_SIZE * scale + val arrowLeftPointY = arrowEndPointY - aY * ARROW_SIZE * scale + + val arrowRightPointX = arrowEndPointX - bX * ARROW_SIZE * scale + val arrowRightPointY = arrowEndPointY - bY * ARROW_SIZE * scale + + return listOf( + Pair(arrowEndPointX, arrowEndPointY), + Pair(arrowLeftPointX, arrowLeftPointY), + Pair(arrowRightPointX, arrowRightPointY) + ) + } +} diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt new file mode 100644 index 00000000..11ff8ee2 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -0,0 +1,355 @@ +package viewmodel.graph + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import model.algorithms.* +import model.algorithms.clustering.CommunitiesFinder +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import viewmodel.TFDPLayout + + +class GraphViewModel( + private val currentGraph: Graph, + private val showVerticesData: State, + var showVerticesID: MutableState, + val graphType: String, + val isDirected: Boolean, + val isWeighted: Boolean +) { + + val updateIsRequired = mutableStateOf(false) + + private var _verticesViewModels = mutableMapOf, VertexViewModel>() + var _edgeViewModels = mutableMapOf, EdgeViewModel>() + + val verticesVM: List> get() = _verticesViewModels.values.toList() + val edgesVM: List> get() = _edgeViewModels.values.toList() + + val graph: Graph get() = currentGraph + + private fun updateEdgeViewModels(edge: Edge) { + val firstVertex: VertexViewModel = + _verticesViewModels[edge.vertex1] + ?: throw NoSuchElementException("No such View Model, with mentioned edges") + + val secondVertex: VertexViewModel = + _verticesViewModels[edge.vertex2] + ?: throw NoSuchElementException("No such View Model, with mentioned edges") + + _edgeViewModels[edge] = EdgeViewModel(firstVertex, secondVertex, isDirected) + } + + private fun updateVertexViewModels(vertex: Vertex) { + _verticesViewModels[vertex] = VertexViewModel( + dataVisible = showVerticesData, + idVisible = showVerticesID, + vertex = vertex, + ) + } + + fun checkVertexById(id: Int): Boolean { + return _verticesViewModels.keys.any { it.id == id } + } + + @Suppress("UNCHECKED_CAST") + fun addVertex(data: String): Int { + val newVertex = graph.addVertex(data as D) + + updateVertexViewModels(newVertex) + + return newVertex.id + } + + fun addEdge(firstId: Int, secondId: Int, weight: Int = 1) { + val firstVertex = graph.getVertices()[firstId] + val secondVertex = graph.getVertices()[secondId] + + val firstVertexVM = _verticesViewModels[firstVertex] + ?: throw NoSuchElementException("No ViewModel found for vertex (${firstVertex.id}, ${firstVertex.data})") + val secondVertexVM = _verticesViewModels[secondVertex] + ?: throw NoSuchElementException("No ViewModel found for vertex (${secondVertex.id}, ${secondVertex.data})") + + val newEdge = graph.addEdge(firstVertexVM.vertex, secondVertexVM.vertex) + graph.getWeightMap()[newEdge] = weight + updateEdgeViewModels(newEdge) + } + + fun applyForceDirectedLayout( + width: Double, height: Double, longRangeAttractionConstant: Double, + nearAttractionConstant: Double, + repulsiveConstant: Double + ) { + TFDPLayout.place( + width, + height, + verticesVM, + 128, + longRangeAttractionConstant, + nearAttractionConstant, + repulsiveConstant + ) + } + + fun randomize(width: Double, height: Double) { + TFDPLayout.randomize(width, height, verticesVM) + } + + fun findCommunities(): Boolean { + val communitiesFinder = CommunitiesFinder() + val communities = communitiesFinder.findCommunities(graph) + if (communities.isEmpty()) return false + + return highlightVerticesSets(communities) + } + + fun findKeyVertices(): Boolean { + val keyVerticesFinder = KeyVerticesFinder() + val keyVertices = keyVerticesFinder.findKeyVertices(graph) + if (keyVertices?.isEmpty() == true) return false + + return highlightVertices(keyVertices) + } + + fun findBridges(): Boolean { + val bridgesFinder = BridgesFinder() + if (graph is UndirectedGraph) { + val bridges = bridgesFinder.findBridges(graph as UndirectedGraph) + if (bridges.isEmpty()) return false + + return highlightEdges(bridges.toSet()) + } + + return false + } + + private var cycles: List, Vertex>>>? = null + private var currentCycleIndex = 0 + + fun findCycles(srcVertexId: Int): Boolean { + val cyclesFinder = CyclesFinder() + if (graph is DirectedGraph) { + val foundCycles = cyclesFinder.findCycles(graph as DirectedGraph, graph.getVertices()[srcVertexId]) + if (foundCycles.isEmpty()) return false + + cycles = foundCycles.toList() + + currentCycleIndex = 0 + if (cycles != null) return true + } + + return false + } + + fun findMinSpanningTree(): Boolean { + val minSpanningTreeFinder = MinSpanningTreeFinder() + if (graph is WeightedUndirectedGraph) { + val minSpanningTree = minSpanningTreeFinder.findMinSpanningTree(graph as WeightedUndirectedGraph) + if (minSpanningTree.isEmpty()) return false + + return highlightEdges(minSpanningTree.toSet()) + } + + return false + } + + fun findSCCs(): Boolean { + val sccFinder = SCCFinder() + if (graph is DirectedGraph) { + val SCCs = sccFinder.findSCC(graph as DirectedGraph) + if (SCCs.isEmpty()) return false + + return highlightVerticesSets(SCCs) + } + + return false + } + + fun findShortestPath(srcVertexId: Int, destVertexId: Int): Boolean { + val shortestPathFinder = ShortestPathFinder() + + val src = graph.getVertices()[srcVertexId] + val dest = graph.getVertices()[destVertexId] + + if (graph is WeightedDirectedGraph) { + val shortestPath = shortestPathFinder.findShortestPath(graph as WeightedDirectedGraph, src, dest) + if (shortestPath?.isEmpty() == true) return false + + return highlightPath(shortestPath) + } else if (graph is WeightedUndirectedGraph) { + val shortestPath = shortestPathFinder.findShortestPath(graph as WeightedUndirectedGraph, src, dest) + if (shortestPath?.isEmpty() == true) return false + + return highlightPath(shortestPath) + } + + return false + } + + private fun highlightVertices(verticesSet: Set>?): Boolean { + if (verticesSet == null) return false + + clearGraph() + + for (vertex in graph.getVertices()) { + if (vertex in verticesSet) { + _verticesViewModels[vertex]?.highlightColor?.value = Color.Black + } else { + _verticesViewModels[vertex]?.highlightColor?.value = Color.LightGray + } + } + + return true + } + + private fun highlightEdges(edgesSet: Set>?): Boolean { + if (edgesSet == null) return false + + clearGraph() + + for (edge in graph.getEdges()) { + if (edge in edgesSet) { + _edgeViewModels[edge]?.highlightColor?.value = Color.Black + } else { + _edgeViewModels[edge]?.highlightColor?.value = Color.LightGray + } + } + + return true + } + + private fun highlightVerticesSets(verticesSets: Set>>?): Boolean { + if (verticesSets == null) return false + + clearGraph() + + val colors = arrayOf( + Color.Red, + Color.Blue, + Color.Green, + Color.Yellow, + Color.Cyan, + Color.Magenta, + Color.Black, + Color.White, + Color.DarkGray, + Color(0xebab34), + Color(0xaeeb34), + Color(0x5e34eb), + Color(0x8334eb), + Color(0xd834eb), + Color(0xeb34a1), + ) + + var i = 0 + for (verticesSet in verticesSets) { + val currentColor = colors[i] + for (vertex in verticesSet) { + _verticesViewModels[vertex]?.highlightColor?.value = currentColor + } + + i++ + if (i > colors.size - 1) throw ArrayIndexOutOfBoundsException("Only 15 colors supported") + } + + return true + } + + private fun highlightPath(path: List, Vertex>>?): Boolean { + if (path == null) return false + + clearGraph() + + val srcVertex = path.first().first.vertex1 + _verticesViewModels[srcVertex]?.highlightColor?.value = Color.Black + + for (pair in path) { + val edge = pair.first + val vertex = pair.second + + _edgeViewModels[edge]?.highlightColor?.value = Color.Black + _verticesViewModels[vertex]?.highlightColor?.value = Color.Black + } + + return true + } + + fun highlighNextCycle(): Boolean { + val returnValue: Boolean = highlightPath(cycles?.get(currentCycleIndex)) + + val size = cycles?.size + ?: return false + + if (++currentCycleIndex > size - 1) currentCycleIndex = 0 + + return returnValue + } + + fun clearGraph() { + for (vertex in graph.getVertices()) { + _verticesViewModels[vertex]?.highlightColor?.value = Color.LightGray + } + for (edge in graph.getEdges()) { + _edgeViewModels[edge]?.highlightColor?.value = Color.LightGray + } + } + + fun getAvailableAlgorithms(): List { + val algorithms = mutableListOf( + Algorithm.LAYOUT, + Algorithm.FIND_COMMUNITIES, + Algorithm.FIND_KEY_VERTICES + ) + + if (graph is DirectedGraph) { + algorithms += Algorithm.FIND_SCCS + algorithms += Algorithm.FIND_CYCLES + } + + if (graph is UndirectedGraph) { + algorithms += Algorithm.FIND_BRIDGES + } + + if (graph is WeightedUndirectedGraph) { + algorithms += Algorithm.MIN_SPANNING_TREE + algorithms += Algorithm.FIND_SHORTEST_PATH + } + + if (graph is WeightedDirectedGraph) { + algorithms += Algorithm.FIND_SHORTEST_PATH + } + + return algorithms + } +} + +enum class Algorithm { + LAYOUT, + FIND_COMMUNITIES, + FIND_KEY_VERTICES, + FIND_SCCS, + FIND_CYCLES, + FIND_BRIDGES, + MIN_SPANNING_TREE, + FIND_SHORTEST_PATH +} + +fun getAlgorithmDisplayName(algorithm: Algorithm): String { + return when (algorithm) { + Algorithm.LAYOUT -> "Layout" + Algorithm.FIND_COMMUNITIES -> "Find communities" + Algorithm.FIND_KEY_VERTICES -> "Find key vertices" + Algorithm.FIND_SCCS -> "Find SCCs" + Algorithm.FIND_CYCLES -> "Find cycles" + Algorithm.FIND_BRIDGES -> "Find bridges" + Algorithm.MIN_SPANNING_TREE -> "Min spanning tree" + Algorithm.FIND_SHORTEST_PATH -> "Find shortest path" + } +} diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModelFactory.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModelFactory.kt new file mode 100644 index 00000000..99a19719 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModelFactory.kt @@ -0,0 +1,127 @@ +package viewmodel.graph + +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.* +import model.io.sql.SQLDatabaseModule +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Graph +import model.io.sql.SQLDatabaseModule.importGraph +import view.MainScreen +import view.components.dialogWindows.ErrorWindow +import viewmodel.MainScreenViewModel +import java.sql.SQLException +import kotlin.system.exitProcess + +object GraphViewModelFactory { + sealed class GraphType { + data object Integer : GraphType() + data object UInteger : GraphType() + data object String : GraphType() + } + + sealed class GraphStructure { + data object Directed : GraphStructure() + data object Undirected : GraphStructure() + } + + sealed class Weight { + data object Weighted : Weight() + data object Unweighted : Weight() + } + + // Simplified function to create a GraphViewModel based on the graph type + fun createGraphViewModel( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight + ): Graph> { + return when { + weight is Weight.Weighted && graphStructure is GraphStructure.Directed && storedData is GraphType.Integer -> WeightedDirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Directed && storedData is GraphType.UInteger -> WeightedDirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Directed && storedData is GraphType.String -> WeightedDirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.Integer -> WeightedUndirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.UInteger -> WeightedUndirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.String -> WeightedUndirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Directed && storedData is GraphType.Integer -> DirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Directed && storedData is GraphType.UInteger -> DirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Directed && storedData is GraphType.String -> DirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.Integer -> UndirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.UInteger -> UndirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.String -> UndirectedGraph() + else -> throw IllegalArgumentException("Invalid combination of parameters") + } + } + + @Composable + fun createGraphAndApplyScreen( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight + ) { + val graph = createGraphViewModel(storedData, graphStructure, weight) + MainScreen(MainScreenViewModel(graph, storedData.toString())) + } + + @Suppress("UNCHECKED_CAST") + fun createGraphObject( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight, + graphId: Int, + graphVMState: MutableState>?> + ) { + val graph = createGraphViewModel(storedData, graphStructure, weight) as Graph> + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + graph, + graphId, + graphVMState as MutableState>?> + ) + } +} + +// Utility function to get the graph parameters +fun getGraphVMParameter( + storedDataType: Int, + structureType: Int, + weightType: Int +): Triple { + val storedData = when (storedDataType) { + 0 -> GraphViewModelFactory.GraphType.Integer + 1 -> GraphViewModelFactory.GraphType.UInteger + 2 -> GraphViewModelFactory.GraphType.String + else -> GraphViewModelFactory.GraphType.Integer // default to integer + } + + val graphStructure = when (structureType) { + 0 -> GraphViewModelFactory.GraphStructure.Undirected + 1 -> GraphViewModelFactory.GraphStructure.Directed + else -> GraphViewModelFactory.GraphStructure.Undirected // default to directed + } + + val weight = when (weightType) { + 0 -> GraphViewModelFactory.Weight.Unweighted + 1 -> GraphViewModelFactory.Weight.Weighted + else -> GraphViewModelFactory.Weight.Unweighted // default to weighted + } + + return Triple(storedData, graphStructure, weight) +} + +@Composable +fun createGraphFromTypesIndices( + viewModel: GraphViewModelFactory, + storedDataIndex: Int, + orientationIndex: Int, + weightnessIndex: Int +) { + val (storedData, graphStructure, weight) = getGraphVMParameter(storedDataIndex, orientationIndex, weightnessIndex) + viewModel.createGraphAndApplyScreen(storedData, graphStructure, weight) +} + + + + + diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt new file mode 100644 index 00000000..2072b698 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -0,0 +1,39 @@ +package viewmodel.graph + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import model.graphs.abstractGraph.Vertex + +class VertexViewModel( + var x: MutableState = mutableStateOf(0.dp), + var y: MutableState = mutableStateOf(0.dp), + var dataVisible: State, + var idVisible: State, + val vertex: Vertex, + val radius: Dp = 20.dp, +) { + var highlightColor = mutableStateOf(Color.LightGray) + + var isSelected = mutableStateOf(false) + + val getVertexData + get() = vertex.data.toString() + + fun onDrag(dragAmount: DpOffset) { + x.value += dragAmount.x + y.value += dragAmount.y + } + + fun switchSelection() { + isSelected.value = !isSelected.value + } + + val getVertexID + get() = vertex.id +} + diff --git a/app/src/main/resources/drawable/question.svg b/app/src/main/resources/drawable/question.svg new file mode 100644 index 00000000..dd76bc8f --- /dev/null +++ b/app/src/main/resources/drawable/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/test/kotlin/integration/IntegrationTest.kt b/app/src/test/kotlin/integration/IntegrationTest.kt new file mode 100644 index 00000000..d7a3224d --- /dev/null +++ b/app/src/test/kotlin/integration/IntegrationTest.kt @@ -0,0 +1,61 @@ +package integration + +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import kotlinx.coroutines.runBlocking +import model.graphs.UndirectedGraph +import org.junit.Rule +import org.junit.Test +import view.components.FAQBox +import viewmodel.MainScreenViewModel + + +class IntegrationTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `graph is undirected, check its view and FAQ button`() { + // I'm not sure if we can consider this as an integration test + // Imho it's an integration test, due to UI's connection with VM + there is more than 2 components (view/vm, or front/back) + + // SETUP + val interactionSource = MutableInteractionSource() + val viewmodel = MainScreenViewModel(UndirectedGraph(),"UndirectedGraph") + + composeTestRule.setContent { + FAQBox(interactionSource, viewmodel.graphType) + } + + // UI TEST + + // Verify initial state (not hovered) + composeTestRule.onNodeWithTag("FAQBoxNotHovered").assertExists() + + // Simulate hover enter by changing the state manually + runBlocking { + interactionSource.tryEmit(HoverInteraction.Enter()) + } + + // Verify hovered state + composeTestRule.onNodeWithTag("FAQBoxHovered").assertExists() + composeTestRule.onNodeWithTag("FAQBoxHovered").assertExists() + composeTestRule.onNodeWithTag("HoveredText").assertTextEquals("UndirectedGraph") + + composeTestRule.onNodeWithTag("") + + // CHECK VM + val edgeViewModels = viewmodel.graphViewModel._edgeViewModels + val allEdgesDirected = edgeViewModels.all { it.value.isDirected() } + + assert(allEdgesDirected) { "Not all edges are directed" } + } + + @Test + fun `check import from DB and save with other name`() { + + } +} diff --git a/app/src/test/kotlin/model/algorithms/BridgesFinderTest.kt b/app/src/test/kotlin/model/algorithms/BridgesFinderTest.kt new file mode 100644 index 00000000..d508746d --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/BridgesFinderTest.kt @@ -0,0 +1,162 @@ +package model.algorithms.clustering + +import model.algorithms.BridgesFinder +import model.graphs.UndirectedGraph +import model.graphs.abstractGraph.Edge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import util.annotations.TestAllUndirectedGraphs + +class BridgesFinderTest { + val bridgesFinder = BridgesFinder() + + @Nested + inner class `All bridges should be found`() { + @TestAllUndirectedGraphs + fun `if graph has one edge`(graph: UndirectedGraph) { + val vertex0 = graph.addVertex(0) + val vertex1 = graph.addVertex(1) + + val expectedBridges = listOf(graph.addEdge(vertex0, vertex1)) + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if two components are connected via one edge`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v1, v2) + + addEdge(v3, v4) + addEdge(v3, v5) + addEdge(v4, v5) + } + + val expectedBridges = listOf(graph.addEdge(v0, v3)) + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph is chain-like`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e12 = graph.addEdge(v1, v2) + val e23 = graph.addEdge(v2, v3) + + val expectedBridges = setOf(e01, e12, e23) + val actualBridges = bridgesFinder.findBridges(graph).toSet() + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph is star-like`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e02 = graph.addEdge(v0, v2) + val e03 = graph.addEdge(v0, v3) + + val expectedBridges = setOf(e01, e02, e03) + val actualBridges = bridgesFinder.findBridges(graph).toSet() + + assertEquals(expectedBridges, actualBridges) + } + } + + @Nested + inner class `No bridge should be found`() { + @TestAllUndirectedGraphs + fun `if graph has no vertices`(graph: UndirectedGraph) { + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph has no edges`(graph: UndirectedGraph) { + graph.apply { + addVertex(0) + addVertex(1) + addVertex(2) + } + + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph is circle-like`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v0, v1) + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v4) + addEdge(v4, v0) + } + + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if two components are connected via more than one edge`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v1, v2) + + addEdge(v3, v4) + addEdge(v3, v5) + addEdge(v4, v5) + } + + graph.addEdge(v0, v3) + graph.addEdge(v1, v4) + + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/CyclesFinderTest.kt b/app/src/test/kotlin/model/algorithms/CyclesFinderTest.kt new file mode 100644 index 00000000..5169d2b2 --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/CyclesFinderTest.kt @@ -0,0 +1,147 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import util.annotations.TestAllDirectedGraphs + +class CyclesFinderTest { + val cyclesFinder = CyclesFinder() + + @Nested + inner class `There are some cycles` { + @TestAllDirectedGraphs + fun `all cycles should be returned`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val e01 = graph.addEdge(v0, v1) + val e07 = graph.addEdge(v0, v7) + val e04 = graph.addEdge(v0, v4) + val e18 = graph.addEdge(v1, v8) + val e12 = graph.addEdge(v1, v2) + val e20 = graph.addEdge(v2, v0) + val e21 = graph.addEdge(v2, v1) + val e25 = graph.addEdge(v2, v5) + val e23 = graph.addEdge(v2, v3) + val e53 = graph.addEdge(v5, v3) + val e34 = graph.addEdge(v3, v4) + val e41 = graph.addEdge(v4, v1) + val e78 = graph.addEdge(v7, v8) + val e87 = graph.addEdge(v8, v7) + + val expectedCycle1 = listOf(e12 to v2, e21 to v1) + val expectedCycle2 = listOf(e12 to v2, e20 to v0, e01 to v1) + val expectedCycle3 = listOf(e12 to v2, e20 to v0, e04 to v4, e41 to v1) + val expectedCycle4 = listOf(e12 to v2, e23 to v3, e34 to v4, e41 to v1) + val expectedCycle5 = listOf(e12 to v2, e25 to v5, e53 to v3, e34 to v4, e41 to v1) + + val actualValue = cyclesFinder.findCycles(graph, v1) + val expectedValue = + setOf( + expectedCycle1, + expectedCycle2, + expectedCycle3, + expectedCycle4, + expectedCycle5 + ) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `SCC of 2 vertices should have one cycle`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e12 = graph.addEdge(v1, v2) + val e21 = graph.addEdge(v2, v1) + val e23 = graph.addEdge(v2, v3) + + val actualValue = cyclesFinder.findCycles(graph, v1) + val expectedValue = setOf(listOf(e12 to v2, e21 to v1)) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `There are no cycles` { + @TestAllDirectedGraphs + fun `vertex without outgoing edges shouldn't have cycles`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1) + + val actualValue = cyclesFinder.findCycles(graph, v1) + val expectedValue = emptySet, Vertex>>>() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `SCC of 1 vertex shouldn't have cycles`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1) + + val actualValue = cyclesFinder.findCycles(graph, v0) + val expectedValue = emptySet, Vertex>>>() + + assertEquals(expectedValue, actualValue) + } + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v7) + addEdge(v0, v4) + addEdge(v1, v8) + addEdge(v1, v2) + addEdge(v2, v0) + addEdge(v2, v1) + addEdge(v2, v5) + addEdge(v2, v3) + addEdge(v5, v3) + addEdge(v3, v4) + addEdge(v4, v1) + addEdge(v7, v8) + addEdge(v8, v7) + } + + val expectedGraph = graph.getVertices() to graph.getEdges().toSet() + + cyclesFinder.findCycles(graph, v1) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + } +} + diff --git a/app/src/test/kotlin/model/algorithms/KeyVerticesFinderTest.kt b/app/src/test/kotlin/model/algorithms/KeyVerticesFinderTest.kt new file mode 100644 index 00000000..348feaba --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/KeyVerticesFinderTest.kt @@ -0,0 +1,262 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import util.annotations.TestAllDirectedGraphs +import util.annotations.TestAllUndirectedGraphs + +class KeyVerticesFinderTest { + val keyVerticesFinder = KeyVerticesFinder() + @Nested + inner class `Graph is directed and unweighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val graph = DirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + addEdge(v1, v2) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = DirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + } + + @Nested + inner class `Graph is undirected and unweighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = DirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + } + + @Nested + inner class `Graph is directed and weighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + addEdge(v2, v3, 1) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if its sum of distances to other vertices is less`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 2) + addEdge(v2, v3, 2) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `Returns null`() { + @Test + fun `if graph has negative edges`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v0, v2, -1) + + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertNull(actualResult) + } + } + } + + @Nested + inner class `Graph is undirected and weighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if its sum of distances to other vertices is less`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 2) + addEdge(v2, v3, 2) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `Returns null`() { + @Test + fun `if graph has negative edges`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v0, v2, -1) + + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertNull(actualResult) + } + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/MinSpanningTreeFinderTest.kt b/app/src/test/kotlin/model/algorithms/MinSpanningTreeFinderTest.kt new file mode 100644 index 00000000..56aa5d0c --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/MinSpanningTreeFinderTest.kt @@ -0,0 +1,145 @@ +package model.algorithms + +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class MinSpanningTreeFinderTest { + private lateinit var graph: WeightedUndirectedGraph + + @BeforeEach + fun init() { + graph = WeightedUndirectedGraph() + } + + val minSpanningTreeFinder = MinSpanningTreeFinder() + + @Nested + inner class `An edge is picked over another` { + @Test + fun `if it has lesser weight but both have positive`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Heavy = graph.addEdge(v0, v1, 5) + val e01Light = graph.addEdge(v0, v1, 3) + + val expectedReturn = listOf(e01Light) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has lesser weight but both have negative`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Heavy = graph.addEdge(v0, v1, -5) + val e01Light = graph.addEdge(v0, v1, -10) + + val expectedReturn = listOf(e01Light) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has zero weight and other has positive`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Pos = graph.addEdge(v0, v1, 5) + val e01Zero = graph.addEdge(v0, v1, 0) + + val expectedReturn = listOf(e01Zero) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has negative weight and other has positive or zero`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Pos = graph.addEdge(v0, v1, 5) + val e01Zero = graph.addEdge(v0, v1, 0) + val e01Neg = graph.addEdge(v0, v1, -5) + + val expectedReturn = listOf(e01Neg) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + } + + @Nested + inner class `An edge is not picked over another` { + @Test + fun `if it forms a cycle and has greatest weight in it`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1, 1) + val e12 = graph.addEdge(v1, v2, 1) + val e23 = graph.addEdge(v2, v3, 1) + + val e30 = graph.addEdge(v3, v0, 5) + + val expectedReturn = setOf(e01, e12, e23) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph).toSet() + + assertEquals(expectedReturn, actualReturn) + } + } + + @Nested + inner class `All edges should be returned` { + @Test + fun `if graph is a tree`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val eva01 = graph.addEdge(v0, v1, 1) + val eva02 = graph.addEdge(v0, v2, 10) + val e23 = graph.addEdge(v2, v3, 0) + val e24 = graph.addEdge(v2, v4, -20) + + val expectedResult = setOf(e24, e23, eva01, eva02) + val actualResult = minSpanningTreeFinder.findMinSpanningTree(graph).toSet() + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No edge should be returned` { + @Test + fun `if graph has no vertices`() { + val expectedResult = listOf>() + val actualResult = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has no edges`() { + graph.addVertex(0) + graph.addVertex(1) + + val expectedResult = listOf>() + val actualResult = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedResult, actualResult) + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/SCCFinderTest.kt b/app/src/test/kotlin/model/algorithms/SCCFinderTest.kt new file mode 100644 index 00000000..0076d2ab --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/SCCFinderTest.kt @@ -0,0 +1,352 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested +import util.annotations.TestAllDirectedGraphs + +class SCCFinderTest { + val SCCFinder = SCCFinder() + + @Nested + inner class `SCC should return not empty array` { + @TestAllDirectedGraphs + fun `graph has two connected vertices`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v1) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `complex graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph has multiple SCCs`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v1) + addEdge(v3, v4) + addEdge(v4, v3) + addEdge(v5, v1) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v3, v4), mutableSetOf(v1, v2), mutableSetOf(v5)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with nested cycles`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + addEdge(v4, v5) + addEdge(v5, v6) + addEdge(v6, v4) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with cross connections`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + addEdge(v4, v5) + addEdge(v5, v6) + addEdge(v6, v4) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with disconnected subgraphs`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v1) + addEdge(v3, v4) + addEdge(v4, v3) + addEdge(v5, v6) + addEdge(v6, v5) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2), mutableSetOf(v3, v4), mutableSetOf(v5, v6)) + assertEquals(expectedValue, actualValue) + } + + @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") + @TestAllDirectedGraphs + fun `graph with single vertex cycle`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v3) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3)) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `SCC should return single-element SCCs` { + @TestAllDirectedGraphs + fun `graph has single vertex`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with multiple disconnected vertices`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = + mutableSetOf( + mutableSetOf(v1), + mutableSetOf(v2), + mutableSetOf(v3), + mutableSetOf(v4) + ) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `Additional edge cases`() { + @TestAllDirectedGraphs + fun `empty graph`(graph: DirectedGraph) { + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf>>() + + assertEquals(expectedValue, actualValue) + } + + @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") + @TestAllDirectedGraphs + fun `graph with self-loops`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v1, v1) + graph.addEdge(v2, v2) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1), mutableSetOf(v2)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `linear graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v3), mutableSetOf(v2), mutableSetOf(v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with cycles and tail`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4), mutableSetOf(v5)) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `Side-effects check` { + @TestAllDirectedGraphs + fun `check vertices in complex graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } + + val expectedValue = graph.getVertices() + SCCFinder.findSCC(graph) + val actualValue = graph.getVertices() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check edges in complex graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } + + val expectedValue = graph.getEdges() + SCCFinder.findSCC(graph) + val actualValue = graph.getEdges() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check edges in graph with cycles and tail`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } + + val expectedValue = graph.getEdges() + SCCFinder.findSCC(graph) + val actualValue = graph.getEdges() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check vertices graph with cycles and tail`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } + + val expectedValue = graph.getVertices() + SCCFinder.findSCC(graph) + val actualValue = graph.getVertices() + + assertEquals(expectedValue, actualValue) + } + } +} + diff --git a/app/src/test/kotlin/model/algorithms/ShortestPathFinderTest.kt b/app/src/test/kotlin/model/algorithms/ShortestPathFinderTest.kt new file mode 100644 index 00000000..281cc032 --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/ShortestPathFinderTest.kt @@ -0,0 +1,636 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import util.setupAbstractGraph +import util.setupDirectedGraphWithCycle +import util.setupWeightedUndirected + +class ShortestPathFinderTest { + val shortestPathFinder = ShortestPathFinder() + + @Nested + inner class `Graph is weighted and directed` { + private lateinit var graph: WeightedDirectedGraph + + @BeforeEach + fun init() { + graph = WeightedDirectedGraph() + } + + @Nested + inner class `There are no negative weights (Dijkstra)` { + @Nested + inner class `Normal path should be returned`() { + @Test + fun `all is as usual, should return default`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 10) + val e1 = graph.addEdge(v0, v4, 100) + val e2 = graph.addEdge(v0, v3, 30) + val e3 = graph.addEdge(v1, v2, 2) + val e4 = graph.addEdge(v2, v4, 10) + val e5 = graph.addEdge(v3, v2, 20) + val e6 = graph.addEdge(v3, v4, 60) + + val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `direct path should be the shortest in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 5) + val e1 = graph.addEdge(v1, v2, 5) + val e2 = graph.addEdge(v0, v2, 10) + + val expectedResult = listOf(e2 to v2) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has multiple paths and equal weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3) + val expectedResult2 = listOf(e1 to v2, e3 to v3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has single edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e0 = graph.addEdge(v0, v1, 5) + + val expectedResult = listOf(e0 to v1) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v1) + + assertEquals(expectedResult, actualResult) + } + + @Disabled("Dijkstra's algorithm doesn't work with negative weights") + @Test + fun `if graph has negative weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, -1) + val e1 = graph.addEdge(v1, v2, -2) + val e2 = graph.addEdge(v2, v3, -3) + + val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `graph has multiple equal shortest paths`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + val e4 = graph.addEdge(v3, v4, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) + val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has a cycle`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v1, v2, 2) + val e2 = graph.addEdge(v2, v0, 3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + val expectedResult = listOf(e2 to v2) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if all the edges have zero weight in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(e0 to v1, e1 to v2) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No path should be returned`() { + @Test + fun `no path exists in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v0, v1, 10) + graph.addEdge(v1, v2, 20) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + assertEquals(actualResult, null) + } + + @Test + fun `if start and end vertices are the same`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v1, v2, 2) + addEdge(v2, v0, 2) + } + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if graph has single vertex`() { + val v0 = graph.addVertex(0) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if path is in other way (not how edges were set)`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 0) + graph.addEdge(v1, v2, 0) + + val actualResult = shortestPathFinder.findShortestPath(graph, v2, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + } + } + + @Nested + inner class `There are negative weights (Ford-Bellman)` { + @Nested + inner class `Path exists` { + @Test + fun `path between neighbours should consist of one edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val edge = graph.addEdge(v0, v1, 12345) + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v1) + val expectedValue = listOf(edge to v1) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `shortest path should be returned`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val v4 = defaultVertices[4] + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v4) + val expectedValue = + listOf( + graph.getEdge(v0, v1) to v1, + graph.getEdge(v1, v2) to v2, + graph.getEdge(v2, v4) to v4 + ) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `path from vertex to itself should exist and be empty`() { + val v0 = graph.addVertex(69) + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v0) + val expectedValue = emptyList, Vertex>>() + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v3 = defaultVertices[3] + val v4 = defaultVertices[4] + + val expectedGraph = graphStructure + shortestPathFinder.findShortestPath(graph, v3, v4) + val actualGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Path doesn't exist` { + @Test + fun `there is simply no path between vertices`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v5 = defaultVertices[5] + + val actualValue = shortestPathFinder.findShortestPath(graph, v1, v5) + + assertNull(actualValue) + } + + @Test + fun `order of arguments should matter`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v2 = defaultVertices[2] + + val actualValue = shortestPathFinder.findShortestPath(graph, v2, v0) + + assertNull(actualValue) + } + + @Test + fun `there is a negative cycle on the path`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v8 = defaultVertices[8] + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v8) + + assertNull(actualValue) + } + + @Test + fun `srcVertex is a part of negative cycle`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v6 = defaultVertices[6] + val v8 = defaultVertices[8] + + val actualValue = shortestPathFinder.findShortestPath(graph, v6, v8) + + assertNull(actualValue) + } + + @Test + fun `vertex without outgoing edges shouldn't have any paths`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val v3 = defaultVertices[3] + val v4 = defaultVertices[4] + val v5 = defaultVertices[5] + val v6 = defaultVertices[6] + val v7 = defaultVertices[7] + val v8 = defaultVertices[8] + + assertNull(shortestPathFinder.findShortestPath(graph, v8, v0)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v1)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v2)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v3)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v4)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v5)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v6)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v7)) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v8 = defaultVertices[8] + + val expectedGraph = graphStructure + shortestPathFinder.findShortestPath(graph, v8, v1) + val actualGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + } + } + + @Nested + inner class `Graph is weighted and undirected` { + private lateinit var graph: WeightedUndirectedGraph + + @BeforeEach + fun init() { + graph = WeightedUndirectedGraph() + } + + @Nested + inner class `There are no negative weights (Dijkstra)` { + @Nested + inner class `Normal path should be returned` { + @Test + fun `all is as usual, should return default`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 10) + val e1 = graph.addEdge(v0, v4, 100) + val e2 = graph.addEdge(v0, v3, 30) + val e3 = graph.addEdge(v1, v2, 2) + val e4 = graph.addEdge(v2, v4, 10) + val e5 = graph.addEdge(v3, v2, 20) + val e6 = graph.addEdge(v3, v4, 60) + + val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has multiple paths and equal weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3) + val expectedResult2 = listOf(e1 to v2, e3 to v3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has single edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e0 = graph.addEdge(v0, v1, 5) + + val expectedResult = listOf(e0 to v1) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v1) + + assertEquals(expectedResult, actualResult) + } + + @Disabled("Dijkstra's algorithm doesn't work with negative weights") + @Test + fun `if graph has negative weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, -1) + val e1 = graph.addEdge(v1, v2, -2) + val e2 = graph.addEdge(v2, v3, -3) + + val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `graph has multiple equal shortest paths`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + val e4 = graph.addEdge(v3, v4, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) + val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has a cycle`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v1, v2, 2) + val e2 = graph.addEdge(v2, v0, 3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + val expectedResult = listOf(e2 to v2) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if path is in other way (not how edges were set)`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(e1 to v1, e0 to v0) + val actualResult = shortestPathFinder.findShortestPath(graph, v2, v0) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if all the edges have zero weight in undirected graph`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(e0 to v1, e1 to v2) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No path should be returned`() { + @Test + fun `no path exists in undirected graph`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v1, v2, 2) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertNull(actualResult) + } + + @Test + fun `if start and end vertices are the same`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v1, v2, 2) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if graph has single vertex`() { + val v0 = graph.addVertex(0) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + } + } + + @Nested + inner class `There are negative weights` { + @Test + fun `shortest path shouldn't exist (null should be returned)`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[4] + + assertNull(shortestPathFinder.findShortestPath(graph, v1, v4)) + } + } + } + + @Nested + inner class `Graph is unweighted` { + @Test + fun `directed unweighted graphs aren't supported`() { + val graph = DirectedGraph() + val graphStructure = setupAbstractGraph(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v2 = defaultVertices[2] + + assertNull(shortestPathFinder.findShortestPath(graph, v0, v2)) + } + + @Test + fun `undirected unweighted graphs aren't supported`() { + val graph = DirectedGraph() + val graphStructure = setupAbstractGraph(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + + assertNull(shortestPathFinder.findShortestPath(graph, v1, v3)) + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt b/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt new file mode 100644 index 00000000..8d3f2e25 --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt @@ -0,0 +1,41 @@ +package model.algorithms.clustering + +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import util.annotations.TestAllGraphTypes +import util.setupAbstractGraph + +class CommunitiesFinderTest { + val louvain = CommunitiesFinder() + + @TestAllGraphTypes + fun `graph of 1 vertex should have one community`(graph: Graph) { + val v0 = graph.addVertex(0) + + val actualValue = louvain.findCommunities(graph) + val expectedValue = setOf(setOf(v0)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllGraphTypes + fun `empty graph should have no communities`(graph: Graph) { + val actualValue = louvain.findCommunities(graph) + val expectedValue = emptySet>>() + + assertEquals(expectedValue, actualValue) + } + + @TestAllGraphTypes + fun `graph doesn't change`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + + louvain.findCommunities(graph) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } +} diff --git a/app/src/test/kotlin/model/graphs/DirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/DirectedGraphTest.kt new file mode 100644 index 00000000..ac2254f2 --- /dev/null +++ b/app/src/test/kotlin/model/graphs/DirectedGraphTest.kt @@ -0,0 +1,437 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import util.annotations.TestAllDirectedGraphs +import util.emptyEdgesSet +import util.emptyVerticesList +import util.setupAbstractGraph + +class DirectedGraphTest { + @Nested + inner class GetEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllDirectedGraphs + fun `edge should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v2 = defaultVerticesList[2] + + val newEdge = graph.addEdge(v0, v2) + + val actualValue = newEdge + val expectedValue = graph.getEdge(v0, v2) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + graph.getEdge(v2, v3) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllDirectedGraphs + fun `order of arguments should matter`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(v1, v0) + } + } + + @TestAllDirectedGraphs + fun `trying to get non-existent edge should throw an exception`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(Vertex(2, 12), Vertex(85, 6)) + } + } + } + } + + @Nested + inner class GetNeighboursTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllDirectedGraphs + fun `neighbours should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getNeighbours(v3).toSet() + val expectedValue = setOf(v4, v1) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + graph.getNeighbours(v0) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getNeighbours(Vertex(2201, 2006)) + } + } + } + } + + @Nested + inner class GetOutgoingEdgesTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllDirectedGraphs + fun `outgoing edges should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getOutgoingEdges(v3).toSet() + val expectedValue = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v4 = defaultVerticesList[4] + + graph.getOutgoingEdges(v4) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getOutgoingEdges(Vertex(2611, 2005)) + } + } + } + } + + @Nested + inner class AddEdgeTest { + @Nested + inner class `Two vertices are in the graph` { + @Nested + inner class `Vertices are different` { + @TestAllDirectedGraphs + fun `Added edge should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val actualValue = graph.addEdge(v0, v4) + val expectedValue = graph.getEdge(v0, v4) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `Edge should be added to graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val newEdge = graph.addEdge(v4, v0) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet + newEdge + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `one vertex has to be added to the other's adjacency map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v0, v2) + + val actualVertices = graph.getNeighbours(v0).toSet() + val expectedVertices = setOf(v1, v2) + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllDirectedGraphs + fun `edge has to be added to first vertex's outgoing edges map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v3, v0) + + val actualEdges = graph.getOutgoingEdges(v3).toSet() + val expectedEdges = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1), graph.getEdge(v3, v0)) + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `adding already existing edge shouldn't change anything`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedNeighbours = graph.getNeighbours(v4).toSet() + val expectedOutgoingEdges = graph.getOutgoingEdges(v4).toSet() + + graph.addEdge(v4, v1) + + val actualNeighbours = graph.getNeighbours(v4).toSet() + val actualOutgoingEdges = graph.getOutgoingEdges(v4).toSet() + + val expectedGraph = graphStructure + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + assertEquals(expectedNeighbours, actualNeighbours) + assertEquals(expectedOutgoingEdges, actualOutgoingEdges) + } + + @TestAllDirectedGraphs + fun `second vertex's map values shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v3 = defaultVerticesList[3] + + val expectedNeighbours = graph.getNeighbours(v0).toSet() + val expectedOutgoingEdges = graph.getOutgoingEdges(v0).toSet() + + graph.addEdge(v3, v0) + + val actualNeighbours = graph.getNeighbours(v0).toSet() + val actualOutgoingEdges = graph.getOutgoingEdges(v0).toSet() + + assertEquals(expectedNeighbours, actualNeighbours) + assertEquals(expectedOutgoingEdges, actualOutgoingEdges) + } + } + + @Nested + inner class `Vertices are the same` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v2, v2) + } + } + } + } + + @Nested + inner class `One of the vertices isn't in the graph` { + @TestAllDirectedGraphs + fun `first vertex isn't in the graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(2210, 2005), v0) + } + } + + @TestAllDirectedGraphs + fun `second vertex isn't in the graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v0, Vertex(2510, 1917)) + } + } + + @TestAllDirectedGraphs + fun `both vertices aren't in the graph`(graph: DirectedGraph) { + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(3010, 1978), Vertex(1002, 1982)) + } + } + } + } + + @Nested + inner class RemoveEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllDirectedGraphs + fun `removed edge should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v3, v1) + + val actualValue = graph.removeEdge(edgeToRemove) + val expectedValue = edgeToRemove + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `edge should be removed from graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v4, v1) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet - edgeToRemove + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `second vertex should be removed from first's adjacency map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + + val edgeToRemove = graph.getEdge(v1, v2) + graph.removeEdge(edgeToRemove) + + val actualVertices = graph.getNeighbours(v1).toSet() + val expectedVertices = emptyVerticesList.toSet() + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllDirectedGraphs + fun `edge should be removed from first vertex's outgoing edges map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v2, v3) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getOutgoingEdges(v2).toSet() + val expectedEdges = emptyEdgesSet + + assertEquals(expectedEdges, actualEdges) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllDirectedGraphs + fun `wrong order of the arguments should throw an exception`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(graph.getEdge(v4, v3)) + } + } + + @TestAllDirectedGraphs + fun `non-existing edge should throw an exception`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(Edge(Vertex(0, 0), Vertex(1, 1))) + } + } + } + } +} diff --git a/app/src/test/kotlin/model/graphs/UndirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/UndirectedGraphTest.kt new file mode 100644 index 00000000..c350f48b --- /dev/null +++ b/app/src/test/kotlin/model/graphs/UndirectedGraphTest.kt @@ -0,0 +1,538 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Nested +import util.annotations.TestAllUndirectedGraphs +import util.setupAbstractGraph + +class UndirectedGraphTest { + @Nested + inner class GetEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllUndirectedGraphs + fun `edge should be returned`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v2 = defaultVerticesList[2] + + val newEdge = graph.addEdge(v0, v2) + + val actualValue = graph.getEdge(v0, v2) + val expectedValue = newEdge + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `order of the arguments shouldn't matter`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + + assertEquals(graph.getEdge(v0, v1), graph.getEdge(v1, v0)) + } + + @TestAllUndirectedGraphs + fun `graph shouldn't change`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + graph.getEdge(v2, v3) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllUndirectedGraphs + fun `trying to get non-existent edge should throw an exception`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(Vertex(2, 12), Vertex(85, 6)) + } + } + } + } + + @Nested + inner class GetNeighboursTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllUndirectedGraphs + fun `neighbours should be returned`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getNeighbours(v3).toSet() + val expectedValue = setOf(v1, v2, v4) + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `graph shouldn't change`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + graph.getNeighbours(v0) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllUndirectedGraphs + fun `exception should be thrown`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getNeighbours(Vertex(2201, 2006)) + } + } + } + } + + @Nested + inner class GetOutgoingEdgesTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllUndirectedGraphs + fun `outgoing edges should be returned`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getOutgoingEdges(v3).toSet() + val expectedValue = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1), graph.getEdge(v3, v2)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `graph shouldn't change`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v4 = defaultVerticesList[4] + + graph.getOutgoingEdges(v4) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllUndirectedGraphs + fun `exception should be thrown`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getOutgoingEdges(Vertex(2611, 2005)) + } + } + } + } + + @Nested + inner class AddEdgeTest { + @Nested + inner class `Two vertices are in the graph` { + @Nested + inner class `Vertices are different` { + @TestAllUndirectedGraphs + fun `Added edge should be returned`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val actualValue = graph.addEdge(v0, v4) + val expectedValue = graph.getEdge(v0, v4) + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `Edge should be added to graph`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val newEdge = graph.addEdge(v4, v0) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet + newEdge + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllUndirectedGraphs + fun `vertices have to be added to each other's adjacency map values`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v0, v2) + + val actualVertices1 = graph.getNeighbours(v0).toSet() + val expectedVertices1 = setOf(v1, v2) + + val actualVertices2 = graph.getNeighbours(v2).toSet() + val expectedVertices2 = setOf(v0, v1, v3) + + assertEquals(expectedVertices1, actualVertices1) + assertEquals(expectedVertices2, actualVertices2) + } + + @TestAllUndirectedGraphs + fun `edge has to be added to both vertices' outgoing edges map values`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v3, v0) + + val actualEdges1 = graph.getOutgoingEdges(v0).toSet() + val expectedEdges1 = setOf(graph.getEdge(v0, v1), graph.getEdge(v0, v3)) + + val actualEdges2 = graph.getOutgoingEdges(v3).toSet() + val expectedEdges2 = + setOf( + graph.getEdge(v3, v0), + graph.getEdge(v3, v1), + graph.getEdge(v3, v2), + graph.getEdge(v3, v4) + ) + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + + @TestAllUndirectedGraphs + fun `adding already existing edge shouldn't change graph`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + graph.addEdge(v4, v1) + + val expectedGraph = graphStructure + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + } + + @TestAllUndirectedGraphs + fun `adding already existing edge shouldn't change adjacency map`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedNeighbours1 = graph.getNeighbours(v1).toSet() + val expectedNeighbours2 = graph.getNeighbours(v4).toSet() + + graph.addEdge(v4, v1) + + val actualNeighbours1 = graph.getNeighbours(v1).toSet() + val actualNeighbours2 = graph.getNeighbours(v4).toSet() + + assertEquals(expectedNeighbours1, actualNeighbours1) + assertEquals(expectedNeighbours2, actualNeighbours2) + } + + @TestAllUndirectedGraphs + fun `adding already existing edge shouldn't change outgoing edges map`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedEdges1 = graph.getOutgoingEdges(v1).toSet() + val expectedEdges2 = graph.getOutgoingEdges(v4).toSet() + + graph.addEdge(v4, v1) + + val actualEdges1 = graph.getOutgoingEdges(v1).toSet() + val actualEdges2 = graph.getOutgoingEdges(v4).toSet() + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + + @TestAllUndirectedGraphs + fun `adding edge with reversed arguments shouldn't change graph`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + graph.addEdge(v1, v4) + + val expectedGraph = graphStructure + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + } + + @TestAllUndirectedGraphs + fun `adding edge with reversed arguments shouldn't change adjacency map`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedNeighbours1 = graph.getNeighbours(v1).toSet() + val expectedNeighbours2 = graph.getNeighbours(v4).toSet() + + graph.addEdge(v1, v4) + + val actualNeighbours1 = graph.getNeighbours(v1).toSet() + val actualNeighbours2 = graph.getNeighbours(v4).toSet() + + assertEquals(expectedNeighbours1, actualNeighbours1) + assertEquals(expectedNeighbours2, actualNeighbours2) + } + + @TestAllUndirectedGraphs + fun `adding edge with reversed arguments shouldn't change outgoing edges map`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedEdges1 = graph.getOutgoingEdges(v1).toSet() + val expectedEdges2 = graph.getOutgoingEdges(v4).toSet() + + graph.addEdge(v1, v4) + + val actualEdges1 = graph.getOutgoingEdges(v1).toSet() + val actualEdges2 = graph.getOutgoingEdges(v4).toSet() + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + } + + @Nested + inner class `Vertices are the same` { + @TestAllUndirectedGraphs + fun `exception should be thrown`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v2, v2) + } + } + } + } + + @Nested + inner class `One of the vertices isn't in the graph` { + @TestAllUndirectedGraphs + fun `first vertex isn't in the graph`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(2210, 2005), v0) + } + } + + @TestAllUndirectedGraphs + fun `second vertex isn't in the graph`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v0, Vertex(2510, 1917)) + } + } + + @TestAllUndirectedGraphs + fun `both vertices aren't in the graph`(graph: UndirectedGraph) { + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(3010, 1978), Vertex(1002, 1982)) + } + } + } + } + + @Nested + inner class RemoveEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllUndirectedGraphs + fun `removed edge should be returned`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v3, v1) + + val actualValue = graph.removeEdge(edgeToRemove) + val expectedValue = edgeToRemove + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `order of the arguments shouldn't matter`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v1, v3) + + val actualValue = graph.removeEdge(edgeToRemove) + val expectedValue = edgeToRemove + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `edge should be removed from graph`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v4, v1) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet - edgeToRemove + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllUndirectedGraphs + fun `vertices should be removed from each other's adjacency map values`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v1, v2) + graph.removeEdge(edgeToRemove) + + val actualVertices1 = graph.getNeighbours(v1).toSet() + val expectedVertices1 = setOf(v0, v3, v4) + + val actualVertices2 = graph.getNeighbours(v2).toSet() + val expectedVertices2 = setOf(v3) + + assertEquals(expectedVertices1, actualVertices1) + assertEquals(expectedVertices2, actualVertices2) + } + + @TestAllUndirectedGraphs + fun `edge should be removed from vertices' outgoing edges map values`(graph: UndirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v1, v2) + graph.removeEdge(edgeToRemove) + + val actualEdges1 = graph.getOutgoingEdges(v1).toSet() + val expectedEdges1 = + setOf( + graph.getEdge(v1, v0), + graph.getEdge(v1, v3), + graph.getEdge(v1, v4), + ) + + val actualEdges2 = graph.getOutgoingEdges(v2).toSet() + val expectedEdges2 = setOf(graph.getEdge(v2, v3)) + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllUndirectedGraphs + fun `non-existing edge should throw an exception`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(Edge(Vertex(0, 0), Vertex(1, 1))) + } + } + } + } +} diff --git a/app/src/test/kotlin/model/graphs/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/WeightedDirectedGraphTest.kt new file mode 100644 index 00000000..dab249ae --- /dev/null +++ b/app/src/test/kotlin/model/graphs/WeightedDirectedGraphTest.kt @@ -0,0 +1,99 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import util.setupWeightedDirected + +class WeightedDirectedGraphTest { + private lateinit var graph: WeightedDirectedGraph + + @BeforeEach + fun init() { + graph = WeightedDirectedGraph() + } + + @Nested + inner class GetWeightTest { + @Nested + inner class `Edge is in the graph` { + @Test + fun `edge's weight should be returned`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + val edge = graph.getEdge(v3, v1) + + val actualValue = graph.getWeight(edge) + val expectedValue = 3 + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[3] + val edge = graph.getEdge(v4, v1) + + graph.getWeight(edge) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @Test + fun `exception should be thrown`() { + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) + } + } + } + } + + // most of the functionality is tested in the DirectedGraphTest class, + // as weighted graphs call super methods inside their methods + @Nested + inner class AddEdgeTest { + @Test + fun `added edge's weight should be added to weight map`() { + val v0 = graph.addVertex(30) + val v1 = graph.addVertex(31) + + val newEdge = graph.addEdge(v0, v1, 62) + + val actualValue = graph.getWeight(newEdge) + val expectedValue = 62 + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class RemoveEdgeTest { + @Test + fun `removed edge should be removed from the weight map`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val edge = graph.getEdge(v1, v2) + + graph.removeEdge(edge) + + assertThrows(NoSuchElementException::class.java) { graph.getWeight(edge) } + } + } +} diff --git a/app/src/test/kotlin/model/graphs/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/WeightedUndirectedGraphTest.kt new file mode 100644 index 00000000..c346dc91 --- /dev/null +++ b/app/src/test/kotlin/model/graphs/WeightedUndirectedGraphTest.kt @@ -0,0 +1,103 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import util.setupWeightedUndirected + +class WeightedUndirectedGraphTest { + private lateinit var graph: WeightedUndirectedGraph + + @BeforeEach + fun init() { + graph = WeightedUndirectedGraph() + } + + @Nested + inner class GetWeightTest { + @Nested + inner class `Edge is in the graph` { + @Test + fun `edge's weight should be returned`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + val edge = graph.getEdge(v3, v1) + + val actualValue = graph.getWeight(edge) + val expectedValue = 3 + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[3] + val edge = graph.getEdge(v4, v1) + + graph.getWeight(edge) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @Test + fun `exception should be thrown`() { + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) + } + } + } + } + + // most of the functionality is tested in the DirectedGraphTest class, + // as weighted graphs call super methods inside their methods + @Nested + inner class AddEdgeTest { + @Test + fun `added edge's weight should be added to weight map`() { + val v0 = graph.addVertex(30) + val v1 = graph.addVertex(31) + + val newEdge = graph.addEdge(v0, v1, 62) + + val actualValue = graph.getWeight(newEdge) + val expectedValue = 62 + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class RemoveEdgeTest { + @Test + fun `removed edge should be removed from the weight map`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val edge = graph.getEdge(v1, v2) + + graph.removeEdge(edge) + + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(edge) + } + } + } +} diff --git a/app/src/test/kotlin/model/graphs/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/graphs/abstractGraph/GraphTest.kt new file mode 100644 index 00000000..df12fbf8 --- /dev/null +++ b/app/src/test/kotlin/model/graphs/abstractGraph/GraphTest.kt @@ -0,0 +1,297 @@ +package model.graphs.abstractGraph + +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import util.annotations.TestAllGraphTypes +import util.emptyEdgesSet +import util.emptyGraph +import util.setupAbstractGraph + +class GraphTest { + @Nested + inner class GetVerticesTest { + @Nested + inner class `Graph is not empty` { + @TestAllGraphTypes + fun `non-empty list of vertices should be returned`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val actualList = graph.getVertices() + val expectedList = defaultVerticesList + + assertEquals(expectedList, actualList) + } + + @TestAllGraphTypes + fun `graph should not change`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + + graph.getVertices() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Graph is empty` { + @TestAllGraphTypes + fun `empty list should be returned`(graph: Graph) { + val actualList = graph.getVertices() + val expectedList: List = listOf() + + assertEquals(expectedList, actualList) + } + + @TestAllGraphTypes + fun `empty graph should not change`(graph: Graph) { + graph.getVertices() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = emptyGraph + + assertEquals(expectedGraph, actualGraph) + } + } + } + + @Nested + inner class GetEdgesTest { + @Nested + inner class `Graph is not empty` { + @TestAllGraphTypes + fun `non-empty list of edges should be returned`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultEdgesSet = graphStructure.second + + val actualSet = graph.getEdges().toSet() + val expectedSet = defaultEdgesSet + + assertEquals(expectedSet, actualSet) + } + + @TestAllGraphTypes + fun `graph should not change`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + + graph.getEdges() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Graph is empty` { + @TestAllGraphTypes + fun `empty list should be returned`(graph: Graph) { + val actualSet = graph.getEdges().toSet() + val expectedSet = emptyEdgesSet + + assertEquals(expectedSet, actualSet) + } + + @TestAllGraphTypes + fun `empty graph should not change`(graph: Graph) { + graph.getEdges() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = emptyGraph + + assertEquals(expectedGraph, actualGraph) + } + } + } + + @Nested + inner class AddVertexTest { + @TestAllGraphTypes + fun `added vertex should be returned`(graph: Graph) { + val returnedVertex = graph.addVertex(0) + + assertTrue(returnedVertex.id == 0 && returnedVertex.data == 0) + } + + @TestAllGraphTypes + fun `vertex should be added to graph`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val newVertex = graph.addVertex(5) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = (defaultVerticesList + newVertex) to defaultEdgesSet + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class RemoveVertexTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllGraphTypes + fun `removed vertex should be returned`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val returnedVertex = graph.removeVertex(defaultVerticesList[2]) + + assertTrue(returnedVertex.id == 2 && returnedVertex.data == 2) + } + + @TestAllGraphTypes + fun `vertex added after removal should have right id`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + graph.removeVertex(defaultVerticesList[3]) + val newVertex = graph.addVertex(5) + + assertTrue(newVertex.id == 4) + } + + @Nested + inner class `Vertex is last` { + @TestAllGraphTypes + fun `vertex should be removed from vertices list`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val removedVertex = graph.removeVertex(defaultVerticesList[4]) + + val actualVertices = graph.getVertices() + val expectedVertices = defaultVerticesList - removedVertex + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllGraphTypes + fun `incident edges should be removed`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val e0 = graph.getEdge(v3, v4) + val e1 = graph.getEdge(v4, v1) + + graph.removeVertex(v4) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet - e0 - e1 + + assertEquals(expectedEdges, actualEdges) + } + } + + @Nested + inner class `Vertex isn't last` { + @TestAllGraphTypes + fun `last added vertex should be moved to removed vertex's place`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val oldV0 = defaultVerticesList[0] + val oldV1 = defaultVerticesList[1] + val oldV2 = defaultVerticesList[2] + val oldV3 = defaultVerticesList[3] + val oldV4 = defaultVerticesList[4] + + graph.removeVertex(oldV2) + + val newVertices = graph.getVertices() + + val newV0 = newVertices[0] + val newV1 = newVertices[1] + val newV2 = newVertices[2] + val newV3 = newVertices[3] + + assertTrue(newV0 == oldV0 && newV1 == oldV1 && newV2.id == 2 && newV2.data == 4 && newV3 == oldV3) + } + + @TestAllGraphTypes + fun `last added vertex's incident edges should change`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val oldV0 = defaultVerticesList[0] + val oldV1 = defaultVerticesList[1] + val oldV2 = defaultVerticesList[2] + val oldV3 = defaultVerticesList[3] + val oldV4 = defaultVerticesList[4] + + graph.removeVertex(oldV2) + + val newVertices = graph.getVertices() + + val newV0 = newVertices[0] + val newV1 = newVertices[1] + val newV2 = newVertices[2] + val newV3 = newVertices[3] + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = + setOf( + graph.getEdge(newV0, newV1), + graph.getEdge(newV3, newV2), + graph.getEdge(newV2, newV1), + graph.getEdge(newV3, newV1) + ) + + assertEquals(expectedEdges, actualEdges) + } + } + } + + @Nested + inner class `Vertex is not in the graph` { + @TestAllGraphTypes + fun `removing vertex from an empty graph should cause exception`(graph: Graph) { + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(0, 0)) + } + } + + @TestAllGraphTypes + fun `removing non-existing vertex from a non-empty graph should cause exception`(graph: Graph) { + setupAbstractGraph(graph) + + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(1904, -360)) + } + } + + @TestAllGraphTypes + fun `removing vertex with wrong id should cause exception`(graph: Graph) { + setupAbstractGraph(graph) + + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(6, 3)) + } + } + + @TestAllGraphTypes + fun `removing vertex with wrong data should cause exception`(graph: Graph) { + setupAbstractGraph(graph) + + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(0, 35)) + } + } + } + } +} diff --git a/app/src/test/kotlin/util/annotations/TestAllDirectedGraphs.kt b/app/src/test/kotlin/util/annotations/TestAllDirectedGraphs.kt new file mode 100644 index 00000000..55f1acac --- /dev/null +++ b/app/src/test/kotlin/util/annotations/TestAllDirectedGraphs.kt @@ -0,0 +1,11 @@ +package util.annotations + +import util.annotations.argumentProviders.WeightedAndUnweightedDirectedGraphsProvider +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +@ParameterizedTest(name = "{0}") +@ArgumentsSource(WeightedAndUnweightedDirectedGraphsProvider::class) +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class TestAllDirectedGraphs diff --git a/app/src/test/kotlin/util/annotations/TestAllGraphTypes.kt b/app/src/test/kotlin/util/annotations/TestAllGraphTypes.kt new file mode 100644 index 00000000..516ec930 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/TestAllGraphTypes.kt @@ -0,0 +1,12 @@ +package util.annotations + +import util.annotations.argumentProviders.AllGraphTypesProvider +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +@ParameterizedTest(name = "{0}") +@ArgumentsSource(AllGraphTypesProvider::class) +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class TestAllGraphTypes + diff --git a/app/src/test/kotlin/util/annotations/TestAllUndirectedGraphs.kt b/app/src/test/kotlin/util/annotations/TestAllUndirectedGraphs.kt new file mode 100644 index 00000000..d7a35640 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/TestAllUndirectedGraphs.kt @@ -0,0 +1,11 @@ +package util.annotations + +import util.annotations.argumentProviders.WeightedAndUnweightedUndirectedGraphsProvider +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +@ParameterizedTest(name = "{0}") +@ArgumentsSource(WeightedAndUnweightedUndirectedGraphsProvider::class) +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class TestAllUndirectedGraphs diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt new file mode 100644 index 00000000..71368b65 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt @@ -0,0 +1,19 @@ +package util.annotations.argumentProviders + +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class AllGraphTypesProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of(UndirectedGraph()), + Arguments.of(DirectedGraph()), + Arguments.of(WeightedUndirectedGraph()), + Arguments.of(WeightedDirectedGraph()) + ) +} \ No newline at end of file diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt new file mode 100644 index 00000000..fe552ba8 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt @@ -0,0 +1,15 @@ +package util.annotations.argumentProviders + +import model.graphs.DirectedGraph +import model.graphs.WeightedDirectedGraph +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class WeightedAndUnweightedDirectedGraphsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of(DirectedGraph()), + Arguments.of(WeightedDirectedGraph()) + ) +} diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt new file mode 100644 index 00000000..d765e8e1 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt @@ -0,0 +1,15 @@ +package util.annotations.argumentProviders + +import model.graphs.UndirectedGraph +import model.graphs.WeightedUndirectedGraph +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class WeightedAndUnweightedUndirectedGraphsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of(UndirectedGraph()), + Arguments.of(WeightedUndirectedGraph()) + ) +} diff --git a/app/src/test/kotlin/util/emptyGraphs.kt b/app/src/test/kotlin/util/emptyGraphs.kt new file mode 100644 index 00000000..fe9cb4e0 --- /dev/null +++ b/app/src/test/kotlin/util/emptyGraphs.kt @@ -0,0 +1,19 @@ +package util + +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex + +val emptyDirectedGraph = DirectedGraph() +val emptyUndirectedGraph = UndirectedGraph() + +val emptyWDGrapgh = WeightedDirectedGraph() +val emptyWUGrapgh = WeightedUndirectedGraph() + +val emptyVerticesList = listOf>() +val emptyEdgesSet = setOf>() + +val emptyGraph = emptyVerticesList to emptyEdgesSet \ No newline at end of file diff --git a/app/src/test/kotlin/util/setup.kt b/app/src/test/kotlin/util/setup.kt new file mode 100644 index 00000000..cead670d --- /dev/null +++ b/app/src/test/kotlin/util/setup.kt @@ -0,0 +1,134 @@ +package util + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import model.graphs.WeightedUndirectedGraph +import model.graphs.WeightedDirectedGraph + +fun setupAbstractGraph(graph: Graph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1), + graph.addEdge(v1, v2), + graph.addEdge(v2, v3), + graph.addEdge(v3, v4), + graph.addEdge(v4, v1), + graph.addEdge(v3, v1) + ) + + return defaultVerticesList to defaultEdgesSet +} + +fun setupWeightedDirected(graph: WeightedDirectedGraph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1, -3), + graph.addEdge(v1, v2, -2), + graph.addEdge(v2, v3, -1), + graph.addEdge(v3, v4, 1), + graph.addEdge(v4, v1, 2), + graph.addEdge(v3, v1, 3) + ) + + return defaultVerticesList to defaultEdgesSet +} + +fun setupWeightedUndirected(graph: WeightedUndirectedGraph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1, -3), + graph.addEdge(v1, v2, -2), + graph.addEdge(v2, v3, -1), + graph.addEdge(v3, v4, 1), + graph.addEdge(v4, v1, 2), + graph.addEdge(v3, v1, 3) + ) + + return defaultVerticesList to defaultEdgesSet +} + +fun setupDirectedGraphWithCycle(graph: WeightedDirectedGraph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4, v5, v6, v7, v8) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1, -1), + graph.addEdge(v1, v2, 4), + graph.addEdge(v1, v3, 7), + graph.addEdge(v2, v4, -2), + graph.addEdge(v3, v4, -3), + graph.addEdge(v0, v5, 3), + graph.addEdge(v5, v6, 0), + graph.addEdge(v6, v7, 10), + graph.addEdge(v7, v5, -2000), + graph.addEdge(v7, v8, 2) + ) + + return defaultVerticesList to defaultEdgesSet +} + +fun setupGraphForFindingCycles(graph: DirectedGraph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4, v5, v6, v7, v8) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1), + graph.addEdge(v0, v7), + graph.addEdge(v0, v4), + graph.addEdge(v1, v8), + graph.addEdge(v1, v6), + graph.addEdge(v1, v2), + graph.addEdge(v2, v0), + graph.addEdge(v2, v1), + graph.addEdge(v2, v5), + graph.addEdge(v2, v3), + graph.addEdge(v5, v3), + graph.addEdge(v3, v4), + graph.addEdge(v4, v1), + graph.addEdge(v7, v8), + graph.addEdge(v8, v7) + ) + + return defaultVerticesList to defaultEdgesSet +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..8b3c8522 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.compose) apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..7c777834 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +kotlin.code.style=official +kotlinVersion=1.9.22 +composeVersion=1.6.2 +junitVersion=5.8.1 +neo4jDriverVersion=5.6.0 +kotlinxCoroutinesVersion=1.7.3 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 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 00000000..600bcce9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Apr 27 23:16:58 MSK 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# 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/master/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 \ + "$@" + +# 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 00000000..ac1b06f9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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=. +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%" == "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%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..a8b80820 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,29 @@ +val neo4jDriverVersion: String by settings +val composeVersion: String by settings +val kotlinVersion: String by settings +val junitVersion: String by settings + +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") { + plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").version(kotlinVersion) + plugin("compose", "org.jetbrains.compose").version(composeVersion) + library("junit-jupiter", "org.junit.jupiter:junit-jupiter:$junitVersion") + } + } +} + +rootProject.name = "graphs-2" +include("app")