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)
+ }
+}