diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..1bd9031 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,25 @@ +name: Test # autorun of the project build and tests + +on: + workflow_dispatch: + pull_request: + paths: + - '**.kt' + push: + paths: + - '**.kt' +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: zulu + - name: Build + run: ./gradlew build -x test + - name: Test + run: ./gradlew test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f35ca9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c636ad1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Nabieva Liya, Tenyaeva Ekaterina, Migunova Anastasia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b8f9010 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +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(compose.desktop.currentOs) + implementation(compose.material3) + implementation(compose.materialIconsExtended) + implementation("org.neo4j.driver", "neo4j-java-driver", "5.6.0") + implementation("com.google.code.gson:gson:2.10.1") + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("io.kotest:kotest-runner-junit5-jvm:4.6.0") +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(21) +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "graph13" + packageVersion = "1.0.0" + } + } +} diff --git a/examples/dirLotsOfEdges.json b/examples/dirLotsOfEdges.json new file mode 100644 index 0000000..9402105 --- /dev/null +++ b/examples/dirLotsOfEdges.json @@ -0,0 +1 @@ +{"adjacency":{"1":{},"2":{},"3":{},"4":{},"5":{},"6":{},"7":{"8":0},"8":{"9":19},"9":{"7":2},"11":{},"12":{},"15":{},"19":{}},"vertices":{"1":{"id":1,"data":"1"},"2":{"id":2,"data":"2"},"3":{"id":3,"data":"3"},"4":{"id":4,"data":"4"},"5":{"id":5,"data":"5"},"6":{"id":6,"data":"6"},"7":{"id":7,"data":"7"},"8":{"id":8,"data":"8"},"9":{"id":9,"data":"9"},"10":{"id":10,"data":"10"},"11":{"id":11,"data":"11"},"12":{"id":12,"data":"12"},"13":{"id":13,"data":"13"},"14":{"id":14,"data":"14"},"15":{"id":15,"data":"15"},"16":{"id":16,"data":"16"},"17":{"id":17,"data":"17"},"18":{"id":18,"data":"18"},"19":{"id":19,"data":"19"},"20":{"id":20,"data":"20"}},"edges":[{"vertices":{"first":7,"second":8},"weight":0},{"vertices":{"first":8,"second":9},"weight":19},{"vertices":{"first":9,"second":7},"weight":2},{"vertices":{"first":1,"second":2},"weight":0},{"vertices":{"first":1,"second":3},"weight":0},{"vertices":{"first":1,"second":4},"weight":0},{"vertices":{"first":5,"second":6},"weight":0},{"vertices":{"first":18,"second":9},"weight":0},{"vertices":{"first":20,"second":14},"weight":0},{"vertices":{"first":12,"second":20},"weight":0},{"vertices":{"first":17,"second":13},"weight":0},{"vertices":{"first":1,"second":20},"weight":0},{"vertices":{"first":12,"second":6}},{"vertices":{"first":19,"second":12}},{"vertices":{"first":6,"second":18}},{"vertices":{"first":1,"second":14}},{"vertices":{"first":1,"second":15}},{"vertices":{"first":6,"second":20}},{"vertices":{"first":12,"second":1}},{"vertices":{"first":9,"second":19}},{"vertices":{"first":12,"second":9}},{"vertices":{"first":15,"second":6}},{"vertices":{"first":15,"second":5}},{"vertices":{"first":4,"second":11}},{"vertices":{"first":11,"second":20}},{"vertices":{"first":1,"second":9}},{"vertices":{"first":1,"second":18}},{"vertices":{"first":19,"second":1}},{"vertices":{"first":15,"second":19}},{"vertices":{"first":6,"second":19}}]} \ No newline at end of file diff --git a/examples/undirAverageNumberOfEdges.json b/examples/undirAverageNumberOfEdges.json new file mode 100644 index 0000000..958096b --- /dev/null +++ b/examples/undirAverageNumberOfEdges.json @@ -0,0 +1 @@ +{"adjacency":{"1":{"10":5},"2":{"3":52,"4":10,"6":17,"9":65,"10":8},"3":{"2":52,"6":32,"8":14},"4":{"2":10,"5":18},"5":{"4":18,"8":20},"6":{"2":17,"3":32,"10":71},"7":{},"8":{"3":14,"5":20,"9":3,"10":21},"9":{"2":65,"8":3},"10":{"1":5,"2":8,"6":71,"8":21},"11":{"12":4},"12":{"11":4}},"vertices":{"1":{"id":1,"data":"1"},"2":{"id":2,"data":"2"},"3":{"id":3,"data":"3"},"4":{"id":4,"data":"4"},"5":{"id":5,"data":"5"},"6":{"id":6,"data":"6"},"7":{"id":7,"data":"7"},"8":{"id":8,"data":"8"},"9":{"id":9,"data":"9"},"10":{"id":10,"data":"10"},"11":{"id":11,"data":"11"},"12":{"id":12,"data":"12"}},"edges":[{"vertices":{"first":8,"second":5},"weight":20},{"vertices":{"first":5,"second":8},"weight":20},{"vertices":{"first":5,"second":4},"weight":18},{"vertices":{"first":4,"second":5},"weight":18},{"vertices":{"first":8,"second":9},"weight":3},{"vertices":{"first":9,"second":8},"weight":3},{"vertices":{"first":8,"second":3},"weight":14},{"vertices":{"first":3,"second":8},"weight":14},{"vertices":{"first":3,"second":6},"weight":32},{"vertices":{"first":6,"second":3},"weight":32},{"vertices":{"first":2,"second":9},"weight":65},{"vertices":{"first":9,"second":2},"weight":65},{"vertices":{"first":2,"second":3},"weight":52},{"vertices":{"first":3,"second":2},"weight":52},{"vertices":{"first":2,"second":4},"weight":10},{"vertices":{"first":4,"second":2},"weight":10},{"vertices":{"first":2,"second":6},"weight":17},{"vertices":{"first":6,"second":2},"weight":17},{"vertices":{"first":11,"second":12},"weight":4},{"vertices":{"first":12,"second":11},"weight":4},{"vertices":{"first":10,"second":8},"weight":21},{"vertices":{"first":8,"second":10},"weight":21},{"vertices":{"first":10,"second":6},"weight":71},{"vertices":{"first":6,"second":10},"weight":71},{"vertices":{"first":10,"second":1},"weight":5},{"vertices":{"first":1,"second":10},"weight":5},{"vertices":{"first":10,"second":2},"weight":8},{"vertices":{"first":2,"second":10},"weight":8}]} \ No newline at end of file diff --git a/examples/undirSimpleGraph.json b/examples/undirSimpleGraph.json new file mode 100644 index 0000000..504ae06 --- /dev/null +++ b/examples/undirSimpleGraph.json @@ -0,0 +1 @@ +{"adjacency":{"1":{"2":3,"3":45,"5":21},"2":{"1":3,"4":20,"5":17},"3":{"1":45,"4":17,"5":8},"4":{"2":20,"3":17,"5":8},"5":{"1":21,"2":17,"3":8,"4":8}},"vertices":{"1":{"id":1,"data":"1"},"2":{"id":2,"data":"2"},"3":{"id":3,"data":"3"},"4":{"id":4,"data":"4"},"5":{"id":5,"data":"5"}},"edges":[{"vertices":{"first":1,"second":2},"weight":3},{"vertices":{"first":2,"second":1},"weight":3},{"vertices":{"first":2,"second":4},"weight":20},{"vertices":{"first":4,"second":2},"weight":20},{"vertices":{"first":1,"second":5},"weight":21},{"vertices":{"first":5,"second":1},"weight":21},{"vertices":{"first":4,"second":3},"weight":17},{"vertices":{"first":3,"second":4},"weight":17},{"vertices":{"first":5,"second":2},"weight":17},{"vertices":{"first":2,"second":5},"weight":17},{"vertices":{"first":3,"second":5},"weight":8},{"vertices":{"first":5,"second":3},"weight":8},{"vertices":{"first":3,"second":1},"weight":45},{"vertices":{"first":1,"second":3},"weight":45},{"vertices":{"first":4,"second":5},"weight":8},{"vertices":{"first":5,"second":4},"weight":8}]} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1208625 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +kotlin.code.style=official +kotlin.version=1.9.22 +compose.version=1.6.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6b932e8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..bd23cd6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + gradlePluginPortal() + mavenCentral() + } + + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) + } +} + +rootProject.name = "Graphs" + diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..a493e10 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,82 @@ +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import databases.FileSystem +import databases.Neo4jRepository +import model.graph.DirectedGraph +import model.graph.Graph +import model.graph.UndirectedGraph +import view.ErrorDialog +import view.MainScreen +import view.Neo4jDialog +import view.WelcomeScreen +import viewmodel.MainScreenViewModel +import viewmodel.graph.CircularPlacementStrategy + +@Composable +@Preview +fun App() { + MaterialTheme { + val fileSystem = FileSystem() + var graphType by remember { mutableStateOf(null) } + var graph by remember { mutableStateOf(null) } + var showErrorDialog by remember { mutableStateOf(false) } + var showNeo4jDialog by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + fun catchErrorOrGetGraph(result: Pair) { + if (result.second == null) { + graph = result.first + } else { + errorMessage = result.second + showErrorDialog = true + } + } + + if (graph == null) { + WelcomeScreen { selectedGraphType -> + graphType = selectedGraphType + when (graphType) { + "Directed" -> graph = DirectedGraph() + "Undirected" -> graph = UndirectedGraph() + "Neo4j" -> showNeo4jDialog = true + else -> catchErrorOrGetGraph(fileSystem.openGraph()) + } + } + } else { + graph?.let { + MainScreen(MainScreenViewModel(it, CircularPlacementStrategy())) + } + } + if (showErrorDialog) { + ErrorDialog( + onDismiss = { showErrorDialog = false }, + errorMessage!! + ) + } + if (showNeo4jDialog) { + Neo4jDialog( + onDismiss = { showNeo4jDialog = false }, + onRunAlgorithm = { uri, user, password -> + try { + val neo4j = Neo4jRepository(uri, user, password) + catchErrorOrGetGraph(neo4j.loadGraph()) + } catch (e: Exception) { + catchErrorOrGetGraph( + null to "Error loading:\n" + + (e.message?.substringAfter("Exception: ") ?: "unable to load graph") + ) + } + showNeo4jDialog = false + } + ) + } + } +} + +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + App() + } +} \ No newline at end of file diff --git a/src/main/kotlin/databases/FileSystem.kt b/src/main/kotlin/databases/FileSystem.kt new file mode 100644 index 0000000..3ac01f6 --- /dev/null +++ b/src/main/kotlin/databases/FileSystem.kt @@ -0,0 +1,115 @@ +package databases + +import com.google.gson.Gson +import model.graph.DirectedGraph +import model.graph.Graph +import model.graph.UndirectedGraph +import java.awt.FileDialog +import java.awt.Frame +import java.io.File +import java.nio.file.Files + +class FileSystem { + + /** From-to Json-conversation functions: + * with the help of the Google's Gson library, it is possible + * to get an object from a file and vice versa: + * to get a file from an object **/ + + private fun fromGsonConversation(jsonString: String, graph: Graph): Graph { + + val gson = Gson() + if (graph is UndirectedGraph) { + return gson.fromJson(jsonString, UndirectedGraph()::class.java) + } + + return gson.fromJson(jsonString, DirectedGraph()::class.java) + + } + + private fun toGsonConversation(graph: Graph): String { + + val gson = Gson() + return gson.toJson(graph) + + } + + /**If the graph is successfully saved the graph to a file, + * the function returns null, in case of a save error, + * it returns an error message**/ + + fun saveGraph(graph: Graph): String? { + + val frame = Frame() + val fileDialog= FileDialog(frame, "Save your Json file:", FileDialog.SAVE) + fileDialog.setFile("*.json") + fileDialog.isVisible = true + + if( fileDialog.file == null) { // case: click the "cross" or "cancel" buttons + frame.dispose() + return null + } + + if(graph is UndirectedGraph) { + fileDialog.file = "undir" + fileDialog.file + }else { + fileDialog.file = "dir" + fileDialog.file + } + val savedFile = File(fileDialog.directory, fileDialog.file) + try { + savedFile.writeText(toGsonConversation(graph)) + frame.dispose() + return null + } catch (e: Exception) { + frame.dispose() + return "To Gson conversation error:\n" + + (e.message?.substringAfter("Exception: ") ?: + "incorrect converted graph") + } + + } + + /**The function returns pair, the fist element of that is + * opened graph in successful case and null in failure case and second + * element is an error message in failure case + * and null in successful case.**/ + + fun openGraph(): Pair { + var graph: Graph + val frame = Frame() + val fileDialog= FileDialog(frame, "Open your Json file:", FileDialog.LOAD) + fileDialog.setFile("*.json") + fileDialog.isVisible = true + + val fileName = fileDialog.file + val directory = fileDialog.directory + if( fileName == null) { // case: click the "cross" or "cancel" buttons + frame.dispose() + return null to null + } + if(fileName.startsWith("dir")) { + graph = DirectedGraph() + }else if(fileName.startsWith("undir")) { + graph = UndirectedGraph() + }else { + return null to "Incorrect name of the selected file: it must begin with 'dir' or 'undir' ." + } + + val openedFile = File(directory, fileName) + + try { + + graph = fromGsonConversation( + Files.readString(openedFile.toPath()), graph + ) + frame.dispose() + return graph to null + + } catch (e: Exception) { + frame.dispose() + return null to "The contents of the file are incorrect:\n" + + (e.message?.substringAfter("Exception: ") ?: + "fix this file or try to open another one") + } + } +} diff --git a/src/main/kotlin/databases/Neo4jRepository.kt b/src/main/kotlin/databases/Neo4jRepository.kt new file mode 100644 index 0000000..995c4f0 --- /dev/null +++ b/src/main/kotlin/databases/Neo4jRepository.kt @@ -0,0 +1,106 @@ +package databases + +import model.graph.DirectedGraph +import model.graph.Graph +import model.graph.UndirectedGraph +import org.neo4j.driver.* +import java.io.Closeable + +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() + + private fun addVertex(vertexId: Int, vertexData: String, transaction: Transaction) { + transaction.run( + "CREATE (:Vertex {id: \$id, data: \$data})", + Values.parameters("id", vertexId, "data", vertexData) + ) + } + + private fun addEdge(verticesId: Pair, weight: Int?, transaction: Transaction) { + transaction.run( + "MATCH (v1:Vertex {id:\$id1}) MATCH (v2:Vertex {id:\$id2}) " + + "MERGE (v1)-[r:EDGE${if (weight != null) " {weight: \$weight}" else ""}]->(v2)", + Values.parameters("id1", verticesId.first, "id2", verticesId.second, "weight", weight) + ) + } + + /** Saves the graph in Neo4j, in case of an error, returns a + * string with error information, otherwise null */ + fun saveGraph(graph: Graph): String? { + val typeGraph = if (graph is DirectedGraph) "DIRECTED" else "UNDIRECTED" + val transaction = session.beginTransaction() + try { + transaction.run("MATCH (n) DETACH DELETE n") + for ((id, vertex) in graph.vertices) { + addVertex(id, vertex.data, transaction) + } + for (edge in graph.edges) { + addEdge(edge.vertices, edge.weight, transaction) + } + transaction.run("CREATE (g:Graph {type: '${typeGraph}'})") + transaction.commit() + return null + } catch (e: Exception) { + transaction.rollback() + return "Failed to save graph:\n" + + (e.message?.substringAfter("Exception: ") ?: "incorrect graph saving") + } finally { + transaction.close() + } + } + + /** The function loads the graph from neo4j, if unsuccessful, returns an error message. + * @return pair graph (Graph?) and error string (String?), if loading is successful, + * returns graph and null string, if error returns null graph and error string + */ + fun loadGraph(): Pair { + val graph: Graph + val transaction = session.beginTransaction() + try { + val graphTypeResult = transaction.run("MATCH (g:Graph) RETURN g.type AS type LIMIT 1") + val graphType = if (graphTypeResult.hasNext()) { + val record = graphTypeResult.next() + record.get("type").asString() + } else { + return (null to "Graph type not found in the database") + } + graph = when (graphType) { + "DIRECTED" -> DirectedGraph() + "UNDIRECTED" -> UndirectedGraph() + else -> return (null to "Unknown graph type: $graphType") + } + val verticesResult = transaction.run("MATCH (v:Vertex) RETURN v.id AS id, v.data AS data") + while (verticesResult.hasNext()) { + val record = verticesResult.next() + val id = record.get("id").asInt() + val data = record.get("data").asString() + graph.addVertex(id, data) + } + val edgesResult = transaction.run( + "MATCH (v1:Vertex)-[r:EDGE]->(v2:Vertex) " + + "RETURN v1.id AS id1, v2.id AS id2, r.weight AS weight" + ) + while (edgesResult.hasNext()) { + val record = edgesResult.next() + val id1 = record.get("id1").asInt() + val id2 = record.get("id2").asInt() + val weight = if (record.containsKey("weight")) record.get("weight").asInt() else null + graph.addEdge(Pair(id1, id2), weight) + } + transaction.commit() + return (graph to null) + } catch (e: Exception) { + transaction.rollback() + return null to "Failed to load graph:\n" + + (e.message?.substringAfter("Exception: ") ?: "incorrect graph loading") + } finally { + transaction.close() + } + } + + override fun close() { + session.close() + driver.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/algorithms/BellmanFord.kt b/src/main/kotlin/model/algorithms/BellmanFord.kt new file mode 100644 index 0000000..825d9e7 --- /dev/null +++ b/src/main/kotlin/model/algorithms/BellmanFord.kt @@ -0,0 +1,42 @@ +package model.algorithms + +import model.graph.Graph + +class BellmanFord(private val graph: Graph) { + fun findShortestPath(src: Int, dest: Int): Pair>? { + val dist = mutableMapOf().withDefault { Int.MAX_VALUE } + val pred = mutableMapOf() + dist[src] = 0 + + for (i in 1 until graph.vertices.size) { + for (edge in graph.edges) { + val (u, v) = edge.vertices + val weight = edge.weight ?: continue + if (dist.getValue(u) != Int.MAX_VALUE && dist.getValue(u) + weight < dist.getValue(v)) { + dist[v] = dist.getValue(u) + weight + pred[v] = u + } + } + } + + for (edge in graph.edges) { + val (u, v) = edge.vertices + val weight = edge.weight ?: continue + if (dist.getValue(u) != Int.MAX_VALUE && dist.getValue(u) + weight < dist.getValue(v)) { + println("Graph contains negative weight cycle") + return null + } + } + + val path = mutableListOf() + var current: Int? = dest + while (current != null) { + path.add(current) + current = pred[current] + } + path.reverse() + + return if (dist[dest] != Int.MAX_VALUE) Pair(dist[dest]!!, path) else null + } +} + diff --git a/src/main/kotlin/model/algorithms/BridgeFinder.kt b/src/main/kotlin/model/algorithms/BridgeFinder.kt new file mode 100644 index 0000000..7d44233 --- /dev/null +++ b/src/main/kotlin/model/algorithms/BridgeFinder.kt @@ -0,0 +1,51 @@ +package model.algorithms + +import model.graph.Graph + +class BridgeFinder { + private var time = 0 + private val NIL = -1 + + fun findBridges(graph: Graph): List> { + val visited = BooleanArray(graph.vertices.size) + val disc = IntArray(graph.vertices.size) + val low = IntArray(graph.vertices.size) + val parent = IntArray(graph.vertices.size) + val bridges = mutableListOf>() + + for (i in 0 until graph.vertices.size) { + parent[i] = NIL + visited[i] = false + } + + for (i in 0 until graph.vertices.size) { + if (!visited[i]) { + bridgeUtil(i, visited, disc, low, parent, graph, bridges) + } + } + + return bridges + } + + private fun bridgeUtil(u: Int, visited: BooleanArray, disc: IntArray, low: IntArray, parent: IntArray, graph: Graph, bridges: MutableList>) { + visited[u] = true + disc[u] = ++time + low[u] = time + + val neighbors = graph.adjacency[u] ?: return + for (v in neighbors.keys) { + if (!visited[v]) { + parent[v] = u + bridgeUtil(v, visited, disc, low, parent, graph, bridges) + + low[u] = minOf(low[u], low[v]) + + if (low[v] > disc[u]) { + bridges.add(Pair(u, v)) + } + } else if (v != parent[u]) { + low[u] = minOf(low[u], disc[v]) + } + } + } +} diff --git a/src/main/kotlin/model/algorithms/CycleSearch.kt b/src/main/kotlin/model/algorithms/CycleSearch.kt new file mode 100644 index 0000000..6ade7ad --- /dev/null +++ b/src/main/kotlin/model/algorithms/CycleSearch.kt @@ -0,0 +1,163 @@ +package model.algorithms + +import model.graph.Edge +import model.graph.UndirectedGraph +import model.graph.Vertex + +/** + * The class [CycleSearch] implements the algorithm for finding a cycle around + * selected vertex of the undirected graph. + * + * At the same time, the algorithm has some minimization of the desired cycle + * by iterating through possible pairs of neighbors of the selected vertex + * through which the desired cycle will pass. + * @property [graph] a undirected graph for whose vertex we will search for a cycle + * @constructor Creates a graph, based on [graph],for which it will be possible to apply + * a cycle search algorithm around the vertex selected in it. + */ + +class CycleSearch(private val graph: UndirectedGraph) { + + private var cycleIsFound: Boolean = false + /** + * This auxiliary function for the function [findAnyCycle] aims to get from + * some hashmap with extra elements the hashmap the elements of which + * accurately describe found cycle path. + * + * @param cyclePath hashmap with extra elements + * @param cycleVertexId the index of the vertex around which the cycle must be found + * @return cycle path + * @receiver private fun [findAnyCycle] + */ + private fun getCyclePath(cyclePath: HashMap, cycleVertexId: Int): HashMap { + + var current: Int = cycleVertexId + var next: Int + val returnCyclePath = HashMap() + do { + next = cyclePath.filter { it.key == current }.values.first() + returnCyclePath[current] = next + current = next + } while (current != cycleVertexId) + + return returnCyclePath + + } + + /**Searches for a cycle by dfs algorithm and writes it into [cyclePath] + * + * + * @param cycleVertexId the index of the vertex around which the cycle must be found + * @param currentVertexId using for recording the [cyclePath] + * @param visited stores information about which vertices of the graph + * have already been processed by [dfs] + * @param cyclePath hashmap consisting of elements that can be used to restore the found cycle path + * by applying the auxiliary function [getCyclePath] to it in function [findAnyCycle]. + * @receiver private fun [findAnyCycle] + */ + private fun dfs( + cycleVertexId: Int, + currentVertexId: Int, + visited: HashMap, + cyclePath: HashMap + ) { + + for (idAdjacency in graph.adjacency[currentVertexId]!!.keys) { + + if (cycleIsFound){ + return + } + if (visited[idAdjacency] == false) { + visited[idAdjacency] = true + cyclePath[currentVertexId] = idAdjacency + dfs(cycleVertexId, idAdjacency, visited, cyclePath) + } else if (idAdjacency == cycleVertexId && cyclePath[cycleVertexId] != currentVertexId) { + cyclePath[currentVertexId] = idAdjacency + cycleIsFound = true + return + } + } + } + + /** Finds any existing cycle in the graph around a given vertex + * + * @param vertexId the index of the vertex around which the cycle must be found + * @return found cycle path or null if the cycle doesn't exist around vertex with id = [vertexId] + * @receiver fun [findCycle] + */ + private fun findAnyCycle(vertexId: Int): HashMap? { + + val devCyclePath = HashMap() + val visited = hashMapOf() + for (idVertex in graph.vertices.keys) { + visited[idVertex] = false + } + visited[vertexId] = true + dfs(vertexId, vertexId, visited, devCyclePath) + cycleIsFound = false + + if (devCyclePath.filter { it.value == vertexId }.isEmpty()) { + return null + } + return getCyclePath(devCyclePath, vertexId) + + } + + /** + * The function implements the algorithm for finding a cycle around + * vertex with id = [vertex] of the undirected graph. + * + * The function searches for a cycle and performs some minimization of it. + * During the cycle minimization we iterate through variants of cycle, + * based on the choice of a pair of neighbors of [vertex] through which the cycle will pass. + * Each of the found cycle variants is written to a variable 'currentCyclePath' + * and its size compared with 'minCycleSize'. + * @param vertex the vertex around which the cycle must be found + * @return cycle path or null if it doesn't exist + */ + fun findCycle(vertex: Vertex): UndirectedGraph? { + + var returnCyclePath = hashMapOf() + var currentCyclePath: HashMap? + var minCycleSize: Int = Int.MAX_VALUE + val returnGraph = UndirectedGraph() + if (graph.adjacency[vertex.id]!!.size < 2) { + return null + } else { // we consider all possible cases of a cycle by choosing a pair of neighbors to minimize the found cycle + val adjacencyOfVertex: MutableList = arrayListOf() + graph.adjacency[vertex.id]!!.keys.forEach { adjacencyOfVertex.add(it) } + val adjacencyWas: MutableList = arrayListOf() + val removedEdges: MutableList = arrayListOf() + for (firstAdjacency in adjacencyOfVertex) { + adjacencyWas.add(firstAdjacency) + for (secondAdjacency in adjacencyOfVertex.filter { it !in adjacencyWas && it != vertex.id }) { + + for (adjacency in graph.adjacency[vertex.id]!!.filter { it.key != firstAdjacency && it.key != secondAdjacency }) { + removedEdges.add(Edge(vertex.id to adjacency.key, adjacency.value)) + graph.removeEdge(vertex.id to adjacency.key) + } + + currentCyclePath = findAnyCycle(vertex.id) + if (currentCyclePath != null && currentCyclePath.size < minCycleSize) { + minCycleSize = currentCyclePath.size + returnCyclePath = currentCyclePath + } + + for (edge in removedEdges) { + graph.addEdge(edge.vertices, edge.weight) + } + removedEdges.clear() + } + } + } + + if (minCycleSize == Int.MAX_VALUE) { + return null + } + returnCyclePath.forEach { returnGraph.addVertex(it.key, graph.vertices[it.key]!!.data) } + returnCyclePath.forEach { returnGraph.addEdge(it.key to it.value, graph.adjacency[it.key]!![it.value]) } + return returnGraph + + } + +} diff --git a/src/main/kotlin/model/algorithms/Dijkstra.kt b/src/main/kotlin/model/algorithms/Dijkstra.kt new file mode 100644 index 0000000..3e959c9 --- /dev/null +++ b/src/main/kotlin/model/algorithms/Dijkstra.kt @@ -0,0 +1,64 @@ +package model.algorithms + +import model.graph.Graph +import java.util.PriorityQueue + +/** + * The Dijkstra class is used to find the shortest paths in a weighted graph using Dijkstra's algorithm. + * + * @property graph A directed graph in which shortest paths are searched. + * @property distances Stores the lengths of the shortest paths from the starting vertex to other vertices. + * @property previousVertices Stores previous vertices along the shortest path to each vertex. + * @property queue Queue for processing vertices in order of increasing distances. + */ +class Dijkstra(private val graph: Graph) { + private val distances = hashMapOf() + private val previousVertices = hashMapOf() + private val queue = PriorityQueue>(compareBy { it.second }) + + /** + * Finds the shortest path from one vertex (start) to another vertex (end) and returns a list of id vertices of the path. + * + * @param start The starting vertex from which to start searching for the shortest path. + * @param end The end vertex to which the shortest path is sought. + * @return On success List of vertices that form the shortest path from the beginning of the vertex + * to the end of the vertex. In case of error, the string with the error. + */ + fun findShortestPaths(start: Int, end: Int): Pair?, String?> { + if (!graph.vertices.contains(start) || !graph.vertices.contains(end)) { + return (null to "The vertex doesn't exist in the graph.") + } + distances[start] = 0 + queue.add(Pair(start, 0)) + + while (queue.isNotEmpty()) { + val (currentVertex, currentDistance) = queue.poll() + if (currentVertex == end) break + graph.adjacency[currentVertex]?.forEach { (neighbor, weight) -> + if (weight == null) { + return (null to "Edge without weights in Dijkstra's algorithm.") + } else if (weight < 0) { + return (null to "Edge with negative weights in Dijkstra's algorithm.") + } else { + val newDistance = currentDistance + weight + if (newDistance < (distances[neighbor] ?: Int.MAX_VALUE)) { + distances[neighbor] = newDistance + previousVertices[neighbor] = currentVertex + queue.add(Pair(neighbor, newDistance)) + } + } + } + } + // construct final path using previous vertices + val path = mutableListOf() + previousVertices[end] ?: return (path to null) + var current = end + while (current != start) { + path.add(current) + current = previousVertices[current] ?: break + } + path.add(start) + path.reverse() + return (path to null) + } +} diff --git a/src/main/kotlin/model/algorithms/HarmonicCentrality.kt b/src/main/kotlin/model/algorithms/HarmonicCentrality.kt new file mode 100644 index 0000000..83c3356 --- /dev/null +++ b/src/main/kotlin/model/algorithms/HarmonicCentrality.kt @@ -0,0 +1,89 @@ +package model.algorithms + +import model.graph.DirectedGraph +import model.graph.Graph +import model.graph.UndirectedGraph +import kotlin.math.roundToInt + +/** The class [HarmonicCentrality] implements the normalized harmonic centrality algorithm for + * a graph of any kind. For implementation using the Dijkstra algorithm. + * + * Harmonic centrality is a kind of closeness centrality, but the difference is that + * this algorithm is relevant not only for connected graphs, but also for disconnected ones. + * The harmonic centrality index for each vertex of the graph is calculated using the formula: + * Index = sum(1/the length of the shortest path to i-th vertex), + * for i in 1..n-1, where 'n' is amount of graph vertices. + * So that the index value lies in the interval from 0 to 1, we use the normalization of the centrality index + * by dividing the result by n, where 'n' is amount of graph vertices: + * Index = sum(1/the length of the shortest path to i-th vertex) / n, + * for i in 1..n-1, where 'n' is amount of graph vertices. + * In this case the length of the path is the amount of edges of this path. + * + * @property [graph] a graph (of any kind) for the vertices of which it is necessary to calculate the centrality index + * @constructor Creates a graph, based on [graph], for which the algorithm for calculating the + * normalized harmonic centrality can be applied. + */ + +class HarmonicCentrality(private val graph: Graph) { + + /** + * This auxiliary function for the function [getIndex], that rounds + * the value [number] to the 4th digit after the decimal point. + * + * @return result of rounding + * @receiver [getIndex] + */ + private fun roundTo(number: Double): Double { + return (number * 10000.0).roundToInt() / 10000.0 + } + + /** + * This function calculate the centrality index for vertex with id = [vertexId]. + * + * @param graph graph for the vertex of which we calculate the centrality index. + * But weight of each graph edge is 1, what is used for applying Dijkstra algorithm. + * @param vertexId the index of the vertex for which calculating the centrality index. + * @return centrality index of vertex with id = [vertexId] + * @receiver [harmonicCentrality] + */ + private fun getIndex(graph: Graph, vertexId: Int): Double { + + var index = 0.00 + + graph.vertices.filter { it.key != vertexId } + .forEach { + if (Dijkstra(graph).findShortestPaths(vertexId, it.key).first!!.isNotEmpty()) { + index += 1.0 / ((Dijkstra(graph).findShortestPaths(vertexId, it.key)).first!!.size - 1) + } + } + + return roundTo(index / (graph.vertices.size - 1)) + + } + + /** + * This function calculates the centrality index for each graph vertex. + * + * @return hashmap, where key = vertex id; value = centrality index. + */ + fun harmonicCentrality(): HashMap { + + val centralityIndexes = HashMap() + + val graphForCentrality: Graph = if (graph is UndirectedGraph) { + UndirectedGraph() + } else { + DirectedGraph() + } + graph.vertices.forEach { graphForCentrality.addVertex(it.key, it.value.data) } + graph.edges.forEach { graphForCentrality.addEdge(it.vertices, 1) } + + for (vertexId in graphForCentrality.vertices.keys) { + centralityIndexes[vertexId] = getIndex(graphForCentrality, vertexId) + } + + return centralityIndexes + + } + +} diff --git a/src/main/kotlin/model/algorithms/Kosaraju.kt b/src/main/kotlin/model/algorithms/Kosaraju.kt new file mode 100644 index 0000000..8703faa --- /dev/null +++ b/src/main/kotlin/model/algorithms/Kosaraju.kt @@ -0,0 +1,71 @@ +package model.algorithms + +import model.graph.DirectedGraph + +/** + * The Kosaraju class implements the Kosaraju algorithm for finding strongly connected + * components (scc) in a directed graph. + * @property graph is a directed graph in which we are looking for scc + * @property visited map storing information about visited vertices + * @property stack a stack for storing vertices in the order in which they were traversed + * @property stronglyConnectedComponents list of strongly connected components in graph + */ +class Kosaraju(private val graph: DirectedGraph) { + private val visited = hashMapOf() + private val stack = mutableListOf() + private val stronglyConnectedComponents = mutableListOf>() + + /** + * The findStronglyConnectedComponents method runs an algorithm to find a scc. + * @return list of graph components + */ + fun findStronglyConnectedComponents(): List> { + for (vertexID in graph.vertices.keys) { + if (visited[vertexID] != true) { + dfs(vertexID, stack, graph) + } + } + val transposedGraph = transposeGraph() + visited.replaceAll { _, _ -> false } + while (stack.isNotEmpty()) { + val vertexID = stack.removeAt(stack.size - 1) + if (visited[vertexID] != true) { + val component = mutableListOf() + dfs(vertexID, component, transposedGraph) + stronglyConnectedComponents.add(component) + } + } + return stronglyConnectedComponents + } + + /** + * The dfs method performs a depth-first traversal of the graph, starting from a given vertex. + * @param vertexID identifier of the vertex from which the traversal begins + * @param stack for storing vertices in traversed order + * @param graph graph in which the traversal occurs + */ + private fun dfs(vertexID: Int, stack: MutableList, graph: DirectedGraph) { + visited[vertexID] = true + for (nextVertexID in (graph.adjacency[vertexID]?.keys ?: emptyList())) + if (visited[nextVertexID] != true) { + dfs(nextVertexID, stack, graph) + } + stack.add(vertexID) + } + + /** + * The transposeGraph method transposes the graph. + * @return the transposed graph + */ + private fun transposeGraph(): DirectedGraph { + val transposedGraph = DirectedGraph() + for ((id, vertex) in graph.vertices) { + transposedGraph.addVertex(id, vertex.data) + } + for (edge in graph.edges) { + val (firstVertexID, secondVertexID) = edge.vertices + transposedGraph.addEdge(Pair(secondVertexID, firstVertexID), edge.weight) + } + return transposedGraph + } +} diff --git a/src/main/kotlin/model/algorithms/Louvain.kt b/src/main/kotlin/model/algorithms/Louvain.kt new file mode 100644 index 0000000..7cd806f --- /dev/null +++ b/src/main/kotlin/model/algorithms/Louvain.kt @@ -0,0 +1,78 @@ +package model.algorithms + +import model.graph.Graph + +/** + * Louvain algorithm for detecting communities in a graph. + * @param graph The graph in which communities should be discovered. + */ +class Louvain(private val graph: Graph) { + + /** + * Method for detecting communities in a graph using the Louvain algorithm. + * @return A list of vertex sets representing the detected communities. + */ + fun detectCommunities(): List> { + // each vertex starts in its own classer + val communities = graph.vertices.keys.associateWith { it }.toMutableMap() + var bestCommunities = communities.toMap() + var bestModularity = calculateModularity(bestCommunities) + var changed: Boolean + while (true) { // until modularity stops increasing + changed = false + // all vertices of the network are enumerated, and each vertex tries to move to the classer + // with a maximum increase in modularity with such movement + for (vertex in graph.vertices.keys) { + for (community in communities.keys) { + if (vertex != community && communities[vertex] != community) { + val newCommunities = bestCommunities.toMutableMap() + newCommunities[vertex] = communities[community]!! + val newModularity = calculateModularity(newCommunities) + + if (newModularity > bestModularity) { + bestModularity = newModularity + bestCommunities = newCommunities + communities[vertex] = communities[community]!! + changed = true + break + } + } + } + if (changed) break + } + if (!changed) break + } + val resultCommunities = mutableListOf>() + for (resultCommunity in bestCommunities.values.toSet()) { + val verticesOfCommunity = mutableSetOf() + for ((vertex, community) in bestCommunities) { + if (community == resultCommunity) + verticesOfCommunity.add(vertex) + } + resultCommunities.add(verticesOfCommunity) + } + return resultCommunities + } + + /** + * Method for calculating community modularity. + * @param communities Communities for which modularity needs to be calculated. + * @return The meaning of modularity for the community. + */ + private fun calculateModularity(communities: Map): Double { + val edgesNum = graph.edges.size.toDouble() + var modularity = 0.0 + + for ((vertex, neighbors) in graph.adjacency) { + for (neighbor in neighbors.keys) { + val delta = if (communities[vertex] == communities[neighbor]) 1.0 else 0.0 + + // calculate modularity taking into account the direction of the edges + val outDegree = graph.adjacency[vertex]!!.size // outgoing vertex degree + val inDegree = neighbors.size // incoming vertex degree + modularity += delta - (outDegree * inDegree) / edgesNum + } + } + return modularity / edgesNum + } +} diff --git a/src/main/kotlin/model/algorithms/Prim.kt b/src/main/kotlin/model/algorithms/Prim.kt new file mode 100644 index 0000000..015d5b0 --- /dev/null +++ b/src/main/kotlin/model/algorithms/Prim.kt @@ -0,0 +1,167 @@ +package model.algorithms + +import model.graph.UndirectedGraph + +/** + * The class [Prim] implements the Prim's algorithm for construction Minimum spanning tree (MST) from + * undirected weighted graph. + * + * At the same time, if the graph consists of several connectivity components, + * the algorithm constructs a forest, each tree of which is MST. + * + * @property [graph] a undirected weighted graph, whose MST we want to construct + * @constructor Creates a graph, based on [graph], to which the following functions can be applied: + * function [treePrim] : returns MST for [graph] + * function [weightPrim] : returns the weight of received MST + */ + +class Prim(private val graph: UndirectedGraph) { + + /** + * This function implements Prim's algorithm for + * constuction MST for transmitted connectivity component + * + * @param graph a connectivity component, + * @return MST of connectivity component + * @receiver fun [treePrim] + */ + private fun getMST(graph: UndirectedGraph): UndirectedGraph { + + val graphMst = UndirectedGraph() + + for (vertex in graph.vertices.keys) { + graphMst.addVertex(vertex, graph.vertices[vertex]!!.data) + } + + val priorityQueue = hashMapOf>() + for (idVertex in graph.vertices.keys) { + priorityQueue[idVertex] = Int.MAX_VALUE to null + } + + //init Prim's algorithm + + val rootIdVertex: Int = graph.vertices.keys.first() + for (idAdjacency in graph.adjacency[rootIdVertex]!!.keys) { // неправильные соседи + priorityQueue[idAdjacency] = graph.adjacency[rootIdVertex]!![idAdjacency]!! to rootIdVertex + } + priorityQueue.remove(rootIdVertex) + + //Prim's algorithm + + var fromQueuePrioritet: Int + var fromQueueId: Int? = null + while (priorityQueue.isNotEmpty()) { + + fromQueuePrioritet = Int.MAX_VALUE + for (element in priorityQueue) { + if (element.value.first <= fromQueuePrioritet) { + fromQueuePrioritet = element.value.first + fromQueueId = element.key + } + } + + for (adjacency in graph.adjacency[fromQueueId]!!.filter { it.key in priorityQueue.keys }) { + if (graph.adjacency[fromQueueId]!![adjacency.key]!! < priorityQueue[adjacency.key]!!.first) { + priorityQueue.remove(adjacency.key) + priorityQueue[adjacency.key] = graph.adjacency[fromQueueId]!![adjacency.key]!! to fromQueueId + } + } + + graphMst.addEdge(fromQueueId!! to priorityQueue[fromQueueId]!!.second!!, fromQueuePrioritet) + priorityQueue.remove(fromQueueId) + + } + + return graphMst + + } + + /** + * The function finds the connectivity component + * and returns it to apply the Prim's algorithm to it + * + * @param idVertex init vertex for dfs + * @param visited stores information about which vertices of the graph + * have already been processed by the Prim's algorithm + * @param component found connectivity component of the graph + * @return found connectivity component + * @receiver fun [treePrim] + */ + private fun dfs( + idVertex: Int, + visited: HashMap, + component: UndirectedGraph + ): UndirectedGraph { + + visited[idVertex] = true + for (idAdjacency in graph.adjacency[idVertex]!!.keys) { + if (visited[idAdjacency] == false) { + visited[idAdjacency] = true + component.addVertex(idAdjacency, graph.vertices[idAdjacency]!!.data) + dfs(idAdjacency, visited, component) + } + } + for (edge in this.graph.edges) { + if (edge.vertices.first in component.vertices && edge.vertices.second in component.vertices && edge !in component.edges) { + component.addEdge(edge.vertices, edge.weight) + } + } + return component + } + + /** + * The function finds the connectivity components of the graph + * and then builds MST for each of them + * + * @return list of MST for each connectivity component of graph + * @receiver fun [weightPrim] + */ + fun treePrim(): MutableList { + + val returnListOfMST = mutableListOf() + + val visited = hashMapOf() + for (vertexId in graph.vertices.keys) { + visited[vertexId] = false + } + + var initIndex: Int + var component: UndirectedGraph + while (visited.values.contains(false)) { + + initIndex = visited.filterValues { !it }.keys.first() + component = UndirectedGraph() + component.addVertex(initIndex, graph.vertices[initIndex]!!.data) + component = dfs(initIndex, visited, component) + returnListOfMST.add(getMST(component)) + + } + + return returnListOfMST + + } + + /** + * Counts the weight of received MST + * + * @return weight of MST + */ + fun weightPrim(): Int { + + var treeWeight: Int = 0 + for (element in treePrim()) { + for (edge in element.edges) { + + if (edge.weight != null) { + treeWeight += edge.weight!! + } + + } + + } + + return treeWeight / 2 + + } + +} diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt new file mode 100644 index 0000000..86afc56 --- /dev/null +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -0,0 +1,55 @@ +package model.graph + +class DirectedGraph() : Graph() { + override fun addEdge(v: Pair, w: Int?): String? { + if (!vertices.containsKey(v.first)) { + return "Vertex with id: ${v.first} not exists in the graph." + } + if (!vertices.containsKey(v.second)) { + return "Vertex with id: ${v.second} not exists in the graph." + } + if (v.first == v.second) { + return "Can't add edge from vertex to itself." + } + edges.add(Edge(Pair(v.first, v.second), w)) + adjacency.getOrPut(v.first) { hashMapOf() }[v.second] = w + return null + } + + override fun removeEdge(v: Pair): String? { + + if (!vertices.containsKey(v.first)) { + return "Vertex with id: ${v.first} not exists in the graph." + } + if (!vertices.containsKey(v.second)) { + return "Vertex with id: ${v.second} not exists in the graph." + } + if (v.second !in adjacency[v.first]!!.keys) { + return "Edge from ${v.first} to ${v.second} not exists in the graph." + } + edges.find { it.vertices == Pair(v.first, v.second) }?.let { edge -> + edges.remove(edge) + } + adjacency[v.first]!!.remove(v.second) + return null + } + + override fun removeVertex(id: Int): String? { + if (!vertices.containsKey(id)) { + return "Vertex with id: $id doesn't exist in the graph." + } + if (adjacency[id]!!.isNotEmpty()) { + for (idAdjacency in adjacency[id]!!.keys) { + removeEdge(id to idAdjacency) + } + } + if (adjacency.filterValues { it.keys.contains(id) }.isNotEmpty()) { + for (adj in adjacency.filterValues { it.keys.contains(id) }) { + removeEdge(adj.key to id) + } + } + vertices.remove(id) + return null + } + +} diff --git a/src/main/kotlin/model/graph/Edge.kt b/src/main/kotlin/model/graph/Edge.kt new file mode 100644 index 0000000..ddc9278 --- /dev/null +++ b/src/main/kotlin/model/graph/Edge.kt @@ -0,0 +1,4 @@ +package model.graph + +data class Edge(val vertices: Pair, var weight: Int?) { +} diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt new file mode 100644 index 0000000..9ada71e --- /dev/null +++ b/src/main/kotlin/model/graph/Graph.kt @@ -0,0 +1,21 @@ +package model.graph + +abstract class Graph { + val adjacency = hashMapOf>() + val vertices = hashMapOf() + val edges = mutableListOf() + + fun getVertices(): Collection = vertices.values + abstract fun addEdge(v: Pair, w: Int?): String? + abstract fun removeEdge(v: Pair): String? + abstract fun removeVertex(id: Int): String? + fun addVertex(id: Int, data: String): String? { + if (vertices.containsKey(id)) { + return "Vertex with id: $id already exists in the graph." + } + vertices[id] = Vertex(id, data) + adjacency[id] = hashMapOf() + return null + } + +} diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt new file mode 100644 index 0000000..f8e58ac --- /dev/null +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -0,0 +1,56 @@ +package model.graph + +class UndirectedGraph() : Graph() { + override fun addEdge(v: Pair, w: Int?): String? { + if (!vertices.containsKey(v.first)) { + return "Vertex with id: ${v.first} not exists in the graph." + } + if (!vertices.containsKey(v.second)) { + return "Vertex with id: ${v.second} not exists in the graph." + } + if (v.first == v.second) { + return "Can't add edge from vertex to itself." + } + edges.add(Edge(Pair(v.first, v.second), w)) + edges.add(Edge(Pair(v.second, v.first), w)) + adjacency.getOrPut(v.first) { hashMapOf() }[v.second] = w + adjacency.getOrPut(v.second) { hashMapOf() }[v.first] = w + return null + } + + override fun removeEdge(v: Pair): String? { + + if (!vertices.containsKey(v.first)) { + return "Vertex with id: ${v.first} not exists in the graph." + } + if (!vertices.containsKey(v.second)) { + return "Vertex with id: ${v.second} not exists in the graph." + } + if (v.second !in adjacency[v.first]!!.keys) { + return "Edge from ${v.first} to ${v.second} not exists in the graph." + } + edges.find { it.vertices == Pair(v.first, v.second) }?.let { edge -> + edges.remove(edge) + } + edges.find { it.vertices == Pair(v.second, v.first) }?.let { edge -> + edges.remove(edge) + } + adjacency[v.first]!!.remove(v.second) + adjacency[v.second]!!.remove(v.first) + return null + } + + override fun removeVertex(id: Int): String? { + if (!vertices.containsKey(id)) { + return "Vertex with id: $id doesn't exist in the graph." + } + if (adjacency[id]!!.isNotEmpty()) { + for (idAdjacency in adjacency[id]!!.keys) { + removeEdge(id to idAdjacency) + } + } + vertices.remove(id) + return null + } + +} diff --git a/src/main/kotlin/model/graph/Vertex.kt b/src/main/kotlin/model/graph/Vertex.kt new file mode 100644 index 0000000..a4cca61 --- /dev/null +++ b/src/main/kotlin/model/graph/Vertex.kt @@ -0,0 +1,4 @@ +package model.graph + +data class Vertex(var id: Int, var data: String) { +} diff --git a/src/main/kotlin/view/Color.kt b/src/main/kotlin/view/Color.kt new file mode 100644 index 0000000..7bdb13b --- /dev/null +++ b/src/main/kotlin/view/Color.kt @@ -0,0 +1,38 @@ +package view + +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +val DarkRed00 = Color(125, 21, 21) +val DarkRed01 = Color(41, 28, 28) +val DarkRed02 = Color(189, 0, 0) +val LightRed = Color(204, 0, 0) +val LightGray = Color(199, 199, 199) +val DarkGray = Color(74, 74, 74) +val Gray = Color(150, 150, 150) +val White = Color(245, 245, 245) +val Black = Color(0, 0, 0) + +val NastyaColorPalette = lightColorScheme( +) + +val KatyaColorPalette = lightColorScheme( + primary = DarkRed00, + onPrimary = White, + primaryContainer = LightGray, + onPrimaryContainer = Black, + secondary = DarkGray, + onSecondary = White, + error = LightRed, + onError = Black, + background = LightGray, + onBackground = DarkGray, + surface = Gray, + onSurface = DarkRed01, + outline = DarkRed01, + errorContainer = DarkRed02, + onErrorContainer = Black, +) + +val LiyaColorPalette = lightColorScheme( +) \ No newline at end of file diff --git a/src/main/kotlin/view/CommonDialogs.kt b/src/main/kotlin/view/CommonDialogs.kt new file mode 100644 index 0000000..2da988b --- /dev/null +++ b/src/main/kotlin/view/CommonDialogs.kt @@ -0,0 +1,497 @@ +package view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun Neo4jDialog(onDismiss: () -> Unit, onRunAlgorithm: (String, String, String) -> Unit) { + var uri by remember { mutableStateOf("") } + var user by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Enter your Neo4j details") + }, + text = { + Column(modifier = Modifier.padding(16.dp)) { + TextField( + value = uri, + onValueChange = { uri = it }, + label = { Text("Uri") }, + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + TextField( + value = user, + onValueChange = { user = it }, + label = { Text("User") }, + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + TextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + } + }, + confirmButton = { + Button( + onClick = { + onRunAlgorithm(uri, user, password) + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary) + ) { + Text("Ok", color = MaterialTheme.colorScheme.onPrimary) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Cancel", color = MaterialTheme.colorScheme.onPrimary) + } + } + ) +} + +@Composable +fun DijkstraDialog(onDismiss: () -> Unit, onRunAlgorithm: (Int, Int) -> Unit) { + var start by remember { mutableStateOf("") } + var end by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Select vertices to find shortest path") + }, + text = { + Column(modifier = Modifier.padding(16.dp)) { + TextField( + value = start, + onValueChange = { start = it }, + label = { Text("Enter the id of the starting vertex:") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + TextField( + value = end, + onValueChange = { end = it }, + label = { Text("Enter the id of the destination vertex:") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + } + }, + confirmButton = { + Button( + onClick = { + val startInt = start.toIntOrNull() + val endInt = end.toIntOrNull() + + if (startInt != null && endInt != null) { + onRunAlgorithm(startInt, endInt) + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary) + ) { + Text("Find the way", color = MaterialTheme.colorScheme.onPrimary) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Cancel", color = MaterialTheme.colorScheme.onPrimary) + } + } + ) +} + +@Composable +fun CycleSearchDialog(onDismiss: () -> Unit, onRunAlgorithm: (Int) -> Unit) { + var vertexId by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Input vertex id around that you want to find the cycle:") + }, + text = { + Column( + modifier = Modifier.padding(top = 16.dp) + ) { + TextField( + value = vertexId, + onValueChange = { vertexId = it }, + label = { Text("Your vertex id is") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp) + ) + } + }, + confirmButton = { + Button( + onClick = { + val vertexIdInt = vertexId.toIntOrNull() + + if (vertexIdInt != null) { + onRunAlgorithm(vertexIdInt) + } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = Color(139, 0, 0)) + ) { + Text("Find the cycle", color = Color(255, 250, 250)) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = Color(139, 0, 0)) + ) { + Text("Cancel", color = Color(255, 250, 250)) + } + } + ) +} + +@Composable +fun AddVertexDialog(onDismiss: () -> Unit, onRunAlgorithm: (Int, String) -> Unit) { + var id by remember { mutableStateOf("") } + var data by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Enter id and data vertex") + }, + text = { + Column(modifier = Modifier.padding(16.dp)) { + TextField( + value = id, + onValueChange = { id = it }, + label = { Text("Enter the id:") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + TextField( + value = data, + onValueChange = { data = it }, + label = { Text("Enter the data:") }, + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + } + }, + confirmButton = { + Button( + onClick = { + val idInt = id.toIntOrNull() + + if (idInt != null) { + onRunAlgorithm(idInt, data) + } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Add the vertex", color = MaterialTheme.colorScheme.onPrimary) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Cancel", color = MaterialTheme.colorScheme.onPrimary) + } + } + ) +} + +@Composable +fun RemoveVertexDialog(onDismiss: () -> Unit, onRunAlgorithm: (Int) -> Unit) { + var id by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Enter id vertex") + }, + text = { + Column(modifier = Modifier.padding(16.dp)) { + TextField( + value = id, + onValueChange = { id = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + } + }, + confirmButton = { + Button( + onClick = { + val idInt = id.toIntOrNull() + + if (idInt != null) { + onRunAlgorithm(idInt) + } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Remove the vertex", color = MaterialTheme.colorScheme.onPrimary) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Cancel", color = MaterialTheme.colorScheme.onPrimary) + } + } + ) +} + +@Composable +fun AddEdgeDialog(onDismiss: () -> Unit, onRunAlgorithm: (Int, Int, Int?) -> Unit) { + var from by remember { mutableStateOf("") } + var to by remember { mutableStateOf("") } + var w by remember { mutableStateOf(null) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Enter edge parameters") + }, + text = { + Column(modifier = Modifier.padding(16.dp)) { + TextField( + value = from, + onValueChange = { from = it }, + label = { Text("Enter the start id:") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + TextField( + value = to, + onValueChange = { to = it }, + label = { Text("Enter the end id:") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + TextField( + value = w ?: "", + onValueChange = { newValue -> + w = newValue.ifEmpty { null } + }, + label = { Text("Enter the weight (optional):") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + } + }, + confirmButton = { + Button( + onClick = { + val fromInt = from.toIntOrNull() + val toInt = to.toIntOrNull() + val wInt = w?.toIntOrNull() + + if (fromInt != null && toInt != null) { + onRunAlgorithm(fromInt, toInt, wInt) + } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Add the edge", color = MaterialTheme.colorScheme.onPrimary) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Cancel", color = MaterialTheme.colorScheme.onPrimary) + } + } + ) +} + +@Composable +fun RemoveEdgeDialog(onDismiss: () -> Unit, onRunAlgorithm: (Int, Int) -> Unit) { + var from by remember { mutableStateOf("") } + var to by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Enter edge parameters") + }, + text = { + Column(modifier = Modifier.padding(16.dp)) { + TextField( + value = from, + onValueChange = { from = it }, + label = { Text("Enter the start id:") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + TextField( + value = to, + onValueChange = { to = it }, + label = { Text("Enter the end id:") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.padding(bottom = 12.dp), + colors = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.secondary, + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.secondary + ) + ) + } + }, + confirmButton = { + Button( + onClick = { + val fromInt = from.toIntOrNull() + val toInt = to.toIntOrNull() + + if (fromInt != null && toInt != null) { + onRunAlgorithm(fromInt, toInt) + } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Remove the edge", color = MaterialTheme.colorScheme.onPrimary) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.primary) + ) { + Text("Cancel", color = MaterialTheme.colorScheme.onPrimary) + } + } + ) +} + +@Composable +fun ErrorDialog(onDismiss: () -> Unit, errorMessage: String) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = "Error:", color = MaterialTheme.colorScheme.errorContainer, + fontSize = 20.sp + ) + }, + text = { Text(text = errorMessage, color = MaterialTheme.colorScheme.onErrorContainer) }, + confirmButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colorScheme.errorContainer) + ) { + Text(text = "ОК", color = MaterialTheme.colorScheme.onErrorContainer) + } + } + ) + +} diff --git a/src/main/kotlin/view/MainScreen.kt b/src/main/kotlin/view/MainScreen.kt new file mode 100644 index 0000000..b495803 --- /dev/null +++ b/src/main/kotlin/view/MainScreen.kt @@ -0,0 +1,389 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.graph.GraphView +import viewmodel.MainScreenViewModel + +@Composable +fun MainScreen(viewModel: MainScreenViewModel) { + var theme by remember { mutableStateOf(Theme.NASTYA) } + var expandedAlgorithmsMenu by remember { mutableStateOf(false) } + var expandedAddMenu by remember { mutableStateOf(false) } + var expandedRemoveMenu by remember { mutableStateOf(false) } + var expandedSaveMenu by remember { mutableStateOf(false) } + + var showDijkstraDialog by remember { mutableStateOf(false) } + var showCycleSearchDialog by remember { mutableStateOf(false) } + var showAddEdgeDialog by remember { mutableStateOf(false) } + var showAddVertexDialog by remember { mutableStateOf(false) } + var showRemoveVertexDialog by remember { mutableStateOf(false) } + var showRemoveEdgeDialog by remember { mutableStateOf(false) } + var showNeo4jDialog by remember { mutableStateOf(false) } + var showErrorDialog by remember { mutableStateOf(false) } + var errorMessage: String? = null + fun catchError(messageOfError: String?) { + if (messageOfError != null) { + errorMessage = messageOfError + showErrorDialog = true + } + } + + Material3AppTheme(theme) { + Row { + Column(modifier = Modifier.width(270.dp).fillMaxHeight().background(MaterialTheme.colorScheme.surface)) { + Row { + Checkbox( + checked = viewModel.showVerticesLabels.value, onCheckedChange = { + viewModel.showVerticesLabels.value = it + }, colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.primary + ) + ) + Text( + text = "Show vertices data", + fontSize = 18.sp, + modifier = Modifier.padding(10.dp), + color = MaterialTheme.colorScheme.onSurface + ) + } + Row { + Checkbox( + checked = viewModel.showVerticesId.value, onCheckedChange = { + viewModel.showVerticesId.value = it + }, colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.primary + ) + ) + Text( + text = "Show vertices id", + fontSize = 18.sp, + modifier = Modifier.padding(10.dp), + color = MaterialTheme.colorScheme.onSurface + ) + } + Row { + Checkbox( + checked = viewModel.showEdgesLabels.value, onCheckedChange = { + viewModel.showEdgesLabels.value = it + }, colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.primary + ) + ) + Text( + text = "Show edges weight", + fontSize = 18.sp, + modifier = Modifier.padding(10.dp), + color = MaterialTheme.colorScheme.onSurface + ) + } + Button( + onClick = viewModel::resetGraphView, + enabled = true, + modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Text( + text = "Reset default settings", + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + Box(modifier = Modifier.padding(horizontal = 10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { expandedAlgorithmsMenu = true }, colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Algorithm", + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Icon(Icons.Default.ArrowDropDown, contentDescription = "Select algorithm") + } + } + DropdownMenu( + expanded = expandedAlgorithmsMenu, + onDismissRequest = { expandedAlgorithmsMenu = false }, + modifier = Modifier.background(MaterialTheme.colorScheme.secondary) + ) { + DropdownMenuItem(onClick = { + expandedAlgorithmsMenu = false + showDijkstraDialog = true + }) { + Text(text = "Dijkstra", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedAlgorithmsMenu = false + catchError(viewModel.runKosarajuAlgorithm()) + }) { + Text(text = "Kosaraju", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedAlgorithmsMenu = false + viewModel.runLouvainAlgorithm() + }) { + Text(text = "Louvain", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedAlgorithmsMenu = false + catchError(viewModel.runPrimAlgorithm()) + }) { + Text(text = "Prim", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedAlgorithmsMenu = false + showCycleSearchDialog = true + }) { + Text(text = "CycleSearch", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedAlgorithmsMenu = false + viewModel.runHarmonicCentralityAlgorithm() + }) { + Text(text = "HarmonicCentrality", color = MaterialTheme.colorScheme.onSecondary) + } + } + } + Row(modifier = Modifier.padding(horizontal = 10.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Box(modifier = Modifier.weight(1f).padding(end = 2.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { expandedAddMenu = true }, colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Add", fontSize = 18.sp, color = MaterialTheme.colorScheme.onPrimary) + Icon(Icons.Default.ArrowDropDown, contentDescription = "Add") + } + } + DropdownMenu( + expanded = expandedAddMenu, + onDismissRequest = { expandedAddMenu = false }, + modifier = Modifier.background(MaterialTheme.colorScheme.secondary) + ) { + DropdownMenuItem(onClick = { + expandedAddMenu = false + showAddVertexDialog = true + }) { + Text("Add vertex", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedAddMenu = false + showAddEdgeDialog = true + }) { + Text("Add edge", color = MaterialTheme.colorScheme.onSecondary) + } + } + } + Box(modifier = Modifier.weight(1f).padding(end = 2.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { expandedRemoveMenu = true }, colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Remove", fontSize = 18.sp, color = MaterialTheme.colorScheme.onPrimary) + Icon(Icons.Default.ArrowDropDown, contentDescription = "Remove") + } + } + DropdownMenu( + expanded = expandedRemoveMenu, + onDismissRequest = { expandedRemoveMenu = false }, + modifier = Modifier.background(MaterialTheme.colorScheme.secondary) + ) { + DropdownMenuItem(onClick = { + expandedRemoveMenu = false + showRemoveVertexDialog = true + }) { + Text("Remove vertex", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedRemoveMenu = false + showRemoveEdgeDialog = true + }) { + Text("Remove edge", color = MaterialTheme.colorScheme.onSecondary) + } + } + } + } + Box(modifier = Modifier.padding(horizontal = 10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { expandedSaveMenu = true }, colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Save graph", fontSize = 18.sp, color = MaterialTheme.colorScheme.onPrimary) + Icon(Icons.Default.ArrowDropDown, contentDescription = "Save graph") + } + } + DropdownMenu( + expanded = expandedSaveMenu, + onDismissRequest = { expandedSaveMenu = false }, + modifier = Modifier.background(MaterialTheme.colorScheme.secondary) + ) { + DropdownMenuItem(onClick = { + expandedSaveMenu = false + showNeo4jDialog = true + }) { + Text("Save to Neo4j", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + expandedSaveMenu = false + catchError(viewModel.saveToFile()) + }) { + Text("Save to json-file", color = MaterialTheme.colorScheme.onSecondary) + } + } + } + } + Surface( + modifier = Modifier.weight(1f).background(MaterialTheme.colorScheme.background), + ) { + Box(modifier = Modifier.fillMaxSize()) { + GraphView(viewModel.graphViewModel) // График занимает всё пространство + + var expandedThemeMenu by remember { mutableStateOf(false) } + + // Кнопка темы + Box( + modifier = Modifier + .align(Alignment.TopEnd) // Выровнено по правому верхнему углу + .padding(16.dp) + ) { + // Кнопка + Button( + onClick = { expandedThemeMenu = true }, + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Text(text = "Theme", fontSize = 18.sp, color = MaterialTheme.colorScheme.onPrimary) + Icon(Icons.Default.ArrowDropDown, contentDescription = "Select theme") + } + // Выпадающее меню + DropdownMenu( + expanded = expandedThemeMenu, + onDismissRequest = { expandedThemeMenu = false }, + modifier = Modifier.background(MaterialTheme.colorScheme.secondary) + ) { + DropdownMenuItem(onClick = { + theme = Theme.NASTYA + expandedThemeMenu = false + }) { + Text("Nastya's theme", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + theme = Theme.LIYA + expandedThemeMenu = false + }) { + Text("Liya's theme", color = MaterialTheme.colorScheme.onSecondary) + } + DropdownMenuItem(onClick = { + theme = Theme.KATYA + expandedThemeMenu = false + }) { + Text("Katya's theme", color = MaterialTheme.colorScheme.onSecondary) + } + } + } + } + } + } + if (showDijkstraDialog) { + DijkstraDialog( + onDismiss = { showDijkstraDialog = false }, + onRunAlgorithm = { start, end -> + catchError(viewModel.runDijkstraAlgorithm(start, end)) + showDijkstraDialog = false + } + ) + } + if (showAddVertexDialog) { + AddVertexDialog( + onDismiss = { showAddVertexDialog = false }, + onRunAlgorithm = { id, data -> + catchError(viewModel.addVertex(id, data)) + showAddVertexDialog = false + } + ) + } + if (showCycleSearchDialog) { + CycleSearchDialog( + onDismiss = { showCycleSearchDialog = false }, + onRunAlgorithm = { vertexId -> + catchError(viewModel.runCycleSearchAlgorithm(vertexId)) + showCycleSearchDialog = false + } + ) + } + if (showRemoveVertexDialog) { + RemoveVertexDialog( + onDismiss = { showRemoveVertexDialog = false }, + onRunAlgorithm = { id -> + catchError(viewModel.removeVertex(id)) + showRemoveVertexDialog = false + } + ) + } + if (showAddEdgeDialog) { + AddEdgeDialog( + onDismiss = { showAddEdgeDialog = false }, + onRunAlgorithm = { from, to, w -> + catchError(viewModel.addEdge(from, to, w)) + showAddEdgeDialog = false + } + ) + } + if (showRemoveEdgeDialog) { + RemoveEdgeDialog( + onDismiss = { showRemoveEdgeDialog = false }, + onRunAlgorithm = { from, to -> + catchError(viewModel.removeEdge(from, to)) + showRemoveEdgeDialog = false + } + ) + } + if (showNeo4jDialog) { + Neo4jDialog( + onDismiss = { showNeo4jDialog = false }, + onRunAlgorithm = { uri, user, password -> + catchError(viewModel.saveToNeo4j(uri, user, password)) + showNeo4jDialog = false + } + ) + } + if (showErrorDialog) { + ErrorDialog( + onDismiss = { showErrorDialog = false }, + errorMessage!! + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/Theme.kt b/src/main/kotlin/view/Theme.kt new file mode 100644 index 0000000..cfa2a53 --- /dev/null +++ b/src/main/kotlin/view/Theme.kt @@ -0,0 +1,21 @@ +package view + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + +@Composable +fun Material3AppTheme(theme: Theme, content: @Composable () -> Unit) { + val colors = when (theme) { + Theme.NASTYA -> NastyaColorPalette + Theme.KATYA -> KatyaColorPalette + else -> LiyaColorPalette + } + MaterialTheme( + colorScheme = colors, + content = content + ) +} + +enum class Theme { + NASTYA, KATYA, LIYA +} \ No newline at end of file diff --git a/src/main/kotlin/view/WelcomeScreen.kt b/src/main/kotlin/view/WelcomeScreen.kt new file mode 100644 index 0000000..42a8f69 --- /dev/null +++ b/src/main/kotlin/view/WelcomeScreen.kt @@ -0,0 +1,30 @@ +package view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Button +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun WelcomeScreen(selectType: (String) -> Unit) { + Surface(modifier = Modifier.fillMaxSize()) { + Column { + Text("Welcome to the Graph Application") + Button(onClick = { selectType("Undirected") }) { + Text("Select Undirected graph") + } + Button(onClick = { selectType("Directed") }) { + Text("Select Directed graph") + } + Button(onClick = { selectType("Neo4j") }) { + Text("Open graph from Neo4j") + } + Button(onClick = { selectType("File") }) { + Text("Open graph from json-file") + } + } + } +} diff --git a/src/main/kotlin/view/graph/EdgeView.kt b/src/main/kotlin/view/graph/EdgeView.kt new file mode 100644 index 0000000..f586bdc --- /dev/null +++ b/src/main/kotlin/view/graph/EdgeView.kt @@ -0,0 +1,79 @@ +package view.graph + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import viewmodel.graph.EdgeViewModel +import kotlin.math.pow +import kotlin.math.sqrt + +@Composable +fun EdgeView( + viewModel: EdgeViewModel, + modifier: Modifier = Modifier, +) { + + fun calculateEdge(): Pair { + val startX = (viewModel.u.x + viewModel.u.radius).value + val startY = (viewModel.u.y + viewModel.u.radius).value + val endX = (viewModel.v.x + viewModel.v.radius).value + val endY = (viewModel.v.y + viewModel.v.radius).value + val returnStart: DpOffset + val returnEnd: DpOffset + + val difXStart: Float + val difYStart: Float + val difXEnd: Float + val difYEnd: Float + val vertexDistance: Float = + sqrt((startX - endX).pow(2) + (startY - endY).pow(2)) + + val coefficientStart: Float = viewModel.u.radius.value / vertexDistance + val coefficientEnd: Float = viewModel.v.radius.value / vertexDistance + difXStart = coefficientStart * (endX - startX) + difYStart = coefficientStart * (endY - startY) + difXEnd = coefficientEnd * (startX - endX) + difYEnd = coefficientEnd * (startY - endY) + + returnStart = DpOffset( + (startX + difXStart).dp, + (startY + difYStart).dp + ) + returnEnd = DpOffset( + (endX + difXEnd).dp, + (endY + difYEnd).dp + ) + return returnStart to returnEnd + } + + Canvas(modifier = modifier.fillMaxSize()) { + drawLine( + start = Offset( + x = calculateEdge().first.x.toPx(), + y = calculateEdge().first.y.toPx() + ), + end = Offset( + x = calculateEdge().second.x.toPx(), + y = calculateEdge().second.y.toPx() + ), + color = viewModel.color, + strokeWidth = viewModel.strokeWidth, + ) + } + if (viewModel.labelVisible) { + Text( + modifier = Modifier + .offset( + viewModel.u.x + (viewModel.v.x - viewModel.u.x) / 2, + viewModel.u.y + (viewModel.v.y - viewModel.u.y) / 2 + ), + text = viewModel.label, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/GraphView.kt b/src/main/kotlin/view/graph/GraphView.kt new file mode 100644 index 0000000..81d21ce --- /dev/null +++ b/src/main/kotlin/view/graph/GraphView.kt @@ -0,0 +1,29 @@ +package view.graph + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import viewmodel.graph.GraphViewModel +import viewmodel.graph.VertexViewModel + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun GraphView( + viewModel: GraphViewModel, +) { + Box(modifier = Modifier + .fillMaxSize() + + ) { + viewModel.verticesView.values.forEach { v -> + VertexView(v, Modifier) + } + viewModel.edgesView.values.forEach { e -> + EdgeView(e, Modifier) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/VertexView.kt b/src/main/kotlin/view/graph/VertexView.kt new file mode 100644 index 0000000..760ceb0 --- /dev/null +++ b/src/main/kotlin/view/graph/VertexView.kt @@ -0,0 +1,61 @@ +package view.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +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, +) { + + fun isColorDark(color: Color): Boolean { + val red = color.red + val green = color.green + val blue = color.blue + val brightness = (red * 299 + green * 587 + blue * 114) / 1000 + return brightness < 0.5 + } + + Box(modifier = modifier + .size(viewModel.radius * 2, viewModel.radius * 2) + .offset(viewModel.x, viewModel.y) + .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), + text = viewModel.label, + color = if (isColorDark(viewModel.color)) Color.White else Color.Black + ) + } + if (viewModel.idVisible) { + Text( + modifier = Modifier + .align(Alignment.Center).offset(0.dp, -viewModel.radius - 10.dp), + text = viewModel.id, + color = Color.Black + ) + } + } +} diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt new file mode 100644 index 0000000..d8bb219 --- /dev/null +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -0,0 +1,217 @@ +package viewmodel + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import databases.FileSystem +import databases.Neo4jRepository +import model.graph.Graph +import viewmodel.graph.GraphViewModel +import viewmodel.graph.RepresentationStrategy +import model.algorithms.* +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import model.graph.Vertex + +var defaultColorLine: Color = Color.Black +var defaultColorVertex: Color = Color.Gray +var defaultStrokeWidth: Float = 4f +var defaultRadius = 25.0.dp + +class MainScreenViewModel(private val graph: Graph, private val representationStrategy: RepresentationStrategy) { + val showVerticesLabels = mutableStateOf(false) + val showVerticesId = mutableStateOf(false) + val showEdgesLabels = mutableStateOf(false) + val graphViewModel = GraphViewModel(graph, showVerticesLabels, showVerticesId, showEdgesLabels) + + init { + representationStrategy.place(800.0, 600.0, graphViewModel.verticesView.values) + } + + fun resetGraphView() { + representationStrategy.place(800.0, 600.0, graphViewModel.verticesView.values) + graphViewModel.verticesView.values.forEach { v -> + v.color = defaultColorVertex + v.radius = defaultRadius + } + graphViewModel.edgesView.values.forEach { e -> + e.color = defaultColorLine + e.strokeWidth = defaultStrokeWidth + } + } + + fun addVertex(id: Int, data: String): String? { + return graphViewModel.addVertex(id, data) + } + + fun addEdge(from: Int, to: Int, w: Int?): String? { + return graphViewModel.addEdge(from, to, w) + } + + fun removeVertex(id: Int): String? { + return graphViewModel.removeVertex(id) + } + + fun removeEdge(from: Int, to: Int): String? { + return graphViewModel.removeEdge(from, to) + } + + fun saveToNeo4j(uri: String, user: String, password: String): String? { + try { + val neo4j = Neo4jRepository(uri, user, password) + return neo4j.saveGraph(graph) + } catch (e: Exception) { + return "Error saving:\n" + + (e.message?.substringAfter("Exception: ") ?: "unable to save graph") + } + } + + fun saveToFile(): String? { + val fileSystem = FileSystem() + return fileSystem.saveGraph(graph) + } + + /** Paint the vertices and edges of the found path. + */ + fun runDijkstraAlgorithm(start: Int, end: Int): String? { + resetGraphView() + val dijkstra = Dijkstra(graph) + val result = dijkstra.findShortestPaths(start, end) + if (result.first == null) return result.second!! + for (vertexId in result.first!!) { + graphViewModel.verticesView[vertexId]?.color = Color(125, 21, 21) + } + for (i in 1 until result.first!!.size) { + val a = graphViewModel.edgesView.keys.find { it.vertices == Pair(result.first!![i - 1], result.first!![i]) } + graphViewModel.edgesView[a]!!.color = Color(86, 29, 39) + } + return null + } + + /** Paint each ccs its own color. The number of colors is limited, + * so if there are more than 10 ccs, the colors will begin to repeat. + */ + fun runKosarajuAlgorithm(): String? { + if (graph is UndirectedGraph) { + return "Kosaraju's algorithm cannot be run on undirected graphs." + } + val colors = listOf( + Color(125, 21, 21), + Color(41, 37, 37), + Color(145, 86, 86), + Color(56, 4, 4), + Color(161, 161, 161), + Color(115, 72, 101), + Color(38, 11, 29), + Color(255, 133, 141), + Color(99, 48, 37), + Color(61, 67, 74) + ) + resetGraphView() + val kosaraju = Kosaraju(graph as DirectedGraph) + val result = kosaraju.findStronglyConnectedComponents() + for ((i, ccs) in result.withIndex()) { + for (vertexId in ccs) { + graphViewModel.verticesView[vertexId]?.color = colors[i % 10] + } + } + return null + } + + /** Paint each community its own color. The number of colors is limited, + * so if there are more than 10 communities, the colors will begin to repeat. + */ + fun runLouvainAlgorithm() { + val colors = listOf( + Color(125, 21, 21), + Color(41, 37, 37), + Color(145, 86, 86), + Color(56, 4, 4), + Color(161, 161, 161), + Color(115, 72, 101), + Color(38, 11, 29), + Color(255, 133, 141), + Color(99, 48, 37), + Color(61, 67, 74) + ) + resetGraphView() + val louvain = Louvain(graph) + val result = louvain.detectCommunities() + for ((i, community) in result.withIndex()) { + for (vertexId in community) { + graphViewModel.verticesView[vertexId]?.color = colors[i % 10] + } + } + } + + /** Paints over the vertices and edges that belong to the found MST. + */ + fun runPrimAlgorithm(): String? { + if (graph is DirectedGraph) { + return "Prims's algorithm cannot be run on directed graphs." + } + for (edge in graph.edges) { + if (edge.weight == null) { + return "Each edge of graph for Prim's algorithm must have a weight:\nthe edge with weight = 'null' is incorrect." + } + } + val prim = Prim(graph as UndirectedGraph) + val result = prim.treePrim() + val weight = prim.weightPrim() + resetGraphView() + for (graphComponent in result) { + for (vertexId in graphComponent.vertices.keys) { + graphViewModel.verticesView[vertexId]?.color = Color(10, 230, 208) + } + for (edgeView in graphViewModel.edgesView) { + if (edgeView.key in graphComponent.edges) { + edgeView.value.color = Color(10, 230, 248) + edgeView.value.strokeWidth = 9f + } + } + } + return null + } + + fun runCycleSearchAlgorithm(vertexId: Int): String? { + + if (graph is DirectedGraph) { + return "CycleSearch algorithm can't be run on directed graphs." + } + if (vertexId !in graph.vertices.keys) { + return "Vertex with id = $vertexId doesn't exists in the graph." + } + val cycleSearch = CycleSearch(graph as UndirectedGraph) + val result = cycleSearch.findCycle(Vertex(vertexId, graph.vertices[vertexId]!!.data)) + resetGraphView() + if (result != null) { + for (idVertex in result.vertices.keys) { + graphViewModel.verticesView[idVertex]?.color = Color(10, 230, 208) + } + for (edgeView in graphViewModel.edgesView) { + if (edgeView.key in result.edges) { + edgeView.value.color = Color(10, 230, 248) + edgeView.value.strokeWidth = 9f + } + } + return null + } + return "The vertex with id = $vertexId doesn't have a cycle around it. " + } + + fun runHarmonicCentralityAlgorithm(): HashMap { + + val graphForAlgorithm = HarmonicCentrality(graph) + val vertexIdAndIndex: HashMap = graphForAlgorithm.harmonicCentrality() + + resetGraphView() + for(vertex in vertexIdAndIndex) { + graphViewModel.verticesView[vertex.key]?.radius = ((17.0 + vertex.value*30).toInt()).dp + graphViewModel.verticesView[vertex.key]?.color = + Color( red = 255 - (vertex.value*190).toInt(), green = 0, blue = 154 - (vertex.value*110).toInt() ) + } + return vertexIdAndIndex + + } + +} diff --git a/src/main/kotlin/viewmodel/graph/CircularPlacementStrategy.kt b/src/main/kotlin/viewmodel/graph/CircularPlacementStrategy.kt new file mode 100644 index 0000000..032ff79 --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/CircularPlacementStrategy.kt @@ -0,0 +1,42 @@ +package viewmodel.graph + +import androidx.compose.ui.unit.dp +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin + +class CircularPlacementStrategy : RepresentationStrategy { + override fun place(width: Double, height: Double, vertices: Collection) { + if (vertices.isEmpty()) { + return + } + val center = Pair(width * (4.0/5), height * (3.0/5) ) + 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 + + sorted + .drop(1) + .onEach { + point = point.rotate(center, angle) + it.x = point.first.dp + it.y = point.second.dp + } + } + + 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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt new file mode 100644 index 0000000..a86a7d3 --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -0,0 +1,20 @@ +package viewmodel.graph + +import androidx.compose.runtime.State +import androidx.compose.ui.graphics.Color +import model.graph.Edge + +class EdgeViewModel( + val u: VertexViewModel, + val v: VertexViewModel, + var color: Color, + var strokeWidth: Float, + private val e: Edge, + private val _labelVisible: State, +) { + val label + get() = e.weight?.toString() ?: "" + + val labelVisible + get() = _labelVisible.value +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/src/main/kotlin/viewmodel/graph/GraphViewModel.kt new file mode 100644 index 0000000..5b220cc --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -0,0 +1,76 @@ +package viewmodel.graph + +import androidx.compose.runtime.State +import androidx.compose.ui.unit.dp +import model.graph.Edge +import model.graph.Graph +import model.graph.UndirectedGraph +import viewmodel.defaultColorLine +import viewmodel.defaultColorVertex +import viewmodel.defaultStrokeWidth + +class GraphViewModel( + private val graph: Graph, + private val showVerticesLabels: State, + private val showVerticesId: State, + private val showEdgesLabels: State, +) { + internal val verticesView: HashMap = hashMapOf() + internal val edgesView: HashMap = hashMapOf() + + init { + graph.getVertices().forEach { vertex -> + verticesView[vertex.id] = VertexViewModel(0.dp, 0.dp, defaultColorVertex, vertex, showVerticesLabels, showVerticesId) + } + graph.edges.forEach { edge -> + val fst = verticesView[edge.vertices.first] + ?: throw IllegalStateException("VertexView for vertex with id: ${edge.vertices.first} not found") + val snd = verticesView[edge.vertices.second] + ?: throw IllegalStateException("VertexView for vertex with id: ${edge.vertices.second} not found") + edgesView[edge] = EdgeViewModel(fst, snd, defaultColorLine, defaultStrokeWidth, edge, showEdgesLabels) + } + } + + fun addVertex(id: Int, data: String): String? { + val addedResult = graph.addVertex(id, data) + if (addedResult != null) return addedResult + verticesView[id] = VertexViewModel(0.dp, 0.dp, defaultColorVertex, graph.vertices[id]!!, showVerticesLabels, showVerticesId) + return null + } + + fun removeVertex(id: Int): String? { + val removedResult = graph.removeVertex(id) + if (removedResult != null) return removedResult + verticesView.remove(id) + val edgesToRemove = edgesView.keys.filter { edge -> edge.vertices.first == id || edge.vertices.second == id } + edgesToRemove.forEach { edge -> + edgesView.remove(edge) + } + return null + } + + fun addEdge(from: Int, to: Int, w: Int?): String? { + val addedResult = graph.addEdge(Pair(from, to), w) + if (addedResult != null) return addedResult + val fst = verticesView[from] ?: return "VertexView for vertex with id: $from not found" + val snd = verticesView[to] ?: return "VertexView for vertex with id: $to not found" + graph.edges.find { it.vertices == Pair(from, to) }?.let { edge -> + edgesView[edge] = EdgeViewModel(fst, snd, defaultColorLine, defaultStrokeWidth, edge, showEdgesLabels) + } + return null + } + + fun removeEdge(from: Int, to: Int): String? { + val removedResult = graph.removeEdge(Pair(from, to)) + if (removedResult != null) return removedResult + edgesView.keys.find { it.vertices == Pair(from, to) }?.let { edge -> + edgesView.remove(edge) + } + if (graph is UndirectedGraph) { + edgesView.keys.find { it.vertices == Pair(to, from) }?.let { edge -> + edgesView.remove(edge) + } + } + return null + } +} diff --git a/src/main/kotlin/viewmodel/graph/RepresentationStrategy.kt b/src/main/kotlin/viewmodel/graph/RepresentationStrategy.kt new file mode 100644 index 0000000..0b56ffc --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/RepresentationStrategy.kt @@ -0,0 +1,5 @@ +package viewmodel.graph + +interface RepresentationStrategy { + fun place(width: Double, height: Double, vertices: Collection) +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt new file mode 100644 index 0000000..0a50a87 --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -0,0 +1,55 @@ +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 +import viewmodel.defaultRadius + +class VertexViewModel( + x: Dp = 0.dp, + y: Dp = 0.dp, + color: Color, + private val v: Vertex, + private val _labelVisible: State, + private val _idVisible: State, + var radius: Dp = defaultRadius +) { + 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() = v.data + + val labelVisible + get() = _labelVisible.value + val id + get() = v.id.toString() + + val idVisible + get() = _idVisible.value + + fun onDrag(offset: Offset) { + _x.value += offset.x.dp + _y.value += offset.y.dp + } +} \ No newline at end of file diff --git a/src/test/kotlin/BellmanFordTest.kt b/src/test/kotlin/BellmanFordTest.kt new file mode 100644 index 0000000..2d443d6 --- /dev/null +++ b/src/test/kotlin/BellmanFordTest.kt @@ -0,0 +1,66 @@ +import kotlin.test.assertNull +import kotlin.test.assertEquals +import model.algorithms.BellmanFord +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import org.junit.jupiter.api.Test + +class BellmanFordTest { + private val dirGraph = DirectedGraph() + private val undirGraph = UndirectedGraph() + private val bellmanFordDirected = BellmanFord(dirGraph) + private val bellmanFordUndirected = BellmanFord(undirGraph) + + init { + // Добавление вершин для направленного графа + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + + // Добавление рёбер для направленного графа + dirGraph.addEdge(Pair(1, 2), 4) + dirGraph.addEdge(Pair(1, 3), 2) + dirGraph.addEdge(Pair(2, 3), 5) + dirGraph.addEdge(Pair(2, 4), 10) + dirGraph.addEdge(Pair(3, 4), 3) + + // Добавление вершин для ненаправленного графа + undirGraph.addVertex(1, "A") + undirGraph.addVertex(2, "B") + undirGraph.addVertex(3, "C") + undirGraph.addVertex(4, "D") + + // Добавление рёбер для ненаправленного графа + undirGraph.addEdge(Pair(1, 2), 1) + undirGraph.addEdge(Pair(1, 3), 4) + undirGraph.addEdge(Pair(2, 3), 2) + undirGraph.addEdge(Pair(2, 4), 7) + undirGraph.addEdge(Pair(3, 4), 3) + } + + @Test + fun testShortestPathDirectedGraph() { + val result = bellmanFordDirected.findShortestPath(1, 4) + assertEquals(Pair(5, listOf(1, 3, 4)), result) + } + + @Test + fun testShortestPathUndirectedGraph() { + val result = bellmanFordUndirected.findShortestPath(1, 4) + assertEquals(Pair(6, listOf(1, 2, 3, 4)), result) + } + @Test + fun testNegativeWeightCycle() { + dirGraph.addEdge(Pair(4, 1), -8) // Добавляем отрицательный цикл + val result = bellmanFordDirected.findShortestPath(1, 4) + assertNull(result) // Должно вернуть null из-за отрицательного цикла + } + + @Test + fun testShortestPathWithNegativeWeights() { + dirGraph.addEdge(Pair(2, 3), -3) // Изменяем вес ребра + val result = bellmanFordDirected.findShortestPath(1, 4) + assertEquals(Pair(4, listOf(1, 2, 3, 4)), result) + } +} diff --git a/src/test/kotlin/BridgeFinderTest.kt b/src/test/kotlin/BridgeFinderTest.kt new file mode 100644 index 0000000..5527acc --- /dev/null +++ b/src/test/kotlin/BridgeFinderTest.kt @@ -0,0 +1,65 @@ +import kotlin.test.assertEquals +import model.algorithms.BridgeFinder +import model.graph.UndirectedGraph +import org.junit.jupiter.api.Test + +class BridgeFinderTest { + private val undirGraph = UndirectedGraph() + private val bridgeFinder = BridgeFinder() + + init { + // Добавление вершин для ненаправленного графа + undirGraph.addVertex(0, "A") + undirGraph.addVertex(1, "B") + undirGraph.addVertex(2, "C") + undirGraph.addVertex(3, "D") + undirGraph.addVertex(4, "E") + } + + @Test + fun testNoBridges() { + undirGraph.addEdge(Pair(0, 1), 1) + undirGraph.addEdge(Pair(1, 2), 1) + undirGraph.addEdge(Pair(2, 0), 1) + undirGraph.addEdge(Pair(1, 3), 1) + undirGraph.addEdge(Pair(3, 4), 1) + undirGraph.addEdge(Pair(4, 1), 1) + + val bridges = bridgeFinder.findBridges(undirGraph) + assertEquals(emptyList>(), bridges) + } + + @Test + fun testOneBridge() { + undirGraph.addEdge(Pair(0, 1), 1) + undirGraph.addEdge(Pair(1, 2), 1) + undirGraph.addEdge(Pair(2, 3), 1) + undirGraph.addEdge(Pair(3, 4), 1) + + val bridges = bridgeFinder.findBridges(undirGraph) + assertEquals(listOf(Pair(0, 1), Pair(1, 2), Pair(2, 3), Pair(3, 4)), bridges.sortedWith(compareBy({ it.first }, { it.second }))) + } + + @Test + fun testMultipleBridges() { + undirGraph.addEdge(Pair(0, 1), 1) + undirGraph.addEdge(Pair(1, 2), 1) + undirGraph.addEdge(Pair(2, 3), 1) + undirGraph.addEdge(Pair(3, 4), 1) + undirGraph.addEdge(Pair(1, 3), 1) + + val bridges = bridgeFinder.findBridges(undirGraph) + assertEquals(listOf(Pair(0, 1), Pair(3, 4)), bridges.sortedWith(compareBy({ it.first }, { it.second }))) + } + + @Test + fun testDisconnectedGraph() { + undirGraph.addVertex(5, "F") + undirGraph.addEdge(Pair(0, 1), 1) + undirGraph.addEdge(Pair(2, 3), 1) + undirGraph.addEdge(Pair(3, 4), 1) + + val bridges = bridgeFinder.findBridges(undirGraph) + assertEquals(listOf(Pair(0, 1), Pair(2, 3), Pair(3, 4)), bridges.sortedWith(compareBy({ it.first }, { it.second }))) + } +} diff --git a/src/test/kotlin/CycleSearchTest.kt b/src/test/kotlin/CycleSearchTest.kt new file mode 100644 index 0000000..274b99e --- /dev/null +++ b/src/test/kotlin/CycleSearchTest.kt @@ -0,0 +1,80 @@ +import model.algorithms.CycleSearch +import model.graph.UndirectedGraph +import model.graph.Vertex +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import forTests.addVertices +import kotlin.test.assertFailsWith + +class CycleSearchTest { + + private var graph = UndirectedGraph() + private var cycleGraph = CycleSearch(graph) + + @Test + fun `search for a cycle at a single vertex must be correct`() { + + addVertices(graph, 1) + assertEquals(null, cycleGraph.findCycle(graph.vertices[1]!!)) + + } + + @Test + fun `search for a cycle at the graph that is a tree must be correct`() { + + addVertices(graph, 4) + graph.run { + addEdge(1 to 2, 23) + addEdge(2 to 3, 12) + addEdge(4 to 2, 4) + } + assertEquals(null, cycleGraph.findCycle(graph.vertices[1]!!)) + + } + + @Test + fun `search for a cycle at the graph that is a cycle must be correct`() { + + addVertices(graph, 3) + graph.run { + addEdge(1 to 2, 23) + addEdge(2 to 3, 12) + addEdge(1 to 3, 4) + } + assertEquals(true,cycleGraph.findCycle(graph.vertices[1]!!) != null) + + } + + @Test + fun `search for a cycle at the graph around vertex with many adjacency must be correct`() { + + addVertices(graph, 6) + graph.run { + addEdge(1 to 2, 1) + addEdge(2 to 3, 2) + addEdge(2 to 5, 5) + addEdge(2 to 6, 7) + addEdge(3 to 4, 3) + addEdge(4 to 5, 4) + addEdge(5 to 6, 6) + } + assertEquals(true, cycleGraph.findCycle(graph.vertices[2]!!) != null) + + } + + @Test + fun `search for a cycle at the graph containing cycle, but around vertex without cycle must be correct`() { + + addVertices(graph, 5) + graph.run { + addEdge(1 to 2, 13) + addEdge(2 to 3, 21) + addEdge(3 to 1, 67) + addEdge(3 to 4, 41) + addEdge(4 to 5, 10) + } + assertEquals(null, cycleGraph.findCycle(graph.vertices[4]!!)) + + } + +} diff --git a/src/test/kotlin/DijkstraTest.kt b/src/test/kotlin/DijkstraTest.kt new file mode 100644 index 0000000..f96b997 --- /dev/null +++ b/src/test/kotlin/DijkstraTest.kt @@ -0,0 +1,158 @@ +import model.algorithms.Dijkstra +import model.graph.UndirectedGraph +import model.graph.DirectedGraph +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import kotlin.test.assertFailsWith + +class DijkstraTest { + private val dirGraph = DirectedGraph() + private val undirGraph = UndirectedGraph() + private val shortestPathDirected = Dijkstra(dirGraph) + private val shortestPathUndirected = Dijkstra(undirGraph) + + //region Exceptions + @Test + fun `having a negative edge weight in a path should throw an exception`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addEdge(Pair(1, 3), 5) + dirGraph.addEdge(Pair(3, 2), -2) + assertEquals(null to "Edge with negative weights in Dijkstra's algorithm.", shortestPathDirected.findShortestPaths(1, 2)) + } + + @Test + fun `having a null edge weight in a path should throw an exception`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addEdge(Pair(1, 3), 5) + dirGraph.addEdge(Pair(3, 2), null) + assertEquals(null to "Edge without weights in Dijkstra's algorithm.", shortestPathDirected.findShortestPaths(1, 2)) + } + + @Test + fun `searching for a path from vertices that are not in the graph should throw an exception`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addEdge(Pair(1, 3), 5) + dirGraph.addEdge(Pair(3, 2), 2) + assertEquals(null to "The vertex doesn't exist in the graph.", shortestPathDirected.findShortestPaths(4, 5)) + } + + //endregion + //region DirectedGraph + @Test + fun `searching for a non-existent path in a directed graph should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addEdge(Pair(1, 2), 50) + dirGraph.addEdge(Pair(2, 3), 5) + dirGraph.addEdge(Pair(3, 1), 100) + dirGraph.addEdge(Pair(4, 3), 15) + assertEquals(listOf() to null, shortestPathDirected.findShortestPaths(1, 4)) + } + + @Test + fun `searching for an existing path in a directed graph without bidirectional edges should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addVertex(7, "G") + dirGraph.addVertex(8, "H") + dirGraph.addVertex(9, "I") + dirGraph.addEdge(Pair(1, 2), 5) + dirGraph.addEdge(Pair(1, 3), 10) + dirGraph.addEdge(Pair(1, 4), 15) + dirGraph.addEdge(Pair(2, 5), 20) + dirGraph.addEdge(Pair(3, 5), 50) + dirGraph.addEdge(Pair(4, 6), 200) + dirGraph.addEdge(Pair(6, 5), 120) + dirGraph.addEdge(Pair(6, 8), 1) + dirGraph.addEdge(Pair(5, 7), 7) + dirGraph.addEdge(Pair(7, 8), 54) + dirGraph.addEdge(Pair(8, 9), 21) + assertEquals(listOf(1, 2, 5, 7, 8, 9) to null, shortestPathDirected.findShortestPaths(1, 9)) + } + + @Test + fun `searching for an existing path in a directed graph with bidirectional edges should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addVertex(7, "G") + dirGraph.addVertex(8, "H") + dirGraph.addVertex(9, "I") + dirGraph.addEdge(Pair(1, 2), 5) + dirGraph.addEdge(Pair(1, 3), 10) + dirGraph.addEdge(Pair(1, 4), 15) + dirGraph.addEdge(Pair(2, 5), 20) + dirGraph.addEdge(Pair(3, 5), 50) + dirGraph.addEdge(Pair(4, 6), 200) + dirGraph.addEdge(Pair(6, 5), 120) + dirGraph.addEdge(Pair(6, 8), 1) + dirGraph.addEdge(Pair(5, 7), 7) + dirGraph.addEdge(Pair(7, 8), 54) + dirGraph.addEdge(Pair(8, 9), 21) + dirGraph.addEdge(Pair(4, 1), 15) + dirGraph.addEdge(Pair(5, 6), 120) + dirGraph.addEdge(Pair(7, 5), 7) + assertEquals(listOf(1, 2, 5, 7, 8, 9) to null, shortestPathDirected.findShortestPaths(1, 9)) + } + + //endregion + //region UnDirectedGraph + @Test + fun `searching for a non-existent path in a undirected graph should be correct`() { + undirGraph.addVertex(1, "A") + undirGraph.addVertex(2, "B") + undirGraph.addVertex(3, "C") + undirGraph.addVertex(4, "D") + undirGraph.addVertex(5, "E") + undirGraph.addVertex(6, "F") + undirGraph.addEdge(Pair(1, 3), 5) + undirGraph.addEdge(Pair(3, 2), 10) + undirGraph.addEdge(Pair(1, 2), 15) + undirGraph.addEdge(Pair(6, 5), 20) + undirGraph.addEdge(Pair(5, 4), 50) + assertEquals(listOf() to null, shortestPathUndirected.findShortestPaths(2, 5)) + } + + @Test + fun `searching for an existing path in the undirected graph should be correct`() { + undirGraph.addVertex(1, "A") + undirGraph.addVertex(2, "B") + undirGraph.addVertex(3, "C") + undirGraph.addVertex(4, "D") + undirGraph.addVertex(5, "E") + undirGraph.addVertex(6, "F") + undirGraph.addVertex(7, "G") + undirGraph.addVertex(8, "H") + undirGraph.addVertex(9, "I") + undirGraph.addVertex(10, "J") + undirGraph.addVertex(11, "K") + undirGraph.addEdge(Pair(1, 2), 50) + undirGraph.addEdge(Pair(1, 3), 5) + undirGraph.addEdge(Pair(3, 4), 100) + undirGraph.addEdge(Pair(3, 5), 15) + undirGraph.addEdge(Pair(5, 6), 5) + undirGraph.addEdge(Pair(4, 6), 20) + undirGraph.addEdge(Pair(4, 7), 20) + undirGraph.addEdge(Pair(4, 8), 50) + undirGraph.addEdge(Pair(8, 9), 34) + undirGraph.addEdge(Pair(9, 10), 80) + undirGraph.addEdge(Pair(2, 11), 5) + assertEquals(listOf(1, 3, 5, 6, 4, 8, 9) to null, shortestPathUndirected.findShortestPaths(1, 9)) + } + //endregion +} diff --git a/src/test/kotlin/HarmonicCentralityTest.kt b/src/test/kotlin/HarmonicCentralityTest.kt new file mode 100644 index 0000000..20b1411 --- /dev/null +++ b/src/test/kotlin/HarmonicCentralityTest.kt @@ -0,0 +1,128 @@ +import forTests.addVertices +import model.algorithms.HarmonicCentrality +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import org.junit.jupiter.api.Test +import kotlin.math.roundToInt +import kotlin.test.assertEquals + +class HarmonicCentralityTest { + + private var undirGraph = UndirectedGraph() + private var dirGraph = DirectedGraph() + private var undirCentrality = HarmonicCentrality(undirGraph) + private var dirCentrality = HarmonicCentrality(dirGraph) + private fun roundTo(number: Double): Double { + return (number * 10000.0).roundToInt() / 10000.0 + } + + @Test + fun `indexes of single graph's vertices must be equal to zero`() { + + addVertices(undirGraph, 3) + undirGraph.addEdge(1 to 2, 23) + + assertEquals(0.0, (undirCentrality.harmonicCentrality())[3]) + + } + + @Test + fun `the index of the vertex connected with all vertices of the graph must be equal to 1`() { + + addVertices(undirGraph, 3) + undirGraph.addEdge(1 to 2, 23) + undirGraph.addEdge(1 to 3, 23) + + assertEquals(1.0, (undirCentrality.harmonicCentrality())[1]) + + } + + @Test + fun `result of function for simple graph must be correct`() { + + addVertices(undirGraph, 4) + undirGraph.run { + addEdge(1 to 2, null) + addEdge(2 to 3, null) + addEdge(3 to 4, null) + } + + undirCentrality.harmonicCentrality().let { + val expected = listOf(0.0, 11.0 / 18, 5.0 / 6, 5.0 / 6, 11.0 / 18) + for (index in 1..4) { + assertEquals(roundTo(expected[index]), it[index]!!) + } + } + + } + + @Test + fun `result of function for non-trivial(more complex) undirected graph must be correct`() { + + addVertices(undirGraph, 9) + + undirGraph.run { + addEdge(1 to 2, null) + addEdge(3 to 2, null) + addEdge(6 to 2, null) + addEdge(6 to 4, null) + addEdge(6 to 5, null) + addEdge(6 to 3, null) + addEdge(4 to 5, null) + addEdge(5 to 3, null) + addEdge(5 to 8, null) + addEdge(1 to 8, null) + addEdge(1 to 6, null) + addEdge(7 to 9, null) + } + + undirCentrality.harmonicCentrality().let { + val expected = listOf(0.0, 4.5 / 8, 4.5 / 8, 4.5 / 8, 0.5, 5.0 / 8, 11.0 / 16, 1.0 / 8, 0.5, 0.125) + for (index in 1..9) { + assertEquals(roundTo(expected[index]), it[index]) + } + } + + } + + @Test + fun `result of function for non-trivial(more complex) directed graph must be correct`() { + + addVertices(dirGraph, 9) + + dirGraph.run { + addEdge(1 to 3, null) + addEdge(2 to 1, null) + addEdge(2 to 3, null) + addEdge(4 to 3, null) + addEdge(5 to 3, null) + addEdge(3 to 8, null) + addEdge(3 to 9, null) + addEdge(5 to 9, null) + addEdge(8 to 7, null) + addEdge(8 to 9, null) + addEdge(9 to 7, null) + addEdge(7 to 6, null) + } + + dirCentrality.harmonicCentrality().let { + val expected = listOf( + 0.0, + 31.0 / (12 * 8), + 43.0 / (12 * 8), + 17.0 / 48, + 31.0 / (12 * 8), + 10.0 / 24, + 0.0, + 1.0 / 8, + 2.5 / 8, + 3.0 / 16 + ) + for (index in 1..9) { + assertEquals(roundTo(expected[index]), it[index]) + } + } + + } + +} diff --git a/src/test/kotlin/KosarajuTest.kt b/src/test/kotlin/KosarajuTest.kt new file mode 100644 index 0000000..7a8144f --- /dev/null +++ b/src/test/kotlin/KosarajuTest.kt @@ -0,0 +1,108 @@ +import model.algorithms.Kosaraju +import model.graph.DirectedGraph +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals + +class KosarajuTest { + private val dirGraph = DirectedGraph() + private val scc = Kosaraju(dirGraph) + + @Test + fun `searching for scc in a graph without vertices should be correct`() { + assertEquals(listOf>(), scc.findStronglyConnectedComponents()) + } + + @Test + fun `searching for scc in a graph without edges should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + assertEquals(listOf(listOf(3), listOf(2), listOf(1)), scc.findStronglyConnectedComponents()) + } + + @Test + fun `searching for scc in a graph with bidirectional edges should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addVertex(7, "G") + dirGraph.addVertex(8, "H") + dirGraph.addVertex(9, "I") + dirGraph.addEdge(Pair(1, 2), 1) + dirGraph.addEdge(Pair(2, 3), 45) + dirGraph.addEdge(Pair(3, 1), 3) + dirGraph.addEdge(Pair(4, 5), 12) + dirGraph.addEdge(Pair(5, 4), -23) + dirGraph.addEdge(Pair(4, 2), 4) + dirGraph.addEdge(Pair(5, 6), 34) + dirGraph.addEdge(Pair(6, 8), -12) + dirGraph.addEdge(Pair(8, 9), null) + dirGraph.addEdge(Pair(9, 7), 0) + dirGraph.addEdge(Pair(7, 6), null) + assertEquals(listOf(listOf(5, 4), listOf(8, 9, 7, 6), listOf(2, 3, 1)), scc.findStronglyConnectedComponents()) + } + + @Test + fun `searching for scc in a graph without bidirectional edges should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addVertex(7, "G") + dirGraph.addEdge(Pair(1, 2), null) + dirGraph.addEdge(Pair(2, 3), 34) + dirGraph.addEdge(Pair(3, 4), 2) + dirGraph.addEdge(Pair(4, 1), 1) + dirGraph.addEdge(Pair(5, 6), 11) + dirGraph.addEdge(Pair(6, 7), -2) + dirGraph.addEdge(Pair(7, 2), 3) + assertEquals(listOf(listOf(5), listOf(6), listOf(7), listOf(2, 3, 4, 1)), scc.findStronglyConnectedComponents()) + } + + @Test + fun `searching for scc in a disconnected graph should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addEdge(Pair(1, 2), 1) + dirGraph.addEdge(Pair(2, 3), -1) + dirGraph.addEdge(Pair(3, 1), 20) + dirGraph.addEdge(Pair(4, 5), 34) + assertEquals(listOf(listOf(4), listOf(5), listOf(2, 3, 1)), scc.findStronglyConnectedComponents()) + } + + @Test + fun `searching for scc in a full graph should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addEdge(Pair(1, 2), 1) + dirGraph.addEdge(Pair(1, 3), -1) + dirGraph.addEdge(Pair(2, 1), 1) + dirGraph.addEdge(Pair(2, 3), 9) + dirGraph.addEdge(Pair(3, 1), -1) + dirGraph.addEdge(Pair(3, 2), 9) + assertEquals(listOf(listOf(3, 2, 1)), scc.findStronglyConnectedComponents()) + } + + @Test + fun `searching for scc in a graph without cycles should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addEdge(Pair(1, 2), 1) + dirGraph.addEdge(Pair(1, 3), -1) + dirGraph.addEdge(Pair(2, 4), 1) + dirGraph.addEdge(Pair(4, 5), 9) + assertEquals(listOf(listOf(1), listOf(3), listOf(2), listOf(4), listOf(5)), scc.findStronglyConnectedComponents()) + } +} diff --git a/src/test/kotlin/LouvainTest.kt b/src/test/kotlin/LouvainTest.kt new file mode 100644 index 0000000..c70795c --- /dev/null +++ b/src/test/kotlin/LouvainTest.kt @@ -0,0 +1,164 @@ +import model.algorithms.Louvain +import model.graph.UndirectedGraph +import model.graph.DirectedGraph +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals + +class LouvainTest { + private val dirGraph = DirectedGraph() + private val communityDir = Louvain(dirGraph) + private val undirGraph = UndirectedGraph() + private val communityUndir = Louvain(undirGraph) + + //region Special cases + @Test + fun `detected communities in a graph without vertices should be correct`() { + assertEquals(listOf>(), communityUndir.detectCommunities()) + } + + @Test + fun `detected communities in a graph without edges should be correct`() { + undirGraph.addVertex(1, "A") + undirGraph.addVertex(2, "B") + undirGraph.addVertex(3, "C") + assertEquals(listOf(setOf(1), setOf(2), setOf(3)), communityUndir.detectCommunities()) + } + + //endregion + //region DirectedGraph + @Test + fun `detected communities in a directed graph of cycle should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addEdge(Pair(1, 2), 4) + dirGraph.addEdge(Pair(2, 3), 13) + dirGraph.addEdge(Pair(3, 4), 1) + dirGraph.addEdge(Pair(4, 5), null) + dirGraph.addEdge(Pair(5, 6), null) + dirGraph.addEdge(Pair(6, 1), 0) + assertEquals(listOf(setOf(1, 2, 3, 4, 5, 6)), communityDir.detectCommunities()) + } + + @Test + fun `detected communities in a directed graph of cycle with bidirectional edges should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addEdge(Pair(1, 2), 4) + dirGraph.addEdge(Pair(2, 3), 13) + dirGraph.addEdge(Pair(3, 4), 1) + dirGraph.addEdge(Pair(4, 5), null) + dirGraph.addEdge(Pair(5, 6), null) + dirGraph.addEdge(Pair(6, 5), null) + dirGraph.addEdge(Pair(6, 1), 0) + assertEquals(listOf(setOf(1, 2, 3, 4), setOf(5, 6)), communityDir.detectCommunities()) + } + + @Test + fun `detected communities in a directed graph of cycles connected by an edge should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addEdge(Pair(1, 2), null) + dirGraph.addEdge(Pair(2, 3), 1) + dirGraph.addEdge(Pair(1, 3), 4) + dirGraph.addEdge(Pair(3, 4), 3) + dirGraph.addEdge(Pair(4, 5), 1) + dirGraph.addEdge(Pair(5, 6), 0) + dirGraph.addEdge(Pair(4, 6), 12) + assertEquals(listOf(setOf(1, 2, 3), setOf(4, 5, 6)), communityDir.detectCommunities()) + } + + @Test + fun `detected communities in a disconnected directed graph should be correct`() { + dirGraph.addVertex(1, "A") + dirGraph.addVertex(2, "B") + dirGraph.addVertex(3, "C") + dirGraph.addVertex(4, "D") + dirGraph.addVertex(5, "E") + dirGraph.addVertex(6, "F") + dirGraph.addVertex(7, "G") + dirGraph.addVertex(8, "H") + dirGraph.addVertex(9, "I") + dirGraph.addEdge(Pair(1, 2), 4) + dirGraph.addEdge(Pair(2, 1), 4) + dirGraph.addEdge(Pair(2, 3), 12) + dirGraph.addEdge(Pair(3, 4), 3) + dirGraph.addEdge(Pair(4, 3), 56) + dirGraph.addEdge(Pair(5, 6), null) + dirGraph.addEdge(Pair(7, 8), 0) + dirGraph.addEdge(Pair(8, 9), -19) + dirGraph.addEdge(Pair(9, 7), 2) + assertEquals(listOf(setOf(1, 2), setOf(3, 4), setOf(5, 6), setOf(7, 8, 9)), communityDir.detectCommunities()) + } + + //endregion + //region UndirectedGraph + @Test + fun `detected communities in a graph of cycles connected by an edge should be correct`() { + undirGraph.addVertex(1, "A") + undirGraph.addVertex(2, "B") + undirGraph.addVertex(3, "C") + undirGraph.addVertex(4, "D") + undirGraph.addVertex(5, "E") + undirGraph.addVertex(6, "F") + undirGraph.addEdge(Pair(1, 2), null) + undirGraph.addEdge(Pair(2, 3), 1) + undirGraph.addEdge(Pair(1, 3), 4) + undirGraph.addEdge(Pair(3, 4), 3) + undirGraph.addEdge(Pair(4, 5), 1) + undirGraph.addEdge(Pair(5, 6), 0) + undirGraph.addEdge(Pair(4, 6), 12) + assertEquals(listOf(setOf(1, 2, 3), setOf(4, 5, 6)), communityUndir.detectCommunities()) + } + + @Test + fun `detected communities in a graph of cycle should be correct`() { + undirGraph.addVertex(1, "A") + undirGraph.addVertex(2, "B") + undirGraph.addVertex(3, "C") + undirGraph.addVertex(4, "D") + undirGraph.addVertex(5, "E") + undirGraph.addVertex(6, "F") + undirGraph.addEdge(Pair(1, 2), 4) + undirGraph.addEdge(Pair(2, 3), 13) + undirGraph.addEdge(Pair(3, 4), 1) + undirGraph.addEdge(Pair(4, 5), null) + undirGraph.addEdge(Pair(5, 6), null) + undirGraph.addEdge(Pair(6, 1), 0) + assertEquals(listOf(setOf(1, 2, 3, 4, 5, 6)), communityUndir.detectCommunities()) + } + + @Test + fun `detected communities in a disconnected graph should be correct`() { + undirGraph.addVertex(1, "A") + undirGraph.addVertex(2, "B") + undirGraph.addVertex(3, "C") + undirGraph.addVertex(4, "D") + undirGraph.addVertex(5, "E") + undirGraph.addVertex(6, "F") + undirGraph.addVertex(7, "G") + undirGraph.addVertex(8, "H") + undirGraph.addVertex(9, "I") + undirGraph.addEdge(Pair(1, 2), 4) + undirGraph.addEdge(Pair(2, 3), 12) + undirGraph.addEdge(Pair(3, 4), 3) + undirGraph.addEdge(Pair(4, 3), 56) + undirGraph.addEdge(Pair(5, 6), null) + undirGraph.addEdge(Pair(7, 8), 0) + undirGraph.addEdge(Pair(8, 9), -19) + undirGraph.addEdge(Pair(9, 7), 2) + assertEquals(listOf(setOf(1, 2, 3, 4), setOf(5, 6), setOf(7, 8, 9)), communityUndir.detectCommunities()) + } + //endregion +} diff --git a/src/test/kotlin/PrimTest.kt b/src/test/kotlin/PrimTest.kt new file mode 100644 index 0000000..f4e8fea --- /dev/null +++ b/src/test/kotlin/PrimTest.kt @@ -0,0 +1,92 @@ +import model.algorithms.Prim +import model.graph.UndirectedGraph +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import forTests.addVertices + +class PrimTest { + + private val undirGraph = UndirectedGraph() + private var primGraph = Prim(undirGraph) + + @Test + fun `construction of MST from a graph consisting of a single vertex must be correct`() { + + addVertices(undirGraph, 1) + + assertEquals(0, primGraph.weightPrim()) + + } + + @Test + fun `construction of MST from 2 components must be correct`() { + + addVertices(undirGraph, 5) + undirGraph.run { + addEdge(1 to 2, 23) + addEdge(2 to 3, 3) + addEdge(1 to 3, 41) + addEdge(4 to 5, 12) + } + + assertEquals(38, primGraph.weightPrim()) + + } + + @Test + fun `construction of MST from tree must be correct`() { //when building a MST from a tree, we need to get the original tree + + addVertices(undirGraph, 4) + + undirGraph.run { + addEdge(3 to 1, 3) + addEdge(3 to 2, 8) + addEdge(3 to 4, 21) + } + + assertEquals(32, primGraph.weightPrim()) + + } + + @Test + fun `construction of MST from graph consisting of edges with equal weight must be correct`() { + + addVertices(undirGraph, 4) + + undirGraph.run { + addEdge(1 to 2, 20) + addEdge(1 to 4, 20) + addEdge(3 to 2, 20) + addEdge(3 to 4, 20) + } + + assertEquals(60, primGraph.weightPrim()) + + } + + @Test + fun `construction of MST from non-trivial(more complex) graph must be correct`() { + + addVertices(undirGraph, 10) + + undirGraph.run { + addEdge(1 to 2, 5) + addEdge(2 to 3, 11) + addEdge(3 to 4, 25) + addEdge(4 to 8, 24) + addEdge(1 to 4, 1) + addEdge(2 to 5, 42) + addEdge(5 to 3, 11) + addEdge(5 to 4, 54) + addEdge(8 to 9, 73) + addEdge(6 to 7, 130) + addEdge(7 to 9, 4) + addEdge(8 to 7, 9) + addEdge(8 to 10, 42) + } + + assertEquals(237, primGraph.weightPrim()) + + } + +} diff --git a/src/test/kotlin/forTests.kt b/src/test/kotlin/forTests.kt new file mode 100644 index 0000000..4a195e0 --- /dev/null +++ b/src/test/kotlin/forTests.kt @@ -0,0 +1,12 @@ +package forTests + +import model.graph.Graph + +fun addVertices(graph: Graph, amount: Int) { + + var counter = amount + while (counter > 0) { + graph.addVertex(counter, counter.toString()) + counter-- + } +}