diff --git a/.github/formatter.yml b/.github/formatter.yml new file mode 100644 index 0000000..bcebeb0 --- /dev/null +++ b/.github/formatter.yml @@ -0,0 +1,13 @@ +name: formatter +on: [push, pull_request] +jobs: + check-formatted: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Check formatter + run: ./gradlew ktfmtCheck + \ No newline at end of file diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..c925d36 --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,27 @@ +version: 2 +mergeable: + - when: 'pull_request.*, pull_request_review.*' + name: Approvals check + filter: + # ignore 'Feedback' PR + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none + validate: + - do: description + no_empty: + enabled: true + message: 'Description matter and should not be empty. Provide detail with **what** was changed, **why** it was changed, and **how** it was changed.' + - do: approvals + min: + count: 1 + block: + changes_requested: true + limit: + users: + - DronShock + - suvorovrain + - Sem4kok diff --git a/.run/desktop.run.xml b/.run/desktop.run.xml index f91b2c2..a24b2e0 100644 --- a/.run/desktop.run.xml +++ b/.run/desktop.run.xml @@ -1,21 +1,24 @@ - - - - - - true - - + + + + + + true + true + false + false + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c2e13c1 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..161aefd --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ + +# ZeitNot - Graph Analyzer App + Это приложение позволяет выделять сообщества, находить ключевые вершины и делать раскладку направленных и ненаправленных, взвешенных и не взвешенных графов. Также поддерживается набор алгоритмов для анализа графа, в него входят алгоритм Форда-Беллмана, алгоритм Дейкстры и другие. + Приложение поддерживает сохранение в хранилища `SQL`, `CSV`, И `Neo4J` + + +## Установка и запуск + Приложение работает на Java SDK 21 версии. + +- Установка: + +``` +git@github.com:spbu-coding-2023/graphs-graph-7.git +``` +- Запуск: + + +#### Linux + +``` +./gradlew run +``` + +## Раскладка графа + Для раскладки графа используется алгоритм [ForceAtlas2](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0098679). + + +## SQLite + Приложение может читать два типа баз данных. + При этом сохраняются все базы данных во второй тип + + 1) Необработанный граф. Две таблицы -- на ребра и вершины графа + + +### Vertices + +| id | data | community | +|--|--|--| +| *integer* | *text* | -1 | + +`id` - уникальный идентификационный номер +`data` - информация, хранимая в вершине +`community` - информация о том, находится ли вершина в сообществе. Изначально равняется `-1`, после выполнения алгоритма кластеризации, равняется номеру сообщества +### Edges + +| id | first | second | weight | +|--|--|--| -- | +| *integer* | *int* | *int* |*Long* | + + `id` - уникальный идентификационный номер + `first` - **id** первой вершины, в случае направленного графа, считается началом ребра + `second` -**id** второй вершины, в случае направленного графа, считается концом ребра + `weight` - вес ребра. В случае не взвешенного графа, у всех ребер он равен единице + +2. Обработанный граф. В таком случае должна быть третья таблица, а в таблице **Vertices** в поле **community** вместо -1 могут стоять непосредственно номера community + +### _**VerticesView**_ + +| id| vertex | x | y| color | +|--|--|--|--|--| +| *integer* | *int* | *double*| *double*| *text*| + + `id` - уникальный идентификационный номер + + `vertex`- **id** вершины из таблицы **Vertices** + + `x, y` - координаты вершины + + `color` - цвет формата RGB в следующем виде "r:g:b", где r,g,b - float. + + ### Как открыть граф? (SQL) + +Cохраненные графы находятся в `/saves/sqlite/` +Для загрузки базы данных из данной директории, выберите в меню загрузки формат SQL и укажите название графа. Для запуска примера напишите +``` +Shelbiks.db +``` + +### Как сохранить граф? (SQL) +Сохранение происходит в меню сохранения. Достаточно выбрать формать SQL и указать название графа в формате `name.db`. Граф будет сохранен в `/saves/sqlite/` + +--------- +[WIP] diff --git a/build.gradle.kts b/build.gradle.kts index 3903f11..bb04c36 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,12 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { kotlin("jvm") id("org.jetbrains.compose") + id("com.ncorti.ktfmt.gradle") version "0.18.0" +} + +ktfmt { + // KotlinLang style - 4 space indentation - From kotlinlang.org/docs/coding-conventions.html + kotlinLangStyle() } group = "com.graph" @@ -10,16 +16,28 @@ version = "1.0-SNAPSHOT" repositories { mavenCentral() + maven("https://raw.github.com/gephi/gephi/mvn-thirdparty-repo/") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") google() } - +val exposedVersion: String by project +val neo4jDriverVersion = "4.4.5" dependencies { - // Note, if you develop a library, you should use compose.desktop.common. - // compose.desktop.currentOs should be used in launcher-sourceSet - // (in a separate module for demo project and in testMain). - // With compose.desktop.common you will also lose @Preview functionality + implementation("org.gephi", "gephi-toolkit", "0.10.1", classifier = "all") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0") + implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") implementation(compose.desktop.currentOs) + implementation(compose.material3) + implementation(compose.foundation) + implementation("org.slf4j:slf4j-nop:latest.release") + implementation("com.github.doyaaaaaken:kotlin-csv-jvm:0.15.2") + implementation("io.github.blackmo18:kotlin-grass-jvm:0.7.1") + implementation("org.neo4j.driver:neo4j-java-driver:$neo4jDriverVersion") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") } compose.desktop { @@ -33,3 +51,24 @@ compose.desktop { } } } + +tasks.named("test") { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} +tasks.withType { + testLogging { + events("PASSED", "SKIPPED", "FAILED") + } + + tasks.register("copyPreCommitHook") { + description = "Copy pre-commit git hook from the scripts to the .git/hooks folder." + group = "git hooks" + outputs.upToDateWhen { false } + from("$rootDir/scripts/pre-commit") + into("$rootDir/.git/hooks/") + } + tasks.build { + dependsOn("copyPreCommitHook") + } +} diff --git a/gradle.properties b/gradle.properties index 98aed13..5cbac2b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,3 +2,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=official kotlin.version=1.9.22 compose.version=1.6.0 +exposedVersion=0.50.1 diff --git a/saves/csv/Example.csv b/saves/csv/Example.csv new file mode 100644 index 0000000..d7f91d1 --- /dev/null +++ b/saves/csv/Example.csv @@ -0,0 +1,44 @@ +isNode,name,id,x,y,color,radius,community,from,to,weight + +true,13,Sniper,457.3282.dp,342.71585.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,14,Roshan,433.53894.dp,660.1613.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,7,Ryan,421.50995.dp,499.64575.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,12,Lion,725.70276.dp,1026.5232.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,5,Tristan,800.8322.dp,23.990685.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,10,Lycan,592.7067.dp,935.848.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,20,6,1498.49.dp,499.64575.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,17,3,1264.1929.dp,93.83106.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,11,Io,1040.4828.dp,1073.9686.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,15,1,960.0.dp,0.0.dp,0.53333336/0.53333336/0.53333336,25.0.dp,-1,,, +true,2,Andrew,1427.6537.dp,810.0.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,19,5,1462.6719.dp,342.71585.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,4,John,879.51715.dp,1073.9686.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,16,2,1119.1678.dp,23.990685.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,21,7,1486.461.dp,660.1613.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,1,Thomas,537.811.dp,203.3155.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,3,Iakov,1194.2972.dp,1026.5232.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,8,Pudge,492.34628.dp,810.0.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,18,4,1382.189.dp,203.3155.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,6,Arthur,1327.2933.dp,935.848.dp,0.0/0.0/0.0,25.0.dp,-1,,, +true,9,Tiny,655.8072.dp,93.83106.dp,0.0/0.0/0.0,25.0.dp,-1,,, +false,19,,,,,,,20,15,5.0 +false,21,,,,,,,14,20,0.0 +false,10,,,,,,,14,11,6.0 +false,20,,,,,,,17,20,0.0 +false,3,,,,,,,1,3,3.0 +false,4,,,,,,,2,3,4.0 +false,6,,,,,,,3,7,6.0 +false,15,,,,,,,15,17,1.0 +false,14,,,,,,,16,15,22.0 +false,16,,,,,,,15,18,6.0 +false,9,,,,,,,14,10,6.0 +false,12,,,,,,,14,13,5.0 +false,5,,,,,,,5,3,5.0 +false,2,,,,,,,3,4,2.0 +false,18,,,,,,,15,21,3.0 +false,8,,,,,,,14,9,6.0 +false,1,,,,,,,1,2,1.0 +false,11,,,,,,,14,12,6.0 +false,7,,,,,,,14,8,6.0 +false,13,,,,,,,14,3,0.0 +false,17,,,,,,,19,15,2.0 diff --git a/saves/sqlite/Shelbiks.db b/saves/sqlite/Shelbiks.db new file mode 100644 index 0000000..51454cf Binary files /dev/null and b/saves/sqlite/Shelbiks.db differ diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100644 index 0000000..a16b2f5 --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,14 @@ +#!/bin/sh + +echo "> Task: ktfmtFormat" + +./gradlew ktfmtFormat + +echo "> Task: ktfmtCheck" + +./gradlew --no-daemon ktfmtCheck + + ktfmtCheckStatus=$? + +[ $ktfmtCheckStatus -ne 0 ] && exit 1 +exit 0 \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index fed3f24..dc351db 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,31 +1,85 @@ import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.material.Button import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +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 model.graph.Graph +import view.Canvas +import viewmodel.CanvasViewModel +import viewmodel.layouts.CircularLayout + +val graph = + Graph().apply { + addVertex(1, "Thomas") + addVertex(2, "Andrew") + addVertex(3, "Iakov") + addVertex(4, "John") + addVertex(5, "Tristan") + addVertex(6, "Arthur") + addVertex(7, "Ryan") + + addEdge(1, 2, 1f, 1) + addEdge(3, 4, 2f, 2) + addEdge(1, 3, 3f, 3) + addEdge(2, 3, 4f, 4) + addEdge(5, 3, 5f, 5) + addEdge(3, 7, 6f, 6) + + addVertex(8, "Pudge") + addVertex(9, "Tiny") + addVertex(10, "Lycan") + addVertex(11, "Io") + addVertex(12, "Lion") + addVertex(13, "Sniper") + addVertex(14, "Roshan") + + addEdge(14, 8, 6f, 7) + addEdge(14, 9, 6f, 8) + addEdge(14, 10, 6f, 9) + addEdge(14, 11, 6f, 10) + addEdge(14, 12, 6f, 11) + addEdge(14, 13, 5f, 12) + addEdge(14, 3, 0f, 13) + + addVertex(15, "1") + addVertex(16, "2") + addVertex(17, "3") + addVertex(18, "4") + addVertex(19, "5") + addVertex(20, "6") + addVertex(21, "7") + + addEdge(16, 15, 22f, 14) + addEdge(15, 17, 1f, 15) + addEdge(15, 18, 6f, 16) + addEdge(19, 15, 2f, 17) + addEdge(15, 21, 3f, 18) + addEdge(20, 15, 5f, 19) + addEdge(17, 20, 0f, 20) + addEdge(14, 20, 0f, 21) + } +val windowSizeStart = Pair(820, 640) @Composable +@ExperimentalStdlibApi @Preview fun App() { - var text by remember { mutableStateOf("Hello, World!") } - - MaterialTheme { - Button(onClick = { - text = "Hello, Desktop!" - }) { - Text(text) - } - } + val canvasGraph = CanvasViewModel(graph, CircularLayout()) + MaterialTheme { Canvas(canvasGraph) } } +@ExperimentalStdlibApi fun main() = application { - Window(onCloseRequest = ::exitApplication) { + Window( + onCloseRequest = ::exitApplication, + title = "ZeitNot", + state = rememberWindowState(position = WindowPosition(Alignment.Center)) + ) { + window.minimumSize = Dimension(windowSizeStart.first, windowSizeStart.second) App() } } diff --git a/src/main/kotlin/controller/GraphPainterByCommunity.kt b/src/main/kotlin/controller/GraphPainterByCommunity.kt new file mode 100644 index 0000000..41c645f --- /dev/null +++ b/src/main/kotlin/controller/GraphPainterByCommunity.kt @@ -0,0 +1,23 @@ +package controller + +import model.community.Louvain +import model.graph.Graph +import viewmodel.graph.GraphViewModel + +class GraphPainterByCommunity( + private val graph: Graph, + private val graphViewModel: GraphViewModel +) { + private val finder = Louvain(graph) + private val communities = finder.findCommunities() + + fun paint() { + for ((i, community) in communities.withIndex()) { + val communityColor = generateRandomColor(i * 123) + for (vertexID in community) { + val currVertex = graph.vertices[vertexID] + graphViewModel.verticesView[currVertex]!!.color = communityColor + } + } + } +} diff --git a/src/main/kotlin/controller/GraphPainterByCycles.kt b/src/main/kotlin/controller/GraphPainterByCycles.kt new file mode 100644 index 0000000..e89e77e --- /dev/null +++ b/src/main/kotlin/controller/GraphPainterByCycles.kt @@ -0,0 +1,24 @@ +package controller + +import model.graph.Graph +import model.graph.Vertex +import viewmodel.graph.GraphViewModel +import model.algorithms.FindCycles + +class GraphPainterByCycles(private val graph: Graph, private val graphViewModel: GraphViewModel) { + private val algoInitialize = FindCycles() + private val cycles = mutableListOf>() + private val vertices = graph.getVertices() + + fun paint() { + for(vertex in vertices){ + cycles.add(algoInitialize.simpleCycles(graph, vertex)) + } + for ((i, cycle) in cycles.withIndex()) { + val cycleColor = generateRandomColor(i * 451) + for (vertex in cycle) { + graphViewModel.verticesView[vertex]!!.color = cycleColor + } + } + } +} diff --git a/src/main/kotlin/controller/GraphPainterByDjikstra.kt b/src/main/kotlin/controller/GraphPainterByDjikstra.kt new file mode 100644 index 0000000..a21af75 --- /dev/null +++ b/src/main/kotlin/controller/GraphPainterByDjikstra.kt @@ -0,0 +1,24 @@ +package controller + +import model.algorithms.Djikstra +import model.graph.Graph +import viewmodel.graph.GraphViewModel + +class GraphPainterByDjikstra( + private val graph: Graph, + private val graphViewModel: GraphViewModel, + private val startIdx: Int, + private val endIdx: Int +) { + private val pathFinder = Djikstra(graph, startIdx) + private val path = pathFinder.findShortestPaths() + val currPath = pathFinder.reconstructPath(endIdx) + + fun paint() { + val vertexColor = generateRandomColor(startIdx * 123) + for (vertexID in currPath) { + val currVertex = graph.vertices[vertexID] + graphViewModel.verticesView[currVertex]!!.color = vertexColor + } + } +} diff --git a/src/main/kotlin/controller/GraphPainterByKosaraju.kt b/src/main/kotlin/controller/GraphPainterByKosaraju.kt new file mode 100644 index 0000000..e00d90b --- /dev/null +++ b/src/main/kotlin/controller/GraphPainterByKosaraju.kt @@ -0,0 +1,20 @@ +package controller + +import model.algorithms.Kosaraju +import model.graph.Graph +import viewmodel.graph.GraphViewModel + +class GraphPainterByKosaraju(private val graph: Graph, private val graphViewModel: GraphViewModel) { + private val finder = Kosaraju(graph) + private val components = finder.findStronglyConnectedComponents() + + fun paint() { + for ((i, component) in components.withIndex()) { + val communityColor = generateRandomColor(i * 123) + for (vertexID in component) { + val currVertex = graph.vertices[vertexID] + graphViewModel.verticesView[currVertex]!!.color = communityColor + } + } + } +} diff --git a/src/main/kotlin/controller/GraphPainterByKruskal.kt b/src/main/kotlin/controller/GraphPainterByKruskal.kt new file mode 100644 index 0000000..97c2f90 --- /dev/null +++ b/src/main/kotlin/controller/GraphPainterByKruskal.kt @@ -0,0 +1,18 @@ +package controller + +import androidx.compose.ui.graphics.Color +import model.algorithms.KruskalsMST +import model.graph.Graph +import viewmodel.graph.GraphViewModel + +class GraphPainterByKruskal(private val graph: Graph, private val graphViewModel: GraphViewModel) { + private val algoInitialize = KruskalsMST() + private val tree = algoInitialize.kruskals(graph) + + fun paint() { + for (edgeId in tree) { + val currEdge = graph.edges[edgeId] + graphViewModel.edgesView[currEdge]!!.color = Color.Red + } + } +} diff --git a/src/main/kotlin/controller/GraphSizerByCentrality.kt b/src/main/kotlin/controller/GraphSizerByCentrality.kt new file mode 100644 index 0000000..3dabc7c --- /dev/null +++ b/src/main/kotlin/controller/GraphSizerByCentrality.kt @@ -0,0 +1,19 @@ +package controller + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import model.graph.Graph +import model.keyVertices.GraphBetweennessCentrality +import viewmodel.graph.GraphViewModel + +class GraphSizerByCentrality(private val graph: Graph, private val graphViewModel: GraphViewModel) { + private val algoInitialize = GraphBetweennessCentrality() + private val centrality = algoInitialize.getKeyVertices(graph) + + fun changeSize() { + for (vertex in centrality.keys) { + graphViewModel.verticesView[vertex]!!.radius += (centrality[vertex]!!*0.2).dp + graphViewModel.verticesView[vertex]!!.x += 1.dp + } + } +} diff --git a/src/main/kotlin/controller/RandomColor.kt b/src/main/kotlin/controller/RandomColor.kt new file mode 100644 index 0000000..652144f --- /dev/null +++ b/src/main/kotlin/controller/RandomColor.kt @@ -0,0 +1,12 @@ +package controller + +import androidx.compose.ui.graphics.Color +import kotlin.random.Random + +fun generateRandomColor(base: Int): Color { + val mRandom = Random(base) + val red: Int = (base + mRandom.nextInt(256)) / 2 + val green: Int = (base + mRandom.nextInt(256)) / 2 + val blue: Int = (base + mRandom.nextInt(256)) / 2 + return Color(red, green, blue) +} diff --git a/src/main/kotlin/model/algorithms/BridgeFinder.kt b/src/main/kotlin/model/algorithms/BridgeFinder.kt new file mode 100644 index 0000000..aee724f --- /dev/null +++ b/src/main/kotlin/model/algorithms/BridgeFinder.kt @@ -0,0 +1,72 @@ +package model.algorithms + +import kotlin.math.min +import model.graph.Graph + +class BridgeFinder(graph: Graph) { + private val arraySize = graph.vertices.size + private val visitedVertices = Array(arraySize) { false } + private val timeIn = Array(arraySize) { -1 } + private val fUp = Array(arraySize) { -1 } + val bridges = mutableListOf() + private val curGraph = graph + + fun findBridges() { + var timer = 0 + + fun isBridge(edgeID: Int): Int? { + val destination = + curGraph.edges[edgeID]?.vertices?.second ?: throw Exception("Incorrect Database") + val bridge = curGraph.edges[edgeID] + val bridges = + curGraph.vertices[bridge?.vertices?.first]?.incidentEdges + ?: throw Exception("Incorrect Database") + for (curBridge in bridges) { + if ( + curGraph.edges[curBridge]!!.vertices.second == destination && + curBridge != edgeID + ) { + return null + } + } + return edgeID + } + + fun dfs(vertexID: Int, parent: Int = -1) { + visitedVertices[vertexID] = true + timer++ + timeIn[vertexID] = timer + fUp[vertexID] = timer + val incidentEdgesID = curGraph.vertices[vertexID + 1]!!.incidentEdges + for (edgeID in incidentEdgesID) { + val edge = curGraph.edges[edgeID]!!.vertices + val newVertexID = + if (vertexID == edge.first - 1) { + edge.second - 1 + } else { + edge.first - 1 + } + + if (newVertexID == parent) continue + + if (visitedVertices[newVertexID]) { + fUp[vertexID] = min(timeIn[newVertexID], fUp[vertexID]) + } else { + dfs(newVertexID, vertexID) + fUp[vertexID] = min(fUp[newVertexID], fUp[vertexID]) + if (fUp[newVertexID] > timeIn[vertexID]) { + if (isBridge(edgeID) != null) { + bridges.add(edgeID) + } + } + } + } + } + + for (i in 1..arraySize) { + if (!visitedVertices[i - 1]) { + dfs(i - 1) + } + } + } +} diff --git a/src/main/kotlin/model/algorithms/Djikstra.kt b/src/main/kotlin/model/algorithms/Djikstra.kt new file mode 100644 index 0000000..027a140 --- /dev/null +++ b/src/main/kotlin/model/algorithms/Djikstra.kt @@ -0,0 +1,70 @@ +package model.algorithms + +import model.graph.Graph + +class Djikstra(private val graph: Graph, private val startVertexID: Int = -1) { + private val distance = hashMapOf() + private val visited = hashMapOf() + private val from = hashMapOf() + private val n = graph.vertices.size + + fun findShortestPaths() { + if (n == 0 || startVertexID <= -1) return + + for ((id, _) in graph.vertices) { + distance[id] = Float.MAX_VALUE + from[id] = -1 + } + + distance[startVertexID] = 0f + + for (i in 0 until n) { + + var nearest = -1 + for ((vertexID, _) in graph.vertices) { + if ( + !visited.getOrDefault(vertexID, false) && + (nearest == -1 || distance[vertexID]!! < distance[nearest]!!) + ) { + nearest = vertexID + } + } + visited[nearest] = true + + if (distance[nearest] == Float.MAX_VALUE) break + + for (edgeID in graph.vertices[nearest]!!.incidentEdges) { + val edge = graph.edges[edgeID]!! + + val to = + if (nearest == edge.vertices.first) edge.vertices.second + else edge.vertices.first + val weight = edge.weight + + if (distance[nearest]!! + weight < distance[to]!!) { + distance[to] = distance[nearest]!! + weight + from[to] = nearest + } + } + } + } + + fun reconstructPath(endVertexID: Int): List { + val path = mutableListOf() + var finish = endVertexID + if ((n == 0 || startVertexID <= -1) || (endVertexID <= 0)) return path + + while (finish != startVertexID) { + path.add(finish) + if (finish == -1) { + path.clear() + return path + } + finish = from[finish] ?: break + } + + path.add(startVertexID) + path.reverse() + return path + } +} diff --git a/src/main/kotlin/model/algorithms/FindCycles.kt b/src/main/kotlin/model/algorithms/FindCycles.kt new file mode 100644 index 0000000..a4f7147 --- /dev/null +++ b/src/main/kotlin/model/algorithms/FindCycles.kt @@ -0,0 +1,44 @@ +package model.algorithms + +import model.graph.Graph +import model.graph.Vertex + +class FindCycles { + fun simpleCycles(graph: Graph, startingVertex: Vertex): List { + val isDirected = graph.isDirected + val cameFrom = HashMap(graph.vertices.size) + val visited = HashSet(graph.vertices.size) + val stack = mutableListOf(startingVertex) + + while (stack.isNotEmpty()) { + val currentVertex = stack.removeLast() + visited.add(currentVertex) + for (neighbourVertex in currentVertex.adjacentVertices) { + + if (neighbourVertex == startingVertex) { + cameFrom[neighbourVertex] = currentVertex + } + + if (neighbourVertex == startingVertex && (isDirected || cameFrom[currentVertex] != startingVertex)) { + // found path + val path = mutableListOf(startingVertex) + var cur = cameFrom[neighbourVertex] + while (cur != startingVertex) { + if (cur == null) break + path.add(cur) + cur = cameFrom[cur] + } + path.add(startingVertex) + return path.reversed() + } + + if (neighbourVertex !in visited) { + cameFrom[neighbourVertex] = currentVertex + stack.add(neighbourVertex) + } + } + } + + return listOf() + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/algorithms/FordBellman.kt b/src/main/kotlin/model/algorithms/FordBellman.kt new file mode 100644 index 0000000..5c044a7 --- /dev/null +++ b/src/main/kotlin/model/algorithms/FordBellman.kt @@ -0,0 +1,97 @@ +package model.algorithms + +import kotlin.math.max +import model.graph.Graph + +class FordBellman(graph: Graph) { + val INF = Float.MAX_VALUE + private val verticesNumber = graph.vertices.size + private val edgesNumber = graph.edges.size + + private val pathsLength = Array(verticesNumber) { INF } + val pathVertices = Array(verticesNumber) { -1 } + val pathEdges = Array(edgesNumber) { -1 } + val resultPathVertices: MutableList = mutableListOf() + val resultPathEdges: MutableList = mutableListOf() + + private val curGraph = graph + var disconnectedGraphFlag = false + var cycleFlag = false + + private fun negativeCycleBuilder(cycleFlag: Int) { + var tmpCycleFlag = cycleFlag + for (i in 1 until verticesNumber) { + tmpCycleFlag = pathVertices[tmpCycleFlag - 1] + 1 + } + var current = tmpCycleFlag + val cycleEndFlag = true + while (cycleEndFlag) { + + if (current == tmpCycleFlag && resultPathVertices.size > 1) { + break + } + resultPathVertices.add(current) + val destVertexID = current + val sourseVertexID = pathVertices[current - 1] + 1 + for (edgeID in curGraph.vertices[sourseVertexID]!!.incidentEdges) { + if (curGraph.edges[edgeID]!!.vertices.second == destVertexID) { + resultPathEdges.add(edgeID) + break + } + } + current = pathVertices[current - 1] + 1 + } + } + + fun shortestPath(startVertexID: Int, endVertexID: Int) { + pathsLength[startVertexID - 1] = 0f + var curCycleFlag = -1 + for (i in 0 until verticesNumber) { + curCycleFlag = -1 + for (j in 0 until edgesNumber) { + val firstVertexPath = + pathsLength[curGraph.edges[j + 1]!!.vertices.first - 1].toFloat() + val secondVertexPath = + pathsLength[curGraph.edges[j + 1]!!.vertices.second - 1].toFloat() + if (firstVertexPath < INF) { + if (secondVertexPath > firstVertexPath + curGraph.edges[j + 1]!!.weight) { + pathsLength[curGraph.edges[j + 1]!!.vertices.second - 1] = + max(-INF, firstVertexPath + curGraph.edges[j + 1]!!.weight) + pathEdges[curGraph.edges[j + 1]!!.id - 1] = j + 1 + pathVertices[curGraph.edges[j + 1]!!.vertices.second - 1] = + curGraph.edges[j + 1]!!.vertices.first - 1 + curCycleFlag = curGraph.edges[j + 1]!!.vertices.second + } + } + } + } + if (curCycleFlag == -1) { + if (pathsLength[endVertexID - 1] == INF) { + disconnectedGraphFlag = true + return + } else { + pathBuilder(endVertexID) + } + } else { + cycleFlag = true + negativeCycleBuilder(curCycleFlag) + } + } + + private fun pathBuilder(endVertexID: Int) { + resultPathVertices.add(endVertexID) + var tmp = endVertexID + do { + val destVertexID = tmp + val sourceVertexID = pathVertices[tmp - 1] + 1 + for (edgeID: Int in curGraph.vertices[sourceVertexID]!!.incidentEdges) { + if (curGraph.edges[edgeID]!!.vertices.second == destVertexID) { + resultPathEdges.add(edgeID) + break + } + } + resultPathVertices.add(sourceVertexID) + tmp = pathVertices[tmp - 1] + 1 + } while (pathVertices[tmp - 1] != -1) + } +} diff --git a/src/main/kotlin/model/algorithms/Kosaraju.kt b/src/main/kotlin/model/algorithms/Kosaraju.kt new file mode 100644 index 0000000..ea5e02c --- /dev/null +++ b/src/main/kotlin/model/algorithms/Kosaraju.kt @@ -0,0 +1,85 @@ +package model.algorithms + +import model.graph.Graph + +class Kosaraju(private val graph: Graph) { + private val used = hashMapOf() + private val order = mutableListOf() + private val component = mutableListOf() + + fun findStronglyConnectedComponents(): List> { + // Step 1: Transpose the graph + val transposedGraph = transposeGraph() + + // Step 2: Topology sort transposed graph + for (vertexID in transposedGraph.vertices.keys) { + if (used[vertexID] != true) { + topologySort(transposedGraph, vertexID) + } + } + + // Step 3: DFS to find strongly connected components + val components = mutableListOf>() + used.clear() + for (vertexID in order.reversed()) { + if (used[vertexID] != true) { + component.clear() + dfs(vertexID) + components.add(component.toList()) + } + } + + return components + } + + private fun topologySort(graph: Graph, vertexID: Int) { + used[vertexID] = true + val vertex = graph.vertices[vertexID] ?: return + for (edgeID in vertex.incidentEdges) { + val edge = graph.edges[edgeID] ?: continue + val nextVertexID = + if (vertexID == edge.vertices.first) edge.vertices.second else edge.vertices.first + if (used[nextVertexID] != true) { + topologySort(graph, nextVertexID) + } + } + order.add(vertexID) + } + + fun test_TopologySort(graph: Graph, vertexID: Int): List { + topologySort(graph, vertexID) + return order + } + + private fun dfs(vertexID: Int) { + used[vertexID] = true + component.add(vertexID) + val vertex = graph.vertices[vertexID] ?: return + for (edgeID in vertex.incidentEdges) { + val edge = graph.edges[edgeID] ?: continue + val nextVertexID = + if (vertexID == edge.vertices.first) edge.vertices.second else continue + if (used[nextVertexID] != true) { + dfs(nextVertexID) + } + } + } + + private fun transposeGraph(): Graph { + val transposedGraph = Graph() + transposedGraph.isDirected = true // Transposed graph is always directed + + // Add vertices to the transposed graph + for ((id, vertex) in graph.vertices) { + transposedGraph.addVertex(id, vertex.data) + } + + // Add edges with reversed direction to the transposed graph + for ((id, edge) in graph.edges) { + val (firstVertexID, secondVertexID) = edge.vertices + transposedGraph.addEdge(secondVertexID, firstVertexID, edge.weight, id) + } + + return transposedGraph + } +} diff --git a/src/main/kotlin/model/algorithms/KruskalsAlgorithm.kt b/src/main/kotlin/model/algorithms/KruskalsAlgorithm.kt new file mode 100644 index 0000000..9794d8a --- /dev/null +++ b/src/main/kotlin/model/algorithms/KruskalsAlgorithm.kt @@ -0,0 +1,60 @@ +package model.algorithms + +import model.graph.Edge +import model.graph.Graph + +class KruskalsMST { + internal fun kruskals(graph: Graph): List { + val numVertices = graph.getVertices().size + if (numVertices <= 1) return emptyList() + + val results = mutableListOf() + val subsets = mutableMapOf() + + for (vertex in 0 until numVertices) { + subsets[vertex] = Subset(vertex, 0) + } + + val edgesList = graph.edges.values + val sortedEdges = edgesList.sortedWith(compareBy { it.weight }) + + var edgeIndex = 0 + var noOfEdgesAdded = 0 + + while (noOfEdgesAdded < numVertices - 1 && edgeIndex < sortedEdges.size) { + val nextEdge = sortedEdges[edgeIndex] + val x = findRoot(subsets, nextEdge.vertices.first) + val y = findRoot(subsets, nextEdge.vertices.second) + + if (x != y && x != null && y != null) { + results.add(nextEdge) + union(subsets, x, y) + noOfEdgesAdded++ + } + edgeIndex++ + } + + return results.filterNotNull().map { it.id }.sorted() + } + + private fun union(subsets: MutableMap, x: Int, y: Int) { + val rootX = findRoot(subsets, x) + val rootY = findRoot(subsets, y) + + if (subsets[rootY]?.rank ?: 0 < subsets[rootX]?.rank ?: 0) { + subsets[rootY]?.parent = rootX + } else if (subsets[rootX]?.rank ?: 0 < subsets[rootY]?.rank ?: 0) { + subsets[rootX]?.parent = rootY + } else { + subsets[rootY]?.parent = rootX + subsets[rootX]?.rank = (subsets[rootX]?.rank ?: 0) + 1 + } + } + + private fun findRoot(subsets: MutableMap, i: Int): Int { + if (subsets[i]?.parent != i) subsets[i]?.parent = findRoot(subsets, subsets[i]?.parent ?: i) + return subsets[i]?.parent ?: i + } + + internal class Subset(var parent: Int, var rank: Int) +} diff --git a/src/main/kotlin/model/community/Louvain.kt b/src/main/kotlin/model/community/Louvain.kt new file mode 100644 index 0000000..5db05ff --- /dev/null +++ b/src/main/kotlin/model/community/Louvain.kt @@ -0,0 +1,117 @@ +package model.community + +import model.graph.Graph + +class Louvain(private val graph: Graph) { + private val currCommunities = mutableMapOf() + private val n = getNeighbours() + private var targetCommunity = currCommunities + private var modularity = calculateModularity(currCommunities) + + fun findCommunities(): List> { + do { + var stop = false + + for (anchor in n.neighbours.keys) { + + for (neighbourCommunity in n.neighbours.keys) { + if ( + anchor != neighbourCommunity && + currCommunities[anchor] != neighbourCommunity + ) { + val newCommunities = targetCommunity.toMutableMap() + + newCommunities[anchor] = currCommunities[neighbourCommunity]!! + val newModularity = calculateModularity(newCommunities) + + if (newModularity > modularity) { + modularity = newModularity + targetCommunity = newCommunities + currCommunities[anchor] = currCommunities[neighbourCommunity]!! + stop = true + break + } + } + } + + if (stop) break + } + } while (stop) + + val answer = mutableListOf>() + for (value in targetCommunity.values.toSet()) { + val keys = mutableSetOf() + for ((key, value1) in targetCommunity) { + if (value1 == value) keys.add(key) + } + + answer.add(keys) + } + + return paintGraph(answer) + } + + private fun getNeighbours(): Neighbours { + val graphEdges = graph.getEdges() + val n = Neighbours() + + for (vertex in graph.getVertices()) { + n.neighbours[vertex.id] = mutableSetOf() + } + + // initialize network + for (edge in graphEdges) { + val neighbour1 = edge.vertices.first + val neighbour2 = edge.vertices.second + val closeness = edge.weight + if (graph.isDirected) { + n.neighbours + .getOrPut(neighbour1) { mutableSetOf() } + .add(Relation(closeness, neighbour2)) + } else { + n.neighbours + .getOrPut(neighbour1) { mutableSetOf() } + .add(Relation(closeness, neighbour2)) + n.neighbours + .getOrPut(neighbour2) { mutableSetOf() } + .add(Relation(closeness, neighbour1)) + } + } + + // initialize start community position + for (id in n.neighbours.keys) { + currCommunities[id] = id + } + + return n + } + + private fun calculateModularity(communities: Map): Double { + var link = 0.0 + val closeness = n.neighbours.values.sumOf { it.size }.toDouble() / 2 + + for (anchor in n.neighbours.keys) { + for (relation in n.neighbours[anchor]!!) { + val neighbour = relation.id + if (communities[anchor] == communities[neighbour]) { + link += + 1.0 - + (n.neighbours[anchor]!!.size * n.neighbours[neighbour]!!.size) / + (2 * closeness) + } + } + } + + return link / (2 * closeness) + } + + private fun paintGraph(comm: List>): List> { + for ((newCommunityIndex, community) in comm.withIndex()) { + for (vertexIdx in community) { + graph.vertices[vertexIdx]!!.community = newCommunityIndex + } + } + + return comm + } +} diff --git a/src/main/kotlin/model/community/Neighbours.kt b/src/main/kotlin/model/community/Neighbours.kt new file mode 100644 index 0000000..26f84b8 --- /dev/null +++ b/src/main/kotlin/model/community/Neighbours.kt @@ -0,0 +1,5 @@ +package model.community + +class Neighbours { + val neighbours: MutableMap> = mutableMapOf() +} diff --git a/src/main/kotlin/model/community/Relation.kt b/src/main/kotlin/model/community/Relation.kt new file mode 100644 index 0000000..30d34e5 --- /dev/null +++ b/src/main/kotlin/model/community/Relation.kt @@ -0,0 +1,3 @@ +package model.community + +class Relation(val closeness: Float = 1f, val id: Int) {} diff --git a/src/main/kotlin/model/databases/CSV/CSVFileHandler.kt b/src/main/kotlin/model/databases/CSV/CSVFileHandler.kt new file mode 100644 index 0000000..d13c77a --- /dev/null +++ b/src/main/kotlin/model/databases/CSV/CSVFileHandler.kt @@ -0,0 +1,130 @@ +package model.databases.CSV + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.github.doyaaaaaken.kotlincsv.dsl.csvReader +import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter +import io.github.blackmo18.grass.dsl.grass +import java.io.File +import model.databases.CSV.data.CSVGraphData +import model.databases.CSV.data.VertexViewData +import model.graph.Graph +import viewmodel.graph.GraphViewModel + +@ExperimentalStdlibApi +class CSVFileHandler { + + fun save(file: File, graphViewModel: GraphViewModel) { + val data: MutableList> = mutableListOf(mutableListOf()) + graphViewModel.addVerticesToData(data) + graphViewModel.addEdgesToData(data) + + val csvWriter = csvWriter { delimiter = ',' } + val header = + listOf( + "isNode", + "name", + "id", + "x", + "y", + "color", + "radius", + "community", + "from", + "to", + "weight" + ) + + csvWriter.writeAll(listOf(header), file) + csvWriter.writeAll(data, file, append = true) + } + + fun open(file: File): Pair { + try { + val reader = csvReader { skipEmptyLine = true } + val csvContents = reader.readAllWithHeader(file) + val data = grass().harvest(csvContents) + + val vertices = hashMapOf() + val newGraph = Graph() + data.onEach { + if (it.isNode) { + newGraph.addVertex(it.id, it.name) + val rgb: List = + it.color?.split("/")?.map { color -> color.toFloat() } ?: listOf(0f, 0f, 0f) + val vertex = + VertexViewData( + it.x, + it.y, + it.community ?: -1, + it.radius ?: 2.5, + Color(rgb[0], rgb[1], rgb[2]) + ) + vertices[it.id] = vertex + } + } + data.onEach { + if (!it.isNode) + newGraph.addEdge(it.from!!.toInt(), it.to!!.toInt(), it.weight!!, it.id) + } + + val newGraphView = GraphViewModel(newGraph) + newGraphView.verticesViewValues.onEach { + val vertex = vertices[it.vertex.id]!! + vertex.x?.let { x -> it.x } + vertex.y?.let { y -> it.y } + it.vertex.community = vertex.community + it.radius = vertex.radius?.dp ?: 2.5.dp + it.color = vertex.color + } + + return newGraph to newGraphView + } catch (e: Exception) { + return Graph() to null + } + } + + private fun GraphViewModel.addVerticesToData(data: MutableList>) { + verticesViewValues.onEach { + val csvRow = + mutableListOf( + "true", + it.vertex.id.toString(), + it.vertex.data, + it.x.toString(), + it.y.toString(), + it.color.red.toString() + + "/" + + it.color.green.toString() + + "/" + + it.color.blue.toString(), + it.radius.toString(), + it.vertex.community.toString(), + "", + "", + "" + ) + data.add(csvRow) + } + } + + private fun GraphViewModel.addEdgesToData(data: MutableList>) { + edgesViewValues.onEach { + val csvRow = + mutableListOf( + "false", + it.e.id.toString(), + "", + "", + "", + "", + "", + "", + it.u.vertex.id.toString(), + it.v.vertex.id.toString(), + it.weight + ) + data.add(csvRow) + } + } +} diff --git a/src/main/kotlin/model/databases/CSV/data/CSVGraphData.kt b/src/main/kotlin/model/databases/CSV/data/CSVGraphData.kt new file mode 100644 index 0000000..ff83ce5 --- /dev/null +++ b/src/main/kotlin/model/databases/CSV/data/CSVGraphData.kt @@ -0,0 +1,15 @@ +package model.databases.CSV.data + +data class CSVGraphData( + var isNode: Boolean, + var id: Int, + var name: String, + var x: Double?, + var y: Double?, + var color: String?, + var radius: Double?, + var community: Int?, + var from: String?, + var to: String?, + var weight: Float? +) diff --git a/src/main/kotlin/model/databases/CSV/data/VertexViewData.kt b/src/main/kotlin/model/databases/CSV/data/VertexViewData.kt new file mode 100644 index 0000000..492968f --- /dev/null +++ b/src/main/kotlin/model/databases/CSV/data/VertexViewData.kt @@ -0,0 +1,11 @@ +package model.databases.CSV.data + +import androidx.compose.ui.graphics.Color + +data class VertexViewData( + var x: Double?, + var y: Double?, + var community: Int = -1, + var radius: Double?, + var color: Color +) diff --git a/src/main/kotlin/model/databases/neo4j/Neo4jDBHandler.kt b/src/main/kotlin/model/databases/neo4j/Neo4jDBHandler.kt new file mode 100644 index 0000000..02d1917 --- /dev/null +++ b/src/main/kotlin/model/databases/neo4j/Neo4jDBHandler.kt @@ -0,0 +1,32 @@ +package model.databases.neo4j + +import model.graph.Graph + +class Neo4jHandler(private val repository: Neo4jRepository) { + + fun saveGraphToNeo4j(graph: Graph) { + for (vertex in graph.getVertices()) { + repository.addVertex(vertex.id, vertex.data, vertex.community) + } + + if (graph.isDirected) { + for (edge in graph.getEdges()) { + repository.addDirectedEdge( + edge.vertices.first, + edge.vertices.second, + edge.weight, + edge.id + ) + } + } else { + for (edge in graph.getEdges()) { + repository.addEdge(edge.vertices.first, edge.vertices.second, edge.weight, edge.id) + } + } + } + + fun loadGraphFromNeo4j(): Graph { + val graph = repository.getGraph() + return graph + } +} diff --git a/src/main/kotlin/model/databases/neo4j/Repository.kt b/src/main/kotlin/model/databases/neo4j/Repository.kt new file mode 100644 index 0000000..604fead --- /dev/null +++ b/src/main/kotlin/model/databases/neo4j/Repository.kt @@ -0,0 +1,98 @@ +package model.databases.neo4j + +import java.io.Closeable +import model.graph.Graph +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.Driver +import org.neo4j.driver.GraphDatabase +import org.neo4j.driver.Session +import org.neo4j.driver.Values + +class Neo4jRepository(uri: String, user: String, password: String) : Closeable { + private val driver: Driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + private val session: Session = driver.session() + + fun addVertex(vertexId: Int, vertexData: String, vertexCommunity: Int) { + session.writeTransaction { tx -> + tx.run( + "CREATE (:Vertex {id:\$id, data:\$data, community:\$community})", + Values.parameters("id", vertexId, "data", vertexData, "community", vertexCommunity) + ) + } + } + + fun addDirectedEdge(firstVertexId: Int, secondVertexId: Int, weight: Float, edgeId: Int) { + session.writeTransaction { tx -> + tx.run( + "MATCH (v1:Vertex {id:\$id1}) MATCH (v2:Vertex {id:\$id2}) " + + "CREATE (v1)-[:Edge {id:\$edgeId, weight:\$weight}]->(v2)", + Values.parameters( + "id1", + firstVertexId, + "id2", + secondVertexId, + "edgeId", + edgeId, + "weight", + weight + ) + ) + } + } + + fun addEdge(firstVertexId: Int, secondVertexId: Int, weight: Float, edgeId: Int) { + session.writeTransaction { tx -> + tx.run( + "MATCH (v1:Vertex {id:\$id1}) MATCH (v2:Vertex {id:\$id2}) " + + "MERGE (v1)-[:Edge {id:\$edgeId, weight:\$weight}]-(v2)", + Values.parameters( + "id1", + firstVertexId, + "id2", + secondVertexId, + "edgeId", + edgeId, + "weight", + weight + ) + ) + } + } + + fun getGraph(): Graph { + val graph = Graph() + + session.readTransaction { tx -> + val verticesResult = + tx.run( + "MATCH (v:Vertex) RETURN v.id AS id, v.data AS data, v.community AS community", + ) + verticesResult.list().forEach { record -> + val vertexId = record.get("id").asInt() + val vertexData = record.get("data").asString() + val vertexCommunity = record.get("community").asInt() + graph.addVertex(vertexId, vertexData) + graph.vertices[vertexId]!!.community = vertexCommunity + } + + val edgesResult = + tx.run( + "MATCH (v1:Vertex)-[e:Edge]->(v2:Vertex) RETURN e.id AS id, v1.id AS v1, v2.id AS v2, e.weight AS weight" + ) + edgesResult.list().forEach { record -> + val edgeId = record.get("id").asInt() + val firstVertexId = record.get("v1").asInt() + val secondVertexId = record.get("v2").asInt() + val weight = record.get("weight").asFloat() + graph.addEdge(firstVertexId, secondVertexId, weight, edgeId) + } + } + + return graph + } + + override fun close() { + session.close() + driver.close() + } +} diff --git a/src/main/kotlin/model/databases/sqlite/SQLiteDBHandler.kt b/src/main/kotlin/model/databases/sqlite/SQLiteDBHandler.kt new file mode 100644 index 0000000..8c00d54 --- /dev/null +++ b/src/main/kotlin/model/databases/sqlite/SQLiteDBHandler.kt @@ -0,0 +1,122 @@ +package model.databases.sqlite + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import java.io.File +import model.databases.sqlite.dao.edge.Edge +import model.databases.sqlite.dao.edge.Edges +import model.databases.sqlite.dao.vertices.Vertex +import model.databases.sqlite.dao.vertices.Vertices +import model.databases.sqlite.dao.verticesView.VertexView +import model.databases.sqlite.dao.verticesView.VerticesView +import model.graph.Graph +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.exists +import org.jetbrains.exposed.sql.transactions.transaction +import viewmodel.graph.GraphViewModel + +class SQLiteDBHandler { + lateinit var graph: Graph + lateinit var graphViewModel: GraphViewModel + var vertexViewModelFlag = false + + fun open(file: File, weighted: Boolean, directed: Boolean) { + Database.connect("jdbc:sqlite:$file", driver = "org.sqlite.JDBC") + val newGraph = Graph() + newGraph.isDirected = directed + + transaction { + Vertex.all().forEach { vertex -> + newGraph.addVertex(vertex.id.toString().toInt(), vertex.data) + } + Edge.all().forEach { edge -> + var weight = edge.weight + if (!weighted) { + weight = 1f + } + newGraph.addEdge( + edge.first!!.id.toString().toInt(), + edge.second!!.id.toString().toInt(), + weight, + edge.id.toString().toInt() + ) + if (!newGraph.isDirected) { + newGraph.vertices[edge.second!!.id.toString().toInt()]!! + .incidentEdges + .add(edge.id.toString().toInt()) + } + newGraph.vertices[edge.first!!.id.toString().toInt()]!! + .incidentEdges + .add(edge.id.toString().toInt()) + if (VerticesView.exists()) { + vertexViewModelFlag = true + } + } + } + if (vertexViewModelFlag) { + val newGraphViewModel = GraphViewModel(newGraph) + transaction { + newGraphViewModel.verticesView.onEach { + val vertex = Vertex.find { Vertices.id eq it.value.vertex.id }.first() + val tmp = VertexView.find { VerticesView.vertex eq vertex.id }.first() + it.value.x = tmp.x.dp + it.value.y = tmp.y.dp + it.key.community = vertex.community + val rgb = tmp.color.split(":").map { color -> color.toFloat() } + it.value.color = Color(rgb[0], rgb[1], rgb[2]) + it.value.radius = tmp.r.dp + } + } + graphViewModel = newGraphViewModel + } + graph = newGraph + } + + fun save(file: File, graph: Graph, graphView: GraphViewModel, weighted: Boolean) { + Database.connect("jdbc:sqlite:$file", driver = "org.sqlite.JDBC") + transaction { + SchemaUtils.create(Edges) + SchemaUtils.create(Vertices) + SchemaUtils.create(VerticesView) + graph.getVertices().forEach { + Vertex.new { + data = it.data + community = it.community + } + } + graph.getEdges().forEach { + var newWeight = it.weight + if (!weighted) { + newWeight = 1f + } + Edge.new { + first = Vertex.find { Vertices.id eq it.vertices.first }.first() + second = Vertex.find { Vertices.id eq it.vertices.second }.first() + weight = newWeight + } + } + graphView.verticesViewValues.forEach { + val xDoubled: Double = + it.x.toString().substring(0, it.x.toString().length - 4).toDouble() + val yDoubled: Double = + it.y.toString().substring(0, it.x.toString().length - 4).toDouble() + val rDoubled: Double = + it.radius.toString().substring(0, it.x.toString().length - 4).toDouble() + + VertexView.new { + vertex = Vertex.find { Vertices.id eq it.vertex.id }.first() + color = + it.color.red.toString() + + ":" + + it.color.green.toString() + + ":" + + it.color.blue.toString() + x = xDoubled + y = yDoubled + r = rDoubled + } + } + } + } +} diff --git a/src/main/kotlin/model/databases/sqlite/dao/edge/Edge.kt b/src/main/kotlin/model/databases/sqlite/dao/edge/Edge.kt new file mode 100644 index 0000000..d18bbd0 --- /dev/null +++ b/src/main/kotlin/model/databases/sqlite/dao/edge/Edge.kt @@ -0,0 +1,14 @@ +package model.databases.sqlite.dao.edge + +import model.databases.sqlite.dao.vertices.Vertex +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID + +class Edge(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Edges) + + var first by Vertex optionalReferencedOn Edges.first + var second by Vertex optionalReferencedOn Edges.second + var weight by Edges.weight +} diff --git a/src/main/kotlin/model/databases/sqlite/dao/edge/Edges.kt b/src/main/kotlin/model/databases/sqlite/dao/edge/Edges.kt new file mode 100644 index 0000000..667771e --- /dev/null +++ b/src/main/kotlin/model/databases/sqlite/dao/edge/Edges.kt @@ -0,0 +1,10 @@ +package model.databases.sqlite.dao.edge + +import model.databases.sqlite.dao.vertices.Vertices +import org.jetbrains.exposed.dao.id.IntIdTable + +object Edges : IntIdTable("Edges") { + val first = reference("first", Vertices).nullable() + val second = reference("second", Vertices).nullable() + val weight = float("weight") +} diff --git a/src/main/kotlin/model/databases/sqlite/dao/vertices/Vertex.kt b/src/main/kotlin/model/databases/sqlite/dao/vertices/Vertex.kt new file mode 100644 index 0000000..b96086a --- /dev/null +++ b/src/main/kotlin/model/databases/sqlite/dao/vertices/Vertex.kt @@ -0,0 +1,13 @@ +package model.databases.sqlite.dao.vertices + +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID + +class Vertex(id: EntityID) : IntEntity(id) { + + companion object : IntEntityClass(Vertices) + + var data by Vertices.data + var community by Vertices.community +} diff --git a/src/main/kotlin/model/databases/sqlite/dao/vertices/Vertices.kt b/src/main/kotlin/model/databases/sqlite/dao/vertices/Vertices.kt new file mode 100644 index 0000000..baffd24 --- /dev/null +++ b/src/main/kotlin/model/databases/sqlite/dao/vertices/Vertices.kt @@ -0,0 +1,8 @@ +package model.databases.sqlite.dao.vertices + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Vertices : IntIdTable("Vertices") { + val data = varchar("data", 255) + val community = integer("community") +} diff --git a/src/main/kotlin/model/databases/sqlite/dao/verticesView/VertexView.kt b/src/main/kotlin/model/databases/sqlite/dao/verticesView/VertexView.kt new file mode 100644 index 0000000..ad14208 --- /dev/null +++ b/src/main/kotlin/model/databases/sqlite/dao/verticesView/VertexView.kt @@ -0,0 +1,17 @@ +package model.databases.sqlite.dao.verticesView + +import model.databases.sqlite.dao.vertices.Vertex +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID + +class VertexView(id: EntityID) : IntEntity(id) { + + companion object : IntEntityClass(VerticesView) + + var vertex by Vertex optionalReferencedOn VerticesView.vertex + var x by VerticesView.x + var y by VerticesView.y + var r by VerticesView.r + var color by VerticesView.color +} diff --git a/src/main/kotlin/model/databases/sqlite/dao/verticesView/VerticesView.kt b/src/main/kotlin/model/databases/sqlite/dao/verticesView/VerticesView.kt new file mode 100644 index 0000000..bf46353 --- /dev/null +++ b/src/main/kotlin/model/databases/sqlite/dao/verticesView/VerticesView.kt @@ -0,0 +1,12 @@ +package model.databases.sqlite.dao.verticesView + +import model.databases.sqlite.dao.vertices.Vertices +import org.jetbrains.exposed.dao.id.IntIdTable + +object VerticesView : IntIdTable("VerticesView") { + val vertex = reference("vertex", Vertices).nullable() + val x = double("x") + val y = double("y") + val r = double("r") + val color = varchar("color", 255) +} diff --git a/src/main/kotlin/model/graph/Edge.kt b/src/main/kotlin/model/graph/Edge.kt new file mode 100644 index 0000000..73c449f --- /dev/null +++ b/src/main/kotlin/model/graph/Edge.kt @@ -0,0 +1,9 @@ +package model.graph + +class Edge( + val vertices: Pair, + var weight: Float = 1f, + var id: Int = 1, +) { + // fun incident(v: Int) = v == vertices.first || v == vertices.second +} diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt new file mode 100644 index 0000000..e98d625 --- /dev/null +++ b/src/main/kotlin/model/graph/Graph.kt @@ -0,0 +1,29 @@ +package model.graph + +class Graph { + var isDirected: Boolean = false + val vertices = hashMapOf() + val edges = hashMapOf() + + fun getVertices(): Collection = vertices.values + + fun getEdges(): Collection = edges.values + + fun addVertex(id: Int, v: String): Vertex { + val newVertex = Vertex(v) + newVertex.id = id + return vertices.getOrPut(id) { newVertex } + } + + fun addEdge(firstVertexID: Int, secondVertexID: Int, weight: Float = 1f, edgeID: Int): Edge { + if (!isDirected) { + vertices[secondVertexID]?.incidentEdges?.add(edgeID) + ?: throw Exception("Wrong database") + vertices[secondVertexID]?.adjacentVertices?.add(vertices[firstVertexID]!!) + } + vertices[firstVertexID]?.incidentEdges?.add(edgeID) ?: throw Exception("Wrong database") + vertices[firstVertexID]?.adjacentVertices?.add(vertices[secondVertexID]!!) + + return edges.getOrPut(edgeID) { Edge(Pair(firstVertexID, secondVertexID), weight, edgeID) } + } +} diff --git a/src/main/kotlin/model/graph/Vertex.kt b/src/main/kotlin/model/graph/Vertex.kt new file mode 100644 index 0000000..1e4a000 --- /dev/null +++ b/src/main/kotlin/model/graph/Vertex.kt @@ -0,0 +1,10 @@ +package model.graph + +class Vertex( + var data: String, + var incidentEdges: MutableList = mutableListOf(), +) { + var id: Int = 0 + var community: Int = -1 + val adjacentVertices: MutableList = mutableListOf() +} diff --git a/src/main/kotlin/model/keyVertices/KeyVerticesSearch.kt b/src/main/kotlin/model/keyVertices/KeyVerticesSearch.kt new file mode 100644 index 0000000..c32f9ab --- /dev/null +++ b/src/main/kotlin/model/keyVertices/KeyVerticesSearch.kt @@ -0,0 +1,59 @@ +package model.keyVertices + +import model.graph.Graph +import model.graph.Vertex +import java.util.* + +class GraphBetweennessCentrality { + fun getKeyVertices(graph: Graph): Map { + val betweennessMap = mutableMapOf().withDefault { 0f } + val vertices = graph.vertices.values + + for (sourceVertex in vertices) { + val stack = Stack() + val predecessors = mutableMapOf>().withDefault { mutableListOf() } + val sourceVertexWeight = mutableMapOf().withDefault { 0f } + val distance = mutableMapOf().withDefault { -1 } + val sourceVertexDependency = mutableMapOf().withDefault { 0f } + + sourceVertexWeight[sourceVertex] = 1f + distance[sourceVertex] = 0 + + val queue: Queue = LinkedList() + queue.add(sourceVertex) + + while (queue.isNotEmpty()) { + val currentVertex = queue.poll() + stack.push(currentVertex) + + for (successorVertex in currentVertex.adjacentVertices) { + if (!distance.containsKey(successorVertex)) { + queue.offer(successorVertex) + distance[successorVertex] = distance.getOrDefault(currentVertex, 0) + 1 + } + if (distance.getOrDefault(successorVertex, 0) == distance.getOrDefault(currentVertex, 0) + 1) { + sourceVertexWeight[successorVertex] = sourceVertexWeight.getOrDefault(successorVertex, 0f) + sourceVertexWeight.getOrDefault(currentVertex, 0f) + if (!predecessors.containsKey(successorVertex)) { + predecessors[successorVertex] = mutableListOf() + } + predecessors[successorVertex]!!.add(currentVertex) + } + } + } + + while (stack.isNotEmpty()) { + val successorVertex = stack.pop() + for (currentVertex in predecessors.getOrDefault(successorVertex, emptyList())) { + sourceVertexDependency[currentVertex] = sourceVertexDependency.getOrDefault(currentVertex, 0f) + + (sourceVertexWeight.getOrDefault(currentVertex, 0f) / sourceVertexWeight.getOrDefault(successorVertex, 0f)) * + (1 + sourceVertexDependency.getOrDefault(successorVertex, 0f)) + } + if (successorVertex != sourceVertex) { + betweennessMap[successorVertex] = betweennessMap.getOrDefault(successorVertex, 0f) + sourceVertexDependency.getOrDefault(successorVertex, 0f) + } + } + } + + return betweennessMap + } +} diff --git a/src/main/kotlin/view/AlgorithmSubMenu.kt b/src/main/kotlin/view/AlgorithmSubMenu.kt new file mode 100644 index 0000000..c6fd900 --- /dev/null +++ b/src/main/kotlin/view/AlgorithmSubMenu.kt @@ -0,0 +1,122 @@ +package view + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import controller.GraphPainterByCommunity +import controller.GraphPainterByCycles +import controller.GraphPainterByDjikstra +import controller.GraphPainterByKosaraju +import controller.GraphPainterByKruskal +import controller.GraphSizerByCentrality +import view.algos.bridgeHighlighter +import viewmodel.CanvasViewModel + +@Composable +fun AlgorithmSubMenu(viewModel: CanvasViewModel) { + val showDialog = remember { mutableStateOf(false) } + var startIdx by remember { mutableStateOf(0) } + var endIdx by remember { mutableStateOf(0) } + var bridgesHiglight = remember { mutableStateOf(false) } + + Column(Modifier.padding(start = 16.dp, end = 0.dp, top = 15.dp)) { + Button( + onClick = { + val graph = viewModel.graph + val changer = GraphSizerByCentrality(graph, viewModel.graphViewModel) + changer.changeSize() + }, + enabled = true, + ) { + Text( + text = "Выделение ключевых вершин", + ) + } + Button( + onClick = { + val graph = viewModel.graph + val painter = GraphPainterByCommunity(graph, viewModel.graphViewModel) + painter.paint() + }, + enabled = true, + ) { + Text( + text = "Поиск сообществ", + ) + } + Button( + onClick = { + val graph = viewModel.graph + val painter = GraphPainterByKosaraju(graph, viewModel.graphViewModel) + painter.paint() + }, + enabled = true, + ) { + Text( + text = "Выделение компонент сильной связности", + ) + + ShortestPathDialog(showDialog) { enteredStartIdx, enteredEndIdx -> + startIdx = enteredStartIdx + endIdx = enteredEndIdx + showDialog.value = false + + viewModel.graph.let { graph -> + val painter = + GraphPainterByDjikstra(graph, viewModel.graphViewModel, startIdx, endIdx) + painter.paint() + } + } + } + Button(enabled = true, onClick = { bridgesHiglight.value = !bridgesHiglight.value }) { + Text( + text = "Поиск мостов", + ) + } + if (bridgesHiglight.value) { + bridgeHighlighter(viewModel.bridges) + } + Button( + onClick = { + val graph = viewModel.graph + val painter = GraphPainterByKruskal(graph, viewModel.graphViewModel) + painter.paint() + }, + enabled = true, + ) { + Text( + text = "Построение минимального остовного дерева", + ) + } + Button( + onClick = { + val graph = viewModel.graph + val painter = GraphPainterByCycles(graph, viewModel.graphViewModel) + painter.paint() + }, + enabled = true, + ) { + Text( + text = "Поиск циклов", + ) + } + Button( + onClick = { showDialog.value = true }, + enabled = true, + modifier = Modifier.padding(top = 3.dp) + ) { + Text(text = "Кратчайший путь алгоритмом Дейкстры") + } + Button( + onClick = { /*TODO*/}, + enabled = true, + modifier = Modifier.padding(top = 3.dp), + ) { + Text( + text = "Кратчайший путь алгоритмом Форда-Беллмана", + ) + } + } +} diff --git a/src/main/kotlin/view/CanvasView.kt b/src/main/kotlin/view/CanvasView.kt new file mode 100644 index 0000000..0f22fca --- /dev/null +++ b/src/main/kotlin/view/CanvasView.kt @@ -0,0 +1,90 @@ +package view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import view.graph.GraphView +import viewmodel.CanvasViewModel +import viewmodel.LoadGraphMenuViewModel +import viewmodel.SaveGraphMenuViewModel +import viewmodel.layouts.CircularLayout +import viewmodel.layouts.ForceAtlas2Layout + +@ExperimentalStdlibApi +@Composable +fun Canvas(viewModel: CanvasViewModel) { + val drawerState = rememberDrawerState(DrawerValue.Closed) + val scope = rememberCoroutineScope() + val saveGraphMenuViewModel = remember { SaveGraphMenuViewModel(viewModel) } + + Surface( + modifier = Modifier.fillMaxSize(), + contentColor = Color.LightGray, + color = Color.DarkGray + ) { + val showSubMenu = remember { mutableStateOf(false) } + ModalNavigationDrawer( + drawerState = drawerState, + gesturesEnabled = true, + drawerContent = { + ModalDrawerSheet { + Row { + IconButton(onClick = { scope.launch { drawerState.close() } }) { + Icon(Icons.Filled.Menu, contentDescription = "Меню") + } + Text("Menu", modifier = Modifier.padding(16.dp)) + } + HorizontalDivider() + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Button(onClick = { viewModel.switchLayout(ForceAtlas2Layout()) }) { + Text("Force Atlas 2") + } + Button(onClick = { viewModel.switchLayout(CircularLayout()) }) { + Text("Circular Layout") + } + } + NavigationDrawerItem( + label = { Text(text = "Доступные алгоритмы") }, + icon = { Icon(Icons.Filled.List, contentDescription = null) }, + selected = false, + onClick = { showSubMenu.value = !showSubMenu.value } + ) + AnimatedVisibility(visible = showSubMenu.value) { AlgorithmSubMenu(viewModel) } + Button(onClick = { viewModel.isOpenLoadGraph = true }) { Text("Load graph") } + Button(onClick = { viewModel.isOpenSaveGraph.value = true }) { + Text(text = "Save Graph") + } + } + }, + ) { + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Icon(Icons.Filled.Menu, contentDescription = "Меню") + } + GraphView(viewModel.graphViewModel) + } + } + + if (viewModel.isOpenLoadGraph) { + LoadGraph(LoadGraphMenuViewModel(viewModel)) + } + + if (viewModel.isOpenSaveGraph.value) { + SaveGraph(saveGraphMenuViewModel) + } +} diff --git a/src/main/kotlin/view/CustomRadioView.kt b/src/main/kotlin/view/CustomRadioView.kt new file mode 100644 index 0000000..3e6e405 --- /dev/null +++ b/src/main/kotlin/view/CustomRadioView.kt @@ -0,0 +1,95 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.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.unit.dp + +@Composable +fun CustomRadioGroup( + options: List, + selectedOption: String, + onOptionSelected: (String) -> Unit +) { + var isBoxSelected1 by remember { mutableStateOf(false) } + var isBoxSelected2 by remember { mutableStateOf(false) } + var isBoxSelected3 by remember { mutableStateOf(false) } + + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.background(shape = CircleShape, color = Color.Gray) + .background(if (isBoxSelected1) Color.DarkGray else Color.Gray) + .height(50.dp) + .padding(8.dp) + .weight(1f) + ) { + Text("NEO4J", color = Color.Black) + } + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = { + onOptionSelected(StorageType.NEO4J.name) + isBoxSelected3 = false + isBoxSelected2 = false + isBoxSelected1 = !isBoxSelected1 + } + ) { + Text("Select") + } + } + Spacer(modifier = Modifier.height(8.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.background(if (isBoxSelected2) Color.DarkGray else Color.Gray) + .padding(8.dp) + .weight(1f) + ) { + Text("FILE", color = Color.Black) + } + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = { + onOptionSelected(StorageType.FILE.name) + isBoxSelected3 = false + isBoxSelected2 = !isBoxSelected2 + isBoxSelected1 = false + } + ) { + Text("Select") + } + } + Spacer(modifier = Modifier.height(8.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier.background(if (isBoxSelected3) Color.DarkGray else Color.Gray) + .padding(8.dp) + .weight(1f) + ) { + Text("SQLITE", color = Color.Black) + } + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = { + onOptionSelected(StorageType.SQLITE.name) + isBoxSelected3 = !isBoxSelected3 + isBoxSelected2 = false + isBoxSelected1 = false + } + ) { + Text("Select") + } + } + } +} diff --git a/src/main/kotlin/view/LoadGraphMenu.kt b/src/main/kotlin/view/LoadGraphMenu.kt new file mode 100644 index 0000000..912df3b --- /dev/null +++ b/src/main/kotlin/view/LoadGraphMenu.kt @@ -0,0 +1,211 @@ +package view + +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.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberDialogState +import java.io.File +import model.databases.neo4j.Neo4jHandler +import model.databases.neo4j.Neo4jRepository +import model.databases.sqlite.SQLiteDBHandler +import viewmodel.LoadGraphMenuViewModel +import viewmodel.graph.GraphViewModel + +@Composable +fun LoadGraph(viewModel: LoadGraphMenuViewModel) { + + var isWeighted by remember { mutableStateOf(false) } + var isDirected by remember { mutableStateOf(false) } + + val storageType = remember { mutableStateOf(StorageType.FILE) } + var fileAddress by remember { mutableStateOf("") } + var uri by remember { mutableStateOf("") } + var login by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + DialogWindow( + onCloseRequest = { viewModel.canvasViewModel.isOpenLoadGraph = false }, + state = + rememberDialogState( + position = WindowPosition(Alignment.Center), + size = DpSize(800.dp, 640.dp) + ), + title = "Load New Graph", + resizable = false + ) { + Column(Modifier.fillMaxSize().padding(4.dp)) { + val modifierRow = Modifier.padding(0.dp, 5.dp, 0.dp, 5.dp) + val verticalRow = Alignment.CenterVertically + + Row(modifierRow, verticalAlignment = verticalRow) {} + + Row(modifierRow, verticalAlignment = verticalRow) { + Column( + modifier = Modifier.height(450.dp).padding(16.dp), + verticalArrangement = Arrangement.Top + ) { + CustomRadioGroup( + options = + listOf( + StorageType.FILE.toString(), + StorageType.NEO4J.toString(), + StorageType.SQLITE.toString() + ), + selectedOption = storageType.value.toString(), + onOptionSelected = { storageType.value = StorageType.valueOf(it) } + ) + when (storageType.value) { + StorageType.FILE -> { + Text( + "Path to Database:", + modifier = Modifier.weight(0.5f), + textAlign = TextAlign.Center, + ) + OutlinedTextField( + modifier = Modifier.weight(1f), + value = viewModel.graphName.value, + onValueChange = { newValue -> + viewModel.graphName.value = newValue + }, + label = { Text("Path") }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(), + ) + } + StorageType.NEO4J -> { + Text( + "URI:", + modifier = Modifier.weight(0.5f), + textAlign = TextAlign.Center, + ) + OutlinedTextField( + modifier = Modifier.weight(1f), + value = viewModel.graphName.value, + onValueChange = { newValue -> + viewModel.graphName.value = newValue + uri = newValue + }, + label = { Text("URI") }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(), + ) + Text( + "Login:", + modifier = Modifier.weight(0.5f), + textAlign = TextAlign.Center, + ) + OutlinedTextField( + modifier = Modifier.weight(1f), + value = viewModel.graphName.value, + onValueChange = { newValue -> + viewModel.graphName.value = newValue + login = newValue + }, + label = { Text("Login") }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(), + ) + Text( + "Password:", + modifier = Modifier.weight(0.5f), + textAlign = TextAlign.Center, + ) + OutlinedTextField( + modifier = Modifier.weight(1f), + value = viewModel.graphName.value, + onValueChange = { newValue -> + viewModel.graphName.value = newValue + password = newValue + }, + label = { Text("Password") }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(), + ) + } + StorageType.SQLITE -> { + Text( + "Path to Database:", + modifier = Modifier.weight(0.5f), + textAlign = TextAlign.Center, + ) + OutlinedTextField( + modifier = Modifier.weight(1f), + value = viewModel.graphName.value, + onValueChange = { newValue -> + viewModel.graphName.value = newValue + fileAddress = newValue + }, + label = { Text("Path") }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(), + ) + } + } + } + } + Row(modifierRow, verticalAlignment = verticalRow) { + Checkbox(checked = isWeighted, onCheckedChange = { isWeighted = it }) + Text("Weighted") + Checkbox(checked = isDirected, onCheckedChange = { isDirected = it }) + Text("Directed") + } + Row(modifierRow, verticalAlignment = verticalRow) { + Spacer(modifier = Modifier.weight(1f)) + Button(onClick = { viewModel.canvasViewModel.isOpenLoadGraph = false }) { + Text("Cancel") + } + Spacer(modifier = Modifier.weight(0.01f)) + Button( + onClick = { + when (storageType.value) { + StorageType.FILE -> { + // Логика сохранения в файл с использованием fileName и + // isDirectedGraph + } + StorageType.NEO4J -> { + val repository = Neo4jRepository(uri, login, password) + val handler = Neo4jHandler(repository) + val newGraph = handler.loadGraphFromNeo4j() + newGraph.isDirected = isDirected + viewModel.canvasViewModel.graph = newGraph + viewModel.canvasViewModel.graphViewModel = GraphViewModel(newGraph) + viewModel.canvasViewModel.isOpenLoadGraph = false + } + StorageType.SQLITE -> { + fileAddress = "saves/sqlite/$fileAddress" + val dataBase: File = File(fileAddress) + val sqlHandler = SQLiteDBHandler() + sqlHandler.open(dataBase, isWeighted, isDirected) + viewModel.canvasViewModel.graph = sqlHandler.graph + if (sqlHandler.vertexViewModelFlag) { + viewModel.canvasViewModel.graphViewModel.graph = + sqlHandler.graph + viewModel.canvasViewModel.graphViewModel = + sqlHandler.graphViewModel + } else { + viewModel.canvasViewModel.graphViewModel = + GraphViewModel(sqlHandler.graph) + } + viewModel.canvasViewModel.representationStrategy.place( + 1280.0, + 860.0, + viewModel.canvasViewModel.graphViewModel + ) + } + } + viewModel.canvasViewModel.isOpenLoadGraph = false + } + ) { + Text("Load") + } + } + } + } +} diff --git a/src/main/kotlin/view/NavigationDrawer.kt b/src/main/kotlin/view/NavigationDrawer.kt new file mode 100644 index 0000000..185bfb5 --- /dev/null +++ b/src/main/kotlin/view/NavigationDrawer.kt @@ -0,0 +1,84 @@ +package view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch +import view.graph.GraphView +import viewmodel.CanvasViewModel + +@Composable +fun NavigationDrawer(viewModel: CanvasViewModel) { + val drawerState = rememberDrawerState(DrawerValue.Closed) + val scope = rememberCoroutineScope() + + val showSubMenu = remember { mutableStateOf(false) } + ModalNavigationDrawer( + drawerState = drawerState, + gesturesEnabled = true, + drawerContent = { + ModalDrawerSheet { + Row { + IconButton(onClick = { scope.launch { drawerState.close() } }) { + Icon(Icons.Filled.Menu, contentDescription = "Меню") + } + Text("Menu", modifier = Modifier.padding(16.dp)) + } + Divider() + NavigationDrawerItem( + label = { Text(text = "Доступные алгоритмы") }, + icon = { Icon(Icons.Filled.List, contentDescription = null) }, + selected = false, + onClick = { showSubMenu.value = !showSubMenu.value } + ) + AnimatedVisibility(visible = showSubMenu.value) { AlgorithmSubMenu(viewModel) } + Row { + Checkbox( + checked = viewModel.showVerticesLabels.value, + onCheckedChange = { viewModel.showVerticesLabels.value = it } + ) + Text( + "Show vertices labels", + fontSize = 20.sp, + modifier = Modifier.padding(0.dp) + ) + } + Row { + Checkbox( + checked = viewModel.showEdgesLabels.value, + onCheckedChange = { viewModel.showEdgesLabels.value = it } + ) + Text("Show edges labels", fontSize = 20.sp, modifier = Modifier.padding(4.dp)) + } + } + }, + ) { + Scaffold( + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Add graph") }, + icon = { Icon(Icons.Filled.Add, contentDescription = "") }, + onClick = { + scope.launch { drawerState.apply { if (isClosed) open() else close() } } + } + ) + } + ) { + GraphView(viewModel.graphViewModel) + } + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Icon(Icons.Filled.Menu, contentDescription = "Меню") + } + } +} diff --git a/src/main/kotlin/view/SaveGraphMenu.kt b/src/main/kotlin/view/SaveGraphMenu.kt new file mode 100644 index 0000000..e5fbe95 --- /dev/null +++ b/src/main/kotlin/view/SaveGraphMenu.kt @@ -0,0 +1,165 @@ +package view + +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.input.PasswordVisualTransformation +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberDialogState +import java.io.File +import model.databases.CSV.CSVFileHandler +import model.databases.neo4j.Neo4jHandler +import model.databases.neo4j.Neo4jRepository +import model.databases.sqlite.SQLiteDBHandler +import viewmodel.SaveGraphMenuViewModel + +@Composable +@ExperimentalStdlibApi +fun SaveGraph(viewModel: SaveGraphMenuViewModel) { + var isWeighted by remember { mutableStateOf(false) } + var isDirected by remember { mutableStateOf(false) } + + DialogWindow( + onCloseRequest = { viewModel.canvasViewModel.isOpenSaveGraph.value = false }, + state = + rememberDialogState( + position = WindowPosition(Alignment.Center), + size = DpSize(800.dp, 640.dp) + ), + title = "Save New Graph", + resizable = false + ) { + Column(Modifier.fillMaxSize().padding(4.dp)) { + val modifierRow = Modifier.padding(0.dp, 5.dp, 0.dp, 5.dp) + val verticalRow = Alignment.CenterVertically + + Row(modifierRow, verticalAlignment = verticalRow) {} + + Row(modifierRow, verticalAlignment = verticalRow) { + Column( + modifier = Modifier.height(450.dp).padding(16.dp), + verticalArrangement = Arrangement.Top + ) { + CustomRadioGroup( + options = + listOf( + StorageType.FILE.toString(), + StorageType.NEO4J.toString(), + StorageType.SQLITE.toString() + ), + selectedOption = viewModel.storageType.value.toString(), + onOptionSelected = { viewModel.storageType.value = StorageType.valueOf(it) } + ) + + when (viewModel.storageType.value) { + StorageType.FILE -> { + TextField( + value = viewModel.fileName.value, + onValueChange = { viewModel.fileName.value = it }, + label = { Text("File Name") } + ) + Text("Ориентированный ли граф") + Checkbox(checked = isDirected, onCheckedChange = { isDirected = it }) + Spacer(modifier = Modifier.width(8.dp)) + + Text("Взвешанные ли рёбра") + Checkbox(checked = isWeighted, onCheckedChange = { isWeighted = it }) + Spacer(modifier = Modifier.width(8.dp)) + } + StorageType.NEO4J -> { + TextField( + value = viewModel.uri.value, + onValueChange = { viewModel.uri.value = it }, + label = { Text("URI") } + ) + TextField( + value = viewModel.login.value, + onValueChange = { viewModel.login.value = it }, + label = { Text("Login") } + ) + TextField( + value = viewModel.password.value, + onValueChange = { viewModel.password.value = it }, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation() + ) + Text("Ориентированный ли граф") + Checkbox(checked = isDirected, onCheckedChange = { isDirected = it }) + Spacer(modifier = Modifier.width(8.dp)) + + Text("Взвешанные ли рёбра") + Checkbox(checked = isWeighted, onCheckedChange = { isWeighted = it }) + Spacer(modifier = Modifier.width(8.dp)) + } + StorageType.SQLITE -> { + TextField( + value = viewModel.fileName.value, + onValueChange = { viewModel.fileName.value = it }, + label = { Text("File Name") } + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text("Взвешанные ли рёбра") + Checkbox(checked = isWeighted, onCheckedChange = { isWeighted = it }) + Spacer(modifier = Modifier.width(8.dp)) + } + } + } + } + + Row(modifierRow, verticalAlignment = verticalRow) { + Spacer(modifier = Modifier.weight(1f)) + Button(onClick = { viewModel.canvasViewModel.isOpenSaveGraph.value = false }) { + Text("Cancel") + } + Spacer(modifier = Modifier.weight(0.01f)) + Button( + onClick = { + when (viewModel.storageType.value) { + StorageType.FILE -> { + val fileAddress = "saves/csv/${viewModel.fileName.value}.csv" + val file = File(fileAddress) + val csvHandler = CSVFileHandler() + csvHandler.save(file, viewModel.canvasViewModel.graphViewModel) + + viewModel.canvasViewModel.isOpenSaveGraph.value = false + } + StorageType.NEO4J -> { + val repo = + Neo4jRepository( + viewModel.uri.value, + viewModel.login.value, + viewModel.password.value + ) + val handler = Neo4jHandler(repo) + val wasGraphDirected = viewModel.canvasViewModel.graph.isDirected + handler.saveGraphToNeo4j(viewModel.canvasViewModel.graph) + viewModel.canvasViewModel.graph.isDirected = wasGraphDirected + } + StorageType.SQLITE -> { + val fileAddress = "saves/sqlite/${viewModel.fileName.value}" + val dataBase: File = File(fileAddress) + val sqlHandler = SQLiteDBHandler() + sqlHandler.save( + dataBase, + viewModel.canvasViewModel.graph, + viewModel.canvasViewModel.graphViewModel, + isWeighted + ) + viewModel.canvasViewModel.isOpenSaveGraph.value = false + } + } + } + ) { + Text("Save") + } + } + } + } +} diff --git a/src/main/kotlin/view/ShortestPathDialog.kt b/src/main/kotlin/view/ShortestPathDialog.kt new file mode 100644 index 0000000..e0048bc --- /dev/null +++ b/src/main/kotlin/view/ShortestPathDialog.kt @@ -0,0 +1,51 @@ +package view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShortestPathDialog(showDialog: MutableState, onPathSelected: (Int, Int) -> Unit) { + var startIdx by remember { mutableStateOf(0) } + var endIdx by remember { mutableStateOf(0) } + + if (showDialog.value) { + AlertDialog( + onDismissRequest = { showDialog.value = false }, + buttons = { + Button( + onClick = { + onPathSelected(startIdx, endIdx) + showDialog.value = false + } + ) { + Text("Найти кратчайший путь") + } + }, + text = { + Column { + TextField( + value = startIdx.toString(), + onValueChange = { startIdx = it.toIntOrNull() ?: 0 }, + label = { Text("Введите точку отправления") } + ) + Spacer(modifier = Modifier.height(8.dp)) + TextField( + value = endIdx.toString(), + onValueChange = { endIdx = it.toIntOrNull() ?: 0 }, + label = { Text("Введите точку назначения") } + ) + } + } + ) + } +} diff --git a/src/main/kotlin/view/StorageType.kt b/src/main/kotlin/view/StorageType.kt new file mode 100644 index 0000000..2c4af37 --- /dev/null +++ b/src/main/kotlin/view/StorageType.kt @@ -0,0 +1,7 @@ +package view + +enum class StorageType { + FILE, + NEO4J, + SQLITE +} diff --git a/src/main/kotlin/view/algos/BridgeFinderView.kt b/src/main/kotlin/view/algos/BridgeFinderView.kt new file mode 100644 index 0000000..6b0d630 --- /dev/null +++ b/src/main/kotlin/view/algos/BridgeFinderView.kt @@ -0,0 +1,15 @@ +package view.algos + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import viewmodel.algos.BridgeFinderViewModel + +// просто окрашиваю прилегающие вершины в красный цвет +@Composable +fun bridgeHighlighter(bridges: BridgeFinderViewModel) { + val pairs = bridges.pairsList + for (pair in pairs) { + pair.first.color = Color.Red + pair.second.color = Color.Red + } +} diff --git a/src/main/kotlin/view/graph/EdgeView.kt b/src/main/kotlin/view/graph/EdgeView.kt new file mode 100644 index 0000000..250a28f --- /dev/null +++ b/src/main/kotlin/view/graph/EdgeView.kt @@ -0,0 +1,32 @@ +package view.graph + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.zIndex +import viewmodel.graph.EdgeViewModel + +@Composable +fun EdgeView( + viewModel: EdgeViewModel, + modifier: Modifier = Modifier, +) { + Canvas(modifier = modifier.fillMaxSize().zIndex(-1f)) { + drawLine( + start = + Offset( + viewModel.u.x.toPx() + viewModel.u.radius.toPx(), + viewModel.u.y.toPx() + viewModel.u.radius.toPx(), + ), + end = + Offset( + viewModel.v.x.toPx() + viewModel.v.radius.toPx(), + viewModel.v.y.toPx() + viewModel.v.radius.toPx(), + ), + color = viewModel.color + ) + } +} diff --git a/src/main/kotlin/view/graph/GraphView.kt b/src/main/kotlin/view/graph/GraphView.kt new file mode 100644 index 0000000..219a180 --- /dev/null +++ b/src/main/kotlin/view/graph/GraphView.kt @@ -0,0 +1,17 @@ +package view.graph + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import viewmodel.graph.GraphViewModel + +@Composable +fun GraphView( + viewModel: GraphViewModel, +) { + Box(modifier = Modifier.fillMaxSize()) { + viewModel.verticesViewValues.forEach { v -> VertexView(v, Modifier) } + viewModel.edgesViewValues.forEach { e -> EdgeView(e, Modifier) } + } +} diff --git a/src/main/kotlin/view/graph/VertexView.kt b/src/main/kotlin/view/graph/VertexView.kt new file mode 100644 index 0000000..1a7b7a7 --- /dev/null +++ b/src/main/kotlin/view/graph/VertexView.kt @@ -0,0 +1,46 @@ +package view.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +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.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import viewmodel.graph.VertexViewModel + +@Composable +fun VertexView( + viewModel: VertexViewModel, + modifier: Modifier = Modifier, +) { + var diametr = viewModel.radius + viewModel.radius + Box( + modifier = + modifier + .size(diametr, diametr) + .offset(viewModel.x, viewModel.y) + .border(2.dp, Color.Black, CircleShape) + .background(color = viewModel.color, shape = CircleShape) + .pointerInput(viewModel) { + detectDragGestures { change, dragAmount -> + change.consume() + viewModel.onDrag(dragAmount) + } + } + ) { + if (viewModel.labelVisible) { + Text( + modifier = Modifier.align(Alignment.Center).offset(0.dp, -viewModel.radius - 10.dp), + text = viewModel.label, + ) + } + } +} diff --git a/src/main/kotlin/viewmodel/CanvasViewModel.kt b/src/main/kotlin/viewmodel/CanvasViewModel.kt new file mode 100644 index 0000000..0ee2bd7 --- /dev/null +++ b/src/main/kotlin/viewmodel/CanvasViewModel.kt @@ -0,0 +1,39 @@ +package viewmodel + +import androidx.compose.runtime.mutableStateOf +import model.graph.Graph +import viewmodel.algos.BridgeFinderViewModel +import viewmodel.graph.GraphViewModel +import viewmodel.layouts.RepresentationStrategy + +class CanvasViewModel(var graph: Graph, var representationStrategy: RepresentationStrategy) { + val showVerticesLabels = mutableStateOf(false) + val showEdgesLabels = mutableStateOf(false) + val isOpenSaveGraph = mutableStateOf(false) + var graphViewModel = GraphViewModel(graph) + val bridges = BridgeFinderViewModel(graph, graphViewModel) + + private val _isOpenLoadGraph = mutableStateOf(false) + var isOpenLoadGraph: Boolean + get() = _isOpenLoadGraph.value + set(value) { + _isOpenLoadGraph.value = value + } + + init { + representationStrategy.place(1920.0, 1080.0, graphViewModel) + } + + fun switchLayout(newLayout: RepresentationStrategy) { + representationStrategy = newLayout + representationStrategy.place(1920.0, 1080.0, graphViewModel) + } + + fun openSaveGraphDialog() { + isOpenSaveGraph.value = true + } + + fun closeSaveGraphDialog() { + isOpenSaveGraph.value = false + } +} diff --git a/src/main/kotlin/viewmodel/LoadGraphMenuViewModel.kt b/src/main/kotlin/viewmodel/LoadGraphMenuViewModel.kt new file mode 100644 index 0000000..cea530b --- /dev/null +++ b/src/main/kotlin/viewmodel/LoadGraphMenuViewModel.kt @@ -0,0 +1,7 @@ +package viewmodel + +import androidx.compose.runtime.mutableStateOf + +class LoadGraphMenuViewModel(val canvasViewModel: CanvasViewModel) { + val graphName = mutableStateOf("") +} diff --git a/src/main/kotlin/viewmodel/SaveGraphMenuViewModel.kt b/src/main/kotlin/viewmodel/SaveGraphMenuViewModel.kt new file mode 100644 index 0000000..098a6eb --- /dev/null +++ b/src/main/kotlin/viewmodel/SaveGraphMenuViewModel.kt @@ -0,0 +1,12 @@ +package viewmodel + +import androidx.compose.runtime.mutableStateOf +import view.StorageType + +class SaveGraphMenuViewModel(val canvasViewModel: CanvasViewModel) { + val storageType = mutableStateOf(StorageType.FILE) + val fileName = mutableStateOf("") + val uri = mutableStateOf("") + val login = mutableStateOf("") + val password = mutableStateOf("") +} diff --git a/src/main/kotlin/viewmodel/algos/BridgeFinderViewModel.kt b/src/main/kotlin/viewmodel/algos/BridgeFinderViewModel.kt new file mode 100644 index 0000000..fdb2ac2 --- /dev/null +++ b/src/main/kotlin/viewmodel/algos/BridgeFinderViewModel.kt @@ -0,0 +1,22 @@ +package viewmodel.algos + +import model.algorithms.BridgeFinder +import model.graph.Graph +import viewmodel.graph.GraphViewModel +import viewmodel.graph.VertexViewModel + +class BridgeFinderViewModel(graph: Graph, graphView: GraphViewModel) { + val bridgeFinder = BridgeFinder(graph) + val pairsList = mutableListOf>() + + init { + bridgeFinder.findBridges() + val edges = bridgeFinder.bridges + edges.forEach { bridge -> + val key = graph.edges[bridge] + val firstEnd: VertexViewModel = graphView.edgesView[key]!!.u + val secondEnd: VertexViewModel = graphView.edgesView[key]!!.v + pairsList.add(Pair(firstEnd, secondEnd)) + } + } +} diff --git a/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt new file mode 100644 index 0000000..ec5f976 --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -0,0 +1,14 @@ +package viewmodel.graph + +import model.graph.Edge +import androidx.compose.ui.graphics.Color + +class EdgeViewModel( + val u: VertexViewModel, + val v: VertexViewModel, + val e: Edge, + var color: Color, +) { + val weight + get() = e.weight.toString() +} diff --git a/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/src/main/kotlin/viewmodel/graph/GraphViewModel.kt new file mode 100644 index 0000000..fe94dee --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -0,0 +1,46 @@ +package viewmodel.graph + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import model.graph.Edge +import model.graph.Graph +import model.graph.Vertex + +class GraphViewModel( + var graph: Graph, +) { + + val verticesView: HashMap = hashMapOf() + + init { + graph.getVertices().forEach { vertex -> + verticesView[vertex] = VertexViewModel(0.dp, 0.dp, Color.Black, vertex) + } + } + + val edgesView: HashMap = hashMapOf() + + init { + graph.getEdges().forEach { edge -> + val fst = + verticesView[graph.vertices[edge.vertices.first]] + ?: throw IllegalStateException( + "VertexView for vertex with id: ${edge.vertices.first} not found" + ) + val snd = + verticesView[graph.vertices[edge.vertices.second]] + ?: throw IllegalStateException( + "VertexView for vertex with id: ${edge.vertices.second} not found" + ) + val color = Color.Black + val currentEdgeView = EdgeViewModel(fst, snd, edge, color) + edgesView[edge] = currentEdgeView + } + } + + val verticesViewValues: Collection + get() = verticesView.values + + val edgesViewValues: Collection + get() = edgesView.values +} diff --git a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt new file mode 100644 index 0000000..3470036 --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -0,0 +1,51 @@ +package viewmodel.graph + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import model.graph.Vertex + +class VertexViewModel( + x: Dp = 0.dp, + y: Dp = 0.dp, + color: Color, + val vertex: Vertex, + var radius: Dp = 25.dp +) { + private val _labelVisible: State = mutableStateOf(true) + + private var _x = mutableStateOf(x) + var x: Dp + get() = _x.value + set(value) { + _x.value = value + } + + private var _y = mutableStateOf(y) + var y: Dp + get() = _y.value + set(value) { + _y.value = value + } + + private var _color = mutableStateOf(color) + var color: Color + get() = _color.value + set(value) { + _color.value = value + } + + val label + get() = vertex.data + + val labelVisible + get() = _labelVisible.value + + fun onDrag(offset: Offset) { + _x.value += offset.x.dp + _y.value += offset.y.dp + } +} diff --git a/src/main/kotlin/viewmodel/layouts/CircularLayout.kt b/src/main/kotlin/viewmodel/layouts/CircularLayout.kt new file mode 100644 index 0000000..eb8b83f --- /dev/null +++ b/src/main/kotlin/viewmodel/layouts/CircularLayout.kt @@ -0,0 +1,56 @@ +package viewmodel.layouts + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin +import kotlin.random.Random +import viewmodel.graph.GraphViewModel +import viewmodel.graph.VertexViewModel + +class CircularLayout : RepresentationStrategy { + override fun place(width: Double, height: Double, graph: GraphViewModel) { + val vertices = graph.verticesViewValues + if (vertices.isEmpty()) { + println("CircularPlacementStrategy.place: there is nothing to place 👐🏻") + return + } + + val center = Pair(width / 2, height / 2) + val angle = 2 * Math.PI / vertices.size + + val sorted = vertices.sortedBy { it.label } + val first = sorted.first() + var point = Pair(center.first, center.second - min(width, height) / 2) + first.x = point.first.dp + first.y = point.second.dp + first.color = Color.Gray + + sorted.drop(1).onEach { + point = point.rotate(center, angle) + it.x = point.first.dp + it.y = point.second.dp + } + } + + override fun highlight(vertices: Collection) { + vertices.onEach { it.color = if (Random.nextBoolean()) Color.Green else Color.Blue } + } + + private fun Pair.rotate( + pivot: Pair, + angle: Double + ): Pair { + val sin = sin(angle) + val cos = cos(angle) + + val diff = first - pivot.first to second - pivot.second + val rotated = + Pair( + diff.first * cos - diff.second * sin, + diff.first * sin + diff.second * cos, + ) + return rotated.first + pivot.first to rotated.second + pivot.second + } +} diff --git a/src/main/kotlin/viewmodel/layouts/ForceAtlas2Layout.kt b/src/main/kotlin/viewmodel/layouts/ForceAtlas2Layout.kt new file mode 100644 index 0000000..10ca9d3 --- /dev/null +++ b/src/main/kotlin/viewmodel/layouts/ForceAtlas2Layout.kt @@ -0,0 +1,67 @@ +package viewmodel.layouts + +import androidx.compose.ui.unit.dp +import kotlin.random.Random +import org.gephi.graph.api.Edge +import org.gephi.graph.api.GraphController +import org.gephi.graph.api.Node +import org.gephi.layout.plugin.forceAtlas2.ForceAtlas2 +import org.gephi.project.api.ProjectController +import org.openide.util.Lookup +import viewmodel.graph.GraphViewModel +import viewmodel.graph.VertexViewModel + +class ForceAtlas2Layout : RepresentationStrategy { + + override fun place(width: Double, height: Double, graphViewModel: GraphViewModel) { + + val pc = Lookup.getDefault().lookup(ProjectController::class.java) + pc.newProject() + val graphModel = Lookup.getDefault().lookup(GraphController::class.java).graphModel + val graph = graphModel.undirectedGraph + + val verticesMap = mutableMapOf() + for (vertex in graphViewModel.verticesViewValues) { + val v: Node = graphModel.factory().newNode(vertex.vertex.id.toString()) + v.setX(Random.nextFloat() * 10) + v.setY(Random.nextFloat() * 10) + graph.addNode(v) + verticesMap[vertex.vertex.id] = v + } + // TODO добавить возможность получения информации об ориентированности графа + for (edge in graphViewModel.edgesViewValues) { + val e: Edge = + graphModel + .factory() + .newEdge(verticesMap[edge.u.vertex.id], verticesMap[edge.v.vertex.id], 1, false) + graph.addEdge(e) + } + val layout = ForceAtlas2(null) + layout.setGraphModel(graphModel) + layout.initAlgo() + layout.resetPropertiesValues() + layout.isAdjustSizes = true + layout.isBarnesHutOptimize = true + layout.scalingRatio = 60.0 + layout.gravity = 2.0 + + var i = 0 + while (i < 5000 && layout.canAlgo()) { + layout.goAlgo() + i++ + } + layout.endAlgo() + + for (vertex in graphViewModel.verticesViewValues) { + val v: Node = graph.getNode(vertex.vertex.id.toString()) + val x = ((width / 2 + v.x())) + val y = ((height / 2 + v.y())) + vertex.x = (x).toInt().dp + vertex.y = (y).toInt().dp + } + } + + override fun highlight(vertices: Collection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/viewmodel/layouts/RepresentationStrategy.kt b/src/main/kotlin/viewmodel/layouts/RepresentationStrategy.kt new file mode 100644 index 0000000..7421170 --- /dev/null +++ b/src/main/kotlin/viewmodel/layouts/RepresentationStrategy.kt @@ -0,0 +1,10 @@ +package viewmodel.layouts + +import viewmodel.graph.GraphViewModel +import viewmodel.graph.VertexViewModel + +interface RepresentationStrategy { + fun place(width: Double, height: Double, graph: GraphViewModel) + + fun highlight(vertices: Collection) +} diff --git a/src/test/kotlin/graphs/algorithms/BridgeFinderTest.kt b/src/test/kotlin/graphs/algorithms/BridgeFinderTest.kt new file mode 100644 index 0000000..34454a1 --- /dev/null +++ b/src/test/kotlin/graphs/algorithms/BridgeFinderTest.kt @@ -0,0 +1,60 @@ +package graphs.algorithms + +import model.algorithms.BridgeFinder +import model.graph.Graph +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class BridgeFinderTest { + private lateinit var graph: Graph + + @BeforeEach + fun setup() { + graph = + Graph().apply { + addVertex(1, "Thomas Shelby") + addVertex(2, "Andrew Tate") + addVertex(3, "Iakov") + addVertex(4, "John Shelby") + addVertex(5, "Tristan Tate") + addVertex(6, "Arthur Shelby") + addVertex(7, "Ryan Gosling") + + addEdge(1, 2, 1f, 1) + addEdge(3, 4, 2f, 2) + addEdge(1, 3, 3f, 3) + addEdge(2, 4, 4f, 4) + addEdge(2, 5, 5f, 5) + addEdge(5, 7, 6f, 6) + + addVertex(8, "Pudge") + addVertex(9, "Tiny") + addVertex(10, "Lycan") + addVertex(11, "Io") + addVertex(12, "Lion") + addVertex(13, "Sniper") + addVertex(14, "Roshan") + + addEdge(14, 8, 7f, 7) + addEdge(14, 9, 8f, 8) + addEdge(14, 10, 9f, 9) + addEdge(14, 11, 10f, 10) + addEdge(14, 12, 11f, 11) + addEdge(14, 13, 12f, 12) + + addEdge(14, 3, 0f, 13) + } + } + + @Test + @DisplayName("Multiple bridges, no multiple edges") + public fun MultipleBridgesNoMultipleEdges() { + val bridgeFinder = BridgeFinder(graph) + bridgeFinder.findBridges() + val result = bridgeFinder.bridges + val expectedBridges = listOf(7, 8, 9, 10, 11, 12, 13, 6, 5) + assertEquals(expectedBridges, result) + } +} diff --git a/src/test/kotlin/graphs/algorithms/DjikstraTest.kt b/src/test/kotlin/graphs/algorithms/DjikstraTest.kt new file mode 100644 index 0000000..f48dfeb --- /dev/null +++ b/src/test/kotlin/graphs/algorithms/DjikstraTest.kt @@ -0,0 +1,211 @@ +package graphs.algorithms + +import model.algorithms.Djikstra +import model.graph.Graph +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +// table of path's +// _________________________________________________________ +// |_iteration_|____S_____|__w__|_D[2]_|_D[3]_|_D[4]_|_D[5]_| +// |___start___|___{1}____|__-__|__10__|_+00__|__30__|_100__| +// |_____1_____|__{1,2}___|__2__|__10__|__60__|__30__|_100__| +// |_____2_____|_{1,2,4}__|__4__|__10__|__50__|__30__|__90__| +// |_____3_____|{1,2,4,3}_|__3__|__10__|__50__|__30__|__60__| +// |_____4_____|_1,2,4,3,5|__5__|__10__|__50__|__30__|__60__| +// + +fun createSampleGraphDjikstraDirected(): Graph { + val graph = Graph() + graph.isDirected = true + + graph.addVertex(1, "A") + graph.addVertex(2, "B") + graph.addVertex(3, "C") + graph.addVertex(4, "D") + graph.addVertex(5, "E") + + graph.addEdge(1, 2, 10f, 0) + graph.addEdge(1, 5, 100f, 1) + graph.addEdge(1, 4, 30f, 2) + graph.addEdge(2, 3, 50f, 3) + graph.addEdge(3, 5, 10f, 4) + graph.addEdge(4, 3, 20f, 5) + graph.addEdge(4, 5, 60f, 6) + + return graph +} + +fun createSampleGraphDjikstra(): Graph { + val graph = Graph() + graph.isDirected = false + + graph.addVertex(1, "A") + graph.addVertex(2, "B") + graph.addVertex(3, "C") + graph.addVertex(4, "D") + graph.addVertex(5, "E") + + graph.addEdge(1, 2, 10f, 0) + graph.addEdge(1, 5, 100f, 1) + graph.addEdge(1, 4, 30f, 2) + graph.addEdge(2, 3, 50f, 3) + graph.addEdge(3, 5, 10f, 4) + graph.addEdge(4, 3, 20f, 5) + graph.addEdge(4, 5, 60f, 6) + + return graph +} + +class DjikstraTest { + private val graphD = createSampleGraphDjikstraDirected() + private val graph = createSampleGraphDjikstra() + + @Test + fun `test findShortestPaths with sample graph start from 1 to 5 started from 1`() { + // graph and algo initialization + val expected = mutableListOf(1, 4, 3, 5) + val algorithm = Djikstra(graphD, 1) + // path created from 1 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(5) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths with sample graph start from 1 to 2`() { + // graph and algo initialization + val expected = mutableListOf(1, 2) + val algorithm = Djikstra(graphD, 1) + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(2) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths with sample graph start from 2 to 3 started from 2`() { + // graph and algo initialization + val expected = mutableListOf(2, 3) + val algorithm = Djikstra(graphD, 2) + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(3) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths path does not exist`() { + // graph and algo initialization + val expected = mutableListOf() + val algorithm = Djikstra(graphD, 2) + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(1) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths from 2 to 1 not directed graph`() { + // graph and algo initialization + val expected = mutableListOf(2, 1) + val algorithm = Djikstra(graph, 2) + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(1) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths from 5 to 1 not directed graph`() { + // graph and algo initialization + val expected = mutableListOf(5, 3, 4, 1) + val algorithm = Djikstra(graph, 5) + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(1) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths empty-edged graph`() { + // graph and algo initialization + val emptyGraph = Graph() + val expected = mutableListOf() + val algorithm = Djikstra(emptyGraph) + + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(1) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths no start position`() { + // graph and algo initialization + val expected = mutableListOf() + val algorithm = Djikstra(graph, -11) + + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(5) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths no end position and start position`() { + // graph and algo initialization + val expected = mutableListOf() + val algorithm = Djikstra(graph) + + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(-4) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths no end position start exist`() { + // graph and algo initialization + val expected = mutableListOf() + val algorithm = Djikstra(graph, 2) + + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(-4) + + assertTrue(expected == currently) + } + + @Test + fun `test findShortestPaths start and end pos equals`() { + // graph and algo initialization + val expected = mutableListOf(2) + val algorithm = Djikstra(graph, 2) + + // path created from 2 + algorithm.findShortestPaths() + + val currently = algorithm.reconstructPath(2) + + assertTrue(expected == currently) + } +} diff --git a/src/test/kotlin/graphs/algorithms/FordBellmanTest.kt b/src/test/kotlin/graphs/algorithms/FordBellmanTest.kt new file mode 100644 index 0000000..60044f7 --- /dev/null +++ b/src/test/kotlin/graphs/algorithms/FordBellmanTest.kt @@ -0,0 +1,109 @@ +package graphs.algorithms + +import model.algorithms.FordBellman +import model.graph.Graph +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class FordBellmanTest { + private lateinit var graph: Graph + + @Test + @DisplayName("Path without negative cycles") + public fun PathWithoutNegativeCycles() { + graph = + Graph().apply { + addVertex(1, "Thomas Shelby") + addVertex(2, "Andrew Tate") + addVertex(3, "Iakov") + addVertex(4, "John Shelby") + addVertex(5, "Arthur Shelby") + addEdge(1, 2, 3f, 5) + addEdge(2, 3, 6f, 2) + addEdge(1, 4, 2f, 4) + addEdge(5, 3, 1f, 3) + addEdge(1, 3, 5f, 1) + addEdge(4, 5, 1f, 6) + } + val fordBellman = FordBellman(graph) + fordBellman.shortestPath(1, 3) + val resultVertices = fordBellman.resultPathVertices + val resultEdges = fordBellman.resultPathEdges + val resultCycles = fordBellman.cycleFlag + val resultConnectionFlag = fordBellman.disconnectedGraphFlag + val expectedCycles = false + val expectedEdges = listOf(3, 6, 4) + val expectedVertices = listOf(3, 5, 4, 1) + val expectedConnectionFlag = false + assertEquals(expectedConnectionFlag, resultConnectionFlag) + assertEquals(expectedCycles, resultCycles) + assertEquals(expectedEdges, resultEdges) + assertEquals(expectedVertices, resultVertices) + } + + @Test + @DisplayName("Path with negative cycles") + public fun PathWithNegativeCycles() { + graph = + Graph().apply { + addVertex(1, "Thomas Shelby") + addVertex(2, "Andrew Tate") + addVertex(3, "Iakov") + addVertex(4, "John Shelby") + addVertex(5, "John Shelby") + + addEdge(1, 2, -2f, 3) + addEdge(2, 3, 1f, 2) + addEdge(4, 1, -1f, 1) + addEdge(3, 4, 1f, 4) + addEdge(4, 5, 5f, 5) + } + val fordBellman = FordBellman(graph) + fordBellman.shortestPath(1, 5) + val resultVertices = fordBellman.resultPathVertices + val resultEdges = fordBellman.resultPathEdges + val resultCycles = fordBellman.cycleFlag + val resultConnectionFlag = fordBellman.disconnectedGraphFlag + val expectedCycles = true + val expectedEdges = listOf(3, 1, 4, 2) + val expectedVertices = listOf(2, 1, 4, 3) + val expectedConnectionFlag = false + assertEquals(expectedConnectionFlag, resultConnectionFlag) + assertEquals(expectedCycles, resultCycles) + assertEquals(expectedEdges, resultEdges) + assertEquals(expectedVertices, resultVertices) + } + + @Test + @DisplayName("Disconnected graph") + public fun DisconnectedGraph() { + graph = + Graph().apply { + addVertex(1, "Thomas Shelby") + addVertex(2, "Andrew Tate") + addVertex(3, "Iakov") + addVertex(4, "John Shelby") + addVertex(5, "John Shelby") + + addEdge(1, 2, 2f, 3) + addEdge(2, 3, 1f, 2) + addEdge(4, 1, -1f, 1) + addEdge(3, 4, 1f, 4) + } + val fordBellman = FordBellman(graph) + fordBellman.shortestPath(1, 5) + val resultVertices = fordBellman.resultPathVertices + val resultEdges = fordBellman.resultPathEdges + val resultCycles = fordBellman.cycleFlag + val resultConnectionFlag = fordBellman.disconnectedGraphFlag + val expectedCycles = false + val expectedEdges = listOf() + val expectedVertices = listOf() + val expectedConnectionFlag = true + assertEquals(expectedConnectionFlag, resultConnectionFlag) + assertEquals(expectedCycles, resultCycles) + assertEquals(expectedEdges, resultEdges) + assertEquals(expectedVertices, resultVertices) + } +} diff --git a/src/test/kotlin/graphs/algorithms/KosajaruTest.kt b/src/test/kotlin/graphs/algorithms/KosajaruTest.kt new file mode 100644 index 0000000..221568d --- /dev/null +++ b/src/test/kotlin/graphs/algorithms/KosajaruTest.kt @@ -0,0 +1,157 @@ +package graphs.algorithms + +import model.algorithms.Kosaraju +import model.graph.Graph +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +// graph sample +// +// 7 11 → → → → → → → 5 +// ↗ ↓ ↘ ↙ ↘ ↙ ↑ ↘ +// 0 ↓ 1 → → → 9 ← ← ← 2 → → → 10 ↑ 3 +// ↖ ↓ ↗ ↘ ↗ ↘ ↑ ↙ +// 6 → → → → → → 4 8 +// +// components: (0,7,1,6) (11) (9,2,4) (10,8,3,5) + +fun createSampleGraph(): Graph { + // Добавление вершин + val graph = Graph() + graph.isDirected = true + graph.addVertex(0, "A") + graph.addVertex(1, "B") + graph.addVertex(2, "C") + graph.addVertex(3, "D") + graph.addVertex(4, "E") + graph.addVertex(5, "F") + graph.addVertex(6, "G") + graph.addVertex(7, "H") + graph.addVertex(8, "I") + graph.addVertex(9, "J") + graph.addVertex(10, "K") + graph.addVertex(11, "L") + + // Добавление рёбер + graph.addEdge(0, 7, 1f, 0) + graph.addEdge(7, 6, 1f, 1) + graph.addEdge(6, 0, 1f, 2) + graph.addEdge(6, 1, 1f, 3) + graph.addEdge(6, 4, 1f, 4) + graph.addEdge(7, 1, 1f, 5) + graph.addEdge(1, 9, 1f, 6) + graph.addEdge(9, 4, 1f, 7) + graph.addEdge(4, 2, 1f, 8) + graph.addEdge(2, 9, 1f, 9) + graph.addEdge(2, 10, 1f, 10) + graph.addEdge(11, 9, 1f, 11) + graph.addEdge(11, 2, 1f, 12) + graph.addEdge(11, 5, 1f, 13) + graph.addEdge(10, 8, 1f, 14) + graph.addEdge(8, 5, 1f, 15) + graph.addEdge(5, 10, 1f, 16) + graph.addEdge(5, 3, 1f, 17) + graph.addEdge(3, 8, 1f, 18) + + return graph +} + +class KosarajuTest { + @Test + fun `test topologySort with sample graph start from 0's component`() { + // graph and algo initialization + val graph = createSampleGraph() + val algo = Kosaraju(graph) + val expected = mutableListOf(3, 5, 8, 10, 2, 4, 9, 1, 6, 7, 0) + + // algo start from different positions + val currently = algo.test_TopologySort(graph, 0) + + assertTrue(expected == currently) + } + + @Test + fun `test topologySort with sample graph start from 5's component`() { + // graph and algo initialization + val graph = createSampleGraph() + val algo = Kosaraju(graph) + val expected = mutableListOf(8, 10, 3, 5) + + // algo start from different positions + val currently = algo.test_TopologySort(graph, 5) + + // Проверьте результаты + assertTrue(expected == currently) + } + + @Test + fun `test topologySort with sample graph start from 9's component`() { + // graph and algo initialization + val graph = createSampleGraph() + val algo = Kosaraju(graph) + val expected = mutableListOf(3, 5, 8, 10, 2, 4, 9) + + // algo start from different positions + val currently = algo.test_TopologySort(graph, 9) + + // Проверьте результаты + assertTrue(expected == currently) + } + + @Test + fun `test dfs1 with sample graph start from 11's component`() { + // graph and algo initialization + val graph = createSampleGraph() + val algo = Kosaraju(graph) + val expected = mutableListOf(3, 5, 8, 10, 2, 4, 9, 11) + + // algo start from different positions + val currently = algo.test_TopologySort(graph, 11) + + // Проверьте результаты + assertTrue(expected == currently) + } + + @Test + fun `test components output`() { + // graph and algo initialization + val graph = createSampleGraph() + val algo = Kosaraju(graph) + val expected = + mutableListOf( + mutableListOf(3, 8, 5, 10), + mutableListOf(2, 9, 4), + mutableListOf(11), + mutableListOf(1), + mutableListOf(0, 7, 6), + ) + + // algo start from different positions + val currently = algo.findStronglyConnectedComponents() + + // Проверьте результаты + assertTrue(expected == currently) + } + + @Test + fun `test components output non directed`() { + // graph and algo initialization + val graph = createSampleGraph() + graph.isDirected = false + val algo = Kosaraju(graph) + val expected = + mutableListOf( + mutableListOf(3, 8, 5, 10), + mutableListOf(2, 9, 4), + mutableListOf(11), + mutableListOf(1), + mutableListOf(0, 7, 6), + ) + + // algo start from different positions + val currently = algo.findStronglyConnectedComponents() + + // Проверьте результаты + assertTrue(expected == currently) + } +} diff --git a/src/test/kotlin/graphs/algorithms/KruskalTest.kt b/src/test/kotlin/graphs/algorithms/KruskalTest.kt new file mode 100644 index 0000000..03eeb91 --- /dev/null +++ b/src/test/kotlin/graphs/algorithms/KruskalTest.kt @@ -0,0 +1,58 @@ +package graphs.algorithms + +import model.algorithms.KruskalsMST +import model.graph.Graph +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +// Assuming the Edge class and kruskal function are defined here + +class KruskalTest { + + @Test + fun testSimpleGraph() { + val graph = Graph() + for (i in 0..8) { + graph.addVertex(i, "") + } + graph.addEdge(0, 1, 4f, 0) + graph.addEdge(0, 7, 8f, 1) + graph.addEdge(1, 2, 8f, 2) + graph.addEdge(1, 7, 11f, 3) + graph.addEdge(2, 3, 7f, 4) + graph.addEdge(2, 8, 2f, 5) + graph.addEdge(2, 5, 4f, 6) + graph.addEdge(3, 4, 9f, 7) + graph.addEdge(3, 5, 14f, 8) + graph.addEdge(4, 5, 10f, 9) + graph.addEdge(5, 6, 2f, 10) + graph.addEdge(6, 7, 1f, 11) + graph.addEdge(6, 8, 6f, 12) + graph.addEdge(7, 8, 7f, 13) + val expected = setOf(0, 1, 4, 5, 6, 7, 10, 11).sorted() + val algo = KruskalsMST() + val resultsId = algo.kruskals(graph) + assertEquals(expected, resultsId) + } + + @Test + fun testSingleVertexGraph() { + // Test with a single vertex to ensure the algorithm handles this case correctly + val graph = Graph() + graph.addVertex(0, "") + val expected = emptyList() + val algo = KruskalsMST() + val resultsId = algo.kruskals(graph) + assertEquals(expected, resultsId) + } + + @Test + fun testEmptyGraph() { + // Test with an empty graph to ensure the algorithm handles this case correctly + val graph = Graph() + val expected = emptyList() + val algo = KruskalsMST() + val resultsId = algo.kruskals(graph) + assertEquals(expected, resultsId) + } +} diff --git a/src/test/kotlin/graphs/algorithms/LouvainTest.kt b/src/test/kotlin/graphs/algorithms/LouvainTest.kt new file mode 100644 index 0000000..ee76fbe --- /dev/null +++ b/src/test/kotlin/graphs/algorithms/LouvainTest.kt @@ -0,0 +1,159 @@ +package graphs.algorithms + +import model.community.Louvain +import model.graph.Graph +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +// graph sample +// +// 7 11 → → → → → → → 5 +// ↗ ↓ ↘ ↙ ↘ ↙ ↑ ↘ +// 0 ↓ 1 → → → 9 ← ← ← 2 → → → 10 ↑ 3 +// ↖ ↓ ↗ ↘ ↗ ↘ ↑ ↙ +// 6 → → → → → → 4 8 +// + +// graph sample 2 +// +// 1 → → → 2 +// ↘ ↙ +// 3 +// ↓ +// ↓ +// 4 +// ↙ ↘ +// 5 → → → 6 +// + +fun createSampleGraph2(): Graph { + val graph = Graph() + graph.isDirected = true + + graph.addVertex(1, "A") + graph.addVertex(2, "B") + graph.addVertex(3, "C") + graph.addVertex(4, "D") + graph.addVertex(5, "E") + graph.addVertex(6, "F") + + graph.addEdge(1, 2, 10f, 1) + graph.addEdge(1, 3, 5f, 2) + graph.addEdge(2, 3, 5f, 3) + graph.addEdge(3, 4, 15f, 4) + graph.addEdge(4, 5, 5f, 5) + graph.addEdge(4, 6, 5f, 6) + graph.addEdge(5, 6, 10f, 7) + return graph +} + +class LouvainTest { + @Test + fun `test louvain set output directed`() { + // graph and algo initialization + val graph = createSampleGraph() + val algo = Louvain(graph) + val expected = mutableListOf(setOf(0, 7, 1, 6), setOf(9, 11, 2, 4), setOf(10, 5, 3, 8)) + + val currently = algo.findCommunities() + + assertTrue(expected == currently) + } + + @Test + fun `test louvain set output non directed`() { + // graph and algo initialization + val graph = createSampleGraph() + graph.isDirected = false + val algo = Louvain(graph) + val expected = mutableListOf(setOf(0, 1, 2, 4, 6, 7, 9, 11), setOf(10, 5, 3, 8)) + + val currently = algo.findCommunities() + + assertTrue(expected == currently) + } + + @Test + fun `test louvain graph community color`() { + // graph and algo initialization + val graph = createSampleGraph() + val algo = Louvain(graph) + + val expectedCommunities = mutableListOf(0, 0, 1, 2, 1, 2, 0, 0, 2, 1, 2, 1) + + algo.findCommunities() + + val currentlyCommunities = mutableListOf() + for (vertex in graph.getVertices()) { + currentlyCommunities.add(vertex.community) + } + + assertTrue(expectedCommunities == currentlyCommunities) + } + + @Test + fun `test louvain graph sample 2 directed`() { + // graph and algo initialization + val graph = createSampleGraph2() + val algo = Louvain(graph) + + val expected = mutableListOf(setOf(1, 2, 3), setOf(4, 5, 6)) + + algo.findCommunities() + + val currentlyCommunities = algo.findCommunities() + + assertTrue(expected == currentlyCommunities) + } + + @Test + fun `test louvain graph sample 2 not directed`() { + // graph and algo initialization + val graph = createSampleGraph2() + graph.isDirected = false + val algo = Louvain(graph) + + val expected = mutableListOf(setOf(1, 2, 3), setOf(4, 5, 6)) + + algo.findCommunities() + + val currentlyCommunities = algo.findCommunities() + + assertTrue(expected == currentlyCommunities) + } + + @Test + fun `test louvain graph is empty`() { + // graph and algo initialization + val graph = Graph() + graph.isDirected = false + val algo = Louvain(graph) + + val expected = mutableListOf() + + algo.findCommunities() + + val currentlyCommunities = algo.findCommunities() + + assertTrue(expected == currentlyCommunities) + } + + @Test + fun `test louvain graph has no edges`() { + // graph and algo initialization + val graph = Graph() + graph.addVertex(1, "A") + graph.addVertex(2, "B") + graph.addVertex(3, "C") + + val algo = Louvain(graph) + + val expected = mutableListOf(setOf(1), setOf(2), setOf(3)) + + algo.findCommunities() + + val currentlyCommunities = algo.findCommunities() + + assertTrue(expected == currentlyCommunities) + } +}