From f07dc6ed1b677dbe825f89df12289284647e1692 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:37:32 +0000 Subject: [PATCH 01/77] Setting up GitHub Classroom Feedback From bb3a84a990562ee160a397a2fd2dbad827c02193 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Sun, 28 Apr 2024 10:04:30 +0300 Subject: [PATCH 02/77] chore: set up JC project --- app/src/main/kotlin/Main.kt | 31 +++ build.gradle.kts | 35 ++++ gradle.properties | 4 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 +++++++++++++++++++++++ gradlew.bat | 89 +++++++++ settings.gradle.kts | 13 ++ 8 files changed, 411 insertions(+) create mode 100644 app/src/main/kotlin/Main.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt new file mode 100644 index 00000000..fed3f24a --- /dev/null +++ b/app/src/main/kotlin/Main.kt @@ -0,0 +1,31 @@ +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +@Composable +@Preview +fun App() { + var text by remember { mutableStateOf("Hello, World!") } + + MaterialTheme { + Button(onClick = { + text = "Hello, Desktop!" + }) { + Text(text) + } + } +} + +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + App() + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..fb454396 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") +} + +group = "org.graphs-2" +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) +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "demo" + packageVersion = "1.0.0" + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..618b17fa --- /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.20 +compose.version=1.5.10 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..a2e3ca06 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) + } +} + +rootProject.name = "demo" From 0d4e45595ed86d0ccffe77d6874c7f45ab04f5c8 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Sun, 28 Apr 2024 10:04:55 +0300 Subject: [PATCH 03/77] chore: add gitignore --- .gitignore | 219 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8ab266ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,219 @@ +# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,macos,windows,linux,gradle,kotlin +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,macos,windows,linux,gradle,kotlin + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Kotlin ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/intellij+all,macos,windows,linux,gradle,kotlin From a0f53d944bc7f48401e87cf5f264bc05c7e09a55 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Sun, 28 Apr 2024 10:31:43 +0300 Subject: [PATCH 04/77] chore: tuned JC project config --- app/build.gradle.kts | 16 +++++++++++ build.gradle.kts | 35 ++---------------------- gradle/wrapper/gradle-wrapper.properties | 3 +- settings.gradle.kts | 29 ++++++++++++++++---- 4 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 app/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..b2cd64e2 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + + alias(libs.plugins.compose) +} + +dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.koin.core) +} + +compose.desktop { + application { + mainClass = "MainKt" + } +} diff --git a/build.gradle.kts b/build.gradle.kts index fb454396..f892991c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,35 +1,4 @@ -import org.jetbrains.compose.desktop.application.dsl.TargetFormat - plugins { - kotlin("jvm") - id("org.jetbrains.compose") -} - -group = "org.graphs-2" -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) -} - -compose.desktop { - application { - mainClass = "MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "demo" - packageVersion = "1.0.0" - } - } + kotlin("jvm") version "1.9.20" + id("org.jetbrains.compose") version "1.6.2" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index db9a6b82..600bcce9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sat Apr 27 23:16:58 MSK 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index a2e3ca06..06fa12f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,13 +1,30 @@ +rootProject.name = "graphs-2" +include("app") + + pluginManagement { + repositories { + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } +} + +dependencyResolutionManagement { repositories { - gradlePluginPortal() + mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } + versionCatalogs { + create("libs") { + version("kotlin", "1.9.20") + plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").versionRef("kotlin") - plugins { - kotlin("jvm").version(extra["kotlin.version"] as String) - id("org.jetbrains.compose").version(extra["compose.version"] as String) + plugin("compose", "org.jetbrains.compose").version("1.6.2") + + library("koin-core", "io.insert-koin:koin-core:3.5.3") + + library("junit-jupiter", "org.junit.jupiter:junit-jupiter:5.10.2") + } } } - -rootProject.name = "demo" From 6ad230f377a914cb60abc620d16bc64f0e5898ed Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Sun, 28 Apr 2024 10:38:09 +0300 Subject: [PATCH 05/77] docs(readme): add draft --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..4b2108cc --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +### TODO From c3c6eac74bc30509dd97306ecaa37975dd2deb87 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:38:39 +0300 Subject: [PATCH 06/77] docs(lic): add MIT license --- LICENSE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..6a15320c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mike Gavrilenko, Karim Shakirov, Daniel Vlasenco + +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. From 5efce28aa87aad33e06f99663dc970be4f0c6262 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:39:58 +0300 Subject: [PATCH 07/77] chore(fix): correct compose version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 618b17fa..4207204e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=official kotlin.version=1.9.20 -compose.version=1.5.10 +compose.version=1.6.2 From 33697587a6018008bcff973969a688ee1f674b52 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Wed, 1 May 2024 11:43:26 +0300 Subject: [PATCH 08/77] dev: add model draft --- .../main/kotlin/model/graph/DirectedGraph.kt | 38 ++++++++++++++++++ app/src/main/kotlin/model/graph/Edge.kt | 6 +++ app/src/main/kotlin/model/graph/Graph.kt | 10 +++++ .../kotlin/model/graph/UndirectedGraph.kt | 39 +++++++++++++++++++ app/src/main/kotlin/model/graph/Vertex.kt | 5 +++ 5 files changed, 98 insertions(+) create mode 100644 app/src/main/kotlin/model/graph/DirectedGraph.kt create mode 100644 app/src/main/kotlin/model/graph/Edge.kt create mode 100644 app/src/main/kotlin/model/graph/Graph.kt create mode 100644 app/src/main/kotlin/model/graph/UndirectedGraph.kt create mode 100644 app/src/main/kotlin/model/graph/Vertex.kt diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt new file mode 100644 index 00000000..7ead8c27 --- /dev/null +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -0,0 +1,38 @@ +package model.graph + +class DirectedEdge(vertex1: V, vertex2: V) : Edge() { + override val vertices: Pair, Vertex> = Pair(Vertex(vertex1), Vertex(vertex2)) +} + +class DirectedGraph: Graph() { + val adjacencyMap: MutableMap, ArrayList?>> = mutableMapOf() + private val edges: MutableSet> = mutableSetOf() + + override fun addEdge(node1: V, node2: V) { + val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { + addVertex(node1) + adjacencyMap.keys.find { it.data == node1 } + } + val vertex2 = adjacencyMap.keys.find { it.data == node2 } ?: run { + addVertex(node2) + adjacencyMap.keys.find { it.data == node2 } + } + + // val edge = UndirectedEdge(vertex1, vertex2) + adjacencyMap[vertex1]?.add(vertex2) + } + + override fun addVertex(value: V) { + val vertex = Vertex(value) + adjacencyMap[vertex] = ArrayList() + } + + override fun getEdges(): List?> { + return adjacencyMap.values.flatten() + } + + override fun getVertices(): List> { + return adjacencyMap.keys.toList() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/model/graph/Edge.kt b/app/src/main/kotlin/model/graph/Edge.kt new file mode 100644 index 00000000..8f32371e --- /dev/null +++ b/app/src/main/kotlin/model/graph/Edge.kt @@ -0,0 +1,6 @@ +package model.graph + +interface Edge { + val vertex1: Vertex? + val vertex2: Vertex? +} diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/graph/Graph.kt new file mode 100644 index 00000000..ec4f2f4b --- /dev/null +++ b/app/src/main/kotlin/model/graph/Graph.kt @@ -0,0 +1,10 @@ +package model.graph + +abstract class Graph{ + //val adjacencyMap: MutableMap, ArrayList> + + abstract fun addVertex(value: V) + abstract fun addEdge(node1: V, node2: V) + abstract fun getVertices(): List> + abstract fun getEdges(): List +} diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt new file mode 100644 index 00000000..d2aa2926 --- /dev/null +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -0,0 +1,39 @@ +package model.graph + +class UndirectedEdge(override val vertex1: Vertex< V>?, override val vertex2: Vertex< V>?) : Edge< V> + +class UndirectedGraph: Graph>() { + val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + private val edges: MutableSet> = mutableSetOf() + private var currentId: Int = 0 + + override fun addEdge(node1: V, node2: V) { + val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { + addVertex(node1) + adjacencyMap.keys.find { it.data == node1 } + } + val vertex2 = adjacencyMap.keys.find { it.data == node2 } ?: run { + addVertex(node2) + adjacencyMap.keys.find { it.data == node2 } + } + + val edge = UndirectedEdge(vertex1, vertex2) + + adjacencyMap[vertex1]?.add(edge) + adjacencyMap[vertex2]?.add(edge) + } + + override fun addVertex(value: V) { + val vertex = Vertex(currentId, value) + currentId++ + adjacencyMap[vertex] = ArrayList() + } + + override fun getEdges(): List> { + return adjacencyMap.values.flatten() + } + + override fun getVertices(): List> { + return adjacencyMap.keys.toList() + } +} diff --git a/app/src/main/kotlin/model/graph/Vertex.kt b/app/src/main/kotlin/model/graph/Vertex.kt new file mode 100644 index 00000000..167c097a --- /dev/null +++ b/app/src/main/kotlin/model/graph/Vertex.kt @@ -0,0 +1,5 @@ +package model.graph + +class Vertex(val id: Int, val data: V) { +} + From c527315c241372f1bc0dc4500a3e83a664006f44 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Wed, 1 May 2024 17:56:20 +0300 Subject: [PATCH 09/77] fix: switch edge data to non-nullable --- app/src/main/kotlin/model/graph/Edge.kt | 6 +++--- app/src/main/kotlin/model/graph/Vertex.kt | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/model/graph/Edge.kt b/app/src/main/kotlin/model/graph/Edge.kt index 8f32371e..deee6e88 100644 --- a/app/src/main/kotlin/model/graph/Edge.kt +++ b/app/src/main/kotlin/model/graph/Edge.kt @@ -1,6 +1,6 @@ package model.graph -interface Edge { - val vertex1: Vertex? - val vertex2: Vertex? +interface Edge { + val vertex1: Vertex + val vertex2: Vertex } diff --git a/app/src/main/kotlin/model/graph/Vertex.kt b/app/src/main/kotlin/model/graph/Vertex.kt index 167c097a..4dc7d949 100644 --- a/app/src/main/kotlin/model/graph/Vertex.kt +++ b/app/src/main/kotlin/model/graph/Vertex.kt @@ -1,5 +1,4 @@ package model.graph -class Vertex(val id: Int, val data: V) { -} +class Vertex(val id: ULong, val data: D) From ec06f57e62c67eaaf3fea67c69a9783d646280de Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Wed, 1 May 2024 17:57:28 +0300 Subject: [PATCH 10/77] feat: switch from abstract class to interface, due to adjacencyMap --- app/src/main/kotlin/model/graph/Graph.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/graph/Graph.kt index ec4f2f4b..18e7a775 100644 --- a/app/src/main/kotlin/model/graph/Graph.kt +++ b/app/src/main/kotlin/model/graph/Graph.kt @@ -1,10 +1,9 @@ package model.graph -abstract class Graph{ - //val adjacencyMap: MutableMap, ArrayList> - - abstract fun addVertex(value: V) - abstract fun addEdge(node1: V, node2: V) - abstract fun getVertices(): List> - abstract fun getEdges(): List +interface Graph{ + val currentId: ULong + fun addVertex(value: D) + fun addEdge(node1: D, node2: D) + fun getVertices(): List> + fun getEdges(): List> } From 33a5ea8d3ea7c6068e211cc72d7676a860be38fa Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Wed, 1 May 2024 18:00:16 +0300 Subject: [PATCH 11/77] fix: remove EdgeType refinement. small corrections --- .../kotlin/model/graph/UndirectedGraph.kt | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt index d2aa2926..560d697f 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,13 +1,13 @@ package model.graph -class UndirectedEdge(override val vertex1: Vertex< V>?, override val vertex2: Vertex< V>?) : Edge< V> +open class UndirectedEdge(override val vertex1: Vertex< D>, override val vertex2: Vertex< D>) : Edge< D> -class UndirectedGraph: Graph>() { - val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - private val edges: MutableSet> = mutableSetOf() - private var currentId: Int = 0 +class UndirectedGraph: Graph { + private val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + private val edges: MutableSet> = mutableSetOf() + override var currentId: ULong = 0u - override fun addEdge(node1: V, node2: V) { + override fun addEdge(node1: D, node2: D) { val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { addVertex(node1) adjacencyMap.keys.find { it.data == node1 } @@ -16,24 +16,23 @@ class UndirectedGraph: Graph>() { addVertex(node2) adjacencyMap.keys.find { it.data == node2 } } - - val edge = UndirectedEdge(vertex1, vertex2) - - adjacencyMap[vertex1]?.add(edge) - adjacencyMap[vertex2]?.add(edge) + if (vertex1 != null && vertex2 != null) { + val edge = UndirectedEdge(vertex1, vertex2) + adjacencyMap[vertex1]?.add(edge) + adjacencyMap[vertex2]?.add(edge) + } } - override fun addVertex(value: V) { - val vertex = Vertex(currentId, value) - currentId++ + override fun addVertex(value: D) { + val vertex = Vertex(currentId++, value) adjacencyMap[vertex] = ArrayList() } - override fun getEdges(): List> { + override fun getEdges(): List> { return adjacencyMap.values.flatten() } - override fun getVertices(): List> { + override fun getVertices(): List> { return adjacencyMap.keys.toList() } } From 64be1ee722b637c203f29c5dfd457d078814ecd0 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Wed, 1 May 2024 18:01:18 +0300 Subject: [PATCH 12/77] fix: correct addEdge method, +refactor --- .../main/kotlin/model/graph/DirectedGraph.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt index 7ead8c27..b425aa18 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,14 +1,13 @@ package model.graph -class DirectedEdge(vertex1: V, vertex2: V) : Edge() { - override val vertices: Pair, Vertex> = Pair(Vertex(vertex1), Vertex(vertex2)) -} +class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge -class DirectedGraph: Graph() { - val adjacencyMap: MutableMap, ArrayList?>> = mutableMapOf() - private val edges: MutableSet> = mutableSetOf() +class DirectedGraph: Graph { + private val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + private val edges: MutableSet> = mutableSetOf() + override var currentId: ULong = 0u - override fun addEdge(node1: V, node2: V) { + override fun addEdge(node1: D, node2: D) { val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { addVertex(node1) adjacencyMap.keys.find { it.data == node1 } @@ -17,21 +16,22 @@ class DirectedGraph: Graph() { addVertex(node2) adjacencyMap.keys.find { it.data == node2 } } - - // val edge = UndirectedEdge(vertex1, vertex2) - adjacencyMap[vertex1]?.add(vertex2) + if (vertex1 != null && vertex2 != null) { + val edge = DirectedEdge(vertex1, vertex2) + adjacencyMap[vertex1]?.add(edge) + } } - override fun addVertex(value: V) { - val vertex = Vertex(value) + override fun addVertex(value: D) { + val vertex = Vertex(currentId++, value) adjacencyMap[vertex] = ArrayList() } - override fun getEdges(): List?> { - return adjacencyMap.values.flatten() + override fun getEdges(): List> { + TODO() } - override fun getVertices(): List> { + override fun getVertices(): List> { return adjacencyMap.keys.toList() } From 9fa884a5b774332f0085c265a3c362607e129ffc Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Wed, 1 May 2024 18:02:18 +0300 Subject: [PATCH 13/77] feat: implement weighted + undirected graph, add specific edge class --- .../model/graph/WeightedUndirectedGraph.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt diff --git a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt new file mode 100644 index 00000000..5e91cac5 --- /dev/null +++ b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt @@ -0,0 +1,44 @@ +package model.graph + +class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : UndirectedEdge(vertex1, vertex2) + +class WeightedUndirectedGraph: Graph { // its a sub-class of undirected so mb inherit from it + private val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + private val edges: MutableSet> = mutableSetOf() + override var currentId: ULong = 0u + + + override fun getVertices(): List> { + TODO("Not yet implemented") + } + + override fun getEdges(): List> { + TODO("Not yet implemented") + } + + override fun addEdge(node1: D, node2: D) { + TODO("Not yet implemented") + } + + fun addEdge(node1: D, node2: D, weight: Int) { + val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { + addVertex(node1) + adjacencyMap.keys.find { it.data == node1 } + } + val vertex2 = adjacencyMap.keys.find { it.data == node2 } ?: run { + addVertex(node2) + adjacencyMap.keys.find { it.data == node2 } + } + + if (vertex1 != null && vertex2 != null) { + val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) + adjacencyMap[vertex1]?.add(newEdge) + adjacencyMap[vertex2]?.add(newEdge) + } + } + + override fun addVertex(value: D) { + val vertex = Vertex(currentId++, value) + adjacencyMap[vertex] = ArrayList() + } +} \ No newline at end of file From 51613f39a79904ad85a7fd410714f870858f1bbe Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Wed, 1 May 2024 20:57:41 +0300 Subject: [PATCH 14/77] feat(Graph): change to abstract class, add properties, deletion methods and method implementations Change graph to abstract class from interface. Add adjacencyMap and edges properties. Add deleteVertex and deleteEdge methods. Add implementation for addVertex, getVertices, getEdges methods. --- app/src/main/kotlin/model/graph/Graph.kt | 42 ++++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/graph/Graph.kt index 18e7a775..8a6f655d 100644 --- a/app/src/main/kotlin/model/graph/Graph.kt +++ b/app/src/main/kotlin/model/graph/Graph.kt @@ -1,9 +1,39 @@ package model.graph -interface Graph{ - val currentId: ULong - fun addVertex(value: D) - fun addEdge(node1: D, node2: D) - fun getVertices(): List> - fun getEdges(): List> +abstract class Graph { + val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + val edges: MutableSet> = mutableSetOf() + var currentId = 0uL + // TODO("add visibility modifiers for properties") + fun addVertex(data: D) { + val newVertex = Vertex(currentId++, data) + adjacencyMap[newVertex] = ArrayList() + } + + fun deleteVertex(vertexToDelete: Vertex): D? { + val adjacentVertices = adjacencyMap[vertexToDelete] ?: return null + for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToDelete) + adjacencyMap.remove(vertexToDelete) + + return vertexToDelete.data + } + + abstract fun addEdge(vertex1: Vertex, vertex2: Vertex) + + fun deleteEdge(edgeToDelete: Edge): Edge? { + if (edgeToDelete !in edges) return null + + val vertex1 = edgeToDelete.vertex1 + val vertex2 = edgeToDelete.vertex2 + + adjacencyMap[vertex1]?.remove(vertex2) + adjacencyMap[vertex2]?.remove(vertex1) + edges.remove(edgeToDelete) + + return edgeToDelete + } + + fun getVertices() = adjacencyMap.keys.toList() + + fun getEdges() = edges.toList() } From 32acd19c4181d9e6ae1d7438c9f190ce5fddf6f6 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Wed, 1 May 2024 21:38:23 +0300 Subject: [PATCH 15/77] fix: change all types of graphs and add addEdge method implementation for them Fix undirected and directed, weighted and unweighted graphs, add addEdge method implementation for them. Add new line at the end of all files --- .../main/kotlin/model/graph/DirectedGraph.kt | 44 +++++----------- app/src/main/kotlin/model/graph/Graph.kt | 2 +- .../kotlin/model/graph/UndirectedGraph.kt | 43 +++++----------- app/src/main/kotlin/model/graph/Vertex.kt | 1 - .../model/graph/WeightedDirectedGraph.kt | 19 +++++++ .../model/graph/WeightedUndirectedGraph.kt | 51 +++++-------------- 6 files changed, 57 insertions(+), 103 deletions(-) create mode 100644 app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt index b425aa18..e84c9eec 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,38 +1,18 @@ package model.graph -class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge +open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : + Edge -class DirectedGraph: Graph { - private val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - private val edges: MutableSet> = mutableSetOf() - override var currentId: ULong = 0u +open class DirectedGraph: Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): DirectedEdge? { + if (vertex1 == vertex2 || + vertex1 !in adjacencyMap.keys || + vertex2 !in adjacencyMap.keys + ) return null - override fun addEdge(node1: D, node2: D) { - val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { - addVertex(node1) - adjacencyMap.keys.find { it.data == node1 } - } - val vertex2 = adjacencyMap.keys.find { it.data == node2 } ?: run { - addVertex(node2) - adjacencyMap.keys.find { it.data == node2 } - } - if (vertex1 != null && vertex2 != null) { - val edge = DirectedEdge(vertex1, vertex2) - adjacencyMap[vertex1]?.add(edge) - } - } - - override fun addVertex(value: D) { - val vertex = Vertex(currentId++, value) - adjacencyMap[vertex] = ArrayList() - } + val newEdge = DirectedEdge(vertex1, vertex2) + adjacencyMap[vertex1]?.add(vertex2) - override fun getEdges(): List> { - TODO() + return newEdge } - - override fun getVertices(): List> { - return adjacencyMap.keys.toList() - } - -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/graph/Graph.kt index 8a6f655d..210c67dc 100644 --- a/app/src/main/kotlin/model/graph/Graph.kt +++ b/app/src/main/kotlin/model/graph/Graph.kt @@ -18,7 +18,7 @@ abstract class Graph { return vertexToDelete.data } - abstract fun addEdge(vertex1: Vertex, vertex2: Vertex) + abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge? fun deleteEdge(edgeToDelete: Edge): Edge? { if (edgeToDelete !in edges) return null diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt index 560d697f..54a1a3f1 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,38 +1,19 @@ package model.graph -open class UndirectedEdge(override val vertex1: Vertex< D>, override val vertex2: Vertex< D>) : Edge< D> +open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : + Edge -class UndirectedGraph: Graph { - private val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - private val edges: MutableSet> = mutableSetOf() - override var currentId: ULong = 0u +open class UndirectedGraph : Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): UndirectedEdge? { + if (vertex1 == vertex2 || + vertex1 !in adjacencyMap.keys || + vertex2 !in adjacencyMap.keys + ) return null - override fun addEdge(node1: D, node2: D) { - val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { - addVertex(node1) - adjacencyMap.keys.find { it.data == node1 } - } - val vertex2 = adjacencyMap.keys.find { it.data == node2 } ?: run { - addVertex(node2) - adjacencyMap.keys.find { it.data == node2 } - } - if (vertex1 != null && vertex2 != null) { - val edge = UndirectedEdge(vertex1, vertex2) - adjacencyMap[vertex1]?.add(edge) - adjacencyMap[vertex2]?.add(edge) - } - } - - override fun addVertex(value: D) { - val vertex = Vertex(currentId++, value) - adjacencyMap[vertex] = ArrayList() - } - - override fun getEdges(): List> { - return adjacencyMap.values.flatten() - } + val newEdge = UndirectedEdge(vertex1, vertex2) + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) - override fun getVertices(): List> { - return adjacencyMap.keys.toList() + return newEdge } } diff --git a/app/src/main/kotlin/model/graph/Vertex.kt b/app/src/main/kotlin/model/graph/Vertex.kt index 4dc7d949..33496d7f 100644 --- a/app/src/main/kotlin/model/graph/Vertex.kt +++ b/app/src/main/kotlin/model/graph/Vertex.kt @@ -1,4 +1,3 @@ package model.graph class Vertex(val id: ULong, val data: D) - diff --git a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt new file mode 100644 index 00000000..0f97ae1c --- /dev/null +++ b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt @@ -0,0 +1,19 @@ +package model.graph + +class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : + DirectedEdge(vertex1, vertex2) + +class WeightedDirectedGraph : DirectedGraph() { + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedDirectedEdge? { + if (vertex1 == vertex2 || + vertex1 !in adjacencyMap.keys || + vertex2 !in adjacencyMap.keys + ) return null + + val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) + + return newEdge + } +} diff --git a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt index 5e91cac5..1204a743 100644 --- a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt @@ -1,44 +1,19 @@ package model.graph -class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : UndirectedEdge(vertex1, vertex2) +class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : + UndirectedEdge(vertex1, vertex2) -class WeightedUndirectedGraph: Graph { // its a sub-class of undirected so mb inherit from it - private val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - private val edges: MutableSet> = mutableSetOf() - override var currentId: ULong = 0u +class WeightedUndirectedGraph : UndirectedGraph() { + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedUndirectedEdge? { + if (vertex1 == vertex2 || + vertex1 !in adjacencyMap.keys || + vertex2 !in adjacencyMap.keys + ) return null + val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) - override fun getVertices(): List> { - TODO("Not yet implemented") + return newEdge } - - override fun getEdges(): List> { - TODO("Not yet implemented") - } - - override fun addEdge(node1: D, node2: D) { - TODO("Not yet implemented") - } - - fun addEdge(node1: D, node2: D, weight: Int) { - val vertex1 = adjacencyMap.keys.find { it.data == node1 } ?: run { - addVertex(node1) - adjacencyMap.keys.find { it.data == node1 } - } - val vertex2 = adjacencyMap.keys.find { it.data == node2 } ?: run { - addVertex(node2) - adjacencyMap.keys.find { it.data == node2 } - } - - if (vertex1 != null && vertex2 != null) { - val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) - adjacencyMap[vertex1]?.add(newEdge) - adjacencyMap[vertex2]?.add(newEdge) - } - } - - override fun addVertex(value: D) { - val vertex = Vertex(currentId++, value) - adjacencyMap[vertex] = ArrayList() - } -} \ No newline at end of file +} From b647069cc98b61e82f61bb317faa32053dede1d7 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Wed, 1 May 2024 23:02:01 +0300 Subject: [PATCH 16/77] fix(WeightedDirectedGraph): remove extra addition of vertex1 to vertex2's adjacent vertices --- app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt index 0f97ae1c..6d7bebcd 100644 --- a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt @@ -12,7 +12,6 @@ class WeightedDirectedGraph : DirectedGraph() { val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) adjacencyMap[vertex1]?.add(vertex2) - adjacencyMap[vertex2]?.add(vertex1) return newEdge } From 076f3d3932f569a8ecb06a8bb2ecc2a96bc35cf5 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Thu, 2 May 2024 00:11:54 +0300 Subject: [PATCH 17/77] fix: add adding newly created edge to edges set Add adding newly created edge to edges set in all graphs. Add visibility modifiers to abstract graph's properties. Change deleteVertex method's return value to vertex. --- app/src/main/kotlin/model/graph/DirectedGraph.kt | 1 + app/src/main/kotlin/model/graph/Graph.kt | 12 ++++++------ app/src/main/kotlin/model/graph/UndirectedGraph.kt | 1 + .../main/kotlin/model/graph/WeightedDirectedGraph.kt | 1 + .../kotlin/model/graph/WeightedUndirectedGraph.kt | 1 + 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt index e84c9eec..c5cb3963 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -11,6 +11,7 @@ open class DirectedGraph: Graph() { ) return null val newEdge = DirectedEdge(vertex1, vertex2) + edges.add(newEdge) adjacencyMap[vertex1]?.add(vertex2) return newEdge diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/graph/Graph.kt index 210c67dc..6aaf3fbc 100644 --- a/app/src/main/kotlin/model/graph/Graph.kt +++ b/app/src/main/kotlin/model/graph/Graph.kt @@ -1,21 +1,21 @@ package model.graph abstract class Graph { - val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - val edges: MutableSet> = mutableSetOf() - var currentId = 0uL - // TODO("add visibility modifiers for properties") + protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + protected val edges: MutableSet> = mutableSetOf() + private var currentId = 0uL + fun addVertex(data: D) { val newVertex = Vertex(currentId++, data) adjacencyMap[newVertex] = ArrayList() } - fun deleteVertex(vertexToDelete: Vertex): D? { + fun deleteVertex(vertexToDelete: Vertex): Vertex? { val adjacentVertices = adjacencyMap[vertexToDelete] ?: return null for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToDelete) adjacencyMap.remove(vertexToDelete) - return vertexToDelete.data + return vertexToDelete } abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge? diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt index 54a1a3f1..8701e2fb 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -11,6 +11,7 @@ open class UndirectedGraph : Graph() { ) return null val newEdge = UndirectedEdge(vertex1, vertex2) + edges.add(newEdge) adjacencyMap[vertex1]?.add(vertex2) adjacencyMap[vertex2]?.add(vertex1) diff --git a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt index 6d7bebcd..d4f1c67e 100644 --- a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt @@ -11,6 +11,7 @@ class WeightedDirectedGraph : DirectedGraph() { ) return null val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) + edges.add(newEdge) adjacencyMap[vertex1]?.add(vertex2) return newEdge diff --git a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt index 1204a743..0b54b0b9 100644 --- a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt @@ -11,6 +11,7 @@ class WeightedUndirectedGraph : UndirectedGraph() { ) return null val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) + edges.add(newEdge) adjacencyMap[vertex1]?.add(vertex2) adjacencyMap[vertex2]?.add(vertex1) From 744d665d60f48bb1cc2947a9eb85d3628fdeadd6 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Thu, 2 May 2024 10:45:00 +0300 Subject: [PATCH 18/77] fix: add weight parameter to the Edge interface + set the default value of weight to 0 in unweighted graphs --- app/src/main/kotlin/model/graph/DirectedGraph.kt | 2 +- app/src/main/kotlin/model/graph/Edge.kt | 1 + app/src/main/kotlin/model/graph/UndirectedGraph.kt | 2 +- app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt | 2 +- app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt index c5cb3963..f1148470 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : +open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex, override val weight: Int = 0) : Edge open class DirectedGraph: Graph() { diff --git a/app/src/main/kotlin/model/graph/Edge.kt b/app/src/main/kotlin/model/graph/Edge.kt index deee6e88..e520e9d2 100644 --- a/app/src/main/kotlin/model/graph/Edge.kt +++ b/app/src/main/kotlin/model/graph/Edge.kt @@ -3,4 +3,5 @@ package model.graph interface Edge { val vertex1: Vertex val vertex2: Vertex + val weight: Int } diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt index 8701e2fb..721cc10f 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : +open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex, override val weight: Int = 0) : Edge open class UndirectedGraph : Graph() { diff --git a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt index d4f1c67e..f31be187 100644 --- a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : +class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, override val weight: Int) : DirectedEdge(vertex1, vertex2) class WeightedDirectedGraph : DirectedGraph() { diff --git a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt index 0b54b0b9..ebfeeb27 100644 --- a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : +class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, override val weight: Int) : UndirectedEdge(vertex1, vertex2) class WeightedUndirectedGraph : UndirectedGraph() { From 349fc7c6101ed15432c40357568f130f08a3fd59 Mon Sep 17 00:00:00 2001 From: qruty <64466788+qrutyy@users.noreply.github.com> Date: Thu, 2 May 2024 11:08:52 +0300 Subject: [PATCH 19/77] fix: make 'deleteEdge' method abstract Due to different implementations of 'deleteEdge' method, it cant be defined in abstract as common, therefore, added its implementation in 'Undirected' & 'Directed' graph classess --- app/src/main/kotlin/model/graph/DirectedGraph.kt | 12 ++++++++++++ app/src/main/kotlin/model/graph/Graph.kt | 14 +------------- app/src/main/kotlin/model/graph/UndirectedGraph.kt | 13 +++++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt index f1148470..b52af184 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -16,4 +16,16 @@ open class DirectedGraph: Graph() { return newEdge } + + override fun deleteEdge(edgeToDelete: Edge): Edge? { + if (edgeToDelete !in edges) return null + + val vertex1 = edgeToDelete.vertex1 + val vertex2 = edgeToDelete.vertex2 + + adjacencyMap[vertex1]?.remove(vertex2) + edges.remove(edgeToDelete) + + return edgeToDelete + } } diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/graph/Graph.kt index 6aaf3fbc..b0a4c0f0 100644 --- a/app/src/main/kotlin/model/graph/Graph.kt +++ b/app/src/main/kotlin/model/graph/Graph.kt @@ -20,19 +20,7 @@ abstract class Graph { abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge? - fun deleteEdge(edgeToDelete: Edge): Edge? { - if (edgeToDelete !in edges) return null - - val vertex1 = edgeToDelete.vertex1 - val vertex2 = edgeToDelete.vertex2 - - adjacencyMap[vertex1]?.remove(vertex2) - adjacencyMap[vertex2]?.remove(vertex1) - edges.remove(edgeToDelete) - - return edgeToDelete - } - + abstract fun deleteEdge(edgeToDelete: Edge): Edge? fun getVertices() = adjacencyMap.keys.toList() fun getEdges() = edges.toList() diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt index 721cc10f..5d573ae7 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -17,4 +17,17 @@ open class UndirectedGraph : Graph() { return newEdge } + + override fun deleteEdge(edgeToDelete: Edge): Edge? { + if (edgeToDelete !in edges) return null + + val vertex1 = edgeToDelete.vertex1 + val vertex2 = edgeToDelete.vertex2 + + adjacencyMap[vertex1]?.remove(vertex2) + adjacencyMap[vertex2]?.remove(vertex1) + edges.remove(edgeToDelete) + + return edgeToDelete + } } From 080dd2dfe3482f36f328a9dbd3c0cc31549de3fb Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Thu, 2 May 2024 13:32:51 +0300 Subject: [PATCH 20/77] fix: remove weight field from edge interface, add it to weighted edges Remove weight field from edge interface and unweighted edges, add it only to weighted edges. implement not implemented parent class method addEdge without weight parameter in weighted graphs, where weight is set to default value. --- app/src/main/kotlin/model/graph/DirectedGraph.kt | 2 +- app/src/main/kotlin/model/graph/Edge.kt | 1 - app/src/main/kotlin/model/graph/UndirectedGraph.kt | 2 +- app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt | 7 ++++++- app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt | 7 ++++++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt index b52af184..52333e44 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex, override val weight: Int = 0) : +open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge open class DirectedGraph: Graph() { diff --git a/app/src/main/kotlin/model/graph/Edge.kt b/app/src/main/kotlin/model/graph/Edge.kt index e520e9d2..deee6e88 100644 --- a/app/src/main/kotlin/model/graph/Edge.kt +++ b/app/src/main/kotlin/model/graph/Edge.kt @@ -3,5 +3,4 @@ package model.graph interface Edge { val vertex1: Vertex val vertex2: Vertex - val weight: Int } diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt index 5d573ae7..d762722b 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex, override val weight: Int = 0) : +open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge open class UndirectedGraph : Graph() { diff --git a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt index f31be187..34530c95 100644 --- a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, override val weight: Int) : +class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : DirectedEdge(vertex1, vertex2) class WeightedDirectedGraph : DirectedGraph() { @@ -16,4 +16,9 @@ class WeightedDirectedGraph : DirectedGraph() { return newEdge } + + /* + * In case weight is not passed, set it to default value = 1 + */ + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) } diff --git a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt index ebfeeb27..b4cf32fc 100644 --- a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, override val weight: Int) : +class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : UndirectedEdge(vertex1, vertex2) class WeightedUndirectedGraph : UndirectedGraph() { @@ -17,4 +17,9 @@ class WeightedUndirectedGraph : UndirectedGraph() { return newEdge } + + /* + * In case weight is not passed, set it to default value = 1 + */ + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) } From 362f6e78d8859183305a9ed6faa6fadae2b1f01f Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Thu, 2 May 2024 13:35:45 +0300 Subject: [PATCH 21/77] refactor(deleteEdge): rename to removeEdge --- app/src/main/kotlin/model/graph/DirectedGraph.kt | 12 ++++++------ app/src/main/kotlin/model/graph/Graph.kt | 3 ++- app/src/main/kotlin/model/graph/UndirectedGraph.kt | 12 ++++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/graph/DirectedGraph.kt index 52333e44..c1467cfb 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/DirectedGraph.kt @@ -17,15 +17,15 @@ open class DirectedGraph: Graph() { return newEdge } - override fun deleteEdge(edgeToDelete: Edge): Edge? { - if (edgeToDelete !in edges) return null + override fun removeEdge(edgeToRemove: Edge): Edge? { + if (edgeToRemove !in edges) return null - val vertex1 = edgeToDelete.vertex1 - val vertex2 = edgeToDelete.vertex2 + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 adjacencyMap[vertex1]?.remove(vertex2) - edges.remove(edgeToDelete) + edges.remove(edgeToRemove) - return edgeToDelete + return edgeToRemove } } diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/graph/Graph.kt index b0a4c0f0..66a84490 100644 --- a/app/src/main/kotlin/model/graph/Graph.kt +++ b/app/src/main/kotlin/model/graph/Graph.kt @@ -20,7 +20,8 @@ abstract class Graph { abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge? - abstract fun deleteEdge(edgeToDelete: Edge): Edge? + abstract fun removeEdge(edgeToRemove: Edge): Edge? + fun getVertices() = adjacencyMap.keys.toList() fun getEdges() = edges.toList() diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/graph/UndirectedGraph.kt index d762722b..c1669623 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -18,16 +18,16 @@ open class UndirectedGraph : Graph() { return newEdge } - override fun deleteEdge(edgeToDelete: Edge): Edge? { - if (edgeToDelete !in edges) return null + override fun removeEdge(edgeToRemove: Edge): Edge? { + if (edgeToRemove !in edges) return null - val vertex1 = edgeToDelete.vertex1 - val vertex2 = edgeToDelete.vertex2 + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 adjacencyMap[vertex1]?.remove(vertex2) adjacencyMap[vertex2]?.remove(vertex1) - edges.remove(edgeToDelete) + edges.remove(edgeToRemove) - return edgeToDelete + return edgeToRemove } } From 9f760a89f31f0c5f65dcc7d5d34084b256da179d Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Thu, 2 May 2024 19:15:33 +0300 Subject: [PATCH 22/77] chore: change packages structure --- app/src/main/kotlin/model/{graph => }/DirectedGraph.kt | 8 +++++--- app/src/main/kotlin/model/{graph => }/UndirectedGraph.kt | 8 +++++--- .../kotlin/model/{graph => }/WeightedDirectedGraph.kt | 6 +++--- .../kotlin/model/{graph => }/WeightedUndirectedGraph.kt | 6 +++--- .../main/kotlin/model/{graph => abstractGraph}/Edge.kt | 2 +- .../main/kotlin/model/{graph => abstractGraph}/Graph.kt | 2 +- .../main/kotlin/model/{graph => abstractGraph}/Vertex.kt | 2 +- app/src/main/kotlin/model/edges/DirectedEdge.kt | 7 +++++++ app/src/main/kotlin/model/edges/UndirectedEdge.kt | 7 +++++++ app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt | 6 ++++++ app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt | 6 ++++++ 11 files changed, 45 insertions(+), 15 deletions(-) rename app/src/main/kotlin/model/{graph => }/DirectedGraph.kt (83%) rename app/src/main/kotlin/model/{graph => }/UndirectedGraph.kt (85%) rename app/src/main/kotlin/model/{graph => }/WeightedDirectedGraph.kt (81%) rename app/src/main/kotlin/model/{graph => }/WeightedUndirectedGraph.kt (82%) rename app/src/main/kotlin/model/{graph => abstractGraph}/Edge.kt (73%) rename app/src/main/kotlin/model/{graph => abstractGraph}/Graph.kt (96%) rename app/src/main/kotlin/model/{graph => abstractGraph}/Vertex.kt (61%) create mode 100644 app/src/main/kotlin/model/edges/DirectedEdge.kt create mode 100644 app/src/main/kotlin/model/edges/UndirectedEdge.kt create mode 100644 app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt create mode 100644 app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt diff --git a/app/src/main/kotlin/model/graph/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt similarity index 83% rename from app/src/main/kotlin/model/graph/DirectedGraph.kt rename to app/src/main/kotlin/model/DirectedGraph.kt index c1467cfb..24fe154e 100644 --- a/app/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -1,7 +1,9 @@ -package model.graph +package model -open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : - Edge +import model.abstractGraph.Edge +import model.abstractGraph.Graph +import model.abstractGraph.Vertex +import model.edges.DirectedEdge open class DirectedGraph: Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): DirectedEdge? { diff --git a/app/src/main/kotlin/model/graph/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt similarity index 85% rename from app/src/main/kotlin/model/graph/UndirectedGraph.kt rename to app/src/main/kotlin/model/UndirectedGraph.kt index c1669623..543ea93c 100644 --- a/app/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -1,7 +1,9 @@ -package model.graph +package model -open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : - Edge +import model.abstractGraph.Edge +import model.abstractGraph.Graph +import model.abstractGraph.Vertex +import model.edges.UndirectedEdge open class UndirectedGraph : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): UndirectedEdge? { diff --git a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt similarity index 81% rename from app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt rename to app/src/main/kotlin/model/WeightedDirectedGraph.kt index 34530c95..4d315245 100644 --- a/app/src/main/kotlin/model/graph/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -1,7 +1,7 @@ -package model.graph +package model -class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : - DirectedEdge(vertex1, vertex2) +import model.abstractGraph.Vertex +import model.edges.WeightedDirectedEdge class WeightedDirectedGraph : DirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedDirectedEdge? { diff --git a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt similarity index 82% rename from app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt rename to app/src/main/kotlin/model/WeightedUndirectedGraph.kt index b4cf32fc..3295f5df 100644 --- a/app/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -1,7 +1,7 @@ -package model.graph +package model -class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : - UndirectedEdge(vertex1, vertex2) +import model.abstractGraph.Vertex +import model.edges.WeightedUndirectedEdge class WeightedUndirectedGraph : UndirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedUndirectedEdge? { diff --git a/app/src/main/kotlin/model/graph/Edge.kt b/app/src/main/kotlin/model/abstractGraph/Edge.kt similarity index 73% rename from app/src/main/kotlin/model/graph/Edge.kt rename to app/src/main/kotlin/model/abstractGraph/Edge.kt index deee6e88..d17227cf 100644 --- a/app/src/main/kotlin/model/graph/Edge.kt +++ b/app/src/main/kotlin/model/abstractGraph/Edge.kt @@ -1,4 +1,4 @@ -package model.graph +package model.abstractGraph interface Edge { val vertex1: Vertex diff --git a/app/src/main/kotlin/model/graph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt similarity index 96% rename from app/src/main/kotlin/model/graph/Graph.kt rename to app/src/main/kotlin/model/abstractGraph/Graph.kt index 66a84490..b2cc8047 100644 --- a/app/src/main/kotlin/model/graph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -1,4 +1,4 @@ -package model.graph +package model.abstractGraph abstract class Graph { protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() diff --git a/app/src/main/kotlin/model/graph/Vertex.kt b/app/src/main/kotlin/model/abstractGraph/Vertex.kt similarity index 61% rename from app/src/main/kotlin/model/graph/Vertex.kt rename to app/src/main/kotlin/model/abstractGraph/Vertex.kt index 33496d7f..7c8d36bb 100644 --- a/app/src/main/kotlin/model/graph/Vertex.kt +++ b/app/src/main/kotlin/model/abstractGraph/Vertex.kt @@ -1,3 +1,3 @@ -package model.graph +package model.abstractGraph class Vertex(val id: ULong, val data: D) diff --git a/app/src/main/kotlin/model/edges/DirectedEdge.kt b/app/src/main/kotlin/model/edges/DirectedEdge.kt new file mode 100644 index 00000000..9e77e133 --- /dev/null +++ b/app/src/main/kotlin/model/edges/DirectedEdge.kt @@ -0,0 +1,7 @@ +package model.edges + +import model.abstractGraph.Edge +import model.abstractGraph.Vertex + +open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : + Edge diff --git a/app/src/main/kotlin/model/edges/UndirectedEdge.kt b/app/src/main/kotlin/model/edges/UndirectedEdge.kt new file mode 100644 index 00000000..4fd26568 --- /dev/null +++ b/app/src/main/kotlin/model/edges/UndirectedEdge.kt @@ -0,0 +1,7 @@ +package model.edges + +import model.abstractGraph.Edge +import model.abstractGraph.Vertex + +open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : + Edge diff --git a/app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt b/app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt new file mode 100644 index 00000000..222edc77 --- /dev/null +++ b/app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt @@ -0,0 +1,6 @@ +package model.edges + +import model.abstractGraph.Vertex + +class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : + DirectedEdge(vertex1, vertex2) diff --git a/app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt b/app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt new file mode 100644 index 00000000..42f5bdd3 --- /dev/null +++ b/app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt @@ -0,0 +1,6 @@ +package model.edges + +import model.abstractGraph.Vertex + +class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : + UndirectedEdge(vertex1, vertex2) From 6f5a283f03edd6d9bb2e00d783041e1fc0a85f68 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Thu, 2 May 2024 19:29:53 +0300 Subject: [PATCH 23/77] feat: add exceptions for illegal arguments instead of returning null + add return of a new vertex in addVertex method --- app/src/main/kotlin/model/DirectedGraph.kt | 14 +++++++------- app/src/main/kotlin/model/UndirectedGraph.kt | 14 +++++++------- app/src/main/kotlin/model/WeightedDirectedGraph.kt | 10 +++++----- .../main/kotlin/model/WeightedUndirectedGraph.kt | 10 +++++----- app/src/main/kotlin/model/abstractGraph/Graph.kt | 14 +++++++++----- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 24fe154e..d71199c2 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -6,11 +6,11 @@ import model.abstractGraph.Vertex import model.edges.DirectedEdge open class DirectedGraph: Graph() { - override fun addEdge(vertex1: Vertex, vertex2: Vertex): DirectedEdge? { - if (vertex1 == vertex2 || - vertex1 !in adjacencyMap.keys || - vertex2 !in adjacencyMap.keys - ) return null + override fun addEdge(vertex1: Vertex, vertex2: Vertex): DirectedEdge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") val newEdge = DirectedEdge(vertex1, vertex2) edges.add(newEdge) @@ -19,8 +19,8 @@ open class DirectedGraph: Graph() { return newEdge } - override fun removeEdge(edgeToRemove: Edge): Edge? { - if (edgeToRemove !in edges) return null + override fun removeEdge(edgeToRemove: Edge): Edge { + if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index 543ea93c..b76964fc 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -6,11 +6,11 @@ import model.abstractGraph.Vertex import model.edges.UndirectedEdge open class UndirectedGraph : Graph() { - override fun addEdge(vertex1: Vertex, vertex2: Vertex): UndirectedEdge? { - if (vertex1 == vertex2 || - vertex1 !in adjacencyMap.keys || - vertex2 !in adjacencyMap.keys - ) return null + override fun addEdge(vertex1: Vertex, vertex2: Vertex): UndirectedEdge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") val newEdge = UndirectedEdge(vertex1, vertex2) edges.add(newEdge) @@ -20,8 +20,8 @@ open class UndirectedGraph : Graph() { return newEdge } - override fun removeEdge(edgeToRemove: Edge): Edge? { - if (edgeToRemove !in edges) return null + override fun removeEdge(edgeToRemove: Edge): Edge { + if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 4d315245..3b350790 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -4,11 +4,11 @@ import model.abstractGraph.Vertex import model.edges.WeightedDirectedEdge class WeightedDirectedGraph : DirectedGraph() { - fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedDirectedEdge? { - if (vertex1 == vertex2 || - vertex1 !in adjacencyMap.keys || - vertex2 !in adjacencyMap.keys - ) return null + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedDirectedEdge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) edges.add(newEdge) diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 3295f5df..312cd663 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -4,11 +4,11 @@ import model.abstractGraph.Vertex import model.edges.WeightedUndirectedEdge class WeightedUndirectedGraph : UndirectedGraph() { - fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedUndirectedEdge? { - if (vertex1 == vertex2 || - vertex1 !in adjacencyMap.keys || - vertex2 !in adjacencyMap.keys - ) return null + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedUndirectedEdge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) edges.add(newEdge) diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index b2cc8047..71e6962f 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -5,22 +5,26 @@ abstract class Graph { protected val edges: MutableSet> = mutableSetOf() private var currentId = 0uL - fun addVertex(data: D) { + fun addVertex(data: D): Vertex { val newVertex = Vertex(currentId++, data) adjacencyMap[newVertex] = ArrayList() + + return newVertex } - fun deleteVertex(vertexToDelete: Vertex): Vertex? { - val adjacentVertices = adjacencyMap[vertexToDelete] ?: return null + fun deleteVertex(vertexToDelete: Vertex): Vertex { + val adjacentVertices = adjacencyMap[vertexToDelete] + ?: throw IllegalArgumentException("Vertex is not in the graph") + for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToDelete) adjacencyMap.remove(vertexToDelete) return vertexToDelete } - abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge? + abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge - abstract fun removeEdge(edgeToRemove: Edge): Edge? + abstract fun removeEdge(edgeToRemove: Edge): Edge fun getVertices() = adjacencyMap.keys.toList() From f10816f103966c7c51e328bb46f23cd57ba85a8e Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Sat, 4 May 2024 12:15:42 +0300 Subject: [PATCH 24/77] feat: add ktfmt gradle plugin and script for pre-commit hook #4 * chore: add ktfmt plugin to gradle * style: apply ktfmt formatting * feat: add pre-commit hook script * fix: ktfmtCheckMain gradle task dependency --- app/build.gradle.kts | 11 ++++++++++ app/src/main/kotlin/Main.kt | 14 ++----------- app/src/main/kotlin/model/DirectedGraph.kt | 5 ++--- app/src/main/kotlin/model/UndirectedGraph.kt | 3 +-- .../kotlin/model/WeightedDirectedGraph.kt | 3 +-- .../kotlin/model/WeightedUndirectedGraph.kt | 3 +-- .../main/kotlin/model/abstractGraph/Graph.kt | 4 ++-- .../main/kotlin/model/edges/DirectedEdge.kt | 3 +-- .../main/kotlin/model/edges/UndirectedEdge.kt | 3 +-- scripts/pre-commit | 21 +++++++++++++++++++ 10 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 scripts/pre-commit diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b2cd64e2..7f428ea5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,8 @@ plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.compose) + + id("com.ncorti.ktfmt.gradle") version "0.18.0" } dependencies { @@ -14,3 +16,12 @@ compose.desktop { mainClass = "MainKt" } } + +ktfmt { + dropboxStyle() + maxWidth.set(120) +} + +tasks.named("ktfmtCheckMain") { + dependsOn("generateComposeResClass") +} diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt index fed3f24a..dcae2cf1 100644 --- a/app/src/main/kotlin/Main.kt +++ b/app/src/main/kotlin/Main.kt @@ -15,17 +15,7 @@ import androidx.compose.ui.window.application fun App() { var text by remember { mutableStateOf("Hello, World!") } - MaterialTheme { - Button(onClick = { - text = "Hello, Desktop!" - }) { - Text(text) - } - } + MaterialTheme { Button(onClick = { text = "Hello, Desktop!" }) { Text(text) } } } -fun main() = application { - Window(onCloseRequest = ::exitApplication) { - App() - } -} +fun main() = application { Window(onCloseRequest = ::exitApplication) { App() } } diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index d71199c2..2e77e2d1 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -5,10 +5,9 @@ import model.abstractGraph.Graph import model.abstractGraph.Vertex import model.edges.DirectedEdge -open class DirectedGraph: Graph() { +open class DirectedGraph : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): DirectedEdge { - if (vertex1 == vertex2) - throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index b76964fc..7643d46c 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -7,8 +7,7 @@ import model.edges.UndirectedEdge open class UndirectedGraph : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): UndirectedEdge { - if (vertex1 == vertex2) - throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 3b350790..6c355283 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -5,8 +5,7 @@ import model.edges.WeightedDirectedEdge class WeightedDirectedGraph : DirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedDirectedEdge { - if (vertex1 == vertex2) - throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 312cd663..5c545217 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -5,8 +5,7 @@ import model.edges.WeightedUndirectedEdge class WeightedUndirectedGraph : UndirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedUndirectedEdge { - if (vertex1 == vertex2) - throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 71e6962f..d926b585 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -13,8 +13,8 @@ abstract class Graph { } fun deleteVertex(vertexToDelete: Vertex): Vertex { - val adjacentVertices = adjacencyMap[vertexToDelete] - ?: throw IllegalArgumentException("Vertex is not in the graph") + val adjacentVertices = + adjacencyMap[vertexToDelete] ?: throw IllegalArgumentException("Vertex is not in the graph") for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToDelete) adjacencyMap.remove(vertexToDelete) diff --git a/app/src/main/kotlin/model/edges/DirectedEdge.kt b/app/src/main/kotlin/model/edges/DirectedEdge.kt index 9e77e133..2a39f856 100644 --- a/app/src/main/kotlin/model/edges/DirectedEdge.kt +++ b/app/src/main/kotlin/model/edges/DirectedEdge.kt @@ -3,5 +3,4 @@ package model.edges import model.abstractGraph.Edge import model.abstractGraph.Vertex -open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : - Edge +open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge diff --git a/app/src/main/kotlin/model/edges/UndirectedEdge.kt b/app/src/main/kotlin/model/edges/UndirectedEdge.kt index 4fd26568..97ff0344 100644 --- a/app/src/main/kotlin/model/edges/UndirectedEdge.kt +++ b/app/src/main/kotlin/model/edges/UndirectedEdge.kt @@ -3,5 +3,4 @@ package model.edges import model.abstractGraph.Edge import model.abstractGraph.Vertex -open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : - Edge +open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100644 index 00000000..283565df --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,21 @@ +#!/bin/bash + +bold=$(tput bold) +normal=$(tput sgr0) + +echo "Running git pre-commit hook" + +./gradlew ktfmtCheck + +checkStatus=$? + +if [ $checkStatus -ne 0 ] +then + echo "To apply formatter run ${bold}./gradlew ktfmtFormat${normal}" + echo "Don't forget to ${bold}git add${normal} all formatted files (java gnomik is watching you)" + exit 1 +else + exit 0 +fi + + From c74c3811e2c8735ff4f196b40c6e5e8ee7ac87a3 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Sat, 4 May 2024 14:05:05 +0300 Subject: [PATCH 25/77] fix: return type of graphs' methods by edge parameterization (#5) * refactor: change Edge from interface to abstract class * fix: add edge generic type to abstract graph class fix of a problem that methods return wrong edge type * fix: abstract edge class initialization in inheritors after change from interface * refactor: add wrap layer classes for more convenient usage --- app/src/main/kotlin/model/DirectedGraph.kt | 30 ++--------------- app/src/main/kotlin/model/UndirectedGraph.kt | 32 ++---------------- .../kotlin/model/WeightedDirectedGraph.kt | 21 ++---------- .../kotlin/model/WeightedUndirectedGraph.kt | 22 ++----------- .../main/kotlin/model/abstractGraph/Edge.kt | 6 ++-- .../main/kotlin/model/abstractGraph/Graph.kt | 8 ++--- .../main/kotlin/model/edges/DirectedEdge.kt | 2 +- .../main/kotlin/model/edges/UndirectedEdge.kt | 2 +- .../model/internalGraphs/_DirectedGraph.kt | 31 +++++++++++++++++ .../model/internalGraphs/_UndirectedGraph.kt | 33 +++++++++++++++++++ .../internalGraphs/_WeightedDirectedGraph.kt | 23 +++++++++++++ .../_WeightedUndirectedGraph.kt | 24 ++++++++++++++ 12 files changed, 128 insertions(+), 106 deletions(-) create mode 100644 app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt create mode 100644 app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt create mode 100644 app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt create mode 100644 app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 2e77e2d1..94df9686 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -1,32 +1,6 @@ package model -import model.abstractGraph.Edge -import model.abstractGraph.Graph -import model.abstractGraph.Vertex import model.edges.DirectedEdge +import model.internalGraphs._DirectedGraph -open class DirectedGraph : Graph() { - override fun addEdge(vertex1: Vertex, vertex2: Vertex): DirectedEdge { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") - - val newEdge = DirectedEdge(vertex1, vertex2) - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - - return newEdge - } - - override fun removeEdge(edgeToRemove: Edge): Edge { - if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") - - val vertex1 = edgeToRemove.vertex1 - val vertex2 = edgeToRemove.vertex2 - - adjacencyMap[vertex1]?.remove(vertex2) - edges.remove(edgeToRemove) - - return edgeToRemove - } -} +class DirectedGraph : _DirectedGraph>() diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index 7643d46c..cdc139e0 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -1,34 +1,6 @@ package model -import model.abstractGraph.Edge -import model.abstractGraph.Graph -import model.abstractGraph.Vertex import model.edges.UndirectedEdge +import model.internalGraphs._UndirectedGraph -open class UndirectedGraph : Graph() { - override fun addEdge(vertex1: Vertex, vertex2: Vertex): UndirectedEdge { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") - - val newEdge = UndirectedEdge(vertex1, vertex2) - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - adjacencyMap[vertex2]?.add(vertex1) - - return newEdge - } - - override fun removeEdge(edgeToRemove: Edge): Edge { - if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") - - val vertex1 = edgeToRemove.vertex1 - val vertex2 = edgeToRemove.vertex2 - - adjacencyMap[vertex1]?.remove(vertex2) - adjacencyMap[vertex2]?.remove(vertex1) - edges.remove(edgeToRemove) - - return edgeToRemove - } -} +class UndirectedGraph : _UndirectedGraph>() diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 6c355283..8d6b8572 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -1,23 +1,6 @@ package model -import model.abstractGraph.Vertex import model.edges.WeightedDirectedEdge +import model.internalGraphs._WeightedDirectedGraph -class WeightedDirectedGraph : DirectedGraph() { - fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedDirectedEdge { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") - - val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - - return newEdge - } - - /* - * In case weight is not passed, set it to default value = 1 - */ - override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) -} +class WeightedDirectedGraph : _WeightedDirectedGraph>() diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 5c545217..5ef9d38b 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -1,24 +1,6 @@ package model -import model.abstractGraph.Vertex import model.edges.WeightedUndirectedEdge +import model.internalGraphs._WeightedUndirectedGraph -class WeightedUndirectedGraph : UndirectedGraph() { - fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): WeightedUndirectedEdge { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") - - val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - adjacencyMap[vertex2]?.add(vertex1) - - return newEdge - } - - /* - * In case weight is not passed, set it to default value = 1 - */ - override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) -} +class WeightedUndirectedGraph : _WeightedUndirectedGraph>() diff --git a/app/src/main/kotlin/model/abstractGraph/Edge.kt b/app/src/main/kotlin/model/abstractGraph/Edge.kt index d17227cf..0b722e7c 100644 --- a/app/src/main/kotlin/model/abstractGraph/Edge.kt +++ b/app/src/main/kotlin/model/abstractGraph/Edge.kt @@ -1,6 +1,6 @@ package model.abstractGraph -interface Edge { - val vertex1: Vertex - val vertex2: Vertex +abstract class Edge { + abstract val vertex1: Vertex + abstract val vertex2: Vertex } diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index d926b585..56012c66 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -1,8 +1,8 @@ package model.abstractGraph -abstract class Graph { +abstract class Graph> { protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - protected val edges: MutableSet> = mutableSetOf() + protected val edges: MutableSet = mutableSetOf() private var currentId = 0uL fun addVertex(data: D): Vertex { @@ -22,9 +22,9 @@ abstract class Graph { return vertexToDelete } - abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge + abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): E - abstract fun removeEdge(edgeToRemove: Edge): Edge + abstract fun removeEdge(edgeToRemove: E): E fun getVertices() = adjacencyMap.keys.toList() diff --git a/app/src/main/kotlin/model/edges/DirectedEdge.kt b/app/src/main/kotlin/model/edges/DirectedEdge.kt index 2a39f856..5cdc1779 100644 --- a/app/src/main/kotlin/model/edges/DirectedEdge.kt +++ b/app/src/main/kotlin/model/edges/DirectedEdge.kt @@ -3,4 +3,4 @@ package model.edges import model.abstractGraph.Edge import model.abstractGraph.Vertex -open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge +open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge() diff --git a/app/src/main/kotlin/model/edges/UndirectedEdge.kt b/app/src/main/kotlin/model/edges/UndirectedEdge.kt index 97ff0344..dec59131 100644 --- a/app/src/main/kotlin/model/edges/UndirectedEdge.kt +++ b/app/src/main/kotlin/model/edges/UndirectedEdge.kt @@ -3,4 +3,4 @@ package model.edges import model.abstractGraph.Edge import model.abstractGraph.Vertex -open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge +open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge() diff --git a/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt new file mode 100644 index 00000000..fa74fb82 --- /dev/null +++ b/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt @@ -0,0 +1,31 @@ +package model.internalGraphs + +import model.abstractGraph.Graph +import model.abstractGraph.Vertex +import model.edges.DirectedEdge + +abstract class _DirectedGraph> : Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): E { + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + + val newEdge = DirectedEdge(vertex1, vertex2) as E + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + + return newEdge + } + + override fun removeEdge(edgeToRemove: E): E { + if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") + + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 + + adjacencyMap[vertex1]?.remove(vertex2) + edges.remove(edgeToRemove) + + return edgeToRemove + } +} diff --git a/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt new file mode 100644 index 00000000..f807f50d --- /dev/null +++ b/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt @@ -0,0 +1,33 @@ +package model.internalGraphs + +import model.abstractGraph.Graph +import model.abstractGraph.Vertex +import model.edges.UndirectedEdge + +abstract class _UndirectedGraph> : Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): E { + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + + val newEdge = UndirectedEdge(vertex1, vertex2) as E + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) + + return newEdge + } + + override fun removeEdge(edgeToRemove: E): E { + if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") + + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 + + adjacencyMap[vertex1]?.remove(vertex2) + adjacencyMap[vertex2]?.remove(vertex1) + edges.remove(edgeToRemove) + + return edgeToRemove + } +} diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt new file mode 100644 index 00000000..19f41f4e --- /dev/null +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt @@ -0,0 +1,23 @@ +package model.internalGraphs + +import model.abstractGraph.Vertex +import model.edges.WeightedDirectedEdge + +abstract class _WeightedDirectedGraph> : _DirectedGraph() { + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + + val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) as E + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + + return newEdge + } + + /* + * In case weight is not passed, set it to default value = 1 + */ + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) +} diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt new file mode 100644 index 00000000..46e7d4a6 --- /dev/null +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt @@ -0,0 +1,24 @@ +package model.internalGraphs + +import model.abstractGraph.Vertex +import model.edges.WeightedUndirectedEdge + +abstract class _WeightedUndirectedGraph> : _UndirectedGraph() { + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { + if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + + val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) as E + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) + + return newEdge + } + + /* + * In case weight is not passed, set it to default value = 1 + */ + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) +} From d81dbc7e3e5086aa55e75670130611ab5af31d36 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Sat, 4 May 2024 17:00:50 +0300 Subject: [PATCH 26/77] feat: implement Dijkstra's shortest path algorithm #7 * feat: implement Dijkstra's shortest path algo * fix: add path reconstruction * fix: change methods name to distinguish the algorithms --- .../internalGraphs/_WeightedDirectedGraph.kt | 46 ++++++++++++++++++ .../_WeightedUndirectedGraph.kt | 48 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt index 19f41f4e..8a710b75 100644 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt @@ -2,6 +2,7 @@ package model.internalGraphs import model.abstractGraph.Vertex import model.edges.WeightedDirectedEdge +import java.util.* abstract class _WeightedDirectedGraph> : _DirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { @@ -20,4 +21,49 @@ abstract class _WeightedDirectedGraph> : _Directe * In case weight is not passed, set it to default value = 1 */ override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, E>> { + val vertices = getVertices() + val distanceMap = mutableMapOf, Int>().withDefault{ Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } + val visited = mutableSetOf, Int>>() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (node, currentDistance) = priorityQueue.poll() + if (visited.add(node to currentDistance)) { + adjacencyMap[node]?.forEach{ adjacent -> + val currentEdge = edges.find { it.vertex1 == adjacent } + currentEdge?.let { + val totalDist = currentDistance + it.weight + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = node // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, E>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + if (predecessor == null) { + // If no path exists + return emptyList() + } + if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } == null) { + throw IllegalArgumentException("Edge is not in the graph, path cannot be reconstructed.") + } + path.add(Pair(currentVertex, edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex }) + as Pair, E>) + currentVertex = predecessor + } + return path.reversed() + } } diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt index 46e7d4a6..497546f5 100644 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt @@ -2,6 +2,7 @@ package model.internalGraphs import model.abstractGraph.Vertex import model.edges.WeightedUndirectedEdge +import java.util.* abstract class _WeightedUndirectedGraph> : _UndirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { @@ -21,4 +22,51 @@ abstract class _WeightedUndirectedGraph> : _Und * In case weight is not passed, set it to default value = 1 */ override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, E>> { + val vertices = getVertices() + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } + val visited = mutableSetOf, Int>>() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (node, currentDistance) = priorityQueue.poll() + if (visited.add(node to currentDistance)) { + adjacencyMap[node]?.forEach { adjacent -> + val currentEdge = edges.find { (it.vertex1 == adjacent && it.vertex2 == node) || + (it.vertex1 == node && it.vertex2 == adjacent) } + currentEdge?.let { + val totalDist = currentDistance + currentEdge.weight + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = node // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, E>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + if (predecessor == null) { + // If no path exists + return emptyList() + } + if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex || + it.vertex2 == predecessor && it.vertex1 == currentVertex } == null) { + throw IllegalArgumentException("Edge is not in the graph, path cannot be reconstructed.") + } + path.add(Pair(currentVertex, edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex || + it.vertex2 == predecessor && it.vertex1 == currentVertex}) as Pair, E>) + currentVertex = predecessor + } + return path.reversed() + } } From 839b9dd819efc368a752a8387eef9d69948c1ac9 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Mon, 6 May 2024 22:42:20 +0300 Subject: [PATCH 27/77] feat: implement Kosaraju's SCC algorithm #8 * feat: implement Kosaraju's SCC algo * fix: edit return value of SCC * fix: switch stack type from mutable-list to array-deque * refactor: apply formatter, remove redundant parenthesis --- .../model/internalGraphs/_DirectedGraph.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt index fa74fb82..def639d5 100644 --- a/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt @@ -28,4 +28,52 @@ abstract class _DirectedGraph> : Graph() { return edgeToRemove } + + fun findSCC(): ArrayList>> { // SCC - Strongly Connected Components (by Kosaraju) + val visited = mutableMapOf, Boolean>().withDefault { false } + val stack = ArrayDeque>() + val component = arrayListOf>() + val sccList: ArrayList>> = arrayListOf() + + fun auxuiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { + visited[srcVertex] = true + componentList.add(srcVertex) + adjacencyMap[srcVertex]?.forEach { vertex2 -> + if (visited[vertex2] != null && visited[vertex2] != true) { + auxuiliaryDFS(vertex2, componentList) + } + } + stack.add(srcVertex) + } + + for (vertex in adjacencyMap.keys) { + if (visited[vertex] != null && visited[vertex] != true) auxuiliaryDFS(vertex, component) + } + + reverseGraph() + visited.clear() + component.clear() + + while (stack.isNotEmpty()) { + val vertex = stack.removeLast() + if (visited[vertex] != null && visited[vertex] != true) { + val currentComponent = arrayListOf>() + auxuiliaryDFS(vertex, currentComponent) + sccList.add(currentComponent) + } + } + return sccList + } + + private fun reverseGraph() { + val reversedAdjacencyMap = mutableMapOf, ArrayList>>() + for (vertex in adjacencyMap.keys) { + adjacencyMap[vertex]?.forEach { vertex2 -> + reversedAdjacencyMap[vertex2] = reversedAdjacencyMap[vertex2] ?: ArrayList() + reversedAdjacencyMap[vertex2]?.add(vertex) + } + } + adjacencyMap.clear() + adjacencyMap.putAll(reversedAdjacencyMap) + } } From e3581421507810314acc9b418bde006b280caa45 Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Tue, 7 May 2024 13:52:15 +0300 Subject: [PATCH 28/77] feat: implement Kruskal's MST algorithm #10 * fix: change vertex id type from ULong to Int * feat: implement Kruskal's MST algorithm --- .../main/kotlin/model/abstractGraph/Graph.kt | 2 +- .../main/kotlin/model/abstractGraph/Vertex.kt | 2 +- .../_WeightedUndirectedGraph.kt | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 56012c66..23a451b1 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -3,7 +3,7 @@ package model.abstractGraph abstract class Graph> { protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() protected val edges: MutableSet = mutableSetOf() - private var currentId = 0uL + private var currentId = 0 fun addVertex(data: D): Vertex { val newVertex = Vertex(currentId++, data) diff --git a/app/src/main/kotlin/model/abstractGraph/Vertex.kt b/app/src/main/kotlin/model/abstractGraph/Vertex.kt index 7c8d36bb..9038bed7 100644 --- a/app/src/main/kotlin/model/abstractGraph/Vertex.kt +++ b/app/src/main/kotlin/model/abstractGraph/Vertex.kt @@ -1,3 +1,3 @@ package model.abstractGraph -class Vertex(val id: ULong, val data: D) +class Vertex(val id: Int, val data: D) diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt index 497546f5..82b5228b 100644 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt @@ -69,4 +69,53 @@ abstract class _WeightedUndirectedGraph> : _Und } return path.reversed() } + + fun findMinSpanningTree(): List { + val vertexIdList = mutableListOf() + for (v in getVertices()) vertexIdList.add(v.id) + + val graphSize = vertexIdList.size + + // set each vertex parent to be itself and each vertex rank to 0 + val parentList = MutableList(graphSize) { index: Int -> vertexIdList[index] } + val rankList = MutableList(graphSize) { 0 } + + fun findTreeRootId(vId: Int): Int { + if (parentList[vId] == vId) return vId + + parentList[vId] = findTreeRootId(parentList[vId]) + + return parentList[vId] + } + + fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { + val rootId1 = findTreeRootId(vId1) + val rootId2 = findTreeRootId(vId2) + + if (rootId1 == rootId2) return + + if (rankList[rootId1] < rankList[rootId2]) { + parentList[rootId1] = rootId2 + } else { + parentList[rootId2] = rootId1 + if (rankList[rootId1] == rankList[rootId2]) rankList[rootId1]++ + } + } + + val sortedEdges = edges.sortedBy { it.weight } + + val chosenEdges = mutableListOf() + + for (edge in sortedEdges) { + val id1 = edge.vertex1.id + val id2 = edge.vertex2.id + + if (findTreeRootId(id1) != findTreeRootId(id2)) { + uniteTwoTreesByVerticesIds(id1, id2) + chosenEdges.add(edge) + } + } + + return chosenEdges + } } From 6d92a679aa3e302d0320fb55b6105c84fb21c47a Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Tue, 7 May 2024 20:58:29 +0300 Subject: [PATCH 29/77] fix: id fragmentation in removeVertex method #11 * fix: remove incident edges after vertex removal + add isIncident method to abstract edge class and apply formatting * refactor(deleteVertex): rename to removeVertex * fix(removeVertex): id fragmentation add private method that removes vertex from all adjacent vertices and incident edges, add private method that changes last added vertex's id to removed vertex's id --- .../main/kotlin/model/abstractGraph/Edge.kt | 2 + .../main/kotlin/model/abstractGraph/Graph.kt | 44 ++++++++++++++++--- .../internalGraphs/_WeightedDirectedGraph.kt | 29 ++++++------ .../_WeightedUndirectedGraph.kt | 25 ++++++++--- 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/model/abstractGraph/Edge.kt b/app/src/main/kotlin/model/abstractGraph/Edge.kt index 0b722e7c..a6cbe392 100644 --- a/app/src/main/kotlin/model/abstractGraph/Edge.kt +++ b/app/src/main/kotlin/model/abstractGraph/Edge.kt @@ -3,4 +3,6 @@ package model.abstractGraph abstract class Edge { abstract val vertex1: Vertex abstract val vertex2: Vertex + + fun isIncident(vertex: Vertex) = (vertex1 == vertex || vertex2 == vertex) } diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 23a451b1..3f728533 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -12,14 +12,13 @@ abstract class Graph> { return newVertex } - fun deleteVertex(vertexToDelete: Vertex): Vertex { - val adjacentVertices = - adjacencyMap[vertexToDelete] ?: throw IllegalArgumentException("Vertex is not in the graph") + fun removeVertex(vertexToRemove: Vertex): Vertex { + removeVertexFromIncidentEdgesAndAdjacentVerticesMapValues(vertexToRemove) + fixIdFragmentation(vertexToRemove) - for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToDelete) - adjacencyMap.remove(vertexToDelete) + adjacencyMap.remove(vertexToRemove) - return vertexToDelete + return vertexToRemove } abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): E @@ -29,4 +28,37 @@ abstract class Graph> { fun getVertices() = adjacencyMap.keys.toList() fun getEdges() = edges.toList() + + private fun fixIdFragmentation(vertexToRemove: Vertex) { + currentId-- + val lastAddedVertex = + getVertices().find { it.id == currentId } ?: throw NoSuchElementException("No vertex with id $currentId") + + val copyOfLastAddedVertex = Vertex(vertexToRemove.id, lastAddedVertex.data) + adjacencyMap[copyOfLastAddedVertex] = + adjacencyMap[lastAddedVertex] ?: throw NoSuchElementException("No vertex with id $currentId") + + val adjacentVertices = + adjacencyMap[copyOfLastAddedVertex] ?: throw NoSuchElementException("No vertex with id $currentId") + + for (adjacentVertex in adjacentVertices) { + if (adjacencyMap[adjacentVertex]?.remove(lastAddedVertex) ?: false) { + adjacencyMap[adjacentVertex]?.add(copyOfLastAddedVertex) + } + } + + removeVertexFromIncidentEdgesAndAdjacentVerticesMapValues(lastAddedVertex) + adjacencyMap.remove(lastAddedVertex) + } + + private fun removeVertexFromIncidentEdgesAndAdjacentVerticesMapValues(vertexToRemove: Vertex) { + val adjacentVertices = + adjacencyMap[vertexToRemove] ?: throw NoSuchElementException("Vertex is not in the graph") + + for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToRemove) + + for (edge in getEdges()) { + if (edge.isIncident(vertexToRemove)) edges.remove(edge) + } + } } diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt index 8a710b75..4d6adf62 100644 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt @@ -1,8 +1,8 @@ package model.internalGraphs +import java.util.* import model.abstractGraph.Vertex import model.edges.WeightedDirectedEdge -import java.util.* abstract class _WeightedDirectedGraph> : _DirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { @@ -24,7 +24,7 @@ abstract class _WeightedDirectedGraph> : _Directe fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, E>> { val vertices = getVertices() - val distanceMap = mutableMapOf, Int>().withDefault{ Int.MAX_VALUE } + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } val visited = mutableSetOf, Int>>() @@ -34,7 +34,7 @@ abstract class _WeightedDirectedGraph> : _Directe while (priorityQueue.isNotEmpty()) { val (node, currentDistance) = priorityQueue.poll() if (visited.add(node to currentDistance)) { - adjacencyMap[node]?.forEach{ adjacent -> + adjacencyMap[node]?.forEach { adjacent -> val currentEdge = edges.find { it.vertex1 == adjacent } currentEdge?.let { val totalDist = currentDistance + it.weight @@ -51,17 +51,18 @@ abstract class _WeightedDirectedGraph> : _Directe // Reconstruct the path from srcVertex to destVertex val path: MutableList, E>> = mutableListOf() var currentVertex = destVertex - while (currentVertex != srcVertex) { - val predecessor = predecessorMap[currentVertex] - if (predecessor == null) { - // If no path exists - return emptyList() - } - if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } == null) { - throw IllegalArgumentException("Edge is not in the graph, path cannot be reconstructed.") - } - path.add(Pair(currentVertex, edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex }) - as Pair, E>) + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + if (predecessor == null) { + // If no path exists + return emptyList() + } + if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } == null) { + throw IllegalArgumentException("Edge is not in the graph, path cannot be reconstructed.") + } + path.add( + Pair(currentVertex, edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex }) + as Pair, E>) currentVertex = predecessor } return path.reversed() diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt index 82b5228b..d4954e33 100644 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt @@ -1,8 +1,8 @@ package model.internalGraphs +import java.util.* import model.abstractGraph.Vertex import model.edges.WeightedUndirectedEdge -import java.util.* abstract class _WeightedUndirectedGraph> : _UndirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { @@ -36,8 +36,11 @@ abstract class _WeightedUndirectedGraph> : _Und val (node, currentDistance) = priorityQueue.poll() if (visited.add(node to currentDistance)) { adjacencyMap[node]?.forEach { adjacent -> - val currentEdge = edges.find { (it.vertex1 == adjacent && it.vertex2 == node) || - (it.vertex1 == node && it.vertex2 == adjacent) } + val currentEdge = + edges.find { + (it.vertex1 == adjacent && it.vertex2 == node) || + (it.vertex1 == node && it.vertex2 == adjacent) + } currentEdge?.let { val totalDist = currentDistance + currentEdge.weight if (totalDist < distanceMap.getValue(adjacent)) { @@ -59,12 +62,20 @@ abstract class _WeightedUndirectedGraph> : _Und // If no path exists return emptyList() } - if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex || - it.vertex2 == predecessor && it.vertex1 == currentVertex } == null) { + if (edges.find { + it.vertex1 == predecessor && it.vertex2 == currentVertex || + it.vertex2 == predecessor && it.vertex1 == currentVertex + } == null) { throw IllegalArgumentException("Edge is not in the graph, path cannot be reconstructed.") } - path.add(Pair(currentVertex, edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex || - it.vertex2 == predecessor && it.vertex1 == currentVertex}) as Pair, E>) + path.add( + Pair( + currentVertex, + edges.find { + it.vertex1 == predecessor && it.vertex2 == currentVertex || + it.vertex2 == predecessor && it.vertex1 == currentVertex + }) + as Pair, E>) currentVertex = predecessor } return path.reversed() From 042ddce5fbc642d023ca43a9c3eca6d990d27a0b Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Sat, 11 May 2024 21:17:01 +0300 Subject: [PATCH 30/77] refactor: localize exceptions, change formatter, make gradle clean #13 * refactor: apply same code style for exceptions, small refactor * refactor: change exception types * feat: add getNeighbours function to localize exceptions * chore: delete pre-commit hook running ktfmtCheck and scripts directory * chore: remove all ktfmt-related tasks, reformat gradle files * chore: change kotlin version to 1.9.22 --- app/build.gradle.kts | 12 ------- .../main/kotlin/model/abstractGraph/Graph.kt | 33 +++++++++++-------- .../model/internalGraphs/_DirectedGraph.kt | 17 ++++++---- .../model/internalGraphs/_UndirectedGraph.kt | 9 +++-- .../internalGraphs/_WeightedDirectedGraph.kt | 9 +++-- .../_WeightedUndirectedGraph.kt | 9 +++-- build.gradle.kts | 2 +- gradle.properties | 2 +- scripts/pre-commit | 21 ------------ settings.gradle.kts | 5 +-- 10 files changed, 50 insertions(+), 69 deletions(-) delete mode 100644 scripts/pre-commit diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7f428ea5..e574840e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,6 @@ plugins { alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.compose) - - id("com.ncorti.ktfmt.gradle") version "0.18.0" } dependencies { @@ -16,12 +13,3 @@ compose.desktop { mainClass = "MainKt" } } - -ktfmt { - dropboxStyle() - maxWidth.set(120) -} - -tasks.named("ktfmtCheckMain") { - dependsOn("generateComposeResClass") -} diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 3f728533..3874954a 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -25,24 +25,19 @@ abstract class Graph> { abstract fun removeEdge(edgeToRemove: E): E - fun getVertices() = adjacencyMap.keys.toList() - - fun getEdges() = edges.toList() - private fun fixIdFragmentation(vertexToRemove: Vertex) { currentId-- - val lastAddedVertex = - getVertices().find { it.id == currentId } ?: throw NoSuchElementException("No vertex with id $currentId") + val lastAddedVertex = getVertices().find { it.id == currentId } + ?: throw NoSuchElementException("Vertex with id $currentId is not present in the adjacency map.") + + val copyOfLastAddedVertex = Vertex(vertexToRemove.id, lastAddedVertex.data) - val copyOfLastAddedVertex = Vertex(vertexToRemove.id, lastAddedVertex.data) - adjacencyMap[copyOfLastAddedVertex] = - adjacencyMap[lastAddedVertex] ?: throw NoSuchElementException("No vertex with id $currentId") + adjacencyMap[copyOfLastAddedVertex] = getNeighbours(lastAddedVertex) - val adjacentVertices = - adjacencyMap[copyOfLastAddedVertex] ?: throw NoSuchElementException("No vertex with id $currentId") + val adjacentVertices = getNeighbours(copyOfLastAddedVertex) for (adjacentVertex in adjacentVertices) { - if (adjacencyMap[adjacentVertex]?.remove(lastAddedVertex) ?: false) { + if (adjacencyMap[adjacentVertex]?.remove(lastAddedVertex) == true) { adjacencyMap[adjacentVertex]?.add(copyOfLastAddedVertex) } } @@ -52,8 +47,7 @@ abstract class Graph> { } private fun removeVertexFromIncidentEdgesAndAdjacentVerticesMapValues(vertexToRemove: Vertex) { - val adjacentVertices = - adjacencyMap[vertexToRemove] ?: throw NoSuchElementException("Vertex is not in the graph") + val adjacentVertices = getNeighbours(vertexToRemove) for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToRemove) @@ -61,4 +55,15 @@ abstract class Graph> { if (edge.isIncident(vertexToRemove)) edges.remove(edge) } } + + fun getVertices() = adjacencyMap.keys.toList() + + fun getEdges() = edges.toList() + + protected fun getNeighbours(vertex: Vertex): ArrayList> { + val neighbours = adjacencyMap[vertex] + ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the adjacency map.") + + return neighbours + } } diff --git a/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt index def639d5..d160df32 100644 --- a/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt @@ -6,9 +6,11 @@ import model.edges.DirectedEdge abstract class _DirectedGraph> : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): E { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") val newEdge = DirectedEdge(vertex1, vertex2) as E edges.add(newEdge) @@ -18,7 +20,8 @@ abstract class _DirectedGraph> : Graph() { } override fun removeEdge(edgeToRemove: E): E { - if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") + if (edgeToRemove !in edges) + throw NoSuchElementException("Edge is not in the graph") val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 @@ -35,19 +38,19 @@ abstract class _DirectedGraph> : Graph() { val component = arrayListOf>() val sccList: ArrayList>> = arrayListOf() - fun auxuiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { + fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { visited[srcVertex] = true componentList.add(srcVertex) adjacencyMap[srcVertex]?.forEach { vertex2 -> if (visited[vertex2] != null && visited[vertex2] != true) { - auxuiliaryDFS(vertex2, componentList) + auxiliaryDFS(vertex2, componentList) } } stack.add(srcVertex) } for (vertex in adjacencyMap.keys) { - if (visited[vertex] != null && visited[vertex] != true) auxuiliaryDFS(vertex, component) + if (visited[vertex] != null && visited[vertex] != true) auxiliaryDFS(vertex, component) } reverseGraph() @@ -58,7 +61,7 @@ abstract class _DirectedGraph> : Graph() { val vertex = stack.removeLast() if (visited[vertex] != null && visited[vertex] != true) { val currentComponent = arrayListOf>() - auxuiliaryDFS(vertex, currentComponent) + auxiliaryDFS(vertex, currentComponent) sccList.add(currentComponent) } } diff --git a/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt index f807f50d..77b43719 100644 --- a/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt @@ -6,9 +6,11 @@ import model.edges.UndirectedEdge abstract class _UndirectedGraph> : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): E { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") val newEdge = UndirectedEdge(vertex1, vertex2) as E edges.add(newEdge) @@ -19,7 +21,8 @@ abstract class _UndirectedGraph> : Graph() { } override fun removeEdge(edgeToRemove: E): E { - if (edgeToRemove !in edges) throw IllegalArgumentException("Edge is not in the graph") + if (edgeToRemove !in edges) + throw NoSuchElementException("Edge is not in the graph.") val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt index 4d6adf62..f9a892d8 100644 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt @@ -3,12 +3,15 @@ package model.internalGraphs import java.util.* import model.abstractGraph.Vertex import model.edges.WeightedDirectedEdge +import kotlin.NoSuchElementException abstract class _WeightedDirectedGraph> : _DirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) as E edges.add(newEdge) @@ -58,7 +61,7 @@ abstract class _WeightedDirectedGraph> : _Directe return emptyList() } if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } == null) { - throw IllegalArgumentException("Edge is not in the graph, path cannot be reconstructed.") + throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") } path.add( Pair(currentVertex, edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex }) diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt index d4954e33..76c7a393 100644 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt @@ -3,12 +3,15 @@ package model.internalGraphs import java.util.* import model.abstractGraph.Vertex import model.edges.WeightedUndirectedEdge +import kotlin.NoSuchElementException abstract class _WeightedUndirectedGraph> : _UndirectedGraph() { fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { - if (vertex1 == vertex2) throw IllegalArgumentException("Vertices are the same") + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw IllegalArgumentException("Vertex1 or vertex2 are not in the graph") + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) as E edges.add(newEdge) @@ -66,7 +69,7 @@ abstract class _WeightedUndirectedGraph> : _Und it.vertex1 == predecessor && it.vertex2 == currentVertex || it.vertex2 == predecessor && it.vertex1 == currentVertex } == null) { - throw IllegalArgumentException("Edge is not in the graph, path cannot be reconstructed.") + throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") } path.add( Pair( diff --git a/build.gradle.kts b/build.gradle.kts index f892991c..8ed64556 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,4 @@ plugins { - kotlin("jvm") version "1.9.20" + kotlin("jvm") version "1.9.22" id("org.jetbrains.compose") version "1.6.2" } diff --git a/gradle.properties b/gradle.properties index 4207204e..27c0bcb5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=official -kotlin.version=1.9.20 +kotlin.version=1.9.22 compose.version=1.6.2 diff --git a/scripts/pre-commit b/scripts/pre-commit deleted file mode 100644 index 283565df..00000000 --- a/scripts/pre-commit +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -bold=$(tput bold) -normal=$(tput sgr0) - -echo "Running git pre-commit hook" - -./gradlew ktfmtCheck - -checkStatus=$? - -if [ $checkStatus -ne 0 ] -then - echo "To apply formatter run ${bold}./gradlew ktfmtFormat${normal}" - echo "Don't forget to ${bold}git add${normal} all formatted files (java gnomik is watching you)" - exit 1 -else - exit 0 -fi - - diff --git a/settings.gradle.kts b/settings.gradle.kts index 06fa12f7..d0815cf5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,13 +17,10 @@ dependencyResolutionManagement { } versionCatalogs { create("libs") { - version("kotlin", "1.9.20") + version("kotlin", "1.9.22") plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").versionRef("kotlin") - plugin("compose", "org.jetbrains.compose").version("1.6.2") - library("koin-core", "io.insert-koin:koin-core:3.5.3") - library("junit-jupiter", "org.junit.jupiter:junit-jupiter:5.10.2") } } From aa77cbbd26eb4cf917917cc51c30030a7c9771be Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Sat, 11 May 2024 23:30:39 +0300 Subject: [PATCH 31/77] fix: get rid of edge type parametrisation and extern layers, fix algorithms #14 * fix: remove abstract class modifier * fix: remove edge parametrization from abstract Graph * refactor: remove trailing comma * fix: resolve unchecked commit * fix: edge sorting in findMinSpanningTree() Co-authored-by: Vlasenco Daniel --- app/src/main/kotlin/model/DirectedGraph.kt | 83 +++++++++- app/src/main/kotlin/model/UndirectedGraph.kt | 37 ++++- .../kotlin/model/WeightedDirectedGraph.kt | 79 +++++++++- .../kotlin/model/WeightedUndirectedGraph.kt | 144 +++++++++++++++++- .../main/kotlin/model/abstractGraph/Edge.kt | 4 +- .../main/kotlin/model/abstractGraph/Graph.kt | 8 +- .../main/kotlin/model/edges/DirectedEdge.kt | 6 - .../main/kotlin/model/edges/UndirectedEdge.kt | 6 - .../model/edges/WeightedDirectedEdge.kt | 6 - .../model/edges/WeightedUndirectedEdge.kt | 6 - .../model/internalGraphs/_DirectedGraph.kt | 82 ---------- .../model/internalGraphs/_UndirectedGraph.kt | 36 ----- .../internalGraphs/_WeightedDirectedGraph.kt | 73 --------- .../_WeightedUndirectedGraph.kt | 135 ---------------- 14 files changed, 336 insertions(+), 369 deletions(-) delete mode 100644 app/src/main/kotlin/model/edges/DirectedEdge.kt delete mode 100644 app/src/main/kotlin/model/edges/UndirectedEdge.kt delete mode 100644 app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt delete mode 100644 app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt delete mode 100644 app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt delete mode 100644 app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt delete mode 100644 app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt delete mode 100644 app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 94df9686..0a3b3092 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -1,6 +1,83 @@ package model -import model.edges.DirectedEdge -import model.internalGraphs._DirectedGraph +import model.abstractGraph.Edge +import model.abstractGraph.Graph +import model.abstractGraph.Vertex + +open class DirectedGraph : Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + + val newEdge = Edge(vertex1, vertex2) as Edge + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + + return newEdge + } + + override fun removeEdge(edgeToRemove: Edge): Edge { + if (edgeToRemove !in edges) + throw NoSuchElementException("Edge is not in the graph") + + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 + + adjacencyMap[vertex1]?.remove(vertex2) + edges.remove(edgeToRemove) + + return edgeToRemove + } + + fun findSCC(): ArrayList>> { // SCC - Strongly Connected Components (by Kosaraju) + val visited = mutableMapOf, Boolean>().withDefault { false } + val stack = ArrayDeque>() + val component = arrayListOf>() + val sccList: ArrayList>> = arrayListOf() + + fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { + visited[srcVertex] = true + componentList.add(srcVertex) + adjacencyMap[srcVertex]?.forEach { vertex2 -> + if (visited[vertex2] != null && visited[vertex2] != true) { + auxiliaryDFS(vertex2, componentList) + } + } + stack.add(srcVertex) + } + + for (vertex in adjacencyMap.keys) { + if (visited[vertex] != null && visited[vertex] != true) auxiliaryDFS(vertex, component) + } + + reverseGraph() + visited.clear() + component.clear() + + while (stack.isNotEmpty()) { + val vertex = stack.removeLast() + if (visited[vertex] != null && visited[vertex] != true) { + val currentComponent = arrayListOf>() + auxiliaryDFS(vertex, currentComponent) + sccList.add(currentComponent) + } + } + return sccList + } + + private fun reverseGraph() { + val reversedAdjacencyMap = mutableMapOf, ArrayList>>() + for (vertex in adjacencyMap.keys) { + adjacencyMap[vertex]?.forEach { vertex2 -> + reversedAdjacencyMap[vertex2] = reversedAdjacencyMap[vertex2] ?: ArrayList() + reversedAdjacencyMap[vertex2]?.add(vertex) + } + } + adjacencyMap.clear() + adjacencyMap.putAll(reversedAdjacencyMap) + } +} -class DirectedGraph : _DirectedGraph>() diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index cdc139e0..4caec81c 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -1,6 +1,37 @@ package model -import model.edges.UndirectedEdge -import model.internalGraphs._UndirectedGraph +import model.abstractGraph.Edge +import model.abstractGraph.Graph +import model.abstractGraph.Vertex + +open class UndirectedGraph : Graph() { + override fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + + val newEdge = Edge(vertex1, vertex2) as Edge + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) + + return newEdge + } + + override fun removeEdge(edgeToRemove: Edge): Edge { + if (edgeToRemove !in edges) + throw NoSuchElementException("Edge is not in the graph.") + + val vertex1 = edgeToRemove.vertex1 + val vertex2 = edgeToRemove.vertex2 + + adjacencyMap[vertex1]?.remove(vertex2) + adjacencyMap[vertex2]?.remove(vertex1) + edges.remove(edgeToRemove) + + return edgeToRemove + } +} -class UndirectedGraph : _UndirectedGraph>() diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 8d6b8572..68392456 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -1,6 +1,79 @@ package model -import model.edges.WeightedDirectedEdge -import model.internalGraphs._WeightedDirectedGraph +import model.abstractGraph.Edge +import model.abstractGraph.Vertex +import java.util.* +import kotlin.NoSuchElementException -class WeightedDirectedGraph : _WeightedDirectedGraph>() +class WeightedDirectedGraph : DirectedGraph() { + + val weightMap: MutableMap, Int> = mutableMapOf() + + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): Edge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + + val newEdge = Edge(vertex1, vertex2) + weightMap[newEdge] = weight + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + + return newEdge + } + + /* + * In case weight is not passed, set it to default value = 1 + */ + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>> { + val vertices = getVertices() + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } + val visited = mutableSetOf, Int>>() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (node, currentDistance) = priorityQueue.poll() + if (visited.add(node to currentDistance)) { + adjacencyMap[node]?.forEach { adjacent -> + val currentEdge = edges.find { it.vertex1 == adjacent } + currentEdge?.let { + var totalDist = currentDistance + totalDist += weightMap[it] ?: throw NoSuchElementException("Current edge doesn't have weight.") + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = node // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, Edge>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + if (predecessor == null) { + // If no path exists + return emptyList() + } + if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } == null) { + throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") + } + path.add( + Pair(currentVertex, + edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } + ?: throw NoSuchElementException("There is no edge between these vertices"))) + currentVertex = predecessor + } + return path.reversed() + } +} diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 5ef9d38b..20797ed5 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -1,6 +1,144 @@ package model -import model.edges.WeightedUndirectedEdge -import model.internalGraphs._WeightedUndirectedGraph +import model.abstractGraph.Edge +import model.abstractGraph.Vertex +import java.util.* +import kotlin.NoSuchElementException -class WeightedUndirectedGraph : _WeightedUndirectedGraph>() +class WeightedUndirectedGraph : UndirectedGraph() { + + val weightMap: MutableMap, Int> = mutableMapOf() + + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): Edge { + if (vertex1 == vertex2) + throw IllegalArgumentException("Can't add edge from vertex to itself.") + + if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) + throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + + val newEdge = Edge(vertex1, vertex2) + weightMap[newEdge] = weight + edges.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) + adjacencyMap[vertex2]?.add(vertex1) + + return newEdge + } + + /* + * In case weight is not passed, set it to default value = 1 + */ + override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>> { + val vertices = getVertices() + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } + val visited = mutableSetOf, Int>>() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (node, currentDistance) = priorityQueue.poll() + if (visited.add(node to currentDistance)) { + adjacencyMap[node]?.forEach { adjacent -> + val currentEdge = + edges.find { + (it.vertex1 == adjacent && it.vertex2 == node) || + (it.vertex1 == node && it.vertex2 == adjacent) + } + currentEdge?.let { + var totalDist = currentDistance + totalDist += weightMap[it] ?: throw NoSuchElementException("Current edge doesn't have weight.") + + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = node // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, Edge>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + + if (predecessor == null) { + // If no path exists + return emptyList() + } + if (edges.find { + it.vertex1 == predecessor && it.vertex2 == currentVertex || + it.vertex2 == predecessor && it.vertex1 == currentVertex + } == null) { + throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") + } + path.add( + Pair( + currentVertex, + edges.find { + it.vertex1 == predecessor && it.vertex2 == currentVertex || + it.vertex2 == predecessor && it.vertex1 == currentVertex + } ?: throw NoSuchElementException("There is no edge between these vertices") ) + ) + currentVertex = predecessor + } + return path.reversed() + } + + fun findMinSpanningTree(): List> { + val vertexIdList = mutableListOf() + for (v in getVertices()) vertexIdList.add(v.id) + + val graphSize = vertexIdList.size + + // set each vertex parent to be itself and each vertex rank to 0 + val parentList = MutableList(graphSize) { index: Int -> vertexIdList[index] } + val rankList = MutableList(graphSize) { 0 } + + fun findTreeRootId(vId: Int): Int { + if (parentList[vId] == vId) return vId + + parentList[vId] = findTreeRootId(parentList[vId]) + + return parentList[vId] + } + + fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { + val rootId1 = findTreeRootId(vId1) + val rootId2 = findTreeRootId(vId2) + + if (rootId1 == rootId2) return + + if (rankList[rootId1] < rankList[rootId2]) { + parentList[rootId1] = rootId2 + } else { + parentList[rootId2] = rootId1 + if (rankList[rootId1] == rankList[rootId2]) rankList[rootId1]++ + } + } + + val edgeWeightPairs = weightMap.toList() + val sortedEdgeWeightPairs = edgeWeightPairs.sortedBy { it.second } + + val chosenEdges = mutableListOf>() + + for ((edge, _) in sortedEdgeWeightPairs) { + val id1 = edge.vertex1.id + val id2 = edge.vertex2.id + + if (findTreeRootId(id1) != findTreeRootId(id2)) { + uniteTwoTreesByVerticesIds(id1, id2) + chosenEdges.add(edge) + } + } + + + return chosenEdges + } +} diff --git a/app/src/main/kotlin/model/abstractGraph/Edge.kt b/app/src/main/kotlin/model/abstractGraph/Edge.kt index a6cbe392..9d98dacc 100644 --- a/app/src/main/kotlin/model/abstractGraph/Edge.kt +++ b/app/src/main/kotlin/model/abstractGraph/Edge.kt @@ -1,8 +1,6 @@ package model.abstractGraph -abstract class Edge { - abstract val vertex1: Vertex - abstract val vertex2: Vertex +class Edge(val vertex1: Vertex, val vertex2: Vertex) { fun isIncident(vertex: Vertex) = (vertex1 == vertex || vertex2 == vertex) } diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 3874954a..c0d27060 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -1,8 +1,8 @@ package model.abstractGraph -abstract class Graph> { +abstract class Graph { protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - protected val edges: MutableSet = mutableSetOf() + protected val edges: MutableSet> = mutableSetOf() private var currentId = 0 fun addVertex(data: D): Vertex { @@ -21,9 +21,9 @@ abstract class Graph> { return vertexToRemove } - abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): E + abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge - abstract fun removeEdge(edgeToRemove: E): E + abstract fun removeEdge(edgeToRemove: Edge): Edge private fun fixIdFragmentation(vertexToRemove: Vertex) { currentId-- diff --git a/app/src/main/kotlin/model/edges/DirectedEdge.kt b/app/src/main/kotlin/model/edges/DirectedEdge.kt deleted file mode 100644 index 5cdc1779..00000000 --- a/app/src/main/kotlin/model/edges/DirectedEdge.kt +++ /dev/null @@ -1,6 +0,0 @@ -package model.edges - -import model.abstractGraph.Edge -import model.abstractGraph.Vertex - -open class DirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge() diff --git a/app/src/main/kotlin/model/edges/UndirectedEdge.kt b/app/src/main/kotlin/model/edges/UndirectedEdge.kt deleted file mode 100644 index dec59131..00000000 --- a/app/src/main/kotlin/model/edges/UndirectedEdge.kt +++ /dev/null @@ -1,6 +0,0 @@ -package model.edges - -import model.abstractGraph.Edge -import model.abstractGraph.Vertex - -open class UndirectedEdge(override val vertex1: Vertex, override val vertex2: Vertex) : Edge() diff --git a/app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt b/app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt deleted file mode 100644 index 222edc77..00000000 --- a/app/src/main/kotlin/model/edges/WeightedDirectedEdge.kt +++ /dev/null @@ -1,6 +0,0 @@ -package model.edges - -import model.abstractGraph.Vertex - -class WeightedDirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : - DirectedEdge(vertex1, vertex2) diff --git a/app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt b/app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt deleted file mode 100644 index 42f5bdd3..00000000 --- a/app/src/main/kotlin/model/edges/WeightedUndirectedEdge.kt +++ /dev/null @@ -1,6 +0,0 @@ -package model.edges - -import model.abstractGraph.Vertex - -class WeightedUndirectedEdge(vertex1: Vertex, vertex2: Vertex, val weight: Int) : - UndirectedEdge(vertex1, vertex2) diff --git a/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt deleted file mode 100644 index d160df32..00000000 --- a/app/src/main/kotlin/model/internalGraphs/_DirectedGraph.kt +++ /dev/null @@ -1,82 +0,0 @@ -package model.internalGraphs - -import model.abstractGraph.Graph -import model.abstractGraph.Vertex -import model.edges.DirectedEdge - -abstract class _DirectedGraph> : Graph() { - override fun addEdge(vertex1: Vertex, vertex2: Vertex): E { - if (vertex1 == vertex2) - throw IllegalArgumentException("Can't add edge from vertex to itself.") - - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") - - val newEdge = DirectedEdge(vertex1, vertex2) as E - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - - return newEdge - } - - override fun removeEdge(edgeToRemove: E): E { - if (edgeToRemove !in edges) - throw NoSuchElementException("Edge is not in the graph") - - val vertex1 = edgeToRemove.vertex1 - val vertex2 = edgeToRemove.vertex2 - - adjacencyMap[vertex1]?.remove(vertex2) - edges.remove(edgeToRemove) - - return edgeToRemove - } - - fun findSCC(): ArrayList>> { // SCC - Strongly Connected Components (by Kosaraju) - val visited = mutableMapOf, Boolean>().withDefault { false } - val stack = ArrayDeque>() - val component = arrayListOf>() - val sccList: ArrayList>> = arrayListOf() - - fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { - visited[srcVertex] = true - componentList.add(srcVertex) - adjacencyMap[srcVertex]?.forEach { vertex2 -> - if (visited[vertex2] != null && visited[vertex2] != true) { - auxiliaryDFS(vertex2, componentList) - } - } - stack.add(srcVertex) - } - - for (vertex in adjacencyMap.keys) { - if (visited[vertex] != null && visited[vertex] != true) auxiliaryDFS(vertex, component) - } - - reverseGraph() - visited.clear() - component.clear() - - while (stack.isNotEmpty()) { - val vertex = stack.removeLast() - if (visited[vertex] != null && visited[vertex] != true) { - val currentComponent = arrayListOf>() - auxiliaryDFS(vertex, currentComponent) - sccList.add(currentComponent) - } - } - return sccList - } - - private fun reverseGraph() { - val reversedAdjacencyMap = mutableMapOf, ArrayList>>() - for (vertex in adjacencyMap.keys) { - adjacencyMap[vertex]?.forEach { vertex2 -> - reversedAdjacencyMap[vertex2] = reversedAdjacencyMap[vertex2] ?: ArrayList() - reversedAdjacencyMap[vertex2]?.add(vertex) - } - } - adjacencyMap.clear() - adjacencyMap.putAll(reversedAdjacencyMap) - } -} diff --git a/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt deleted file mode 100644 index 77b43719..00000000 --- a/app/src/main/kotlin/model/internalGraphs/_UndirectedGraph.kt +++ /dev/null @@ -1,36 +0,0 @@ -package model.internalGraphs - -import model.abstractGraph.Graph -import model.abstractGraph.Vertex -import model.edges.UndirectedEdge - -abstract class _UndirectedGraph> : Graph() { - override fun addEdge(vertex1: Vertex, vertex2: Vertex): E { - if (vertex1 == vertex2) - throw IllegalArgumentException("Can't add edge from vertex to itself.") - - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") - - val newEdge = UndirectedEdge(vertex1, vertex2) as E - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - adjacencyMap[vertex2]?.add(vertex1) - - return newEdge - } - - override fun removeEdge(edgeToRemove: E): E { - if (edgeToRemove !in edges) - throw NoSuchElementException("Edge is not in the graph.") - - val vertex1 = edgeToRemove.vertex1 - val vertex2 = edgeToRemove.vertex2 - - adjacencyMap[vertex1]?.remove(vertex2) - adjacencyMap[vertex2]?.remove(vertex1) - edges.remove(edgeToRemove) - - return edgeToRemove - } -} diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt deleted file mode 100644 index f9a892d8..00000000 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedDirectedGraph.kt +++ /dev/null @@ -1,73 +0,0 @@ -package model.internalGraphs - -import java.util.* -import model.abstractGraph.Vertex -import model.edges.WeightedDirectedEdge -import kotlin.NoSuchElementException - -abstract class _WeightedDirectedGraph> : _DirectedGraph() { - fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { - if (vertex1 == vertex2) - throw IllegalArgumentException("Can't add edge from vertex to itself.") - - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") - - val newEdge = WeightedDirectedEdge(vertex1, vertex2, weight) as E - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - - return newEdge - } - - /* - * In case weight is not passed, set it to default value = 1 - */ - override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) - - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, E>> { - val vertices = getVertices() - val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } - val predecessorMap = mutableMapOf, Vertex?>() - val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } - val visited = mutableSetOf, Int>>() - - distanceMap[srcVertex] = 0 - - while (priorityQueue.isNotEmpty()) { - val (node, currentDistance) = priorityQueue.poll() - if (visited.add(node to currentDistance)) { - adjacencyMap[node]?.forEach { adjacent -> - val currentEdge = edges.find { it.vertex1 == adjacent } - currentEdge?.let { - val totalDist = currentDistance + it.weight - if (totalDist < distanceMap.getValue(adjacent)) { - distanceMap[adjacent] = totalDist - predecessorMap[adjacent] = node // Update predecessor - priorityQueue.add(adjacent to totalDist) - } - } - } - } - } - - // Reconstruct the path from srcVertex to destVertex - val path: MutableList, E>> = mutableListOf() - var currentVertex = destVertex - while (currentVertex != srcVertex) { - val predecessor = predecessorMap[currentVertex] - if (predecessor == null) { - // If no path exists - return emptyList() - } - if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } == null) { - throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") - } - path.add( - Pair(currentVertex, edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex }) - as Pair, E>) - currentVertex = predecessor - } - return path.reversed() - } -} diff --git a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt deleted file mode 100644 index 76c7a393..00000000 --- a/app/src/main/kotlin/model/internalGraphs/_WeightedUndirectedGraph.kt +++ /dev/null @@ -1,135 +0,0 @@ -package model.internalGraphs - -import java.util.* -import model.abstractGraph.Vertex -import model.edges.WeightedUndirectedEdge -import kotlin.NoSuchElementException - -abstract class _WeightedUndirectedGraph> : _UndirectedGraph() { - fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): E { - if (vertex1 == vertex2) - throw IllegalArgumentException("Can't add edge from vertex to itself.") - - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") - - val newEdge = WeightedUndirectedEdge(vertex1, vertex2, weight) as E - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - adjacencyMap[vertex2]?.add(vertex1) - - return newEdge - } - - /* - * In case weight is not passed, set it to default value = 1 - */ - override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) - - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, E>> { - val vertices = getVertices() - val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } - val predecessorMap = mutableMapOf, Vertex?>() - val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } - val visited = mutableSetOf, Int>>() - - distanceMap[srcVertex] = 0 - - while (priorityQueue.isNotEmpty()) { - val (node, currentDistance) = priorityQueue.poll() - if (visited.add(node to currentDistance)) { - adjacencyMap[node]?.forEach { adjacent -> - val currentEdge = - edges.find { - (it.vertex1 == adjacent && it.vertex2 == node) || - (it.vertex1 == node && it.vertex2 == adjacent) - } - currentEdge?.let { - val totalDist = currentDistance + currentEdge.weight - if (totalDist < distanceMap.getValue(adjacent)) { - distanceMap[adjacent] = totalDist - predecessorMap[adjacent] = node // Update predecessor - priorityQueue.add(adjacent to totalDist) - } - } - } - } - } - - // Reconstruct the path from srcVertex to destVertex - val path: MutableList, E>> = mutableListOf() - var currentVertex = destVertex - while (currentVertex != srcVertex) { - val predecessor = predecessorMap[currentVertex] - if (predecessor == null) { - // If no path exists - return emptyList() - } - if (edges.find { - it.vertex1 == predecessor && it.vertex2 == currentVertex || - it.vertex2 == predecessor && it.vertex1 == currentVertex - } == null) { - throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") - } - path.add( - Pair( - currentVertex, - edges.find { - it.vertex1 == predecessor && it.vertex2 == currentVertex || - it.vertex2 == predecessor && it.vertex1 == currentVertex - }) - as Pair, E>) - currentVertex = predecessor - } - return path.reversed() - } - - fun findMinSpanningTree(): List { - val vertexIdList = mutableListOf() - for (v in getVertices()) vertexIdList.add(v.id) - - val graphSize = vertexIdList.size - - // set each vertex parent to be itself and each vertex rank to 0 - val parentList = MutableList(graphSize) { index: Int -> vertexIdList[index] } - val rankList = MutableList(graphSize) { 0 } - - fun findTreeRootId(vId: Int): Int { - if (parentList[vId] == vId) return vId - - parentList[vId] = findTreeRootId(parentList[vId]) - - return parentList[vId] - } - - fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { - val rootId1 = findTreeRootId(vId1) - val rootId2 = findTreeRootId(vId2) - - if (rootId1 == rootId2) return - - if (rankList[rootId1] < rankList[rootId2]) { - parentList[rootId1] = rootId2 - } else { - parentList[rootId2] = rootId1 - if (rankList[rootId1] == rankList[rootId2]) rankList[rootId1]++ - } - } - - val sortedEdges = edges.sortedBy { it.weight } - - val chosenEdges = mutableListOf() - - for (edge in sortedEdges) { - val id1 = edge.vertex1.id - val id2 = edge.vertex2.id - - if (findTreeRootId(id1) != findTreeRootId(id2)) { - uniteTwoTreesByVerticesIds(id1, id2) - chosenEdges.add(edge) - } - } - - return chosenEdges - } -} From b17d3501352317708d516484e8c757410fab29a3 Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Sun, 12 May 2024 18:13:35 +0300 Subject: [PATCH 32/77] feat: implement Tarjan's bridge finding algorithm #15 * chore: resolve merge conflict * feat: implement Tarjan's bridge finding algorithm * refactor: bridge finding algorithm * refactor: min spanning tree finding algorithm --- app/src/main/kotlin/model/UndirectedGraph.kt | 58 ++++++++++++++++++- .../kotlin/model/WeightedUndirectedGraph.kt | 19 +++--- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index 4caec81c..34aa8016 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -1,5 +1,6 @@ package model +import kotlin.math.min import model.abstractGraph.Edge import model.abstractGraph.Graph import model.abstractGraph.Vertex @@ -33,5 +34,60 @@ open class UndirectedGraph : Graph() { return edgeToRemove } -} + fun findBridges(): List> { + val bridges = mutableListOf>() + + val vertices = getVertices() + val graphSize = vertices.size + + val discoveryTime = MutableList(graphSize) { -1 } + val minDiscoveryTime = MutableList(graphSize) { -1 } + + val visitedList = MutableList(graphSize) { false } + val parentList = MutableList(graphSize) { -1 } + + var iterationCount = 0 + + fun doDFSToFindBridgesFromVertex(vertex: Vertex) { + visitedList[vertex.id] = true + + iterationCount++ + discoveryTime[vertex.id] = iterationCount + minDiscoveryTime[vertex.id] = iterationCount + + for (neighbour in getNeighbours(vertex)) { + if (neighbour.id == parentList[vertex.id]) continue + + if (visitedList[neighbour.id]) { + minDiscoveryTime[vertex.id] = + min(minDiscoveryTime[vertex.id], discoveryTime[neighbour.id]) + + continue + } + + parentList[neighbour.id] = vertex.id + + doDFSToFindBridgesFromVertex(neighbour) + + minDiscoveryTime[vertex.id] = + min(minDiscoveryTime[vertex.id], minDiscoveryTime[neighbour.id]) + + if (minDiscoveryTime[neighbour.id] > discoveryTime[vertex.id]) { + val bridgeFound = edges.find { it.isIncident(vertex) && it.isIncident(neighbour) } + ?: throw NoSuchElementException("Can't find edge between vertices with ids ${vertex.id} and ${neighbour.id}") + + bridges.add(bridgeFound) + } + } + } + + for (v in vertices) { + if (visitedList[v.id]) continue + + doDFSToFindBridgesFromVertex(v) + } + + return bridges + } +} diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 20797ed5..b5879232 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -92,26 +92,26 @@ class WeightedUndirectedGraph : UndirectedGraph() { } fun findMinSpanningTree(): List> { - val vertexIdList = mutableListOf() - for (v in getVertices()) vertexIdList.add(v.id) + val vertexIds = mutableListOf() + for (v in getVertices()) vertexIds.add(v.id) - val graphSize = vertexIdList.size + val graphSize = vertexIds.size // set each vertex parent to be itself and each vertex rank to 0 - val parentList = MutableList(graphSize) { index: Int -> vertexIdList[index] } + val parentList = MutableList(graphSize) { i: Int -> vertexIds[i] } val rankList = MutableList(graphSize) { 0 } - fun findTreeRootId(vId: Int): Int { + fun findRootIdByVertexId(vId: Int): Int { if (parentList[vId] == vId) return vId - parentList[vId] = findTreeRootId(parentList[vId]) + parentList[vId] = findRootIdByVertexId(parentList[vId]) return parentList[vId] } fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { - val rootId1 = findTreeRootId(vId1) - val rootId2 = findTreeRootId(vId2) + val rootId1 = findRootIdByVertexId(vId1) + val rootId2 = findRootIdByVertexId(vId2) if (rootId1 == rootId2) return @@ -132,13 +132,12 @@ class WeightedUndirectedGraph : UndirectedGraph() { val id1 = edge.vertex1.id val id2 = edge.vertex2.id - if (findTreeRootId(id1) != findTreeRootId(id2)) { + if (findRootIdByVertexId(id1) != findRootIdByVertexId(id2)) { uniteTwoTreesByVerticesIds(id1, id2) chosenEdges.add(edge) } } - return chosenEdges } } From 77aa22e5226adc65b179fd6fff162b4025364d95 Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Tue, 14 May 2024 00:19:43 +0300 Subject: [PATCH 33/77] feat: add new collections, optimize, refactor and fix existing code #16 * feat: add vertices array and map of outcoming edges for every vertex and integrate them into existing code * refactor: delete repeating code, reposition some fields and functions * refactor: rename currentId to nextId in abstract graph class * feat: add check if removed vertex is last added vertex in IdFragmentation + rename method with very long name to removeVertexFromEverywhere and add deletion of the vertex itself inside the method * feat: add getVertices() again * feat: optimize check if vertex is in vertices array * refactor: remove unnecessary TODO and rename outgoingEdgesMap * fix: remove vertex from outgoing edges map and vertices array in removeVertex method + add getOutgoingEdges method to localize exceptions * feat: add getEdge method and use it where possible * fix: make weightMap private and add getWeight method + replace map accessing with getWeight method --------- Co-authored-by: Karim Shakirov --- app/src/main/kotlin/model/DirectedGraph.kt | 19 +++-- app/src/main/kotlin/model/UndirectedGraph.kt | 21 ++++-- .../kotlin/model/WeightedDirectedGraph.kt | 24 +++--- .../kotlin/model/WeightedUndirectedGraph.kt | 44 +++++------ .../main/kotlin/model/abstractGraph/Graph.kt | 73 ++++++++++++++----- 5 files changed, 109 insertions(+), 72 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 0a3b3092..ee54be0d 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -9,11 +9,13 @@ open class DirectedGraph : Graph() { if (vertex1 == vertex2) throw IllegalArgumentException("Can't add edge from vertex to itself.") - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + if (vertex1.id > vertices.size || vertex2.id > vertices.size) + throw NoSuchElementException("Vertex1 or vertex2 is not in the vertices array.") - val newEdge = Edge(vertex1, vertex2) as Edge + val newEdge = Edge(vertex1, vertex2) edges.add(newEdge) + + outgoingEdgesMap[vertex1]?.add(newEdge) adjacencyMap[vertex1]?.add(vertex2) return newEdge @@ -26,13 +28,16 @@ open class DirectedGraph : Graph() { val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 - adjacencyMap[vertex1]?.remove(vertex2) edges.remove(edgeToRemove) + outgoingEdgesMap[vertex1]?.remove(edgeToRemove) + adjacencyMap[vertex1]?.remove(vertex2) + return edgeToRemove } - fun findSCC(): ArrayList>> { // SCC - Strongly Connected Components (by Kosaraju) + // SCC - Strongly Connected Components (by Kosaraju) + fun findSCC(): ArrayList>> { val visited = mutableMapOf, Boolean>().withDefault { false } val stack = ArrayDeque>() val component = arrayListOf>() @@ -49,7 +54,7 @@ open class DirectedGraph : Graph() { stack.add(srcVertex) } - for (vertex in adjacencyMap.keys) { + for (vertex in vertices) { if (visited[vertex] != null && visited[vertex] != true) auxiliaryDFS(vertex, component) } @@ -70,7 +75,7 @@ open class DirectedGraph : Graph() { private fun reverseGraph() { val reversedAdjacencyMap = mutableMapOf, ArrayList>>() - for (vertex in adjacencyMap.keys) { + for (vertex in vertices) { adjacencyMap[vertex]?.forEach { vertex2 -> reversedAdjacencyMap[vertex2] = reversedAdjacencyMap[vertex2] ?: ArrayList() reversedAdjacencyMap[vertex2]?.add(vertex) diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index 34aa8016..ffae904a 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -10,11 +10,15 @@ open class UndirectedGraph : Graph() { if (vertex1 == vertex2) throw IllegalArgumentException("Can't add edge from vertex to itself.") - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + if (vertex1.id > vertices.size || vertex2.id > vertices.size) + throw NoSuchElementException("Vertex1 or vertex2 is not in the vertices array.") - val newEdge = Edge(vertex1, vertex2) as Edge + val newEdge = Edge(vertex1, vertex2) edges.add(newEdge) + + outgoingEdgesMap[vertex1]?.add(newEdge) + outgoingEdgesMap[vertex2]?.add(newEdge) + adjacencyMap[vertex1]?.add(vertex2) adjacencyMap[vertex2]?.add(vertex1) @@ -28,9 +32,13 @@ open class UndirectedGraph : Graph() { val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 + edges.remove(edgeToRemove) + + outgoingEdgesMap[vertex1]?.remove(edgeToRemove) + outgoingEdgesMap[vertex2]?.remove(edgeToRemove) + adjacencyMap[vertex1]?.remove(vertex2) adjacencyMap[vertex2]?.remove(vertex1) - edges.remove(edgeToRemove) return edgeToRemove } @@ -38,7 +46,6 @@ open class UndirectedGraph : Graph() { fun findBridges(): List> { val bridges = mutableListOf>() - val vertices = getVertices() val graphSize = vertices.size val discoveryTime = MutableList(graphSize) { -1 } @@ -74,9 +81,7 @@ open class UndirectedGraph : Graph() { min(minDiscoveryTime[vertex.id], minDiscoveryTime[neighbour.id]) if (minDiscoveryTime[neighbour.id] > discoveryTime[vertex.id]) { - val bridgeFound = edges.find { it.isIncident(vertex) && it.isIncident(neighbour) } - ?: throw NoSuchElementException("Can't find edge between vertices with ids ${vertex.id} and ${neighbour.id}") - + val bridgeFound = getEdge(vertex, neighbour) bridges.add(bridgeFound) } } diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 68392456..d0aa66ba 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -6,31 +6,29 @@ import java.util.* import kotlin.NoSuchElementException class WeightedDirectedGraph : DirectedGraph() { - - val weightMap: MutableMap, Int> = mutableMapOf() + private val weightMap: MutableMap, Int> = mutableMapOf() fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): Edge { - if (vertex1 == vertex2) - throw IllegalArgumentException("Can't add edge from vertex to itself.") - - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + val newEdge = super.addEdge(vertex1, vertex2) - val newEdge = Edge(vertex1, vertex2) weightMap[newEdge] = weight - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) return newEdge } - /* + /** * In case weight is not passed, set it to default value = 1 */ override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + fun getWeight(edge: Edge): Int { + val weight = weightMap[edge] + ?: throw NoSuchElementException("No weight found for edge $edge") + + return weight + } + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>> { - val vertices = getVertices() val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } @@ -45,7 +43,7 @@ class WeightedDirectedGraph : DirectedGraph() { val currentEdge = edges.find { it.vertex1 == adjacent } currentEdge?.let { var totalDist = currentDistance - totalDist += weightMap[it] ?: throw NoSuchElementException("Current edge doesn't have weight.") + totalDist += getWeight(it) if (totalDist < distanceMap.getValue(adjacent)) { distanceMap[adjacent] = totalDist predecessorMap[adjacent] = node // Update predecessor diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index b5879232..dcd17ac9 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -6,32 +6,29 @@ import java.util.* import kotlin.NoSuchElementException class WeightedUndirectedGraph : UndirectedGraph() { - - val weightMap: MutableMap, Int> = mutableMapOf() + private val weightMap: MutableMap, Int> = mutableMapOf() fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Int): Edge { - if (vertex1 == vertex2) - throw IllegalArgumentException("Can't add edge from vertex to itself.") - - if (vertex1 !in adjacencyMap.keys || vertex2 !in adjacencyMap.keys) - throw NoSuchElementException("Vertex1 or vertex2 is not in the adjacency map.") + val newEdge = super.addEdge(vertex1, vertex2) - val newEdge = Edge(vertex1, vertex2) weightMap[newEdge] = weight - edges.add(newEdge) - adjacencyMap[vertex1]?.add(vertex2) - adjacencyMap[vertex2]?.add(vertex1) return newEdge } - /* + /** * In case weight is not passed, set it to default value = 1 */ override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + fun getWeight(edge: Edge): Int { + val weight = weightMap[edge] + ?: throw NoSuchElementException("No weight found for edge $edge") + + return weight + } + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>> { - val vertices = getVertices() val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } @@ -50,7 +47,7 @@ class WeightedUndirectedGraph : UndirectedGraph() { } currentEdge?.let { var totalDist = currentDistance - totalDist += weightMap[it] ?: throw NoSuchElementException("Current edge doesn't have weight.") + totalDist += getWeight(it) if (totalDist < distanceMap.getValue(adjacent)) { distanceMap[adjacent] = totalDist @@ -92,21 +89,18 @@ class WeightedUndirectedGraph : UndirectedGraph() { } fun findMinSpanningTree(): List> { - val vertexIds = mutableListOf() - for (v in getVertices()) vertexIds.add(v.id) - - val graphSize = vertexIds.size + val graphSize = vertices.size // set each vertex parent to be itself and each vertex rank to 0 - val parentList = MutableList(graphSize) { i: Int -> vertexIds[i] } - val rankList = MutableList(graphSize) { 0 } + val parentIdList = Array(graphSize) { i: Int -> i } + val rankList = Array(graphSize) { 0 } fun findRootIdByVertexId(vId: Int): Int { - if (parentList[vId] == vId) return vId + if (parentIdList[vId] == vId) return vId - parentList[vId] = findRootIdByVertexId(parentList[vId]) + parentIdList[vId] = findRootIdByVertexId(parentIdList[vId]) - return parentList[vId] + return parentIdList[vId] } fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { @@ -116,9 +110,9 @@ class WeightedUndirectedGraph : UndirectedGraph() { if (rootId1 == rootId2) return if (rankList[rootId1] < rankList[rootId2]) { - parentList[rootId1] = rootId2 + parentIdList[rootId1] = rootId2 } else { - parentList[rootId2] = rootId1 + parentIdList[rootId2] = rootId1 if (rankList[rootId1] == rankList[rootId2]) rankList[rootId1]++ } } diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index c0d27060..8bd38608 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -1,38 +1,44 @@ package model.abstractGraph abstract class Graph { - protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + protected val vertices: ArrayList> = arrayListOf() protected val edges: MutableSet> = mutableSetOf() - private var currentId = 0 + + protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() + protected val outgoingEdgesMap: MutableMap, ArrayList>> = mutableMapOf() + + private var nextId = 0 fun addVertex(data: D): Vertex { - val newVertex = Vertex(currentId++, data) + val newVertex = Vertex(nextId++, data) + + outgoingEdgesMap[newVertex] = ArrayList() adjacencyMap[newVertex] = ArrayList() + vertices.add(newVertex) + return newVertex } fun removeVertex(vertexToRemove: Vertex): Vertex { - removeVertexFromIncidentEdgesAndAdjacentVerticesMapValues(vertexToRemove) - fixIdFragmentation(vertexToRemove) + nextId-- - adjacencyMap.remove(vertexToRemove) + removeVertexFromEverywhere(vertexToRemove) + fixIdFragmentation(vertexToRemove) return vertexToRemove } - abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge - - abstract fun removeEdge(edgeToRemove: Edge): Edge - private fun fixIdFragmentation(vertexToRemove: Vertex) { - currentId-- - val lastAddedVertex = getVertices().find { it.id == currentId } - ?: throw NoSuchElementException("Vertex with id $currentId is not present in the adjacency map.") + if (vertexToRemove.id == nextId) return + + val lastAddedVertex = vertices[nextId] val copyOfLastAddedVertex = Vertex(vertexToRemove.id, lastAddedVertex.data) + vertices[copyOfLastAddedVertex.id] = copyOfLastAddedVertex adjacencyMap[copyOfLastAddedVertex] = getNeighbours(lastAddedVertex) + outgoingEdgesMap[copyOfLastAddedVertex] = getOutgoingEdges(lastAddedVertex) val adjacentVertices = getNeighbours(copyOfLastAddedVertex) @@ -42,28 +48,57 @@ abstract class Graph { } } - removeVertexFromIncidentEdgesAndAdjacentVerticesMapValues(lastAddedVertex) - adjacencyMap.remove(lastAddedVertex) + removeVertexFromEverywhere(lastAddedVertex) } - private fun removeVertexFromIncidentEdgesAndAdjacentVerticesMapValues(vertexToRemove: Vertex) { + private fun removeVertexFromEverywhere(vertexToRemove: Vertex) { val adjacentVertices = getNeighbours(vertexToRemove) for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToRemove) - for (edge in getEdges()) { - if (edge.isIncident(vertexToRemove)) edges.remove(edge) + // If vertexToRemove isn't last, it will be overridden by its copy in fixIdFragmentation + if (vertexToRemove.id == nextId) vertices.removeLast() + + for (edge in edges) { + if (edge.isIncident(vertexToRemove)) { + edges.remove(edge) + + val incidentVertex = if (edge.vertex1 == vertexToRemove) edge.vertex2 else edge.vertex1 + outgoingEdgesMap[incidentVertex]?.remove(edge) + } } + + adjacencyMap.remove(vertexToRemove) + outgoingEdgesMap.remove(vertexToRemove) } - fun getVertices() = adjacencyMap.keys.toList() + abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge + + abstract fun removeEdge(edgeToRemove: Edge): Edge fun getEdges() = edges.toList() + fun getVertices() = vertices.toList() + + protected fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { + val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } + ?: throw NoSuchElementException("Can't find edge between vertices with ids ${vertex1.id} and ${vertex2.id}") + + return edge + } + + // This and next method are used to localize exceptions protected fun getNeighbours(vertex: Vertex): ArrayList> { val neighbours = adjacencyMap[vertex] ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the adjacency map.") return neighbours } + + protected fun getOutgoingEdges(vertex: Vertex): ArrayList> { + val outgoingEdges = outgoingEdgesMap[vertex] + ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the outgoing edges map.") + + return outgoingEdges + } } From f5f8ab21a93423ff1d68b5d79d7fbf929d611ef3 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Tue, 14 May 2024 00:21:41 +0300 Subject: [PATCH 34/77] test: add test classes and inner test classes for methods, parameterized test annotation #17 * test: add junit dependency to gradle * test: add test classes and package structure, inner test classes for methods * test: add custom annotation for parameterized tests in abstract graph test class --- app/build.gradle.kts | 8 ++++ .../test/kotlin/model/DirectedGraphTest.kt | 14 +++++++ .../test/kotlin/model/UndirectedGraphTest.kt | 14 +++++++ .../kotlin/model/WeightedDirectedGraphTest.kt | 14 +++++++ .../model/WeightedUndirectedGraphTest.kt | 17 ++++++++ .../kotlin/model/abstractGraph/GraphTest.kt | 42 +++++++++++++++++++ 6 files changed, 109 insertions(+) create mode 100644 app/src/test/kotlin/model/DirectedGraphTest.kt create mode 100644 app/src/test/kotlin/model/UndirectedGraphTest.kt create mode 100644 app/src/test/kotlin/model/WeightedDirectedGraphTest.kt create mode 100644 app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt create mode 100644 app/src/test/kotlin/model/abstractGraph/GraphTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e574840e..72a4fda8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,10 @@ plugins { dependencies { implementation(compose.desktop.currentOs) implementation(libs.koin.core) + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") } compose.desktop { @@ -13,3 +17,7 @@ compose.desktop { mainClass = "MainKt" } } + +tasks.test { + useJUnitPlatform() +} diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt new file mode 100644 index 00000000..d9c5e8ee --- /dev/null +++ b/app/src/test/kotlin/model/DirectedGraphTest.kt @@ -0,0 +1,14 @@ +package model + +import org.junit.jupiter.api.Nested + +class DirectedGraphTest { + @Nested + inner class AddEdgeTest {} + + @Nested + inner class RemoveEdgeTest {} + + @Nested + inner class FindSCCTest {} +} diff --git a/app/src/test/kotlin/model/UndirectedGraphTest.kt b/app/src/test/kotlin/model/UndirectedGraphTest.kt new file mode 100644 index 00000000..27126f5e --- /dev/null +++ b/app/src/test/kotlin/model/UndirectedGraphTest.kt @@ -0,0 +1,14 @@ +package model + +import org.junit.jupiter.api.Nested + +class UndirectedGraphTest { + @Nested + inner class AddEdgeTest {} + + @Nested + inner class RemoveEdgeTest {} + + @Nested + inner class FindBridgesTest {} +} diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt new file mode 100644 index 00000000..5a077eeb --- /dev/null +++ b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt @@ -0,0 +1,14 @@ +package model + +import org.junit.jupiter.api.Nested + +class WeightedDirectedGraphTest { + @Nested + inner class AddEdgeTest {} + + @Nested + inner class RemoveEdgeTest {} + + @Nested + inner class FindShortestPathDijkstraTest {} +} diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt new file mode 100644 index 00000000..4d8d339f --- /dev/null +++ b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt @@ -0,0 +1,17 @@ +package model + +import org.junit.jupiter.api.Nested + +class WeightedUndirectedGraphTest { + @Nested + inner class AddEdgeTest {} + + @Nested + inner class RemoveEdgeTest {} + + @Nested + inner class FindShortestPathDijkstraTest {} + + @Nested + inner class FindMinSpanningTreeTest {} +} diff --git a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt new file mode 100644 index 00000000..acceb173 --- /dev/null +++ b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt @@ -0,0 +1,42 @@ +package model.abstractGraph + +import model.DirectedGraph +import model.UndirectedGraph +import model.WeightedDirectedGraph +import model.WeightedUndirectedGraph +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +@ParameterizedTest +@MethodSource("provideAllGraphTypes") +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +private annotation class TestAllGraphTypes + +fun provideAllGraphTypes(): Stream { + return Stream.of( + Arguments.of(UndirectedGraph()), + Arguments.of(DirectedGraph()), + Arguments.of(WeightedDirectedGraph()), + Arguments.of(WeightedUndirectedGraph()) + ) +} + +class GraphTest { + @Nested + inner class GetVerticesTest {} + + @Nested + inner class GetEdgesTest {} + + @Nested + inner class AddVertexTest {} + + @Nested + inner class RemoveVertexTest {} +} From 7016813bc315bc7f6adc1e83db1d937b76eb10a2 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Tue, 14 May 2024 14:52:52 +0300 Subject: [PATCH 35/77] fix: case if there is no edge from vertex1 to vertex2 in getEdge #19 * fix(getEdge): case if there is no edge from vertex1 to vertex2 * fix(getEdge): method should be different in directed and undirected graphs --- app/src/main/kotlin/model/DirectedGraph.kt | 7 +++++++ app/src/main/kotlin/model/UndirectedGraph.kt | 8 ++++++++ app/src/main/kotlin/model/abstractGraph/Graph.kt | 11 +++-------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index ee54be0d..32f00f4c 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -36,6 +36,13 @@ open class DirectedGraph : Graph() { return edgeToRemove } + override fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { + val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } + ?: throw NoSuchElementException("No edge between vertices with ids ${vertex1.id} and ${vertex2.id}") + + return edge + } + // SCC - Strongly Connected Components (by Kosaraju) fun findSCC(): ArrayList>> { val visited = mutableMapOf, Boolean>().withDefault { false } diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index ffae904a..d25e09c9 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -43,6 +43,14 @@ open class UndirectedGraph : Graph() { return edgeToRemove } + override fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { + val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } + ?: outgoingEdgesMap[vertex2]?.find { it.isIncident(vertex1) } + ?: throw NoSuchElementException("No edge between vertices with ids ${vertex1.id} and ${vertex2.id}") + + return edge + } + fun findBridges(): List> { val bridges = mutableListOf>() diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 8bd38608..e77c9bf5 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -80,14 +80,7 @@ abstract class Graph { fun getVertices() = vertices.toList() - protected fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { - val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } - ?: throw NoSuchElementException("Can't find edge between vertices with ids ${vertex1.id} and ${vertex2.id}") - - return edge - } - - // This and next method are used to localize exceptions + // This and next two methods are used to localize exceptions protected fun getNeighbours(vertex: Vertex): ArrayList> { val neighbours = adjacencyMap[vertex] ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the adjacency map.") @@ -101,4 +94,6 @@ abstract class Graph { return outgoingEdges } + + protected abstract fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge } From 7698832f543c6dcebb56fb170fc9ac9c17bcd1bf Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Tue, 14 May 2024 19:42:40 +0300 Subject: [PATCH 36/77] feat: implement initial UI, add basic structure and logic #18 * refactor: clean up main file * feat: set up ViewModel's file structure * feat: add draft of main screen's UI * feat: set up tab folder structure * feat: allign the text in tabs, add rounding * feat: add window size, title and position * feat: implement basic viewModels * feat: add test representation for graph * feat: implement summarizing main screen VM * feat: implement graph's initial View * refactor: add line wraps and remove unused parameters * fix: switch coord. variables from Dp to MutableState * refactor: move 'tab select' process in a separate file * fix: repair tabs switch * reformat: apply ktfmt * feat: add WindowVM class --- app/src/main/kotlin/Main.kt | 47 ++++++++--- app/src/main/kotlin/view/MainScreen.kt | 84 +++++++++++++++++++ app/src/main/kotlin/view/graph/EdgeView.kt | 31 +++++++ app/src/main/kotlin/view/graph/GraphView.kt | 17 ++++ app/src/main/kotlin/view/graph/VertexView.kt | 55 ++++++++++++ .../main/kotlin/view/tabScreen/AnalyzeTab.kt | 12 +++ .../kotlin/view/tabScreen/FileControlTab.kt | 12 +++ .../main/kotlin/view/tabScreen/GeneralTab.kt | 12 +++ .../kotlin/view/tabScreen/SelectTabRow.kt | 55 ++++++++++++ .../kotlin/viewmodel/MainScreenViewModel.kt | 28 +++++++ .../main/kotlin/viewmodel/WindowViewModel.kt | 30 +++++++ .../kotlin/viewmodel/graph/EdgeViewModel.kt | 9 ++ .../kotlin/viewmodel/graph/GraphViewModel.kt | 41 +++++++++ .../viewmodel/graph/TestRepresentation.kt | 47 +++++++++++ .../kotlin/viewmodel/graph/VertexViewModel.kt | 36 ++++++++ 15 files changed, 506 insertions(+), 10 deletions(-) create mode 100644 app/src/main/kotlin/view/MainScreen.kt create mode 100644 app/src/main/kotlin/view/graph/EdgeView.kt create mode 100644 app/src/main/kotlin/view/graph/GraphView.kt create mode 100644 app/src/main/kotlin/view/graph/VertexView.kt create mode 100644 app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt create mode 100644 app/src/main/kotlin/view/tabScreen/FileControlTab.kt create mode 100644 app/src/main/kotlin/view/tabScreen/GeneralTab.kt create mode 100644 app/src/main/kotlin/view/tabScreen/SelectTabRow.kt create mode 100644 app/src/main/kotlin/viewmodel/MainScreenViewModel.kt create mode 100644 app/src/main/kotlin/viewmodel/WindowViewModel.kt create mode 100644 app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt create mode 100644 app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt create mode 100644 app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt create mode 100644 app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt index dcae2cf1..236fdac8 100644 --- a/app/src/main/kotlin/Main.kt +++ b/app/src/main/kotlin/Main.kt @@ -1,21 +1,48 @@ import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.material.Button import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import java.awt.Dimension +import model.UndirectedGraph +import model.abstractGraph.Graph +import view.MainScreen +import viewmodel.MainScreenViewModel + +val testGraph: Graph = + UndirectedGraph().apply { + val v1 = addVertex(1) + val v2 = addVertex(2) + val v3 = addVertex(3) + val v4 = addVertex(4) + val v5 = addVertex(5) + + addEdge(v1, v5) + addEdge(v1, v4) + addEdge(v1, v3) + addEdge(v1, v2) + addEdge(v2, v4) + } @Composable @Preview fun App() { - var text by remember { mutableStateOf("Hello, World!") } - - MaterialTheme { Button(onClick = { text = "Hello, Desktop!" }) { Text(text) } } + MaterialTheme { MainScreen(MainScreenViewModel(testGraph)) } } -fun main() = application { Window(onCloseRequest = ::exitApplication) { App() } } +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Graphs-2", + state = + rememberWindowState( + position = WindowPosition(alignment = Alignment.Center), + ), + ) { + window.minimumSize = Dimension(1200, 700) + App() + } +} diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt new file mode 100644 index 00000000..d40ac5b6 --- /dev/null +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -0,0 +1,84 @@ +package view + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.unit.dp +import view.graph.GraphView +import view.tabScreen.* +import viewmodel.MainScreenViewModel + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MainScreen(viewmodel: MainScreenViewModel) { + + Row { + Column( + modifier = + Modifier.width(360.dp) + .background(color = Color.White) + .fillMaxHeight() + .clip(shape = RoundedCornerShape(10.dp)) + // TODO: make it rounded only from right side + ) { + val pageState = rememberPagerState(pageCount = { 3 }) + val coroutineScope = rememberCoroutineScope() + val tabs = listOf("General", "Analyze", " File Control") + + TabRow( + selectedTabIndex = pageState.currentPage, + contentColor = Color.Red, + backgroundColor = Color.Gray, + divider = {}, // to remove divider between + indicator = { tabPositions -> + TabRowDefaults.Indicator( + modifier = Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), + height = 0.dp + ) + }, + modifier = Modifier.height(50.dp) + ) { + tabs.forEachIndexed { index, title -> + SelectTabRow(pageState, index, coroutineScope, title) + } + } + + HorizontalPager(state = pageState, userScrollEnabled = true) { + Column( + modifier = Modifier.width(360.dp).background(Color.LightGray).fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + when (pageState.currentPage) { + 0 -> GeneralTab() + 1 -> AnalyzeTab() + 2 -> FileControlTab() + } + } + } + } + + Surface( + modifier = + Modifier.fillMaxSize() + .border(2f.dp, Color.LightGray, RectangleShape) + .clipToBounds(), + color = Color.Transparent + ) { + GraphView(viewmodel.graphViewModel) + } + } +} diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt new file mode 100644 index 00000000..f3df9956 --- /dev/null +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -0,0 +1,31 @@ +package view.graph + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import viewmodel.graph.EdgeViewModel + +@Composable +fun EdgeView(viewModel: EdgeViewModel) { + Canvas(modifier = Modifier.size(300.dp, 300.dp)) { + drawLine( + color = Color.Red, + strokeWidth = 10.0f, + start = + Offset( + viewModel.firstVertex.x.value.toPx() + viewModel.firstVertex.radius.toPx(), + viewModel.firstVertex.y.value.toPx() + viewModel.firstVertex.radius.toPx() + ), + end = + Offset( + viewModel.secondVertex.x.value.toPx() + viewModel.secondVertex.radius.toPx(), + viewModel.secondVertex.y.value.toPx() + viewModel.secondVertex.radius.toPx() + ), + alpha = 1.0f + ) + } +} diff --git a/app/src/main/kotlin/view/graph/GraphView.kt b/app/src/main/kotlin/view/graph/GraphView.kt new file mode 100644 index 00000000..a92d81a3 --- /dev/null +++ b/app/src/main/kotlin/view/graph/GraphView.kt @@ -0,0 +1,17 @@ +package view.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import viewmodel.graph.GraphViewModel + +@Composable +fun GraphView(viewModel: GraphViewModel) { + Box(modifier = Modifier.fillMaxSize().background(Color.White)) { + viewModel.verticesVM.forEach { v -> VertexView(v) } + viewModel.edgesVM.forEach { e -> EdgeView(e) } + } +} diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt new file mode 100644 index 00000000..acaccb73 --- /dev/null +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -0,0 +1,55 @@ +package view.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import kotlinx.coroutines.launch +import viewmodel.graph.VertexViewModel + +@Composable +fun VertexView(viewModel: VertexViewModel) { + val coroutineScope = rememberCoroutineScope() + + Box( + modifier = + Modifier.offset { + IntOffset(viewModel.x.value.roundToPx(), viewModel.y.value.roundToPx()) + } + .size(viewModel.radius * 2) + .background( + if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, + shape = CircleShape + ) + .clip(CircleShape) + .pointerInput(Unit) { + coroutineScope.launch { + detectDragGestures { change, dragAmount -> + viewModel.onDrag(DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp())) + change.consume() + } + detectTapGestures( + onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } + ) + } + }, + contentAlignment = Alignment.Center + ) { + if (viewModel.dataVisible.value) { + Text(modifier = Modifier.align(Alignment.Center), text = viewModel.getVertexData) + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt b/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt new file mode 100644 index 00000000..0b679f74 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt @@ -0,0 +1,12 @@ +package view.tabScreen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun AnalyzeTab() { + Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 2nd tab") } +} diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt new file mode 100644 index 00000000..e68d69aa --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -0,0 +1,12 @@ +package view.tabScreen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun FileControlTab() { + Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 3rd tab") } +} diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt new file mode 100644 index 00000000..5b12e2d5 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -0,0 +1,12 @@ +package view.tabScreen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun GeneralTab() { + Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 1st tab") } +} diff --git a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt new file mode 100644 index 00000000..4e5dd1f4 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt @@ -0,0 +1,55 @@ +package view.tabScreen + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material.Tab +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SelectTabRow( + currentPageState: PagerState, + index: Int, + coroutineScope: CoroutineScope, + title: String +) { + Tab( + selected = currentPageState.currentPage == index, + onClick = { coroutineScope.launch { currentPageState.animateScrollToPage(index) } }, + modifier = Modifier, + content = { + Box( + modifier = + Modifier.background( + if (currentPageState.currentPage == index) Color.Magenta + else Color.Transparent + ) + .padding(10.dp) + .height(30.dp) + .width(120.dp) + .align(Alignment.CenterHorizontally), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + textAlign = TextAlign.Center, + color = if (currentPageState.currentPage == index) Color.White else Color.Black + // Set text color for selected and unselected tabs + ) + } + } + ) +} diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt new file mode 100644 index 00000000..496da3e1 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -0,0 +1,28 @@ +package viewmodel + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.dp +import model.abstractGraph.Graph +import viewmodel.graph.GraphViewModel +import viewmodel.graph.TestRepresentation + +class MainScreenViewModel(graph: Graph) { + val showVerticesData = mutableStateOf(false) + val showVerticesIds = mutableStateOf(false) + val graphViewModel = + GraphViewModel( + graph, + showVerticesIds, + showVerticesData, + WindowViewModel(mutableStateOf(1200.dp), mutableStateOf(700.dp)) + ) + + init { // here will be a placement-function call + TestRepresentation().place(740.0, 650.0, graphViewModel.verticesVM) + } + + // fun setEdgeColor + // + // fun setVerticesColor() + +} diff --git a/app/src/main/kotlin/viewmodel/WindowViewModel.kt b/app/src/main/kotlin/viewmodel/WindowViewModel.kt new file mode 100644 index 00000000..1a687713 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/WindowViewModel.kt @@ -0,0 +1,30 @@ +package viewmodel + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +class WindowViewModel( + private var height: MutableState = mutableStateOf(0.dp), + private var width: MutableState = mutableStateOf(0.dp), +) { + + @OptIn(ExperimentalComposeUiApi::class) + @Composable + fun SetCurrentDimensions() { + val configuration = LocalWindowInfo.current.containerSize + println(configuration) + height.value = configuration.height.dp + width.value = configuration.width.dp + } + + val getHeight + get() = height.value + + val getWidth + get() = width.value +} diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt new file mode 100644 index 00000000..24232e24 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -0,0 +1,9 @@ +package viewmodel.graph + +import model.abstractGraph.Edge + +class EdgeViewModel( + val firstVertex: VertexViewModel, + val secondVertex: VertexViewModel, + private val currentEdge: Edge, // do we really need it? +) {} diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt new file mode 100644 index 00000000..40db44c8 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -0,0 +1,41 @@ +package viewmodel.graph + +import androidx.compose.runtime.State +import model.abstractGraph.Graph +import viewmodel.WindowViewModel + +class GraphViewModel( + private val graph: Graph, + showIds: State, + showVerticesData: State, + WindowVM: WindowViewModel, +) { + private val _verticesViewModels = + graph.getVertices().associateWith { vertex -> + VertexViewModel( + dataVisible = showVerticesData, + idVisible = showIds, + vertex = vertex, + CurrentWindowVM = WindowVM + ) + } + + private val _edgeViewModels = + graph.getEdges().associateWith { edge -> + val firstVertex: VertexViewModel = + _verticesViewModels[edge.vertex1] + ?: throw NoSuchElementException("No such View Model, with mentioned edges") + + val secondVertex: VertexViewModel = + _verticesViewModels[edge.vertex1] + ?: throw NoSuchElementException("No such View Model, with mentioned edges") + + EdgeViewModel(firstVertex, secondVertex, edge) + } + + val verticesVM: Collection> + get() = _verticesViewModels.values + + val edgesVM: Collection> + get() = _edgeViewModels.values +} diff --git a/app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt b/app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt new file mode 100644 index 00000000..175f00a8 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt @@ -0,0 +1,47 @@ +package viewmodel.graph + +import androidx.compose.ui.unit.dp +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin + +class TestRepresentation() { + + fun place(width: Double, height: Double, vertices: Collection>) { + if (vertices.isEmpty()) { + println("CircularPlacementStrategy.place: there is nothing to place 👐🏻") + return + } + + val center = Pair(width / 2, height / 2) + val angle = 2 * Math.PI / vertices.size + + val sorted = vertices.sortedBy { it.getVertexData } + val first = sorted.first() + var point = Pair(center.first, center.second - min(width, height) / 2) + first.x.value = point.first.dp + first.y.value = point.second.dp + + sorted.drop(1).onEach { + point = point.rotate(center, angle) + it.x.value = point.first.dp + it.y.value = 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 + } +} diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt new file mode 100644 index 00000000..c9b03ad9 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -0,0 +1,36 @@ +package viewmodel.graph + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import model.abstractGraph.Vertex +import viewmodel.WindowViewModel + +class VertexViewModel( + var x: MutableState = mutableStateOf(0.dp), + var y: MutableState = mutableStateOf(0.dp), + var dataVisible: State, + var idVisible: State, + private val vertex: Vertex, + val CurrentWindowVM: WindowViewModel, + val radius: Dp = 30.dp, +) { + var isSelected = mutableStateOf(false) + + val getVertexData + get() = vertex.data.toString() + + fun onDrag(dragAmount: DpOffset) { + + if (x.value + dragAmount.x > CurrentWindowVM.getWidth) + x.value = CurrentWindowVM.getWidth - radius + else x.value += dragAmount.x + + if (y.value + dragAmount.y > CurrentWindowVM.getWidth) + y.value = CurrentWindowVM.getWidth - radius + else y.value += dragAmount.y + } +} From 10aa1d0af4617b999727e455832c5ccc81c68c6e Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Wed, 15 May 2024 16:47:00 +0300 Subject: [PATCH 37/77] feat: add border support for surface, make it prettier #22 * feat: add support for WindowViewModel * feat: add border cross protection * refactor: apply ktfmt --- app/src/main/kotlin/view/MainScreen.kt | 14 +++--- app/src/main/kotlin/view/graph/EdgeView.kt | 32 ++++++++------ app/src/main/kotlin/view/graph/VertexView.kt | 44 +++++++++++-------- .../kotlin/view/tabScreen/SelectTabRow.kt | 16 +++---- .../kotlin/viewmodel/MainScreenViewModel.kt | 2 +- .../main/kotlin/viewmodel/WindowViewModel.kt | 3 +- .../kotlin/viewmodel/graph/GraphViewModel.kt | 5 +-- .../kotlin/viewmodel/graph/VertexViewModel.kt | 18 ++++---- 8 files changed, 71 insertions(+), 63 deletions(-) diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt index d40ac5b6..ac921f2d 100644 --- a/app/src/main/kotlin/view/MainScreen.kt +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -28,10 +28,10 @@ fun MainScreen(viewmodel: MainScreenViewModel) { Row { Column( modifier = - Modifier.width(360.dp) - .background(color = Color.White) - .fillMaxHeight() - .clip(shape = RoundedCornerShape(10.dp)) + Modifier.width(360.dp) + .background(color = Color.White) + .fillMaxHeight() + .clip(shape = RoundedCornerShape(10.dp)) // TODO: make it rounded only from right side ) { val pageState = rememberPagerState(pageCount = { 3 }) @@ -73,9 +73,9 @@ fun MainScreen(viewmodel: MainScreenViewModel) { Surface( modifier = - Modifier.fillMaxSize() - .border(2f.dp, Color.LightGray, RectangleShape) - .clipToBounds(), + Modifier.fillMaxSize() + .border(2f.dp, Color.Transparent, RectangleShape) + .clipToBounds(), color = Color.Transparent ) { GraphView(viewmodel.graphViewModel) diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt index f3df9956..3dc17a5e 100644 --- a/app/src/main/kotlin/view/graph/EdgeView.kt +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -1,31 +1,35 @@ package view.graph import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp +import viewmodel.WindowViewModel import viewmodel.graph.EdgeViewModel @Composable fun EdgeView(viewModel: EdgeViewModel) { - Canvas(modifier = Modifier.size(300.dp, 300.dp)) { + + val windowVM = WindowViewModel() + windowVM.SetCurrentDimensions() + + Canvas(modifier = Modifier.fillMaxSize()) { drawLine( - color = Color.Red, - strokeWidth = 10.0f, + color = Color.LightGray, + strokeWidth = 5.0f, start = - Offset( - viewModel.firstVertex.x.value.toPx() + viewModel.firstVertex.radius.toPx(), - viewModel.firstVertex.y.value.toPx() + viewModel.firstVertex.radius.toPx() - ), + Offset( + viewModel.firstVertex.x.value.toPx() + viewModel.firstVertex.radius.toPx(), + viewModel.firstVertex.y.value.toPx() + viewModel.firstVertex.radius.toPx() + ), end = - Offset( - viewModel.secondVertex.x.value.toPx() + viewModel.secondVertex.radius.toPx(), - viewModel.secondVertex.y.value.toPx() + viewModel.secondVertex.radius.toPx() - ), - alpha = 1.0f + Offset( + viewModel.secondVertex.x.value.toPx() + viewModel.secondVertex.radius.toPx(), + viewModel.secondVertex.y.value.toPx() + viewModel.secondVertex.radius.toPx() + ), ) } } + diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt index acaccb73..dd3edb56 100644 --- a/app/src/main/kotlin/view/graph/VertexView.kt +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -18,38 +18,44 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import kotlinx.coroutines.launch +import viewmodel.WindowViewModel import viewmodel.graph.VertexViewModel @Composable fun VertexView(viewModel: VertexViewModel) { val coroutineScope = rememberCoroutineScope() + val windowVM = WindowViewModel() + windowVM.SetCurrentDimensions() Box( modifier = - Modifier.offset { - IntOffset(viewModel.x.value.roundToPx(), viewModel.y.value.roundToPx()) - } - .size(viewModel.radius * 2) - .background( - if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, - shape = CircleShape - ) - .clip(CircleShape) - .pointerInput(Unit) { - coroutineScope.launch { - detectDragGestures { change, dragAmount -> - viewModel.onDrag(DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp())) - change.consume() - } - detectTapGestures( - onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } + Modifier.offset { + IntOffset(viewModel.x.value.roundToPx(), viewModel.y.value.roundToPx()) + } + .size(viewModel.radius * 2) + .background( + if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, + shape = CircleShape + ) + .clip(CircleShape) + .pointerInput(Unit) { + coroutineScope.launch { + detectDragGestures { change, dragAmount -> + viewModel.onDrag( + DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()), + windowVM ) + change.consume() } - }, + detectTapGestures( + onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } + ) + } + }, contentAlignment = Alignment.Center ) { if (viewModel.dataVisible.value) { Text(modifier = Modifier.align(Alignment.Center), text = viewModel.getVertexData) } } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt index 4e5dd1f4..70aa7799 100644 --- a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt +++ b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt @@ -33,14 +33,14 @@ fun SelectTabRow( content = { Box( modifier = - Modifier.background( - if (currentPageState.currentPage == index) Color.Magenta - else Color.Transparent - ) - .padding(10.dp) - .height(30.dp) - .width(120.dp) - .align(Alignment.CenterHorizontally), + Modifier.background( + if (currentPageState.currentPage == index) Color.Magenta + else Color.Transparent + ) + .padding(10.dp) + .height(30.dp) + .width(120.dp) + .align(Alignment.CenterHorizontally), contentAlignment = Alignment.Center ) { Text( diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 496da3e1..85ffa97b 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -9,12 +9,12 @@ import viewmodel.graph.TestRepresentation class MainScreenViewModel(graph: Graph) { val showVerticesData = mutableStateOf(false) val showVerticesIds = mutableStateOf(false) + val graphViewModel = GraphViewModel( graph, showVerticesIds, showVerticesData, - WindowViewModel(mutableStateOf(1200.dp), mutableStateOf(700.dp)) ) init { // here will be a placement-function call diff --git a/app/src/main/kotlin/viewmodel/WindowViewModel.kt b/app/src/main/kotlin/viewmodel/WindowViewModel.kt index 1a687713..2c2672cc 100644 --- a/app/src/main/kotlin/viewmodel/WindowViewModel.kt +++ b/app/src/main/kotlin/viewmodel/WindowViewModel.kt @@ -9,15 +9,14 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp class WindowViewModel( - private var height: MutableState = mutableStateOf(0.dp), private var width: MutableState = mutableStateOf(0.dp), + private var height: MutableState = mutableStateOf(0.dp), ) { @OptIn(ExperimentalComposeUiApi::class) @Composable fun SetCurrentDimensions() { val configuration = LocalWindowInfo.current.containerSize - println(configuration) height.value = configuration.height.dp width.value = configuration.width.dp } diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 40db44c8..34addd1c 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -2,13 +2,11 @@ package viewmodel.graph import androidx.compose.runtime.State import model.abstractGraph.Graph -import viewmodel.WindowViewModel class GraphViewModel( private val graph: Graph, showIds: State, showVerticesData: State, - WindowVM: WindowViewModel, ) { private val _verticesViewModels = graph.getVertices().associateWith { vertex -> @@ -16,7 +14,6 @@ class GraphViewModel( dataVisible = showVerticesData, idVisible = showIds, vertex = vertex, - CurrentWindowVM = WindowVM ) } @@ -27,7 +24,7 @@ class GraphViewModel( ?: throw NoSuchElementException("No such View Model, with mentioned edges") val secondVertex: VertexViewModel = - _verticesViewModels[edge.vertex1] + _verticesViewModels[edge.vertex2] ?: throw NoSuchElementException("No such View Model, with mentioned edges") EdgeViewModel(firstVertex, secondVertex, edge) diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index c9b03ad9..fa688f99 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -15,7 +15,6 @@ class VertexViewModel( var dataVisible: State, var idVisible: State, private val vertex: Vertex, - val CurrentWindowVM: WindowViewModel, val radius: Dp = 30.dp, ) { var isSelected = mutableStateOf(false) @@ -23,14 +22,17 @@ class VertexViewModel( val getVertexData get() = vertex.data.toString() - fun onDrag(dragAmount: DpOffset) { + fun onDrag(dragAmount: DpOffset, currentWindowVM: WindowViewModel) { - if (x.value + dragAmount.x > CurrentWindowVM.getWidth) - x.value = CurrentWindowVM.getWidth - radius - else x.value += dragAmount.x + val maxX = currentWindowVM.getWidth / 2 - 360.dp - radius * 2 + val maxY = currentWindowVM.getHeight / 2 - radius * 2 - if (y.value + dragAmount.y > CurrentWindowVM.getWidth) - y.value = CurrentWindowVM.getWidth - radius - else y.value += dragAmount.y + // calculate the new position after dragging + val newX = (x.value + dragAmount.x).coerceIn(0.dp, maxX) + val newY = (y.value + dragAmount.y).coerceIn(0.dp, maxY) + + // update the position + x.value = newX + y.value = newY } } From 352074011a2f6fa58591e4e278687d2a23f84698 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Wed, 15 May 2024 18:25:06 +0300 Subject: [PATCH 38/77] fix: add exception in case vertex does not exist in removeVertex and refactor #20 * fix(removeVertex): add exception throw if vertex isn't in the graph * refactor: change exception messages * refactor: replace finding edge with getEdge method in Dijkstra algorithm --- app/src/main/kotlin/model/DirectedGraph.kt | 16 +++++++++++----- app/src/main/kotlin/model/UndirectedGraph.kt | 14 +++++++++++--- .../main/kotlin/model/WeightedDirectedGraph.kt | 14 +++++++------- .../main/kotlin/model/WeightedUndirectedGraph.kt | 12 +++++------- app/src/main/kotlin/model/abstractGraph/Graph.kt | 12 ++++++++++-- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 32f00f4c..8a0830aa 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -10,7 +10,10 @@ open class DirectedGraph : Graph() { throw IllegalArgumentException("Can't add edge from vertex to itself.") if (vertex1.id > vertices.size || vertex2.id > vertices.size) - throw NoSuchElementException("Vertex1 or vertex2 is not in the vertices array.") + throw NoSuchElementException( + "One of vertices (${vertex1.id}, ${vertex1.data}) and " + + "(${vertex2.id}, ${vertex2.data}) is not in the vertices array." + ) val newEdge = Edge(vertex1, vertex2) edges.add(newEdge) @@ -22,8 +25,10 @@ open class DirectedGraph : Graph() { } override fun removeEdge(edgeToRemove: Edge): Edge { - if (edgeToRemove !in edges) - throw NoSuchElementException("Edge is not in the graph") + if (edgeToRemove !in edges) throw NoSuchElementException( + "Edge between vertices (${edgeToRemove.vertex1.id}, ${edgeToRemove.vertex1.data}) and " + + "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) is not in the graph" + ) val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 @@ -38,7 +43,9 @@ open class DirectedGraph : Graph() { override fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } - ?: throw NoSuchElementException("No edge between vertices with ids ${vertex1.id} and ${vertex2.id}") + ?: throw NoSuchElementException( + "No edge between vertices (${vertex1.id}, ${vertex1.data}) and (${vertex2.id}, ${vertex2.data})" + ) return edge } @@ -92,4 +99,3 @@ open class DirectedGraph : Graph() { adjacencyMap.putAll(reversedAdjacencyMap) } } - diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index d25e09c9..e72584a3 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -11,7 +11,10 @@ open class UndirectedGraph : Graph() { throw IllegalArgumentException("Can't add edge from vertex to itself.") if (vertex1.id > vertices.size || vertex2.id > vertices.size) - throw NoSuchElementException("Vertex1 or vertex2 is not in the vertices array.") + throw NoSuchElementException( + "One of vertices (${vertex1.id}, ${vertex1.data}) and " + + "(${vertex2.id}, ${vertex2.data}) is not in the vertices array." + ) val newEdge = Edge(vertex1, vertex2) edges.add(newEdge) @@ -27,7 +30,10 @@ open class UndirectedGraph : Graph() { override fun removeEdge(edgeToRemove: Edge): Edge { if (edgeToRemove !in edges) - throw NoSuchElementException("Edge is not in the graph.") + throw NoSuchElementException( + "Edge between vertices (${edgeToRemove.vertex1.id}, ${edgeToRemove.vertex1.data}) and " + + "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) is not in the graph" + ) val vertex1 = edgeToRemove.vertex1 val vertex2 = edgeToRemove.vertex2 @@ -46,7 +52,9 @@ open class UndirectedGraph : Graph() { override fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge { val edge = outgoingEdgesMap[vertex1]?.find { it.isIncident(vertex2) } ?: outgoingEdgesMap[vertex2]?.find { it.isIncident(vertex1) } - ?: throw NoSuchElementException("No edge between vertices with ids ${vertex1.id} and ${vertex2.id}") + ?: throw NoSuchElementException( + "No edge between vertices (${vertex1.id}, ${vertex1.data}) and (${vertex2.id}, ${vertex2.data})" + ) return edge } diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index d0aa66ba..3839c3bb 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -23,7 +23,10 @@ class WeightedDirectedGraph : DirectedGraph() { fun getWeight(edge: Edge): Int { val weight = weightMap[edge] - ?: throw NoSuchElementException("No weight found for edge $edge") + ?: throw NoSuchElementException( + "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data})" + + "and (${edge.vertex2.id}, ${edge.vertex2.data})" + ) return weight } @@ -63,13 +66,10 @@ class WeightedDirectedGraph : DirectedGraph() { // If no path exists return emptyList() } - if (edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } == null) { - throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") - } + path.add( - Pair(currentVertex, - edges.find { it.vertex1 == predecessor && it.vertex2 == currentVertex } - ?: throw NoSuchElementException("There is no edge between these vertices"))) + Pair(currentVertex, getEdge(predecessor, currentVertex)) + ) currentVertex = predecessor } return path.reversed() diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index dcd17ac9..bf26356d 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -23,7 +23,10 @@ class WeightedUndirectedGraph : UndirectedGraph() { fun getWeight(edge: Edge): Int { val weight = weightMap[edge] - ?: throw NoSuchElementException("No weight found for edge $edge") + ?: throw NoSuchElementException( + "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data})" + + "and (${edge.vertex2.id}, ${edge.vertex2.data})" + ) return weight } @@ -76,12 +79,7 @@ class WeightedUndirectedGraph : UndirectedGraph() { throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") } path.add( - Pair( - currentVertex, - edges.find { - it.vertex1 == predecessor && it.vertex2 == currentVertex || - it.vertex2 == predecessor && it.vertex1 == currentVertex - } ?: throw NoSuchElementException("There is no edge between these vertices") ) + Pair(currentVertex, getEdge(predecessor, currentVertex)) ) currentVertex = predecessor } diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index e77c9bf5..e6519c43 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -21,6 +21,12 @@ abstract class Graph { } fun removeVertex(vertexToRemove: Vertex): Vertex { + if (vertexToRemove !in vertices) { + throw NoSuchElementException( + "Vertex (${vertexToRemove.id}, ${vertexToRemove.data}) isn't in the vertices list" + ) + } + nextId-- removeVertexFromEverywhere(vertexToRemove) @@ -83,14 +89,16 @@ abstract class Graph { // This and next two methods are used to localize exceptions protected fun getNeighbours(vertex: Vertex): ArrayList> { val neighbours = adjacencyMap[vertex] - ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the adjacency map.") + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") return neighbours } protected fun getOutgoingEdges(vertex: Vertex): ArrayList> { val outgoingEdges = outgoingEdgesMap[vertex] - ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the outgoing edges map.") + ?: throw NoSuchElementException( + "Vertex (${vertex.id}, ${vertex.data}) is not present in the outgoing edges map." + ) return outgoingEdges } From da878acbed5f547a6ca142b760d530fecff93bc4 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Wed, 15 May 2024 21:06:55 +0300 Subject: [PATCH 39/77] feat: implement basic UI elements for tabs + helpful dialogs #23 * feat: add color palette for UI * feat: implement 'add vertex' action + dial. window * feat: complement color palette * refactor: apply ktfmt * feat: switch palette from pink to ocean blue * feat: add dialogue with inital graph setup * fix: change app name * fix: add new parameter for tab call * feat: implement basic functions of general tab --- app/src/main/kotlin/ColorPalette.kt | 37 ++++ app/src/main/kotlin/Main.kt | 4 +- .../kotlin/view/CreateGraphDialogWindow.kt | 169 +++++++++++++++ app/src/main/kotlin/view/MainScreen.kt | 105 +++++----- .../kotlin/view/SelectInitDialogWindow.kt | 78 +++++++ app/src/main/kotlin/view/graph/VertexView.kt | 44 ++-- .../main/kotlin/view/tabScreen/AnalyzeTab.kt | 3 +- .../kotlin/view/tabScreen/FileControlTab.kt | 3 +- .../main/kotlin/view/tabScreen/GeneralTab.kt | 198 +++++++++++++++++- .../kotlin/view/tabScreen/SelectTabRow.kt | 5 +- 10 files changed, 558 insertions(+), 88 deletions(-) create mode 100644 app/src/main/kotlin/ColorPalette.kt create mode 100644 app/src/main/kotlin/view/CreateGraphDialogWindow.kt create mode 100644 app/src/main/kotlin/view/SelectInitDialogWindow.kt diff --git a/app/src/main/kotlin/ColorPalette.kt b/app/src/main/kotlin/ColorPalette.kt new file mode 100644 index 00000000..042cefec --- /dev/null +++ b/app/src/main/kotlin/ColorPalette.kt @@ -0,0 +1,37 @@ +import androidx.compose.material.MaterialTheme +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +data class ColorPalette( + val primary: Color, + val secondary: Color, + val background: Color, + val surface: Color, + val secondaryVariant: Color, +) + +@Composable +fun MyAppTheme(content: @Composable () -> Unit) { + val mycolors = ColorPalette( + primary = Color(0xFF8AAAC6), + secondary = Color(0xFFAECCE4), + background = Color(0xFFE5F3FD), + secondaryVariant = Color(0xFFF5FBFF), + surface = Color.White, + ) + + fun ColorPalette.toMaterialColors() = lightColors( + primary = primary, + secondary = secondary, + background = background, + secondaryVariant = secondaryVariant, + surface = surface, + ) + + MaterialTheme( + colors = mycolors.toMaterialColors() + ) { + content() + } +} diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt index 236fdac8..b0d68c2a 100644 --- a/app/src/main/kotlin/Main.kt +++ b/app/src/main/kotlin/Main.kt @@ -30,13 +30,13 @@ val testGraph: Graph = @Composable @Preview fun App() { - MaterialTheme { MainScreen(MainScreenViewModel(testGraph)) } + MaterialTheme { MainScreen(MainScreenViewModel(testGraph), true) } } fun main() = application { Window( onCloseRequest = ::exitApplication, - title = "Graphs-2", + title = "WUDU", state = rememberWindowState( position = WindowPosition(alignment = Alignment.Center), diff --git a/app/src/main/kotlin/view/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/CreateGraphDialogWindow.kt new file mode 100644 index 00000000..06a39ada --- /dev/null +++ b/app/src/main/kotlin/view/CreateGraphDialogWindow.kt @@ -0,0 +1,169 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role.Companion.RadioButton +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun CreateGraphDialogWindow() { + + var closeDialog by remember { mutableStateOf(false) } + + if (!closeDialog) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(700.dp).height(390.dp) + ) { + Text( + "Create Graph", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Row(modifier = Modifier.fillMaxWidth().height(300.dp)) { + Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { + Text("Select stored data:") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + val radioOptions = listOf("Integer", "UInteger", "String", "Boolean") + val selectedOptionIndex = remember { mutableStateOf(0) } + Column(modifier = Modifier.width(220.dp)) { + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedOptionIndex.value = index + } + ) { + RadioButton( + selected = selectedOptionIndex.value == index, + onClick = { selectedOptionIndex.value = index }, + colors = + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) + ) + Spacer(Modifier.width(1.dp)) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = + if (selectedOptionIndex.value == index) Color.Black + else Color.Gray + ) + } + } + } + } + + Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { + Text("Select the orientation:") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + val radioOptions = listOf("Undirected", "Directed") + val selectedOptionIndex = remember { mutableStateOf(0) } + Column(modifier = Modifier.width(220.dp)) { + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedOptionIndex.value = index + } + ) { + RadioButton( + selected = selectedOptionIndex.value == index, + onClick = { selectedOptionIndex.value = index }, + colors = + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) + ) + Spacer(Modifier.width(1.dp)) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = + if (selectedOptionIndex.value == index) Color.Black + else Color.Gray + ) + } + } + } + } + + Column(modifier = Modifier.width(250.dp).height(200.dp)) { + Text("Select the weightnes*:") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + Column(modifier = Modifier.width(220.dp)) { + val radioOptions = listOf("Unweighted", "Weighted") + val selectedOptionIndex = remember { mutableStateOf(0) } + + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedOptionIndex.value = index + } + ) { + RadioButton( + selected = selectedOptionIndex.value == index, + onClick = { selectedOptionIndex.value = index }, + colors = + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) + ) + Spacer(Modifier.width(1.dp)) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = + if (selectedOptionIndex.value == index) Color.Black + else Color.Gray + ) + } + } + } + } + } + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { closeDialog = true } + ) { + Text("Apply", color = Color.White) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt index ac921f2d..e6ec8b0c 100644 --- a/app/src/main/kotlin/view/MainScreen.kt +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -1,8 +1,8 @@ package view +import MyAppTheme import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -13,9 +13,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp import view.graph.GraphView import view.tabScreen.* @@ -23,62 +20,62 @@ import viewmodel.MainScreenViewModel @OptIn(ExperimentalFoundationApi::class) @Composable -fun MainScreen(viewmodel: MainScreenViewModel) { - - Row { - Column( - modifier = - Modifier.width(360.dp) - .background(color = Color.White) - .fillMaxHeight() - .clip(shape = RoundedCornerShape(10.dp)) - // TODO: make it rounded only from right side - ) { - val pageState = rememberPagerState(pageCount = { 3 }) - val coroutineScope = rememberCoroutineScope() - val tabs = listOf("General", "Analyze", " File Control") - - TabRow( - selectedTabIndex = pageState.currentPage, - contentColor = Color.Red, - backgroundColor = Color.Gray, - divider = {}, // to remove divider between - indicator = { tabPositions -> - TabRowDefaults.Indicator( - modifier = Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), - height = 0.dp - ) - }, - modifier = Modifier.height(50.dp) +fun MainScreen(viewmodel: MainScreenViewModel, showDialog: Boolean) { + MyAppTheme { + // Content of the main screen + Row { + // Column with tabs and content + Column( + modifier = + Modifier.width(360.dp) + .background(color = MaterialTheme.colors.surface) + .fillMaxHeight() + .clip(shape = RoundedCornerShape(10.dp)) ) { - tabs.forEachIndexed { index, title -> - SelectTabRow(pageState, index, coroutineScope, title) - } - } + // Tab row + val pageState = rememberPagerState(pageCount = { 3 }) + val coroutineScope = rememberCoroutineScope() + val tabs = listOf("General", "Analyze", "Save & Load") - HorizontalPager(state = pageState, userScrollEnabled = true) { - Column( - modifier = Modifier.width(360.dp).background(Color.LightGray).fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, + TabRow( + selectedTabIndex = pageState.currentPage, + contentColor = MaterialTheme.colors.surface, + backgroundColor = MaterialTheme.colors.secondary, + divider = {}, // to remove divider between + indicator = { tabPositions -> + TabRowDefaults.Indicator( + modifier = + Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), + height = 0.dp + ) + }, + modifier = Modifier.height(50.dp) ) { - when (pageState.currentPage) { - 0 -> GeneralTab() - 1 -> AnalyzeTab() - 2 -> FileControlTab() + tabs.forEachIndexed { index, title -> + SelectTabRow(pageState, index, coroutineScope, title) } } - } - } - Surface( - modifier = - Modifier.fillMaxSize() - .border(2f.dp, Color.Transparent, RectangleShape) - .clipToBounds(), - color = Color.Transparent - ) { - GraphView(viewmodel.graphViewModel) + // Content corresponding to the selected tab + HorizontalPager(state = pageState, userScrollEnabled = true) { + Column( + modifier = + Modifier.width(360.dp) + .background(MaterialTheme.colors.background) + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + when (pageState.currentPage) { + 0 -> GeneralTab(viewmodel.graphViewModel) + 1 -> AnalyzeTab(viewmodel.graphViewModel) + 2 -> FileControlTab(viewmodel.graphViewModel) + } + } + } + } + Surface(modifier = Modifier.fillMaxSize()) { GraphView(viewmodel.graphViewModel) } } + GraphInitDialogWindow(showDialog = showDialog) } } diff --git a/app/src/main/kotlin/view/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/SelectInitDialogWindow.kt new file mode 100644 index 00000000..2105293a --- /dev/null +++ b/app/src/main/kotlin/view/SelectInitDialogWindow.kt @@ -0,0 +1,78 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun GraphInitDialogWindow( + showDialog: Boolean, +) { + var showGraphDialog by remember { mutableStateOf(false) } + var showCreateGraphDialog by remember { mutableStateOf(false) } + + DisposableEffect(Unit) { + if (showDialog) { + showGraphDialog = true + } + + onDispose { showGraphDialog = false } + } + + if (showGraphDialog) { + Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) { + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) + ) { + Text( + "Welcome to WUDU!", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Text("Please select how to initialize the graph") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(Color.Red), + onClick = { + showGraphDialog = false + showCreateGraphDialog = true + } + ) { + Text("Create", color = Color.White) + } + + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(Color.Blue), + onClick = { showGraphDialog = false } + ) { + Text("Import", color = Color.White) + } + } + } + } + } + + if (showCreateGraphDialog) { + CreateGraphDialogWindow() + } +} diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt index dd3edb56..7e96c511 100644 --- a/app/src/main/kotlin/view/graph/VertexView.kt +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -29,33 +29,33 @@ fun VertexView(viewModel: VertexViewModel) { Box( modifier = - Modifier.offset { - IntOffset(viewModel.x.value.roundToPx(), viewModel.y.value.roundToPx()) - } - .size(viewModel.radius * 2) - .background( - if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, - shape = CircleShape - ) - .clip(CircleShape) - .pointerInput(Unit) { - coroutineScope.launch { - detectDragGestures { change, dragAmount -> - viewModel.onDrag( - DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()), - windowVM + Modifier.offset { + IntOffset(viewModel.x.value.roundToPx(), viewModel.y.value.roundToPx()) + } + .size(viewModel.radius * 2) + .background( + if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, + shape = CircleShape + ) + .clip(CircleShape) + .pointerInput(Unit) { + coroutineScope.launch { + detectDragGestures { change, dragAmount -> + viewModel.onDrag( + DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()), + windowVM + ) + change.consume() + } + detectTapGestures( + onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } ) - change.consume() } - detectTapGestures( - onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } - ) - } - }, + }, contentAlignment = Alignment.Center ) { if (viewModel.dataVisible.value) { Text(modifier = Modifier.align(Alignment.Center), text = viewModel.getVertexData) } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt b/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt index 0b679f74..c6d65cf4 100644 --- a/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt +++ b/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt @@ -5,8 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import viewmodel.graph.GraphViewModel @Composable -fun AnalyzeTab() { +fun AnalyzeTab(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 2nd tab") } } diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index e68d69aa..6d371d27 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -5,8 +5,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import viewmodel.graph.GraphViewModel @Composable -fun FileControlTab() { +fun FileControlTab(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 3rd tab") } } diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index 5b12e2d5..473f17a2 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -1,12 +1,198 @@ package view.tabScreen -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import viewmodel.graph.GraphViewModel @Composable -fun GeneralTab() { - Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 1st tab") } +fun GeneralTab(graphVM: GraphViewModel) { + var showDialog by remember { mutableStateOf(false) } + var vertexData by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf("") } + var connectVertexId by remember { mutableStateOf("") } + var vertexExistenceStatus by remember { mutableStateOf(false) } + var firstVertexId by remember { mutableStateOf("") } + var secondVertexId by remember { mutableStateOf("") } + val displayId = remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { + Row(modifier = Modifier.height(0.dp)) {} + + Row( + modifier = Modifier.height(75.dp).padding(10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Column(modifier = Modifier.width(200.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = vertexData, + onValueChange = { vertexData = it }, + modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Vertex data", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ), + ) + } + Column(modifier = Modifier.width(120.dp).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(70.dp), + onClick = { if (vertexData.isNotEmpty()) showDialog = true }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("add") + } + } + } + + Row( + modifier = Modifier.height(75.dp).padding(10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Column(modifier = Modifier.width(100.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = firstVertexId, + onValueChange = { firstVertexId = it }, + modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 12.sp), + label = { + Text( + "1 edge ID", + style = MaterialTheme.typography.body1.copy(fontSize = 12.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + + Column(modifier = Modifier.width(100.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = secondVertexId, + onValueChange = { secondVertexId = it }, + modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), + label = { + Text( + "2 edge ID", + style = MaterialTheme.typography.body1.copy(fontSize = 12.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + + Column(modifier = Modifier.width(110.dp).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(70.dp), + onClick = {}, + // TODO add edge + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("add") + } + } + } + + Row( + modifier = Modifier.fillMaxWidth().padding(5.dp).clickable { + displayId.value = !displayId.value + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = displayId.value, + onCheckedChange = { displayId.value = it }, + // TODO display ids + colors = + CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.primary, uncheckedColor = MaterialTheme.colors.secondary) + ) + Text( + text = "Checkbox Text", + modifier = Modifier.padding(start = 10.dp, bottom = 3.dp).align(Alignment.CenterVertically) + ) + + } + } + + if (showDialog) { + Dialog(onDismissRequest = {}) { + vertexData = "" + + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) + ) { + Text("Input ID of vertex to connect with:") + + TextField( + value = connectVertexId, + onValueChange = { newValue -> + errorMessage = "" + connectVertexId = newValue + }, + modifier = Modifier.fillMaxWidth().height(60.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent) + ) + + if (errorMessage.isNotBlank()) { + Text(errorMessage, color = Color.Red, modifier = Modifier.padding(top = 8.dp)) + } + + Button( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + onClick = { + connectVertexId = connectVertexId.replace("\n", "") + + if (!connectVertexId.all { char -> char.isDigit() }) { + errorMessage = "ID should be a numeric" + } else if (!graphVM.checkVertexById(connectVertexId.toInt())) { + errorMessage = "There isn't a Vertex with such ID" + } else if (connectVertexId.isBlank()) { + errorMessage = "Please enter an ID" + } else if ( + connectVertexId.isNotBlank() && connectVertexId.toIntOrNull() == null + ) { + errorMessage = "ID must be an integer" + } else { + // val firstId = graphVM.addVertex(vertexData) + // graphVM.addEdge(firstId, connectVertexId.toInt()) + // TODO + showDialog = false + errorMessage = "" + connectVertexId = "" + } + } + ) { + Text("Connect") + } + } + } + } } diff --git a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt index 70aa7799..6a7693ad 100644 --- a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt +++ b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.PagerState +import androidx.compose.material.MaterialTheme import androidx.compose.material.Tab import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -34,7 +35,7 @@ fun SelectTabRow( Box( modifier = Modifier.background( - if (currentPageState.currentPage == index) Color.Magenta + if (currentPageState.currentPage == index) MaterialTheme.colors.primary else Color.Transparent ) .padding(10.dp) @@ -46,7 +47,7 @@ fun SelectTabRow( Text( text = title, textAlign = TextAlign.Center, - color = if (currentPageState.currentPage == index) Color.White else Color.Black + color = if (currentPageState.currentPage == index) MaterialTheme.colors.surface else Color.Black // Set text color for selected and unselected tabs ) } From 500c1d0b25831613a6f0d672b74ed77b11164570 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Wed, 15 May 2024 21:28:28 +0300 Subject: [PATCH 40/77] docs: add contributing rules #24 --- CONTRIBUTING.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..67fe4143 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# 💪 Interested in contributing? + +Good boy! Please read the few sections below to understand how to implement new features. + +> Be positive! Even if your changes don't get merged in WUDU, please don't be too sad, you will always be able to run workflows directly from your fork! +> Meow + +## 🤝 Accepted contributions + +The following contributions are accepted: + + + + + + + + + + + + + + + + + + + + + + + + +
SectionChangesAdditionsNotes
🧩 Features💢💢 +
    +
  • New features are allowed, but must be optional and backward compatible
  • +
+
🧪 Tests💢✅ + +
    +
  • Everything that makes WUDU more stable is welcomed!
  • +
+
🗃️ Repository +
    +
  • Workflows, license, readmes, etc. usually don't need to be edited
  • +
+
+ +**Legend** +* ✅: Contributions welcomed! +* 💢: Contributions are welcomed, but must be discussed first +* ❌: Only maintainers can manage these files \ No newline at end of file From 503a0d1b90238eae76e5151ed5e1d9d24a267ab7 Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Thu, 16 May 2024 20:48:30 +0300 Subject: [PATCH 41/77] test: add tests for findMinSpanningTree() method of weighted undirected graphs #25 --- .../model/WeightedUndirectedGraphTest.kt | 141 +++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt index 4d8d339f..7f582804 100644 --- a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt @@ -1,8 +1,19 @@ package model +import model.abstractGraph.Edge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test class WeightedUndirectedGraphTest { + private lateinit var graph: WeightedUndirectedGraph + + @BeforeEach + fun init() { + graph = WeightedUndirectedGraph() + } + @Nested inner class AddEdgeTest {} @@ -13,5 +24,133 @@ class WeightedUndirectedGraphTest { inner class FindShortestPathDijkstraTest {} @Nested - inner class FindMinSpanningTreeTest {} + inner class FindMinSpanningTreeTest { + @Nested + inner class `An edge is picked over another`() { + @Test + fun `if it has lesser weight but both have positive`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1, 5) + val lightEdge = graph.addEdge(v0, v1, 3) + + val expectedReturn = listOf(lightEdge) + val actualReturn = graph.findMinSpanningTree() + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has lesser weight but both have negative`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1, -5) + val lightEdge = graph.addEdge(v0, v1, -10) + + val expectedReturn = listOf(lightEdge) + val actualReturn = graph.findMinSpanningTree() + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has zero weight and other has positive`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1, 5) + val zeroEdge = graph.addEdge(v0, v1, 0) + + val expectedReturn = listOf(zeroEdge) + val actualReturn = graph.findMinSpanningTree() + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has negative weight and other has positive or zero`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1, 5) + graph.addEdge(v0, v1, 0) + val negativeEdge = graph.addEdge(v0, v1, -5) + + val expectedReturn = listOf(negativeEdge) + val actualReturn = graph.findMinSpanningTree() + + assertEquals(expectedReturn, actualReturn) + } + } + + @Nested + inner class `An edge is not picked over another`() { + @Test + fun `if it forms a cycle and has greatest weight in it`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1, 1) + val e12 = graph.addEdge(v1, v2, 1) + val e23 = graph.addEdge(v2, v3, 1) + + val cycleEdge30 = graph.addEdge(v3, v0, 5) + + val expectedReturn = setOf(e01, e12, e23) + val actualReturn = graph.findMinSpanningTree().toSet() + + assertEquals(expectedReturn, actualReturn) + } + } + + @Nested + inner class `All edges should be returned`() { + @Test + fun `if graph is a tree`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e01 = graph.addEdge(v0, v1, 1) + val e02 = graph.addEdge(v0, v2, 10) + val e23 = graph.addEdge(v2, v3, 0) + val e24 = graph.addEdge(v2, v4, -20) + + val expectedResult = setOf(e24, e23, e01, e02) + val actualResult = graph.findMinSpanningTree().toSet() + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No edge should be returned`() { + @Test + fun `if graph has no vertices`() { + val expectedResult = listOf>() + val actualResult = graph.findMinSpanningTree() + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has no edges`() { + graph.apply { + addVertex(0) + addVertex(1) + } + + val expectedResult = listOf>() + val actualResult = graph.findMinSpanningTree() + + assertEquals(expectedResult, actualResult) + } + } + } } From 44c59384548b381d991548bedd02094030ce8305 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Sat, 18 May 2024 13:09:35 +0300 Subject: [PATCH 42/77] test: add tests for abstract graph basic methods #21 * test(Graph): add setup method * test: add tests for getter methods + add global values for more convenient assertions * test: add tests for addVertex + rename defaultEdgesSet * test: add tests for removeVertex + refactor setup method and rename methods * feat: add equals method override in vertex and edge for more convenient comparison * test: change method source to argument provider in parameterized tests * feat: make all getter methods of graphs public * test: fix object comparison * fix: remove equals override method in vertex and edge * test: fix logical mistakes * test: remove some unused values * test: fix typo that caused wrong method call --- .../main/kotlin/model/abstractGraph/Edge.kt | 1 - .../main/kotlin/model/abstractGraph/Graph.kt | 7 +- .../kotlin/model/abstractGraph/GraphTest.kt | 324 +++++++++++++++++- 3 files changed, 315 insertions(+), 17 deletions(-) diff --git a/app/src/main/kotlin/model/abstractGraph/Edge.kt b/app/src/main/kotlin/model/abstractGraph/Edge.kt index 9d98dacc..139b87da 100644 --- a/app/src/main/kotlin/model/abstractGraph/Edge.kt +++ b/app/src/main/kotlin/model/abstractGraph/Edge.kt @@ -1,6 +1,5 @@ package model.abstractGraph class Edge(val vertex1: Vertex, val vertex2: Vertex) { - fun isIncident(vertex: Vertex) = (vertex1 == vertex || vertex2 == vertex) } diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index e6519c43..4d9d659c 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -86,15 +86,14 @@ abstract class Graph { fun getVertices() = vertices.toList() - // This and next two methods are used to localize exceptions - protected fun getNeighbours(vertex: Vertex): ArrayList> { + fun getNeighbours(vertex: Vertex): ArrayList> { val neighbours = adjacencyMap[vertex] ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") return neighbours } - protected fun getOutgoingEdges(vertex: Vertex): ArrayList> { + fun getOutgoingEdges(vertex: Vertex): ArrayList> { val outgoingEdges = outgoingEdgesMap[vertex] ?: throw NoSuchElementException( "Vertex (${vertex.id}, ${vertex.data}) is not present in the outgoing edges map." @@ -103,5 +102,5 @@ abstract class Graph { return outgoingEdges } - protected abstract fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge + abstract fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge } diff --git a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt index acceb173..829033d5 100644 --- a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt +++ b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt @@ -6,37 +6,337 @@ import model.WeightedDirectedGraph import model.WeightedUndirectedGraph import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ArgumentsProvider +import org.junit.jupiter.params.provider.ArgumentsSource import java.util.stream.Stream -@ParameterizedTest -@MethodSource("provideAllGraphTypes") +@ParameterizedTest(name = "{0}") +@ArgumentsSource(AllGraphTypesProvider::class) @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) private annotation class TestAllGraphTypes -fun provideAllGraphTypes(): Stream { - return Stream.of( +class AllGraphTypesProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( Arguments.of(UndirectedGraph()), Arguments.of(DirectedGraph()), - Arguments.of(WeightedDirectedGraph()), - Arguments.of(WeightedUndirectedGraph()) + Arguments.of(WeightedUndirectedGraph()), + Arguments.of(WeightedDirectedGraph()) ) } +fun setup(graph: Graph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1), + graph.addEdge(v1, v2), + graph.addEdge(v2, v3), + graph.addEdge(v3, v4), + graph.addEdge(v4, v1) + ) + + return defaultVerticesList to defaultEdgesSet +} + +val emptyVerticesList = listOf>() +val emptyEdgesSet = setOf>() + class GraphTest { @Nested - inner class GetVerticesTest {} + inner class GetVerticesTest { + @Nested + inner class `Graph is not empty` { + @TestAllGraphTypes + fun `non-empty list of vertices should be returned`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val actualList = graph.getVertices() + val expectedList = defaultVerticesList + + assertEquals(expectedList, actualList) + } + + @TestAllGraphTypes + fun `graph should not change`(graph: Graph) { + val graphStructure = setup(graph) + + graph.getVertices() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Graph is empty` { + @TestAllGraphTypes + fun `empty list should be returned`(graph: Graph) { + val actualList = graph.getVertices() + val expectedList: List = listOf() + + assertEquals(expectedList, actualList) + } + + @TestAllGraphTypes + fun `empty graph should not change`(graph: Graph) { + graph.getVertices() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = emptyVerticesList to emptyEdgesSet + + assertEquals(expectedGraph, actualGraph) + } + } + } @Nested - inner class GetEdgesTest {} + inner class GetEdgesTest { + @Nested + inner class `Graph is not empty` { + @TestAllGraphTypes + fun `non-empty list of edges should be returned`(graph: Graph) { + val graphStructure = setup(graph) + val defaultEdgesSet = graphStructure.second + + val actualSet = graph.getEdges().toSet() + val expectedSet = defaultEdgesSet + + assertEquals(expectedSet, actualSet) + } + + @TestAllGraphTypes + fun `graph should not change`(graph: Graph) { + val graphStructure = setup(graph) + + graph.getEdges() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Graph is empty` { + @TestAllGraphTypes + fun `empty list should be returned`(graph: Graph) { + val actualSet = graph.getEdges().toSet() + val expectedSet = emptyEdgesSet + + assertEquals(expectedSet, actualSet) + } + + @TestAllGraphTypes + fun `empty graph should not change`(graph: Graph) { + graph.getEdges() + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = emptyVerticesList to emptyEdgesSet + + assertEquals(expectedGraph, actualGraph) + } + } + } @Nested - inner class AddVertexTest {} + inner class AddVertexTest { + @TestAllGraphTypes + fun `added vertex should be returned`(graph: Graph) { + val returnedVertex = graph.addVertex(0) + + assertTrue(returnedVertex.id == 0 && returnedVertex.data == 0) + } + + @TestAllGraphTypes + fun `vertex should be added to graph`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val newVertex = graph.addVertex(5) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = (defaultVerticesList + newVertex) to defaultEdgesSet + + assertEquals(expectedGraph, actualGraph) + } + } @Nested - inner class RemoveVertexTest {} + inner class RemoveVertexTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllGraphTypes + fun `removed vertex should be returned`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val returnedVertex = graph.removeVertex(defaultVerticesList[2]) + + assertTrue(returnedVertex.id == 2 && returnedVertex.data == 2) + } + + @TestAllGraphTypes + fun `vertex added after removal should have right id`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + graph.removeVertex(defaultVerticesList[3]) + val newVertex = graph.addVertex(5) + + assertTrue(newVertex.id == 4) + } + + @Nested + inner class `Vertex is last` { + @TestAllGraphTypes + fun `vertex should be removed from vertices list`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val removedVertex = graph.removeVertex(defaultVerticesList[4]) + + val actualVertices = graph.getVertices() + val expectedVertices = defaultVerticesList - removedVertex + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllGraphTypes + fun `incident edges should be removed`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val e0 = graph.getEdge(v3, v4) + val e1 = graph.getEdge(v4, v1) + + graph.removeVertex(v4) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet - e0 - e1 + + assertEquals(expectedEdges, actualEdges) + } + } + + @Nested + inner class `Vertex isn't last` { + @TestAllGraphTypes + fun `last added vertex should be moved to removed vertex's place`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val oldV0 = defaultVerticesList[0] + val oldV1 = defaultVerticesList[1] + val oldV2 = defaultVerticesList[2] + val oldV3 = defaultVerticesList[3] + val oldV4 = defaultVerticesList[4] + + graph.removeVertex(oldV2) + + val newVertices = graph.getVertices() + + val newV0 = newVertices[0] + val newV1 = newVertices[1] + val newV2 = newVertices[2] + val newV3 = newVertices[3] + + assertTrue( + newV0 == oldV0 && + newV1 == oldV1 && + newV2.id == 2 && newV2.data == 4 && + newV3 == oldV3 + ) + } + + @TestAllGraphTypes + fun `last added vertex's incident edges should change`(graph: Graph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val oldV0 = defaultVerticesList[0] + val oldV1 = defaultVerticesList[1] + val oldV2 = defaultVerticesList[2] + val oldV3 = defaultVerticesList[3] + val oldV4 = defaultVerticesList[4] + + graph.removeVertex(oldV2) + + val newVertices = graph.getVertices() + + val newV0 = newVertices[0] + val newV1 = newVertices[1] + val newV2 = newVertices[2] + val newV3 = newVertices[3] + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = setOf( + graph.getEdge(newV0, newV1), + graph.getEdge(newV3, newV2), + graph.getEdge(newV2, newV1) + ) + + assertEquals(expectedEdges, actualEdges) + } + } + } + + @Nested + inner class `Vertex is not in the graph` { + @TestAllGraphTypes + fun `removing vertex from an empty graph should cause exception`(graph: Graph) { + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(0,0)) + } + } + + @TestAllGraphTypes + fun `removing non-existing vertex from a non-empty graph should cause exception`(graph: Graph) { + setup(graph) + + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(1904,-360)) + } + } + + @TestAllGraphTypes + fun `removing vertex with wrong id should cause exception`(graph: Graph) { + setup(graph) + + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(6,3)) + } + } + + @TestAllGraphTypes + fun `removing vertex with wrong data should cause exception`(graph: Graph) { + setup(graph) + + assertThrows(NoSuchElementException::class.java) { + graph.removeVertex(Vertex(0,35)) + } + } + } + } } From 1fd17526930fbf173aa869c21ada5601d3e072b0 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Sat, 18 May 2024 13:22:37 +0300 Subject: [PATCH 43/77] fix: make vertex's id var to get rid of a lot of complex code #26 * fix: removal of an element in a for loop * fix: make vertex's id var and simplify removeVertex method The id was val, which made copying last added vertex to the place of removed one very complex --- .../main/kotlin/model/abstractGraph/Graph.kt | 60 +++++-------------- .../main/kotlin/model/abstractGraph/Vertex.kt | 2 +- 2 files changed, 16 insertions(+), 46 deletions(-) diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 4d9d659c..2e22d37a 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -21,61 +21,33 @@ abstract class Graph { } fun removeVertex(vertexToRemove: Vertex): Vertex { - if (vertexToRemove !in vertices) { - throw NoSuchElementException( - "Vertex (${vertexToRemove.id}, ${vertexToRemove.data}) isn't in the vertices list" - ) - } - nextId-- - removeVertexFromEverywhere(vertexToRemove) - fixIdFragmentation(vertexToRemove) - - return vertexToRemove - } - - private fun fixIdFragmentation(vertexToRemove: Vertex) { - if (vertexToRemove.id == nextId) return - - val lastAddedVertex = vertices[nextId] - - val copyOfLastAddedVertex = Vertex(vertexToRemove.id, lastAddedVertex.data) - - vertices[copyOfLastAddedVertex.id] = copyOfLastAddedVertex - adjacencyMap[copyOfLastAddedVertex] = getNeighbours(lastAddedVertex) - outgoingEdgesMap[copyOfLastAddedVertex] = getOutgoingEdges(lastAddedVertex) - - val adjacentVertices = getNeighbours(copyOfLastAddedVertex) - - for (adjacentVertex in adjacentVertices) { - if (adjacencyMap[adjacentVertex]?.remove(lastAddedVertex) == true) { - adjacencyMap[adjacentVertex]?.add(copyOfLastAddedVertex) - } - } - - removeVertexFromEverywhere(lastAddedVertex) - } - - private fun removeVertexFromEverywhere(vertexToRemove: Vertex) { val adjacentVertices = getNeighbours(vertexToRemove) - for (adjacentVertex in adjacentVertices) adjacencyMap[adjacentVertex]?.remove(vertexToRemove) - // If vertexToRemove isn't last, it will be overridden by its copy in fixIdFragmentation - if (vertexToRemove.id == nextId) vertices.removeLast() - - for (edge in edges) { + // iterator is used because an element can't be removed in a for loop + val iterator = edges.iterator() + while (iterator.hasNext()) { + val edge = iterator.next() if (edge.isIncident(vertexToRemove)) { - edges.remove(edge) + iterator.remove() val incidentVertex = if (edge.vertex1 == vertexToRemove) edge.vertex2 else edge.vertex1 outgoingEdgesMap[incidentVertex]?.remove(edge) + adjacencyMap[incidentVertex]?.remove(vertexToRemove) } } + val lastAddedVertex = vertices[nextId] + lastAddedVertex.id = vertexToRemove.id + vertices[vertexToRemove.id] = lastAddedVertex + + vertices.remove(vertexToRemove) adjacencyMap.remove(vertexToRemove) outgoingEdgesMap.remove(vertexToRemove) + + return vertexToRemove } abstract fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge @@ -88,16 +60,14 @@ abstract class Graph { fun getNeighbours(vertex: Vertex): ArrayList> { val neighbours = adjacencyMap[vertex] - ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") + ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the adjacency map.") return neighbours } fun getOutgoingEdges(vertex: Vertex): ArrayList> { val outgoingEdges = outgoingEdgesMap[vertex] - ?: throw NoSuchElementException( - "Vertex (${vertex.id}, ${vertex.data}) is not present in the outgoing edges map." - ) + ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the outgoing edges map.") return outgoingEdges } diff --git a/app/src/main/kotlin/model/abstractGraph/Vertex.kt b/app/src/main/kotlin/model/abstractGraph/Vertex.kt index 9038bed7..721ce29a 100644 --- a/app/src/main/kotlin/model/abstractGraph/Vertex.kt +++ b/app/src/main/kotlin/model/abstractGraph/Vertex.kt @@ -1,3 +1,3 @@ package model.abstractGraph -class Vertex(val id: Int, val data: D) +class Vertex(var id: Int, val data: D) From 4192669f7357465ba52c5c3fea18cdf00b3f3500 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Sat, 18 May 2024 16:11:41 +0300 Subject: [PATCH 44/77] feat: link ViewModel and UI, improve UI #29 * feat: implement dialog window for graph creation * feat: add welcome dialog window * feat: add class for graph creation * fix: remove spare screen calls * feat: add another 'addVertex' scenario * refactor: add a lil bit of spice * fix: upload draft implementations of new methods * feat: implement 'add edges' and 'vertices' * fix: switch to 'secondVertexData' variable * feat: add FAQ button for graph info * feat: add png image for button * feat: throw 'graphType' parameter for FAQ * feat: call representation after 'add' operations --- app/src/main/kotlin/Main.kt | 29 +--- .../kotlin/view/CreateGraphDialogWindow.kt | 122 +++++++++----- app/src/main/kotlin/view/MainScreen.kt | 87 ++++++++-- .../kotlin/view/SelectInitDialogWindow.kt | 98 ++++++------ .../main/kotlin/view/tabScreen/GeneralTab.kt | 151 ++++++++++++------ .../kotlin/viewmodel/MainScreenViewModel.kt | 11 +- .../viewmodel/graph/CreateGraphViewModel.kt | 90 +++++++++++ .../kotlin/viewmodel/graph/GraphViewModel.kt | 49 +++++- .../kotlin/viewmodel/graph/VertexViewModel.kt | 2 +- app/src/main/resources/drawable/question.png | Bin 0 -> 17551 bytes 10 files changed, 449 insertions(+), 190 deletions(-) create mode 100644 app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt create mode 100644 app/src/main/resources/drawable/question.png diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt index b0d68c2a..e7c4d7ff 100644 --- a/app/src/main/kotlin/Main.kt +++ b/app/src/main/kotlin/Main.kt @@ -7,40 +7,21 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import java.awt.Dimension -import model.UndirectedGraph -import model.abstractGraph.Graph -import view.MainScreen -import viewmodel.MainScreenViewModel - -val testGraph: Graph = - UndirectedGraph().apply { - val v1 = addVertex(1) - val v2 = addVertex(2) - val v3 = addVertex(3) - val v4 = addVertex(4) - val v5 = addVertex(5) - - addEdge(v1, v5) - addEdge(v1, v4) - addEdge(v1, v3) - addEdge(v1, v2) - addEdge(v2, v4) - } +import view.SelectInitDialogWindow @Composable @Preview fun App() { - MaterialTheme { MainScreen(MainScreenViewModel(testGraph), true) } + MaterialTheme { + SelectInitDialogWindow(true).GraphInitDialogWindow(true) + } } fun main() = application { Window( onCloseRequest = ::exitApplication, title = "WUDU", - state = - rememberWindowState( - position = WindowPosition(alignment = Alignment.Center), - ), + state = rememberWindowState(position = WindowPosition(alignment = Alignment.Center)), ) { window.minimumSize = Dimension(1200, 700) App() diff --git a/app/src/main/kotlin/view/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/CreateGraphDialogWindow.kt index 06a39ada..b7450cd5 100644 --- a/app/src/main/kotlin/view/CreateGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/CreateGraphDialogWindow.kt @@ -8,27 +8,31 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.Role.Companion.RadioButton import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import viewmodel.graph.CreateGraphViewModel @Composable -fun CreateGraphDialogWindow() { +fun CreateGraphDialogWindow(viewModel: CreateGraphViewModel) { - var closeDialog by remember { mutableStateOf(false) } + var closeDialog = remember { mutableStateOf(false) } + val selectedStoredDataIndex = remember { mutableStateOf(0) } + val selectedOrientationIndex = remember { mutableStateOf(0) } + val selectedWeightinessIndex = remember { mutableStateOf(0) } + val createGraphClicked = remember { mutableStateOf(false) } - if (!closeDialog) { + if (!closeDialog.value) { Dialog( onDismissRequest = {}, properties = DialogProperties(usePlatformDefaultWidth = false) ) { Column( modifier = - Modifier.background(Color.White).padding(16.dp).width(700.dp).height(390.dp) + Modifier.background(Color.White).padding(16.dp).width(700.dp).height(390.dp) ) { Text( "Create Graph", @@ -43,31 +47,31 @@ fun CreateGraphDialogWindow() { Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} val radioOptions = listOf("Integer", "UInteger", "String", "Boolean") - val selectedOptionIndex = remember { mutableStateOf(0) } + Column(modifier = Modifier.width(220.dp)) { radioOptions.forEachIndexed { index, option -> Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedOptionIndex.value = index - } + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedStoredDataIndex.value = index + } ) { RadioButton( - selected = selectedOptionIndex.value == index, - onClick = { selectedOptionIndex.value = index }, + selected = selectedStoredDataIndex.value == index, + onClick = { selectedStoredDataIndex.value = index }, colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary - ) + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) ) Spacer(Modifier.width(1.dp)) Text( text = option, style = TextStyle(fontSize = 16.sp), color = - if (selectedOptionIndex.value == index) Color.Black - else Color.Gray + if (selectedStoredDataIndex.value == index) Color.Black + else Color.Gray ) } } @@ -80,31 +84,30 @@ fun CreateGraphDialogWindow() { Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} val radioOptions = listOf("Undirected", "Directed") - val selectedOptionIndex = remember { mutableStateOf(0) } Column(modifier = Modifier.width(220.dp)) { radioOptions.forEachIndexed { index, option -> Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedOptionIndex.value = index - } + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedOrientationIndex.value = index + } ) { RadioButton( - selected = selectedOptionIndex.value == index, - onClick = { selectedOptionIndex.value = index }, + selected = selectedOrientationIndex.value == index, + onClick = { selectedOrientationIndex.value = index }, colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary - ) + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) ) Spacer(Modifier.width(1.dp)) Text( text = option, style = TextStyle(fontSize = 16.sp), color = - if (selectedOptionIndex.value == index) Color.Black - else Color.Gray + if (selectedOrientationIndex.value == index) Color.Black + else Color.Gray ) } } @@ -118,31 +121,30 @@ fun CreateGraphDialogWindow() { Column(modifier = Modifier.width(220.dp)) { val radioOptions = listOf("Unweighted", "Weighted") - val selectedOptionIndex = remember { mutableStateOf(0) } radioOptions.forEachIndexed { index, option -> Row( verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedOptionIndex.value = index - } + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedWeightinessIndex.value = index + } ) { RadioButton( - selected = selectedOptionIndex.value == index, - onClick = { selectedOptionIndex.value = index }, + selected = selectedWeightinessIndex.value == index, + onClick = { selectedWeightinessIndex.value = index }, colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary - ) + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) ) Spacer(Modifier.width(1.dp)) Text( text = option, style = TextStyle(fontSize = 16.sp), color = - if (selectedOptionIndex.value == index) Color.Black - else Color.Gray + if (selectedWeightinessIndex.value == index) Color.Black + else Color.Gray ) } } @@ -158,7 +160,11 @@ fun CreateGraphDialogWindow() { Button( modifier = Modifier.width(145.dp).height(50.dp), colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), - onClick = { closeDialog = true } + onClick = { + closeDialog.value = true + createGraphClicked.value = true + } + ) { Text("Apply", color = Color.White) } @@ -166,4 +172,42 @@ fun CreateGraphDialogWindow() { } } } + if (createGraphClicked.value) { + onCreateGraphClicked( + viewModel, + selectedStoredDataIndex.value, + selectedOrientationIndex.value, + selectedWeightinessIndex.value + ) + } } + + +@Composable +fun onCreateGraphClicked( + viewModel: CreateGraphViewModel, + storedDataIndex: Int, + orientationIndex: Int, + weightnessIndex: Int +) { + val storedData = when (storedDataIndex) { + 0 -> CreateGraphViewModel.GraphType.Integer + 1 -> CreateGraphViewModel.GraphType.UInteger + 2 -> CreateGraphViewModel.GraphType.String + else -> CreateGraphViewModel.GraphType.Integer // default to integer + } + + val graphStructure = when (orientationIndex) { + 0 -> CreateGraphViewModel.GraphStructure.Directed + 1 -> CreateGraphViewModel.GraphStructure.Undirected + else -> CreateGraphViewModel.GraphStructure.Directed // default to directed + } + + val weight = when (weightnessIndex) { + 0 -> CreateGraphViewModel.Weight.Weighted + 1 -> CreateGraphViewModel.Weight.Unweighted + else -> CreateGraphViewModel.Weight.Weighted // default to weighted + } + + return viewModel.createGraph(storedData, graphStructure, weight) +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt index e6ec8b0c..2f232fe5 100644 --- a/app/src/main/kotlin/view/MainScreen.kt +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -2,7 +2,11 @@ package view import MyAppTheme import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -11,26 +15,36 @@ import androidx.compose.material.* import androidx.compose.material.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import view.graph.GraphView -import view.tabScreen.* +import view.tabScreen.AnalyzeTab +import view.tabScreen.FileControlTab +import view.tabScreen.GeneralTab +import view.tabScreen.SelectTabRow import viewmodel.MainScreenViewModel -@OptIn(ExperimentalFoundationApi::class) + +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Composable -fun MainScreen(viewmodel: MainScreenViewModel, showDialog: Boolean) { +fun MainScreen(viewmodel: MainScreenViewModel) { MyAppTheme { - // Content of the main screen + // state for hover effect + var isHovered by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + Row { // Column with tabs and content Column( modifier = - Modifier.width(360.dp) - .background(color = MaterialTheme.colors.surface) - .fillMaxHeight() - .clip(shape = RoundedCornerShape(10.dp)) + Modifier.width(360.dp) + .background(color = MaterialTheme.colors.surface) + .fillMaxHeight() + .clip(shape = RoundedCornerShape(10.dp)) ) { // Tab row val pageState = rememberPagerState(pageCount = { 3 }) @@ -45,7 +59,7 @@ fun MainScreen(viewmodel: MainScreenViewModel, showDialog: Boolean) { indicator = { tabPositions -> TabRowDefaults.Indicator( modifier = - Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), + Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), height = 0.dp ) }, @@ -60,9 +74,9 @@ fun MainScreen(viewmodel: MainScreenViewModel, showDialog: Boolean) { HorizontalPager(state = pageState, userScrollEnabled = true) { Column( modifier = - Modifier.width(360.dp) - .background(MaterialTheme.colors.background) - .fillMaxHeight(), + Modifier.width(360.dp) + .background(MaterialTheme.colors.background) + .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, ) { @@ -76,6 +90,53 @@ fun MainScreen(viewmodel: MainScreenViewModel, showDialog: Boolean) { } Surface(modifier = Modifier.fillMaxSize()) { GraphView(viewmodel.graphViewModel) } } - GraphInitDialogWindow(showDialog = showDialog) + // Hoverable box over the existing Surface + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.TopEnd + ) { + + Surface( + modifier = Modifier + .padding(top = 20.dp) + .width(220.dp).height(50.dp) + .background(if (isHovered) Color.LightGray else Color.Gray) + .hoverable(interactionSource = interactionSource) + ) { + if (isHovered) { + Box( + modifier = Modifier.width(200.dp), + contentAlignment = Alignment.CenterEnd + ) { + Text( + text = viewmodel.graphViewModel.graphType.value.replace(" ", "\nData type: "), + color = Color.Black + ) + } + } else { + Image( + painterResource("drawable/question.png"), + contentDescription = "Question Mark Icon", + modifier = Modifier.size(10.dp).padding(start = 50.dp) + ) + } + } + + // Observe the interaction source to change the hover state + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is HoverInteraction.Enter -> { + isHovered = true + } + + is HoverInteraction.Exit -> { + isHovered = false + } + } + } + } + } } } diff --git a/app/src/main/kotlin/view/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/SelectInitDialogWindow.kt index 2105293a..a6cb670b 100644 --- a/app/src/main/kotlin/view/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/SelectInitDialogWindow.kt @@ -13,66 +13,70 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import model.abstractGraph.Graph +import viewmodel.graph.CreateGraphViewModel -@Composable -fun GraphInitDialogWindow( - showDialog: Boolean, -) { - var showGraphDialog by remember { mutableStateOf(false) } - var showCreateGraphDialog by remember { mutableStateOf(false) } - DisposableEffect(Unit) { - if (showDialog) { - showGraphDialog = true - } +class SelectInitDialogWindow( + private val showDialog: Boolean, +) { + var showGraphDialog by mutableStateOf(false) + var showCreateGraphDialog by mutableStateOf(false) - onDispose { showGraphDialog = false } - } + @Composable + fun GraphInitDialogWindow( + showDialog: Boolean, + ) { - if (showGraphDialog) { - Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) { - Column( - modifier = - Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) - ) { - Text( - "Welcome to WUDU!", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - modifier = Modifier.padding(bottom = 10.dp) - ) - Text("Please select how to initialize the graph") + DisposableEffect(Unit) { + if (showDialog) { + showGraphDialog = true + } - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + onDispose { showGraphDialog = false } + } - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(30.dp) + if (showGraphDialog) { + Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) { + Column( + modifier = Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) ) { - Button( - modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(Color.Red), - onClick = { - showGraphDialog = false - showCreateGraphDialog = true - } - ) { - Text("Create", color = Color.White) - } + Text( + "Welcome to WUDU!", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Text("Please select how to initialize the graph") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - Button( - modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(Color.Blue), - onClick = { showGraphDialog = false } + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(30.dp) ) { - Text("Import", color = Color.White) + Button(modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(Color.Red), + onClick = { + showGraphDialog = false + showCreateGraphDialog = true + }) { + Text("Create", color = Color.White) + } + + Button(modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(Color.Blue), + onClick = { showGraphDialog = false } + ) { + Text("Import", color = Color.White) + } } } } } - } - if (showCreateGraphDialog) { - CreateGraphDialogWindow() + if (showCreateGraphDialog) { + CreateGraphDialogWindow(CreateGraphViewModel()) + } } } diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index 473f17a2..caa5be71 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -22,10 +22,10 @@ fun GeneralTab(graphVM: GraphViewModel) { var vertexData by remember { mutableStateOf("") } var errorMessage by remember { mutableStateOf("") } var connectVertexId by remember { mutableStateOf("") } - var vertexExistenceStatus by remember { mutableStateOf(false) } var firstVertexId by remember { mutableStateOf("") } var secondVertexId by remember { mutableStateOf("") } - val displayId = remember { mutableStateOf(false) } + var displayId by remember { mutableStateOf(false) } + var secondVertexData by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { Row(modifier = Modifier.height(0.dp)) {} @@ -48,9 +48,9 @@ fun GeneralTab(graphVM: GraphViewModel) { ) }, colors = - TextFieldDefaults.textFieldColors( - backgroundColor = MaterialTheme.colors.secondaryVariant - ), + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ), ) } Column(modifier = Modifier.width(120.dp).fillMaxHeight(), Arrangement.Center) { @@ -82,9 +82,9 @@ fun GeneralTab(graphVM: GraphViewModel) { ) }, colors = - TextFieldDefaults.textFieldColors( - backgroundColor = MaterialTheme.colors.secondaryVariant - ) + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) ) } @@ -101,9 +101,9 @@ fun GeneralTab(graphVM: GraphViewModel) { ) }, colors = - TextFieldDefaults.textFieldColors( - backgroundColor = MaterialTheme.colors.secondaryVariant - ) + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) ) } @@ -121,16 +121,19 @@ fun GeneralTab(graphVM: GraphViewModel) { Row( modifier = Modifier.fillMaxWidth().padding(5.dp).clickable { - displayId.value = !displayId.value + displayId = !displayId }, verticalAlignment = Alignment.CenterVertically ) { Checkbox( - checked = displayId.value, - onCheckedChange = { displayId.value = it }, + checked = displayId, + onCheckedChange = { displayId = it }, // TODO display ids colors = - CheckboxDefaults.colors(checkedColor = MaterialTheme.colors.primary, uncheckedColor = MaterialTheme.colors.secondary) + CheckboxDefaults.colors( + checkedColor = MaterialTheme.colors.primary, + uncheckedColor = MaterialTheme.colors.secondary + ) ) Text( text = "Checkbox Text", @@ -146,51 +149,93 @@ fun GeneralTab(graphVM: GraphViewModel) { Column( modifier = - Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) ) { - Text("Input ID of vertex to connect with:") + if (graphVM.verticesVM.isEmpty()) { + Text("Input data of second vertex to create and connect with") - TextField( - value = connectVertexId, - onValueChange = { newValue -> - errorMessage = "" - connectVertexId = newValue - }, - modifier = Modifier.fillMaxWidth().height(60.dp).clip(RoundedCornerShape(8.dp)), - textStyle = TextStyle(fontSize = 14.sp), - colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent) - ) + TextField( + value = secondVertexData, + onValueChange = { enteredValue -> + errorMessage = "" + secondVertexData = enteredValue + }, + modifier = Modifier.fillMaxWidth().height(60.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent) + ) - if (errorMessage.isNotBlank()) { - Text(errorMessage, color = Color.Red, modifier = Modifier.padding(top = 8.dp)) - } + if (errorMessage.isNotBlank()) { + Text(errorMessage, color = Color.Red, modifier = Modifier.padding(top = 8.dp)) + } - Button( - modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), - onClick = { - connectVertexId = connectVertexId.replace("\n", "") - - if (!connectVertexId.all { char -> char.isDigit() }) { - errorMessage = "ID should be a numeric" - } else if (!graphVM.checkVertexById(connectVertexId.toInt())) { - errorMessage = "There isn't a Vertex with such ID" - } else if (connectVertexId.isBlank()) { - errorMessage = "Please enter an ID" - } else if ( - connectVertexId.isNotBlank() && connectVertexId.toIntOrNull() == null - ) { - errorMessage = "ID must be an integer" - } else { - // val firstId = graphVM.addVertex(vertexData) - // graphVM.addEdge(firstId, connectVertexId.toInt()) - // TODO - showDialog = false + Button( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + onClick = { + secondVertexData = secondVertexData.replace("\n", "") + + if (secondVertexData.isBlank()) { + errorMessage = "Please enter data to store" + } else { + val firstId = graphVM.addVertex(vertexData) + val secondId = graphVM.addVertex(secondVertexData) + graphVM.addEdge(firstId, secondId) + + showDialog = false + errorMessage = "" + connectVertexId = "" + } + + } + ) { + Text("Connect") + } + + } else { + Text("Input ID of vertex to connect with:") + + TextField( + value = connectVertexId, + onValueChange = { newValue -> errorMessage = "" - connectVertexId = "" + connectVertexId = newValue + }, + modifier = Modifier.fillMaxWidth().height(60.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent) + ) + + if (errorMessage.isNotBlank()) { + Text(errorMessage, color = Color.Red, modifier = Modifier.padding(top = 8.dp)) + } + + Button( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + onClick = { + connectVertexId = connectVertexId.replace("\n", "") + + if (!connectVertexId.all { char -> char.isDigit() }) { + errorMessage = "ID should be a numeric" + } else if (!graphVM.checkVertexById(connectVertexId.toInt())) { + errorMessage = "There isn't a Vertex with such ID" + } else if (connectVertexId.isBlank()) { + errorMessage = "Please enter an ID" + } else if ( + connectVertexId.isNotBlank() && connectVertexId.toIntOrNull() == null + ) { + errorMessage = "ID must be an integer" + } else { + val firstId = graphVM.addVertex(vertexData) + graphVM.addEdge(firstId, connectVertexId.toInt()) + + showDialog = false + errorMessage = "" + connectVertexId = "" + } } + ) { + Text("Connect") } - ) { - Text("Connect") } } } diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 85ffa97b..c71176b7 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -1,21 +1,16 @@ package viewmodel import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.unit.dp import model.abstractGraph.Graph import viewmodel.graph.GraphViewModel import viewmodel.graph.TestRepresentation -class MainScreenViewModel(graph: Graph) { +class MainScreenViewModel(graph: Graph, currentGraphType: String) { val showVerticesData = mutableStateOf(false) val showVerticesIds = mutableStateOf(false) + val graphType = mutableStateOf(currentGraphType) - val graphViewModel = - GraphViewModel( - graph, - showVerticesIds, - showVerticesData, - ) + val graphViewModel = GraphViewModel(graph, showVerticesIds, showVerticesData, graphType) init { // here will be a placement-function call TestRepresentation().place(740.0, 650.0, graphViewModel.verticesVM) diff --git a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt new file mode 100644 index 00000000..0a97a078 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt @@ -0,0 +1,90 @@ +package viewmodel.graph + +import androidx.compose.runtime.Composable +import model.DirectedGraph +import model.UndirectedGraph +import model.WeightedDirectedGraph +import model.WeightedUndirectedGraph +import model.abstractGraph.Graph +import view.MainScreen +import viewmodel.MainScreenViewModel + +class CreateGraphViewModel { + sealed class GraphType { + object Integer : GraphType() + object UInteger : GraphType() + object String : GraphType() + } + + sealed class GraphStructure { + object Directed : GraphStructure() + object Undirected : GraphStructure() + } + + sealed class Weight { + object Weighted : Weight() + object Unweighted : Weight() + } + + @Composable + fun createGraph( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight + ) { // TODO looks too shitty (((((((((( + mb any could be changed + return when (weight) { + is Weight.Weighted -> { + when (graphStructure) { + is GraphStructure.Directed -> { + when (storedData) { + is GraphType.Integer -> MainScreen(MainScreenViewModel(WeightedDirectedGraph(), + "WeightedDirectedGraph Int")) + is GraphType.UInteger -> MainScreen(MainScreenViewModel(WeightedDirectedGraph(), + "WeightedDirectedGraph UInt")) + is GraphType.String -> MainScreen(MainScreenViewModel(WeightedDirectedGraph(), + "WeightedDirectedGraph String")) + } + } + + is GraphStructure.Undirected -> { + when (storedData) { + is GraphType.Integer -> MainScreen(MainScreenViewModel(WeightedUndirectedGraph(), + "WeightedUndirectedGraph Int")) + is GraphType.UInteger -> MainScreen(MainScreenViewModel(WeightedUndirectedGraph(), + "WeightedUndirectedGraph UInt")) + is GraphType.String -> MainScreen(MainScreenViewModel(WeightedUndirectedGraph(), + "WeightedUndirectedGraph String") + ) + } + } + } + } + + is Weight.Unweighted -> { + when (graphStructure) { + is GraphStructure.Directed -> { + when (storedData) { + is GraphType.Integer -> MainScreen(MainScreenViewModel(DirectedGraph(), + "DirectedGraph Int")) + is GraphType.UInteger -> MainScreen(MainScreenViewModel(DirectedGraph(), + "DirectedGraph UInt")) + is GraphType.String -> MainScreen(MainScreenViewModel(DirectedGraph(), + "DirectedGraph String")) + } + } + + is GraphStructure.Undirected -> { + when (storedData) { + is GraphType.Integer -> MainScreen(MainScreenViewModel(UndirectedGraph(), + "UndirectedGraph Int")) + is GraphType.UInteger -> MainScreen(MainScreenViewModel(UndirectedGraph(), + "UndirectedGraph UInt")) + is GraphType.String -> MainScreen(MainScreenViewModel(UndirectedGraph(), + "UndirectedGraph String")) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 34addd1c..600b458e 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -1,23 +1,27 @@ package viewmodel.graph +import androidx.compose.runtime.MutableState import androidx.compose.runtime.State +import model.abstractGraph.Edge import model.abstractGraph.Graph class GraphViewModel( private val graph: Graph, - showIds: State, - showVerticesData: State, + private val showIds: State, + private val showVerticesData: State, + val graphType: MutableState, ) { - private val _verticesViewModels = + + private var _verticesViewModels = graph.getVertices().associateWith { vertex -> VertexViewModel( dataVisible = showVerticesData, idVisible = showIds, vertex = vertex, ) - } + }.toMutableMap() - private val _edgeViewModels = + private var _edgeViewModels: Map, EdgeViewModel> = graph.getEdges().associateWith { edge -> val firstVertex: VertexViewModel = _verticesViewModels[edge.vertex1] @@ -28,7 +32,42 @@ class GraphViewModel( ?: throw NoSuchElementException("No such View Model, with mentioned edges") EdgeViewModel(firstVertex, secondVertex, edge) + }.toMutableMap() + + + fun checkVertexById(id: Int): Boolean { + return _verticesViewModels.keys.any { it.id == id } + } + + fun addVertex(data: String): Int { + val newVertex = graph.addVertex(data as D) + _verticesViewModels[newVertex] = + VertexViewModel( + dataVisible = showVerticesData, + idVisible = showIds, + vertex = newVertex, + ) + TestRepresentation().place(740.0, 650.0, verticesVM) + return newVertex.id + } + + fun addEdge(firstId: Int, secondId: Int) { + val firstVertex =graph.getVertices().find { it.id == firstId } + ?: throw NoSuchElementException("No vertex found with id $firstId") + val secondVertex = graph.getVertices().find { it.id == secondId } + ?: throw NoSuchElementException("No vertex found with id $secondId") + + val firstVertexVM = _verticesViewModels[firstVertex] + ?: throw NoSuchElementException("No ViewModel found for vertex1") + val secondVertexVM = _verticesViewModels[secondVertex] + ?: throw NoSuchElementException("No ViewModel found for vertex2") + val edge = graph.addEdge(firstVertexVM.vertex, secondVertexVM.vertex) + + _edgeViewModels = _edgeViewModels.toMutableMap().apply { + this[edge] = EdgeViewModel(firstVertexVM, secondVertexVM, edge) } + TestRepresentation().place(740.0, 650.0, verticesVM) + } val verticesVM: Collection> get() = _verticesViewModels.values diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index fa688f99..23d4a0fe 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -14,7 +14,7 @@ class VertexViewModel( var y: MutableState = mutableStateOf(0.dp), var dataVisible: State, var idVisible: State, - private val vertex: Vertex, + val vertex: Vertex, val radius: Dp = 30.dp, ) { var isSelected = mutableStateOf(false) diff --git a/app/src/main/resources/drawable/question.png b/app/src/main/resources/drawable/question.png new file mode 100644 index 0000000000000000000000000000000000000000..ca4e343a1ac8fe3bef2114b77be17e66c9bc448b GIT binary patch literal 17551 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 e-KG~w+W!Yt0x)vk69tX{0000 Date: Sat, 18 May 2024 16:48:48 +0300 Subject: [PATCH 45/77] refactor: change exception messages and use getEdge where possible #28 * refactor: change exception messages * refactor: use getEdge where possible, change var names * refactor: Dijkstra's algo so it is the same in directed and undirected graphs --- app/src/main/kotlin/model/DirectedGraph.kt | 4 +- app/src/main/kotlin/model/UndirectedGraph.kt | 6 +-- .../kotlin/model/WeightedDirectedGraph.kt | 34 +++++++------- .../kotlin/model/WeightedUndirectedGraph.kt | 46 +++++++------------ .../main/kotlin/model/abstractGraph/Graph.kt | 4 +- 5 files changed, 40 insertions(+), 54 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 8a0830aa..cc3e60c6 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -12,7 +12,7 @@ open class DirectedGraph : Graph() { if (vertex1.id > vertices.size || vertex2.id > vertices.size) throw NoSuchElementException( "One of vertices (${vertex1.id}, ${vertex1.data}) and " + - "(${vertex2.id}, ${vertex2.data}) is not in the vertices array." + "(${vertex2.id}, ${vertex2.data}) isn't in the graph" ) val newEdge = Edge(vertex1, vertex2) @@ -27,7 +27,7 @@ open class DirectedGraph : Graph() { override fun removeEdge(edgeToRemove: Edge): Edge { if (edgeToRemove !in edges) throw NoSuchElementException( "Edge between vertices (${edgeToRemove.vertex1.id}, ${edgeToRemove.vertex1.data}) and " + - "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) is not in the graph" + "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) isn't in the graph" ) val vertex1 = edgeToRemove.vertex1 diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index e72584a3..a156515d 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -8,12 +8,12 @@ import model.abstractGraph.Vertex open class UndirectedGraph : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge { if (vertex1 == vertex2) - throw IllegalArgumentException("Can't add edge from vertex to itself.") + throw IllegalArgumentException("Can't add edge from vertex to itself") if (vertex1.id > vertices.size || vertex2.id > vertices.size) throw NoSuchElementException( "One of vertices (${vertex1.id}, ${vertex1.data}) and " + - "(${vertex2.id}, ${vertex2.data}) is not in the vertices array." + "(${vertex2.id}, ${vertex2.data}) isn't in the graph" ) val newEdge = Edge(vertex1, vertex2) @@ -32,7 +32,7 @@ open class UndirectedGraph : Graph() { if (edgeToRemove !in edges) throw NoSuchElementException( "Edge between vertices (${edgeToRemove.vertex1.id}, ${edgeToRemove.vertex1.data}) and " + - "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) is not in the graph" + "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) isn't in the graph" ) val vertex1 = edgeToRemove.vertex1 diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 3839c3bb..eb911b44 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -16,9 +16,7 @@ class WeightedDirectedGraph : DirectedGraph() { return newEdge } - /** - * In case weight is not passed, set it to default value = 1 - */ + // In case weight is not passed, set it to default value = 1 override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) fun getWeight(edge: Edge): Int { @@ -40,18 +38,17 @@ class WeightedDirectedGraph : DirectedGraph() { distanceMap[srcVertex] = 0 while (priorityQueue.isNotEmpty()) { - val (node, currentDistance) = priorityQueue.poll() - if (visited.add(node to currentDistance)) { - adjacencyMap[node]?.forEach { adjacent -> - val currentEdge = edges.find { it.vertex1 == adjacent } - currentEdge?.let { - var totalDist = currentDistance - totalDist += getWeight(it) - if (totalDist < distanceMap.getValue(adjacent)) { - distanceMap[adjacent] = totalDist - predecessorMap[adjacent] = node // Update predecessor - priorityQueue.add(adjacent to totalDist) - } + val (currentVertex, currentDistance) = priorityQueue.poll() + if (visited.add(currentVertex to currentDistance)) { + adjacencyMap[currentVertex]?.forEach { adjacent -> + val currentEdge = getEdge(adjacent, currentVertex) + + val totalDist = currentDistance + getWeight(currentEdge) + + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = currentVertex // Update predecessor + priorityQueue.add(adjacent to totalDist) } } } @@ -62,14 +59,15 @@ class WeightedDirectedGraph : DirectedGraph() { var currentVertex = destVertex while (currentVertex != srcVertex) { val predecessor = predecessorMap[currentVertex] + if (predecessor == null) { // If no path exists return emptyList() } - path.add( - Pair(currentVertex, getEdge(predecessor, currentVertex)) - ) + val currentEdge = getEdge(predecessor, currentVertex) + path.add(Pair(currentVertex, currentEdge)) + currentVertex = predecessor } return path.reversed() diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index bf26356d..12ebfc85 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -16,9 +16,8 @@ class WeightedUndirectedGraph : UndirectedGraph() { return newEdge } - /** - * In case weight is not passed, set it to default value = 1 - */ + + // In case weight is not passed, set it to default value = 1 override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) fun getWeight(edge: Edge): Int { @@ -40,23 +39,17 @@ class WeightedUndirectedGraph : UndirectedGraph() { distanceMap[srcVertex] = 0 while (priorityQueue.isNotEmpty()) { - val (node, currentDistance) = priorityQueue.poll() - if (visited.add(node to currentDistance)) { - adjacencyMap[node]?.forEach { adjacent -> - val currentEdge = - edges.find { - (it.vertex1 == adjacent && it.vertex2 == node) || - (it.vertex1 == node && it.vertex2 == adjacent) - } - currentEdge?.let { - var totalDist = currentDistance - totalDist += getWeight(it) - - if (totalDist < distanceMap.getValue(adjacent)) { - distanceMap[adjacent] = totalDist - predecessorMap[adjacent] = node // Update predecessor - priorityQueue.add(adjacent to totalDist) - } + val (currentVertex, currentDistance) = priorityQueue.poll() + if (visited.add(currentVertex to currentDistance)) { + adjacencyMap[currentVertex]?.forEach { adjacent -> + val currentEdge = getEdge(adjacent, currentVertex) + + val totalDist = currentDistance + getWeight(currentEdge) + + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = currentVertex // Update predecessor + priorityQueue.add(adjacent to totalDist) } } } @@ -72,15 +65,10 @@ class WeightedUndirectedGraph : UndirectedGraph() { // If no path exists return emptyList() } - if (edges.find { - it.vertex1 == predecessor && it.vertex2 == currentVertex || - it.vertex2 == predecessor && it.vertex1 == currentVertex - } == null) { - throw NoSuchElementException("Edge is not in the graph, path cannot be reconstructed.") - } - path.add( - Pair(currentVertex, getEdge(predecessor, currentVertex)) - ) + + val currentEdge = getEdge(predecessor, currentVertex) + path.add(Pair(currentVertex, currentEdge)) + currentVertex = predecessor } return path.reversed() diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 2e22d37a..13ea2048 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -60,14 +60,14 @@ abstract class Graph { fun getNeighbours(vertex: Vertex): ArrayList> { val neighbours = adjacencyMap[vertex] - ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the adjacency map.") + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") return neighbours } fun getOutgoingEdges(vertex: Vertex): ArrayList> { val outgoingEdges = outgoingEdgesMap[vertex] - ?: throw NoSuchElementException("Vertex with id ${vertex.id} is not present in the outgoing edges map.") + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") return outgoingEdges } From c6f2d1452af3c5e2d7497b9a6464e10cc3dd534d Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Sun, 19 May 2024 00:10:33 +0300 Subject: [PATCH 46/77] feat: add visualization of directed edges in a directed graph #27 * fix: resolve merge conflict * feat: add border support for surface, make it prettier #22 * feat: add support for WindowViewModel * feat: add border cross protection * refactor: apply ktfmt * feat: add visualization of directed edges * fix: resolve merge conflict * feat: move all logic from edge view to edge view model * feat: add isGraphDirected flag to main screen view model * refactor: delete redundant code * chore: add empty lines in end of files --------- Co-authored-by: qruty <64466788+qrutyy@users.noreply.github.com> --- .../kotlin/view/SelectInitDialogWindow.kt | 1 - app/src/main/kotlin/view/graph/EdgeView.kt | 49 +++++++++++-- .../kotlin/viewmodel/MainScreenViewModel.kt | 3 +- .../viewmodel/graph/CreateGraphViewModel.kt | 3 +- .../kotlin/viewmodel/graph/EdgeViewModel.kt | 73 +++++++++++++++++-- .../kotlin/viewmodel/graph/GraphViewModel.kt | 8 +- 6 files changed, 117 insertions(+), 20 deletions(-) diff --git a/app/src/main/kotlin/view/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/SelectInitDialogWindow.kt index a6cb670b..27c70d75 100644 --- a/app/src/main/kotlin/view/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/SelectInitDialogWindow.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import model.abstractGraph.Graph import viewmodel.graph.CreateGraphViewModel diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt index 3dc17a5e..3286eeb7 100644 --- a/app/src/main/kotlin/view/graph/EdgeView.kt +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -6,30 +6,65 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Fill import viewmodel.WindowViewModel import viewmodel.graph.EdgeViewModel @Composable fun EdgeView(viewModel: EdgeViewModel) { - val windowVM = WindowViewModel() windowVM.SetCurrentDimensions() + val firstVertexCenter = viewModel.calculateFirstVertexCenter() + val secondVertexCenter = viewModel.calculateSecondVertexCenter() + + val firstVertexCenterX = firstVertexCenter.first + val firstVertexCenterY = firstVertexCenter.second + val secondVertexCenterX = secondVertexCenter.first + val secondVertexCenterY = secondVertexCenter.second + + val arrowPoints = viewModel.calculateArrowPoints() + Canvas(modifier = Modifier.fillMaxSize()) { drawLine( color = Color.LightGray, - strokeWidth = 5.0f, + strokeWidth = 5f, start = Offset( - viewModel.firstVertex.x.value.toPx() + viewModel.firstVertex.radius.toPx(), - viewModel.firstVertex.y.value.toPx() + viewModel.firstVertex.radius.toPx() + firstVertexCenterX.toPx(), + firstVertexCenterY.toPx() ), end = Offset( - viewModel.secondVertex.x.value.toPx() + viewModel.secondVertex.radius.toPx(), - viewModel.secondVertex.y.value.toPx() + viewModel.secondVertex.radius.toPx() + secondVertexCenterX.toPx(), + secondVertexCenterY.toPx() ), ) + + if (arrowPoints.isNotEmpty()) { + val trianglePath = Path() + + // these points represent the vertices of a triangle + trianglePath.moveTo( + arrowPoints[0].first.toPx(), + arrowPoints[0].second.toPx(), + ) + trianglePath.lineTo( + arrowPoints[1].first.toPx(), + arrowPoints[1].second.toPx() + ) + trianglePath.lineTo( + arrowPoints[2].first.toPx(), + arrowPoints[2].second.toPx() + ) + trianglePath.close() + + drawPath( + path = trianglePath, + color = Color.LightGray, + style = Fill + ) + } } } - diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index c71176b7..6a3be3a9 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -8,9 +8,10 @@ import viewmodel.graph.TestRepresentation class MainScreenViewModel(graph: Graph, currentGraphType: String) { val showVerticesData = mutableStateOf(false) val showVerticesIds = mutableStateOf(false) + val isGraphDirected = mutableStateOf(false) val graphType = mutableStateOf(currentGraphType) - val graphViewModel = GraphViewModel(graph, showVerticesIds, showVerticesData, graphType) + val graphViewModel = GraphViewModel(graph, showVerticesIds, showVerticesData, graphType, isGraphDirected) init { // here will be a placement-function call TestRepresentation().place(740.0, 650.0, graphViewModel.verticesVM) diff --git a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt index 0a97a078..ed5d0536 100644 --- a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt @@ -5,7 +5,6 @@ import model.DirectedGraph import model.UndirectedGraph import model.WeightedDirectedGraph import model.WeightedUndirectedGraph -import model.abstractGraph.Graph import view.MainScreen import viewmodel.MainScreenViewModel @@ -87,4 +86,4 @@ class CreateGraphViewModel { } } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index 24232e24..dd110b50 100644 --- a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -1,9 +1,72 @@ package viewmodel.graph -import model.abstractGraph.Edge +import androidx.compose.runtime.State +import androidx.compose.ui.unit.Dp +import kotlin.math.sqrt + +const val ARROW_SIZE = 20f +const val ARROW_DEPTH = 2.5f +const val SQRT_3 = 1.732f class EdgeViewModel( - val firstVertex: VertexViewModel, - val secondVertex: VertexViewModel, - private val currentEdge: Edge, // do we really need it? -) {} + private val firstVertex: VertexViewModel, + private val secondVertex: VertexViewModel, + private val isDirected: State +) { + + private val radius = firstVertex.radius + + internal fun calculateFirstVertexCenter(): Pair { + val x = firstVertex.x.value + radius + val y = firstVertex.y.value + radius + + return Pair(x, y) + } + + internal fun calculateSecondVertexCenter(): Pair { + val x = secondVertex.x.value + radius + val y = secondVertex.y.value + radius + + return Pair(x, y) + } + + internal fun calculateArrowPoints(): List> { + if (!isDirected.value) return listOf() + + val firstVertexCenterX = calculateFirstVertexCenter().first + val firstVertexCenterY = calculateFirstVertexCenter().second + + val secondVertexCenterX = calculateSecondVertexCenter().first + val secondVertexCenterY = calculateSecondVertexCenter().second + + val vectorX = secondVertexCenterX - firstVertexCenterX + val vectorY = secondVertexCenterY - firstVertexCenterY + + val len = sqrt(vectorX.value * vectorX.value + vectorY.value * vectorY.value) + val normedVectorX = vectorX / len + val normedVectorY = vectorY / len + + // rotate normed vector by Pi/6 + val aX = normedVectorX * SQRT_3 / 2 - normedVectorY * 1 / 2 + val aY = normedVectorX * 1 / 2 + normedVectorY * SQRT_3 / 2 + + // rotate normed vector by negative Pi/6 + val bX = normedVectorX * SQRT_3 / 2 + normedVectorY * 1 / 2 + val bY = -normedVectorX * 1 / 2 + normedVectorY * SQRT_3 / 2 + + val arrowEndPointX = secondVertexCenterX - normedVectorX * (radius.value - ARROW_DEPTH) + val arrowEndPointY = secondVertexCenterY - normedVectorY * (radius.value - ARROW_DEPTH) + + val arrowLeftPointX = arrowEndPointX - aX * ARROW_SIZE + val arrowLeftPointY = arrowEndPointY - aY * ARROW_SIZE + + val arrowRightPointX = arrowEndPointX - bX * ARROW_SIZE + val arrowRightPointY = arrowEndPointY - bY * ARROW_SIZE + + return listOf( + Pair(arrowEndPointX, arrowEndPointY), + Pair(arrowLeftPointX, arrowLeftPointY), + Pair(arrowRightPointX, arrowRightPointY) + ) + } +} diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 600b458e..866cdcf7 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -10,6 +10,7 @@ class GraphViewModel( private val showIds: State, private val showVerticesData: State, val graphType: MutableState, + private val isDirected: State, ) { private var _verticesViewModels = @@ -31,7 +32,7 @@ class GraphViewModel( _verticesViewModels[edge.vertex2] ?: throw NoSuchElementException("No such View Model, with mentioned edges") - EdgeViewModel(firstVertex, secondVertex, edge) + EdgeViewModel(firstVertex, secondVertex, isDirected) }.toMutableMap() @@ -52,7 +53,7 @@ class GraphViewModel( } fun addEdge(firstId: Int, secondId: Int) { - val firstVertex =graph.getVertices().find { it.id == firstId } + val firstVertex = graph.getVertices().find { it.id == firstId } ?: throw NoSuchElementException("No vertex found with id $firstId") val secondVertex = graph.getVertices().find { it.id == secondId } ?: throw NoSuchElementException("No vertex found with id $secondId") @@ -64,9 +65,8 @@ class GraphViewModel( val edge = graph.addEdge(firstVertexVM.vertex, secondVertexVM.vertex) _edgeViewModels = _edgeViewModels.toMutableMap().apply { - this[edge] = EdgeViewModel(firstVertexVM, secondVertexVM, edge) + this[edge] = EdgeViewModel(firstVertexVM, secondVertexVM, isDirected) } - TestRepresentation().place(740.0, 650.0, verticesVM) } val verticesVM: Collection> From 1c42c30e909edd1d294e19a929a279aa999e2bad Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Sun, 19 May 2024 10:48:48 +0300 Subject: [PATCH 47/77] test: add tests for findBridges() method of undirected graphs #31 --- .../test/kotlin/model/UndirectedGraphTest.kt | 163 +++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/model/UndirectedGraphTest.kt b/app/src/test/kotlin/model/UndirectedGraphTest.kt index 27126f5e..6cc4c3c4 100644 --- a/app/src/test/kotlin/model/UndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/UndirectedGraphTest.kt @@ -1,8 +1,19 @@ package model +import model.abstractGraph.Edge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test class UndirectedGraphTest { + private lateinit var graph: UndirectedGraph + + @BeforeEach + fun setup() { + graph = UndirectedGraph() + } + @Nested inner class AddEdgeTest {} @@ -10,5 +21,155 @@ class UndirectedGraphTest { inner class RemoveEdgeTest {} @Nested - inner class FindBridgesTest {} + inner class FindBridgesTest { + @Nested + inner class `All bridges should be found`() { + @Test + fun `if graph has one edge`() { + val vertex0 = graph.addVertex(0) + val vertex1 = graph.addVertex(1) + + val expectedBridges = listOf(graph.addEdge(vertex0, vertex1)) + val actualBridges = graph.findBridges() + + assertEquals(expectedBridges, actualBridges) + } + + @Test + fun `if two components are connected via one edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v1, v2) + + addEdge(v3, v4) + addEdge(v3, v5) + addEdge(v4, v5) + } + + val expectedBridges = listOf(graph.addEdge(v0, v3)) + val actualBridges = graph.findBridges() + + assertEquals(expectedBridges, actualBridges) + } + + @Test + fun `if graph is chain-like`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e12 = graph.addEdge(v1, v2) + val e23 = graph.addEdge(v2, v3) + + val expectedBridges = setOf(e01, e12, e23) + val actualBridges = graph.findBridges().toSet() + + assertEquals(expectedBridges, actualBridges) + } + + @Test + fun `if graph is star-like`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e02 = graph.addEdge(v0, v2) + val e03 = graph.addEdge(v0, v3) + + val expectedBridges = setOf(e01, e02, e03) + val actualBridges = graph.findBridges().toSet() + + assertEquals(expectedBridges, actualBridges) + } + } + + @Nested + inner class `No bridge should be found`() { + @Test + fun `if graph has no vertices`() { + val expectedBridges = listOf>() + val actualBridges = graph.findBridges() + + assertEquals(expectedBridges, actualBridges) + } + + @Test + fun `if graph has no edges`() { + graph.apply { + addVertex(0) + addVertex(1) + addVertex(2) + } + + val expectedBridges = listOf>() + val actualBridges = graph.findBridges() + + assertEquals(expectedBridges, actualBridges) + } + + @Test + fun `if graph is circle-like`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v0, v1) + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v4) + addEdge(v4, v0) + } + + val expectedBridges = listOf>() + val actualBridges = graph.findBridges() + + assertEquals(expectedBridges, actualBridges) + } + + @Test + fun `if two components are connected via more than one edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v1, v2) + + addEdge(v3, v4) + addEdge(v3, v5) + addEdge(v4, v5) + } + + graph.addEdge(v0, v3) + graph.addEdge(v1, v4) + + val expectedBridges = listOf>() + val actualBridges = graph.findBridges() + + assertEquals(expectedBridges, actualBridges) + } + } + } } From 955f89dd3915898087e5bf2a4a95fa79b936ac62 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Sun, 19 May 2024 11:03:37 +0300 Subject: [PATCH 48/77] fix: add process of adding already existing edge #30 * fix: types of map values * fix(addEdge): adding already existing edge --- app/src/main/kotlin/model/DirectedGraph.kt | 7 +++++-- app/src/main/kotlin/model/UndirectedGraph.kt | 3 +++ app/src/main/kotlin/model/abstractGraph/Graph.kt | 16 ++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index cc3e60c6..9aa107ce 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -15,6 +15,9 @@ open class DirectedGraph : Graph() { "(${vertex2.id}, ${vertex2.data}) isn't in the graph" ) + // Don't do anything if the edge is already in the graph + if (vertex2 in getNeighbours(vertex1)) return getEdge(vertex1, vertex2) + val newEdge = Edge(vertex1, vertex2) edges.add(newEdge) @@ -88,10 +91,10 @@ open class DirectedGraph : Graph() { } private fun reverseGraph() { - val reversedAdjacencyMap = mutableMapOf, ArrayList>>() + val reversedAdjacencyMap = mutableMapOf, MutableSet>>() for (vertex in vertices) { adjacencyMap[vertex]?.forEach { vertex2 -> - reversedAdjacencyMap[vertex2] = reversedAdjacencyMap[vertex2] ?: ArrayList() + reversedAdjacencyMap[vertex2] = reversedAdjacencyMap[vertex2] ?: mutableSetOf() reversedAdjacencyMap[vertex2]?.add(vertex) } } diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index a156515d..92a89be1 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -16,6 +16,9 @@ open class UndirectedGraph : Graph() { "(${vertex2.id}, ${vertex2.data}) isn't in the graph" ) + // Don't do anything if the edge is already in the graph + if (vertex2 in getNeighbours(vertex1)) return getEdge(vertex1, vertex2) + val newEdge = Edge(vertex1, vertex2) edges.add(newEdge) diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 13ea2048..2777bbcb 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -4,16 +4,16 @@ abstract class Graph { protected val vertices: ArrayList> = arrayListOf() protected val edges: MutableSet> = mutableSetOf() - protected val adjacencyMap: MutableMap, ArrayList>> = mutableMapOf() - protected val outgoingEdgesMap: MutableMap, ArrayList>> = mutableMapOf() + protected val adjacencyMap: MutableMap, MutableSet>> = mutableMapOf() + protected val outgoingEdgesMap: MutableMap, MutableSet>> = mutableMapOf() private var nextId = 0 fun addVertex(data: D): Vertex { val newVertex = Vertex(nextId++, data) - outgoingEdgesMap[newVertex] = ArrayList() - adjacencyMap[newVertex] = ArrayList() + outgoingEdgesMap[newVertex] = mutableSetOf() + adjacencyMap[newVertex] = mutableSetOf() vertices.add(newVertex) @@ -58,18 +58,18 @@ abstract class Graph { fun getVertices() = vertices.toList() - fun getNeighbours(vertex: Vertex): ArrayList> { + fun getNeighbours(vertex: Vertex): List> { val neighbours = adjacencyMap[vertex] ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") - return neighbours + return neighbours.toList() } - fun getOutgoingEdges(vertex: Vertex): ArrayList> { + fun getOutgoingEdges(vertex: Vertex): List> { val outgoingEdges = outgoingEdgesMap[vertex] ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") - return outgoingEdges + return outgoingEdges.toList() } abstract fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge From 03a260a754e27d8349804b8e7f19140aa445f2d0 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Sun, 19 May 2024 21:59:39 +0300 Subject: [PATCH 49/77] test: graph implementations' basic methods #32 * test: add custom annotations for testing both weighted and unweighted graphs * test: add inner classes for new methods + beforeEach int method in weighted directed graph * chore: move annotation and argument provider classes to new util package * test(DirectedGraph): add tests for getEdge method * test: add one more edge in setup method * test(DirectedGraph): add tests for getNeighbours and getOutgoingEdges methods * fix(addEdge): check if vertices are in the graph + fix exception messages * test(DirectedGraph): add tests for addEdge method * test(DirectedGraph): add tests for removeEdge method * test(DirectedGraph): add more test cases * test(UndirectedGraph): change already written tests to parameterized * test(Undirected Graphs): add tests for basic methods * test: add tests for weighted graphs' basic methods + add two setup methods to util --- app/src/main/kotlin/model/DirectedGraph.kt | 6 +- app/src/main/kotlin/model/UndirectedGraph.kt | 4 +- .../kotlin/model/WeightedDirectedGraph.kt | 2 +- .../kotlin/model/WeightedUndirectedGraph.kt | 2 +- .../main/kotlin/model/abstractGraph/Graph.kt | 2 +- .../test/kotlin/model/DirectedGraphTest.kt | 430 ++++++++++++- .../test/kotlin/model/UndirectedGraphTest.kt | 569 +++++++++++++++++- .../kotlin/model/WeightedDirectedGraphTest.kt | 97 ++- .../model/WeightedUndirectedGraphTest.kt | 87 ++- .../kotlin/model/abstractGraph/GraphTest.kt | 60 +- .../util/annotations/TestAllDirectedGraphs.kt | 11 + .../util/annotations/TestAllGraphTypes.kt | 12 + .../annotations/TestAllUndirectedGraphs.kt | 11 + .../AllGraphTypesProvider.kt | 19 + ...htedAndUnweightedDirectedGraphsProvider.kt | 15 + ...edAndUnweightedUndirectedGraphsProvider.kt | 15 + app/src/test/kotlin/util/emptyGraphs.kt | 19 + app/src/test/kotlin/util/setup.kt | 70 +++ 18 files changed, 1342 insertions(+), 89 deletions(-) create mode 100644 app/src/test/kotlin/util/annotations/TestAllDirectedGraphs.kt create mode 100644 app/src/test/kotlin/util/annotations/TestAllGraphTypes.kt create mode 100644 app/src/test/kotlin/util/annotations/TestAllUndirectedGraphs.kt create mode 100644 app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt create mode 100644 app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt create mode 100644 app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt create mode 100644 app/src/test/kotlin/util/emptyGraphs.kt create mode 100644 app/src/test/kotlin/util/setup.kt diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 9aa107ce..86543711 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -9,9 +9,9 @@ open class DirectedGraph : Graph() { if (vertex1 == vertex2) throw IllegalArgumentException("Can't add edge from vertex to itself.") - if (vertex1.id > vertices.size || vertex2.id > vertices.size) - throw NoSuchElementException( - "One of vertices (${vertex1.id}, ${vertex1.data}) and " + + if (vertex1 !in vertices || vertex2 !in vertices) + throw IllegalArgumentException( + "One of the vertices (${vertex1.id}, ${vertex1.data}) and " + "(${vertex2.id}, ${vertex2.data}) isn't in the graph" ) diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/UndirectedGraph.kt index 92a89be1..c5040d40 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/UndirectedGraph.kt @@ -10,8 +10,8 @@ open class UndirectedGraph : Graph() { if (vertex1 == vertex2) throw IllegalArgumentException("Can't add edge from vertex to itself") - if (vertex1.id > vertices.size || vertex2.id > vertices.size) - throw NoSuchElementException( + if (vertex1 !in vertices || vertex2 !in vertices) + throw IllegalArgumentException( "One of vertices (${vertex1.id}, ${vertex1.data}) and " + "(${vertex2.id}, ${vertex2.data}) isn't in the graph" ) diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index eb911b44..80da33ae 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -22,7 +22,7 @@ class WeightedDirectedGraph : DirectedGraph() { fun getWeight(edge: Edge): Int { val weight = weightMap[edge] ?: throw NoSuchElementException( - "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data})" + + "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data}) " + "and (${edge.vertex2.id}, ${edge.vertex2.data})" ) diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 12ebfc85..3c22722b 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -23,7 +23,7 @@ class WeightedUndirectedGraph : UndirectedGraph() { fun getWeight(edge: Edge): Int { val weight = weightMap[edge] ?: throw NoSuchElementException( - "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data})" + + "No weight found for edge between vertices (${edge.vertex1.id}, ${edge.vertex1.data}) " + "and (${edge.vertex2.id}, ${edge.vertex2.data})" ) diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index 2777bbcb..dbe7f63c 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -67,7 +67,7 @@ abstract class Graph { fun getOutgoingEdges(vertex: Vertex): List> { val outgoingEdges = outgoingEdgesMap[vertex] - ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the outgoing edges map.") return outgoingEdges.toList() } diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt index d9c5e8ee..da70c953 100644 --- a/app/src/test/kotlin/model/DirectedGraphTest.kt +++ b/app/src/test/kotlin/model/DirectedGraphTest.kt @@ -1,13 +1,439 @@ package model +import model.abstractGraph.Edge +import model.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested +import util.annotations.TestAllDirectedGraphs +import util.emptyEdgesSet +import util.emptyVerticesList +import util.setup class DirectedGraphTest { @Nested - inner class AddEdgeTest {} + inner class GetEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllDirectedGraphs + fun `edge should be returned`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v2 = defaultVerticesList[2] + + val newEdge = graph.addEdge(v0, v2) + + val actualValue = newEdge + val expectedValue = graph.getEdge(v0, v2) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + graph.getEdge(v2, v3) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllDirectedGraphs + fun `order of arguments should matter`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(v1, v0) + } + } + + @TestAllDirectedGraphs + fun `trying to get non-existent edge should throw an exception`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(Vertex(2, 12), Vertex(85, 6)) + } + } + } + } @Nested - inner class RemoveEdgeTest {} + inner class GetNeighboursTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllDirectedGraphs + fun `neighbours should be returned`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getNeighbours(v3).toSet() + val expectedValue = setOf(v4, v1) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + graph.getNeighbours(v0) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getNeighbours(Vertex(2201, 2006)) + } + } + } + } + + @Nested + inner class GetOutgoingEdgesTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllDirectedGraphs + fun `outgoing edges should be returned`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getOutgoingEdges(v3).toSet() + val expectedValue = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v4 = defaultVerticesList[4] + + graph.getOutgoingEdges(v4) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getOutgoingEdges(Vertex(2611, 2005)) + } + } + } + } + + @Nested + inner class AddEdgeTest { + @Nested + inner class `Two vertices are in the graph` { + @Nested + inner class `Vertices are different` { + @TestAllDirectedGraphs + fun `Added edge should be returned`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val actualValue = graph.addEdge(v0, v4) + val expectedValue = graph.getEdge(v0, v4) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `Edge should be added to graph`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val newEdge = graph.addEdge(v4, v0) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet + newEdge + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `one vertex has to be added to the other's adjacency map value`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v0, v2) + + val actualVertices = graph.getNeighbours(v0).toSet() + val expectedVertices = setOf(v1, v2) + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllDirectedGraphs + fun `edge has to be added to first vertex's outgoing edges map value`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v3, v0) + + val actualEdges = graph.getOutgoingEdges(v3).toSet() + val expectedEdges = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1), graph.getEdge(v3, v0)) + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `adding already existing edge shouldn't change anything`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedNeighbours = graph.getNeighbours(v4).toSet() + val expectedOutgoingEdges = graph.getOutgoingEdges(v4).toSet() + + graph.addEdge(v4, v1) + + val actualNeighbours = graph.getNeighbours(v4).toSet() + val actualOutgoingEdges = graph.getOutgoingEdges(v4).toSet() + + val expectedGraph = graphStructure + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + assertEquals(expectedNeighbours, actualNeighbours) + assertEquals(expectedOutgoingEdges, actualOutgoingEdges) + } + + @TestAllDirectedGraphs + fun `second vertex's map values shouldn't change`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v3 = defaultVerticesList[3] + + val expectedNeighbours = graph.getNeighbours(v0).toSet() + val expectedOutgoingEdges = graph.getOutgoingEdges(v0).toSet() + + graph.addEdge(v3, v0) + + val actualNeighbours = graph.getNeighbours(v0).toSet() + val actualOutgoingEdges = graph.getOutgoingEdges(v0).toSet() + + assertEquals(expectedNeighbours, actualNeighbours) + assertEquals(expectedOutgoingEdges, actualOutgoingEdges) + } + } + + @Nested + inner class `Vertices are the same` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v2, v2) + } + } + } + } + + @Nested + inner class `One of the vertices isn't in the graph` { + @TestAllDirectedGraphs + fun `first vertex isn't in the graph`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(2210, 2005), v0) + } + } + + @TestAllDirectedGraphs + fun `second vertex isn't in the graph`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v0, Vertex(2510, 1917)) + } + } + + @TestAllDirectedGraphs + fun `both vertices aren't in the graph`(graph: DirectedGraph) { + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(3010, 1978), Vertex(1002, 1982)) + } + } + } + } + + @Nested + inner class RemoveEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllDirectedGraphs + fun `removed edge should be returned`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v3, v1) + + val actualValue = graph.removeEdge(edgeToRemove) + val expectedValue = edgeToRemove + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `edge should be removed from graph`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v4, v1) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet - edgeToRemove + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `second vertex should be removed from first's adjacency map value`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + + val edgeToRemove = graph.getEdge(v1, v2) + graph.removeEdge(edgeToRemove) + + val actualVertices = graph.getNeighbours(v1).toSet() + val expectedVertices = emptyVerticesList.toSet() + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllDirectedGraphs + fun `edge should be removed from first vertex's outgoing edges map value`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v2, v3) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getOutgoingEdges(v2).toSet() + val expectedEdges = emptyEdgesSet + + assertEquals(expectedEdges, actualEdges) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllDirectedGraphs + fun `wrong order of the arguments should throw an exception`(graph: DirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(graph.getEdge(v4, v3)) + } + } + + @TestAllDirectedGraphs + fun `non-existing edge should throw an exception`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(Edge(Vertex(0,0), Vertex(1, 1))) + } + } + } + } @Nested inner class FindSCCTest {} diff --git a/app/src/test/kotlin/model/UndirectedGraphTest.kt b/app/src/test/kotlin/model/UndirectedGraphTest.kt index 6cc4c3c4..c13b62a1 100644 --- a/app/src/test/kotlin/model/UndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/UndirectedGraphTest.kt @@ -1,31 +1,552 @@ package model import model.abstractGraph.Edge +import model.abstractGraph.Vertex import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test +import util.annotations.TestAllUndirectedGraphs +import util.emptyEdgesSet +import util.emptyVerticesList +import util.setup class UndirectedGraphTest { - private lateinit var graph: UndirectedGraph + @Nested + inner class GetEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllUndirectedGraphs + fun `edge should be returned`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v2 = defaultVerticesList[2] + + val newEdge = graph.addEdge(v0, v2) + + val actualValue = newEdge + val expectedValue = graph.getEdge(v0, v2) + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `order of the arguments shouldn't matter`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + + assertEquals(graph.getEdge(v0, v1), graph.getEdge(v1, v0)) + } + + @TestAllUndirectedGraphs + fun `graph shouldn't change`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + graph.getEdge(v2, v3) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllUndirectedGraphs + fun `trying to get non-existent edge should throw an exception`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(Vertex(2, 12), Vertex(85, 6)) + } + } + } + } + + @Nested + inner class GetNeighboursTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllUndirectedGraphs + fun `neighbours should be returned`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getNeighbours(v3).toSet() + val expectedValue = setOf(v1, v2, v4) + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `graph shouldn't change`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + graph.getNeighbours(v0) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllUndirectedGraphs + fun `exception should be thrown`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getNeighbours(Vertex(2201, 2006)) + } + } + } + } + + @Nested + inner class GetOutgoingEdgesTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllUndirectedGraphs + fun `outgoing edges should be returned`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getOutgoingEdges(v3).toSet() + val expectedValue = setOf( + graph.getEdge(v3, v4), + graph.getEdge(v3, v1), + graph.getEdge(v3, v2) + ) + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `graph shouldn't change`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first - @BeforeEach - fun setup() { - graph = UndirectedGraph() + val v4 = defaultVerticesList[4] + + graph.getOutgoingEdges(v4) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllUndirectedGraphs + fun `exception should be thrown`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getOutgoingEdges(Vertex(2611, 2005)) + } + } + } } @Nested - inner class AddEdgeTest {} + inner class AddEdgeTest { + @Nested + inner class `Two vertices are in the graph` { + @Nested + inner class `Vertices are different` { + @TestAllUndirectedGraphs + fun `Added edge should be returned`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val actualValue = graph.addEdge(v0, v4) + val expectedValue = graph.getEdge(v0, v4) + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `Edge should be added to graph`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val newEdge = graph.addEdge(v4, v0) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet + newEdge + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllUndirectedGraphs + fun `vertices have to be added to each other's adjacency map values`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v0, v2) + + val actualVertices1 = graph.getNeighbours(v0).toSet() + val expectedVertices1 = setOf(v1, v2) + + val actualVertices2 = graph.getNeighbours(v2).toSet() + val expectedVertices2 = setOf(v0, v1, v3) + + assertEquals(expectedVertices1, actualVertices1) + assertEquals(expectedVertices2, actualVertices2) + } + + @TestAllUndirectedGraphs + fun `edge has to be added to both vertices' outgoing edges map values`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v3, v0) + + val actualEdges1 = graph.getOutgoingEdges(v0).toSet() + val expectedEdges1 = setOf(graph.getEdge(v0, v1), graph.getEdge(v0, v3)) + + val actualEdges2 = graph.getOutgoingEdges(v3).toSet() + val expectedEdges2 = setOf( + graph.getEdge(v3, v0), + graph.getEdge(v3, v1), + graph.getEdge(v3, v2), + graph.getEdge(v3, v4) + ) + + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + + @TestAllUndirectedGraphs + fun `adding already existing edge shouldn't change graph`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + graph.addEdge(v4, v1) + + val expectedGraph = graphStructure + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + } + + @TestAllUndirectedGraphs + fun `adding already existing edge shouldn't change adjacency map`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedNeighbours1 = graph.getNeighbours(v1).toSet() + val expectedNeighbours2 = graph.getNeighbours(v4).toSet() + + graph.addEdge(v4, v1) + + val actualNeighbours1 = graph.getNeighbours(v1).toSet() + val actualNeighbours2 = graph.getNeighbours(v4).toSet() + + assertEquals(expectedNeighbours1, actualNeighbours1) + assertEquals(expectedNeighbours2, actualNeighbours2) + } + + @TestAllUndirectedGraphs + fun `adding already existing edge shouldn't change outgoing edges map`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedEdges1 = graph.getOutgoingEdges(v1).toSet() + val expectedEdges2 = graph.getOutgoingEdges(v4).toSet() + + graph.addEdge(v4, v1) + + val actualEdges1 = graph.getOutgoingEdges(v1).toSet() + val actualEdges2 = graph.getOutgoingEdges(v4).toSet() + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + + @TestAllUndirectedGraphs + fun `adding edge with reversed arguments shouldn't change graph`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + graph.addEdge(v1, v4) + + val expectedGraph = graphStructure + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + } + + @TestAllUndirectedGraphs + fun `adding edge with reversed arguments shouldn't change adjacency map`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedNeighbours1 = graph.getNeighbours(v1).toSet() + val expectedNeighbours2 = graph.getNeighbours(v4).toSet() + + graph.addEdge(v1, v4) + + val actualNeighbours1 = graph.getNeighbours(v1).toSet() + val actualNeighbours2 = graph.getNeighbours(v4).toSet() + + assertEquals(expectedNeighbours1, actualNeighbours1) + assertEquals(expectedNeighbours2, actualNeighbours2) + } + + @TestAllUndirectedGraphs + fun `adding edge with reversed arguments shouldn't change outgoing edges map`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedEdges1 = graph.getOutgoingEdges(v1).toSet() + val expectedEdges2 = graph.getOutgoingEdges(v4).toSet() + + graph.addEdge(v1, v4) + + val actualEdges1 = graph.getOutgoingEdges(v1).toSet() + val actualEdges2 = graph.getOutgoingEdges(v4).toSet() + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + } + + @Nested + inner class `Vertices are the same` { + @TestAllUndirectedGraphs + fun `exception should be thrown`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v2, v2) + } + } + } + } + + @Nested + inner class `One of the vertices isn't in the graph` { + @TestAllUndirectedGraphs + fun `first vertex isn't in the graph`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(2210, 2005), v0) + } + } + + @TestAllUndirectedGraphs + fun `second vertex isn't in the graph`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v0, Vertex(2510, 1917)) + } + } + + @TestAllUndirectedGraphs + fun `both vertices aren't in the graph`(graph: UndirectedGraph) { + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(3010, 1978), Vertex(1002, 1982)) + } + } + } + } @Nested - inner class RemoveEdgeTest {} + inner class RemoveEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllUndirectedGraphs + fun `removed edge should be returned`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v3, v1) + + val actualValue = graph.removeEdge(edgeToRemove) + val expectedValue = edgeToRemove + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `order of the arguments shouldn't matter`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v1, v3) + + val actualValue = graph.removeEdge(edgeToRemove) + val expectedValue = edgeToRemove + + assertEquals(expectedValue, actualValue) + } + + @TestAllUndirectedGraphs + fun `edge should be removed from graph`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v4, v1) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet - edgeToRemove + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllUndirectedGraphs + fun `vertices should be removed from each other's adjacency map values`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v1, v2) + graph.removeEdge(edgeToRemove) + + val actualVertices1 = graph.getNeighbours(v1).toSet() + val expectedVertices1 = setOf(v0, v3, v4) + + val actualVertices2 = graph.getNeighbours(v2).toSet() + val expectedVertices2 = setOf(v3) + + assertEquals(expectedVertices1, actualVertices1) + assertEquals(expectedVertices2, actualVertices2) + } + + @TestAllUndirectedGraphs + fun `edge should be removed from vertices' outgoing edges map values`(graph: UndirectedGraph) { + val graphStructure = setup(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v1, v2) + graph.removeEdge(edgeToRemove) + + val actualEdges1 = graph.getOutgoingEdges(v1).toSet() + val expectedEdges1 = setOf( + graph.getEdge(v1, v0), + graph.getEdge(v1, v3), + graph.getEdge(v1, v4), + ) + + val actualEdges2 = graph.getOutgoingEdges(v2).toSet() + val expectedEdges2 = setOf(graph.getEdge(v2, v3)) + + assertEquals(expectedEdges1, actualEdges1) + assertEquals(expectedEdges2, actualEdges2) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllUndirectedGraphs + fun `non-existing edge should throw an exception`(graph: UndirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(Edge(Vertex(0,0), Vertex(1, 1))) + } + } + } + } @Nested inner class FindBridgesTest { @Nested inner class `All bridges should be found`() { - @Test - fun `if graph has one edge`() { + @TestAllUndirectedGraphs + fun `if graph has one edge`(graph: UndirectedGraph) { val vertex0 = graph.addVertex(0) val vertex1 = graph.addVertex(1) @@ -35,8 +556,8 @@ class UndirectedGraphTest { assertEquals(expectedBridges, actualBridges) } - @Test - fun `if two components are connected via one edge`() { + @TestAllUndirectedGraphs + fun `if two components are connected via one edge`(graph: UndirectedGraph) { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) @@ -61,8 +582,8 @@ class UndirectedGraphTest { assertEquals(expectedBridges, actualBridges) } - @Test - fun `if graph is chain-like`() { + @TestAllUndirectedGraphs + fun `if graph is chain-like`(graph: UndirectedGraph) { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) @@ -78,8 +599,8 @@ class UndirectedGraphTest { assertEquals(expectedBridges, actualBridges) } - @Test - fun `if graph is star-like`() { + @TestAllUndirectedGraphs + fun `if graph is star-like`(graph: UndirectedGraph) { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) @@ -98,16 +619,16 @@ class UndirectedGraphTest { @Nested inner class `No bridge should be found`() { - @Test - fun `if graph has no vertices`() { + @TestAllUndirectedGraphs + fun `if graph has no vertices`(graph: UndirectedGraph) { val expectedBridges = listOf>() val actualBridges = graph.findBridges() assertEquals(expectedBridges, actualBridges) } - @Test - fun `if graph has no edges`() { + @TestAllUndirectedGraphs + fun `if graph has no edges`(graph: UndirectedGraph) { graph.apply { addVertex(0) addVertex(1) @@ -120,8 +641,8 @@ class UndirectedGraphTest { assertEquals(expectedBridges, actualBridges) } - @Test - fun `if graph is circle-like`() { + @TestAllUndirectedGraphs + fun `if graph is circle-like`(graph: UndirectedGraph) { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) @@ -142,8 +663,8 @@ class UndirectedGraphTest { assertEquals(expectedBridges, actualBridges) } - @Test - fun `if two components are connected via more than one edge`() { + @TestAllUndirectedGraphs + fun `if two components are connected via more than one edge`(graph: UndirectedGraph) { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt index 5a077eeb..00c4fc31 100644 --- a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt @@ -1,13 +1,106 @@ package model +import model.abstractGraph.Edge +import model.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import util.setupWeightedDirected class WeightedDirectedGraphTest { + private lateinit var graph: WeightedDirectedGraph + + @BeforeEach + fun init() { + graph = WeightedDirectedGraph() + } + @Nested - inner class AddEdgeTest {} + inner class GetWeightTest { + @Nested + inner class `Edge is in the graph` { + @Test + fun `edge's weight should be returned`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + val edge = graph.getEdge(v3, v1) + + val actualValue = graph.getWeight(edge) + val expectedValue = 3 + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[3] + val edge = graph.getEdge(v4, v1) + + graph.getWeight(edge) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + @Nested + inner class `Edge isn't in the graph` { + @Test + fun `exception should be thrown`() { + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) + } + } + } + } + + // most of the functionality is tested in the DirectedGraphTest class, + // as weighted graphs call super methods inside their methods @Nested - inner class RemoveEdgeTest {} + inner class AddEdgeTest { + @Test + fun `added edge's weight should be added to weight map`() { + val v0 = graph.addVertex(30) + val v1 = graph.addVertex(31) + + val newEdge = graph.addEdge(v0, v1, 62) + + val actualValue = graph.getWeight(newEdge) + val expectedValue = 62 + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class RemoveEdgeTest { + @Test + fun `removed edge should be removed from the weight map`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val edge = graph.getEdge(v1, v2) + + graph.removeEdge(edge) + + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(edge) + } + } + } @Nested inner class FindShortestPathDijkstraTest {} diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt index 7f582804..10bd9bbd 100644 --- a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt @@ -1,10 +1,14 @@ package model import model.abstractGraph.Edge +import model.abstractGraph.Vertex import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import util.setup +import util.setupWeightedUndirected class WeightedUndirectedGraphTest { private lateinit var graph: WeightedUndirectedGraph @@ -15,10 +19,89 @@ class WeightedUndirectedGraphTest { } @Nested - inner class AddEdgeTest {} + inner class GetWeightTest { + @Nested + inner class `Edge is in the graph` { + @Test + fun `edge's weight should be returned`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + val edge = graph.getEdge(v3, v1) + + val actualValue = graph.getWeight(edge) + val expectedValue = 3 + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[3] + val edge = graph.getEdge(v4, v1) + + graph.getWeight(edge) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @Test + fun `exception should be thrown`() { + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) + } + } + } + } + // most of the functionality is tested in the DirectedGraphTest class, + // as weighted graphs call super methods inside their methods @Nested - inner class RemoveEdgeTest {} + inner class AddEdgeTest { + @Test + fun `added edge's weight should be added to weight map`() { + val v0 = graph.addVertex(30) + val v1 = graph.addVertex(31) + + val newEdge = graph.addEdge(v0, v1, 62) + + val actualValue = graph.getWeight(newEdge) + val expectedValue = 62 + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class RemoveEdgeTest { + @Test + fun `removed edge should be removed from the weight map`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val edge = graph.getEdge(v1, v2) + + graph.removeEdge(edge) + + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(edge) + } + } + } @Nested inner class FindShortestPathDijkstraTest {} diff --git a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt index 829033d5..83810f21 100644 --- a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt +++ b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt @@ -1,55 +1,12 @@ package model.abstractGraph -import model.DirectedGraph -import model.UndirectedGraph -import model.WeightedDirectedGraph -import model.WeightedUndirectedGraph import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.ArgumentsProvider -import org.junit.jupiter.params.provider.ArgumentsSource -import java.util.stream.Stream - -@ParameterizedTest(name = "{0}") -@ArgumentsSource(AllGraphTypesProvider::class) -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -private annotation class TestAllGraphTypes - -class AllGraphTypesProvider : ArgumentsProvider { - override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( - Arguments.of(UndirectedGraph()), - Arguments.of(DirectedGraph()), - Arguments.of(WeightedUndirectedGraph()), - Arguments.of(WeightedDirectedGraph()) - ) -} - -fun setup(graph: Graph): Pair>, Set>> { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - val defaultVerticesList = listOf(v0, v1, v2, v3, v4) - - val defaultEdgesSet = setOf( - graph.addEdge(v0, v1), - graph.addEdge(v1, v2), - graph.addEdge(v2, v3), - graph.addEdge(v3, v4), - graph.addEdge(v4, v1) - ) - - return defaultVerticesList to defaultEdgesSet -} - -val emptyVerticesList = listOf>() -val emptyEdgesSet = setOf>() +import util.annotations.TestAllGraphTypes +import util.setup +import util.emptyVerticesList +import util.emptyEdgesSet +import util.emptyGraph class GraphTest { @Nested @@ -95,7 +52,7 @@ class GraphTest { graph.getVertices() val actualGraph = graph.getVertices() to graph.getEdges().toSet() - val expectedGraph = emptyVerticesList to emptyEdgesSet + val expectedGraph = emptyGraph assertEquals(expectedGraph, actualGraph) } @@ -145,7 +102,7 @@ class GraphTest { graph.getEdges() val actualGraph = graph.getVertices() to graph.getEdges().toSet() - val expectedGraph = emptyVerticesList to emptyEdgesSet + val expectedGraph = emptyGraph assertEquals(expectedGraph, actualGraph) } @@ -294,7 +251,8 @@ class GraphTest { val expectedEdges = setOf( graph.getEdge(newV0, newV1), graph.getEdge(newV3, newV2), - graph.getEdge(newV2, newV1) + graph.getEdge(newV2, newV1), + graph.getEdge(newV3, newV1) ) assertEquals(expectedEdges, actualEdges) diff --git a/app/src/test/kotlin/util/annotations/TestAllDirectedGraphs.kt b/app/src/test/kotlin/util/annotations/TestAllDirectedGraphs.kt new file mode 100644 index 00000000..55f1acac --- /dev/null +++ b/app/src/test/kotlin/util/annotations/TestAllDirectedGraphs.kt @@ -0,0 +1,11 @@ +package util.annotations + +import util.annotations.argumentProviders.WeightedAndUnweightedDirectedGraphsProvider +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +@ParameterizedTest(name = "{0}") +@ArgumentsSource(WeightedAndUnweightedDirectedGraphsProvider::class) +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class TestAllDirectedGraphs diff --git a/app/src/test/kotlin/util/annotations/TestAllGraphTypes.kt b/app/src/test/kotlin/util/annotations/TestAllGraphTypes.kt new file mode 100644 index 00000000..516ec930 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/TestAllGraphTypes.kt @@ -0,0 +1,12 @@ +package util.annotations + +import util.annotations.argumentProviders.AllGraphTypesProvider +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +@ParameterizedTest(name = "{0}") +@ArgumentsSource(AllGraphTypesProvider::class) +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class TestAllGraphTypes + diff --git a/app/src/test/kotlin/util/annotations/TestAllUndirectedGraphs.kt b/app/src/test/kotlin/util/annotations/TestAllUndirectedGraphs.kt new file mode 100644 index 00000000..d7a35640 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/TestAllUndirectedGraphs.kt @@ -0,0 +1,11 @@ +package util.annotations + +import util.annotations.argumentProviders.WeightedAndUnweightedUndirectedGraphsProvider +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +@ParameterizedTest(name = "{0}") +@ArgumentsSource(WeightedAndUnweightedUndirectedGraphsProvider::class) +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class TestAllUndirectedGraphs diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt new file mode 100644 index 00000000..eb2744ed --- /dev/null +++ b/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt @@ -0,0 +1,19 @@ +package util.annotations.argumentProviders + +import model.DirectedGraph +import model.UndirectedGraph +import model.WeightedDirectedGraph +import model.WeightedUndirectedGraph +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class AllGraphTypesProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of(UndirectedGraph()), + Arguments.of(DirectedGraph()), + Arguments.of(WeightedUndirectedGraph()), + Arguments.of(WeightedDirectedGraph()) + ) +} \ No newline at end of file diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt new file mode 100644 index 00000000..740607c7 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt @@ -0,0 +1,15 @@ +package util.annotations.argumentProviders + +import model.DirectedGraph +import model.WeightedDirectedGraph +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class WeightedAndUnweightedDirectedGraphsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of(DirectedGraph()), + Arguments.of(WeightedDirectedGraph()) + ) +} diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt new file mode 100644 index 00000000..98e5c043 --- /dev/null +++ b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt @@ -0,0 +1,15 @@ +package util.annotations.argumentProviders + +import model.UndirectedGraph +import model.WeightedUndirectedGraph +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class WeightedAndUnweightedUndirectedGraphsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of(UndirectedGraph()), + Arguments.of(WeightedUndirectedGraph()) + ) +} diff --git a/app/src/test/kotlin/util/emptyGraphs.kt b/app/src/test/kotlin/util/emptyGraphs.kt new file mode 100644 index 00000000..1d3af414 --- /dev/null +++ b/app/src/test/kotlin/util/emptyGraphs.kt @@ -0,0 +1,19 @@ +package util + +import model.DirectedGraph +import model.UndirectedGraph +import model.WeightedDirectedGraph +import model.WeightedUndirectedGraph +import model.abstractGraph.Edge +import model.abstractGraph.Vertex + +val emptyDirectedGraph = DirectedGraph() +val emptyUndirectedGraph = UndirectedGraph() + +val emptyWDGrapgh = WeightedDirectedGraph() +val emptyWUGrapgh = WeightedUndirectedGraph() + +val emptyVerticesList = listOf>() +val emptyEdgesSet = setOf>() + +val emptyGraph = emptyVerticesList to emptyEdgesSet \ No newline at end of file diff --git a/app/src/test/kotlin/util/setup.kt b/app/src/test/kotlin/util/setup.kt new file mode 100644 index 00000000..60578fe3 --- /dev/null +++ b/app/src/test/kotlin/util/setup.kt @@ -0,0 +1,70 @@ +package util + +import model.abstractGraph.Edge +import model.abstractGraph.Graph +import model.abstractGraph.Vertex +import model.WeightedUndirectedGraph +import model.WeightedDirectedGraph + +fun setup(graph: Graph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1), + graph.addEdge(v1, v2), + graph.addEdge(v2, v3), + graph.addEdge(v3, v4), + graph.addEdge(v4, v1), + graph.addEdge(v3, v1) + ) + + return defaultVerticesList to defaultEdgesSet +} + +fun setupWeightedDirected(graph: WeightedDirectedGraph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1, -3), + graph.addEdge(v1, v2, -2), + graph.addEdge(v2, v3, -1), + graph.addEdge(v3, v4, 1), + graph.addEdge(v4, v1, 2), + graph.addEdge(v3, v1, 3) + ) + + return defaultVerticesList to defaultEdgesSet +} + +fun setupWeightedUndirected(graph: WeightedUndirectedGraph): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1, -3), + graph.addEdge(v1, v2, -2), + graph.addEdge(v2, v3, -1), + graph.addEdge(v3, v4, 1), + graph.addEdge(v4, v1, 2), + graph.addEdge(v3, v1, 3) + ) + + return defaultVerticesList to defaultEdgesSet +} \ No newline at end of file From 711a2d467601a37002f62756a2cb2cedd5dc110e Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Sun, 19 May 2024 22:00:11 +0300 Subject: [PATCH 50/77] fix: removing edge from weightMap #33 --- app/src/main/kotlin/model/WeightedDirectedGraph.kt | 8 ++++++++ app/src/main/kotlin/model/WeightedUndirectedGraph.kt | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 80da33ae..8ff39699 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -19,6 +19,14 @@ class WeightedDirectedGraph : DirectedGraph() { // In case weight is not passed, set it to default value = 1 override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + override fun removeEdge(edgeToRemove: Edge): Edge { + val removedEdge = super.removeEdge(edgeToRemove) + + weightMap.remove(edgeToRemove) + + return removedEdge + } + fun getWeight(edge: Edge): Int { val weight = weightMap[edge] ?: throw NoSuchElementException( diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 3c22722b..b3c64815 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -20,6 +20,14 @@ class WeightedUndirectedGraph : UndirectedGraph() { // In case weight is not passed, set it to default value = 1 override fun addEdge(vertex1: Vertex, vertex2: Vertex) = addEdge(vertex1, vertex2, 1) + override fun removeEdge(edgeToRemove: Edge): Edge { + val removedEdge = super.removeEdge(edgeToRemove) + + weightMap.remove(edgeToRemove) + + return removedEdge + } + fun getWeight(edge: Edge): Int { val weight = weightMap[edge] ?: throw NoSuchElementException( From 1a1840111b4c1ee1214b43d298578528a6ee59f2 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Mon, 20 May 2024 17:35:05 +0300 Subject: [PATCH 51/77] ci: add approval check and building app in PR attempts #34 * ci: add description and approval PR checks * ci: add build and test github workflow * ci: exclude main branch from PR approval check --- .github/mergeable.yml | 22 ++++++++++++++++++++++ .github/workflows/test.yml | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/mergeable.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 00000000..44aeabe8 --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,22 @@ +version: 2 +mergeable: + - when: pull_request.* + name: "Description check" + validate: + - do: description + no_empty: + enabled: true + message: Description matter and should not be empty. Provide detail with **what** was changed, **why** it was changed, and **how** it was changed. + + - when: pull_request.*, pull_request_review.* + name: 'Approval check' + branches-ignore: + - 'main' + validate: + - do: approvals + min: + count: 2 + required: + requested_reviewers: true + limit: + users: [ 'qrutyy', 'spisladqo', 'kar1mgh' ] \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..46b23b41 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Build & Run JUnit5 tests + +on: + workflow_dispatch: + pull_request: + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: zulu + + - name: Build & Test by Gradle + run: ./gradlew build \ No newline at end of file From b8261e46157737530782c476ed69989123b805b2 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Tue, 21 May 2024 10:59:50 +0300 Subject: [PATCH 52/77] ci: fix excluding main in mergeable #35 + add ignore of feedback branch and message if PR isn't approved --- .github/.keep | 0 .github/mergeable.yml | 21 ++++++++++++++++++++- .github/workflows/test.yml | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) delete mode 100644 .github/.keep diff --git a/.github/.keep b/.github/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/.github/mergeable.yml b/.github/mergeable.yml index 44aeabe8..67b8290b 100644 --- a/.github/mergeable.yml +++ b/.github/mergeable.yml @@ -2,6 +2,15 @@ version: 2 mergeable: - when: pull_request.* name: "Description check" + + filter: # to ignore Feedback branch + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none + validate: - do: description no_empty: @@ -10,13 +19,23 @@ mergeable: - when: pull_request.*, pull_request_review.* name: 'Approval check' + + filter: # to ignore Feedback branch + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none + branches-ignore: - - 'main' + - main validate: - do: approvals min: count: 2 required: requested_reviewers: true + message: All requested reviewers must approve changes before merging. limit: users: [ 'qrutyy', 'spisladqo', 'kar1mgh' ] \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46b23b41..81322b15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,8 @@ on: jobs: build: + if: github.actor!= 'github-classroom[bot]' # to ignore Feedback branch + runs-on: ${{ matrix.os }} strategy: matrix: From d2f1e581bf3692653f72e8a9fea98d58dde8914b Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Tue, 21 May 2024 19:45:50 +0300 Subject: [PATCH 53/77] test: implement tests for Dijkstra's shortest path algorithm #36 * fix: remove 'destVertex' initialization * test: implement Dijkstra tests for weighted&undirected * fix: correct vertex placement in edge, remove 'destVertex' init. * fix: change return types * test: implement Dijkstra tests for weighted&directed * test: correct asserts due to change in return types * fix: replace adjacency map calls with 'getNeighbours' * refactor: remove annoying lines --- .../kotlin/model/WeightedDirectedGraph.kt | 16 +- .../kotlin/model/WeightedUndirectedGraph.kt | 14 +- .../kotlin/model/WeightedDirectedGraphTest.kt | 212 +++++++++++++++++- .../model/WeightedUndirectedGraphTest.kt | 194 +++++++++++++++- 4 files changed, 409 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 8ff39699..5e530a56 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -37,19 +37,21 @@ class WeightedDirectedGraph : DirectedGraph() { return weight } - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>> { + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>>? { val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() - val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } val visited = mutableSetOf, Int>>() + if (srcVertex == destVertex) return emptyList() + distanceMap[srcVertex] = 0 while (priorityQueue.isNotEmpty()) { val (currentVertex, currentDistance) = priorityQueue.poll() if (visited.add(currentVertex to currentDistance)) { - adjacencyMap[currentVertex]?.forEach { adjacent -> - val currentEdge = getEdge(adjacent, currentVertex) + getNeighbours(currentVertex).forEach { adjacent -> + val currentEdge = getEdge(currentVertex, adjacent) // Ensure correct edge direction val totalDist = currentDistance + getWeight(currentEdge) @@ -67,11 +69,7 @@ class WeightedDirectedGraph : DirectedGraph() { var currentVertex = destVertex while (currentVertex != srcVertex) { val predecessor = predecessorMap[currentVertex] - - if (predecessor == null) { - // If no path exists - return emptyList() - } + ?: return null // path doesn't exist val currentEdge = getEdge(predecessor, currentVertex) path.add(Pair(currentVertex, currentEdge)) diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index b3c64815..39aeffef 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -38,18 +38,20 @@ class WeightedUndirectedGraph : UndirectedGraph() { return weight } - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>> { + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>>? { val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() - val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(destVertex to 0) } + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } val visited = mutableSetOf, Int>>() + if (srcVertex == destVertex) return emptyList() + distanceMap[srcVertex] = 0 while (priorityQueue.isNotEmpty()) { val (currentVertex, currentDistance) = priorityQueue.poll() if (visited.add(currentVertex to currentDistance)) { - adjacencyMap[currentVertex]?.forEach { adjacent -> + getNeighbours(currentVertex).forEach { adjacent -> val currentEdge = getEdge(adjacent, currentVertex) val totalDist = currentDistance + getWeight(currentEdge) @@ -68,11 +70,7 @@ class WeightedUndirectedGraph : UndirectedGraph() { var currentVertex = destVertex while (currentVertex != srcVertex) { val predecessor = predecessorMap[currentVertex] - - if (predecessor == null) { - // If no path exists - return emptyList() - } + ?: return null // path doesn't exist val currentEdge = getEdge(predecessor, currentVertex) path.add(Pair(currentVertex, currentEdge)) diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt index 00c4fc31..ae711032 100644 --- a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt @@ -2,11 +2,8 @@ package model import model.abstractGraph.Edge import model.abstractGraph.Vertex -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* import util.setupWeightedDirected class WeightedDirectedGraphTest { @@ -103,5 +100,208 @@ class WeightedDirectedGraphTest { } @Nested - inner class FindShortestPathDijkstraTest {} + inner class FindShortestPathDijkstraTest { + @Nested + inner class `Normal path should be returned`() { + @Test + fun `all is as usual, should return default`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 10) + val e1 = graph.addEdge(v0, v4, 100) + val e2 = graph.addEdge(v0, v3, 30) + val e3 = graph.addEdge(v1, v2, 2) + val e4 = graph.addEdge(v2, v4, 10) + val e5 = graph.addEdge(v3, v2, 20) + val e6 = graph.addEdge(v3, v4, 60) + + val expectedResult = listOf, Edge>>(Pair(v1, e0), Pair(v2, e3), Pair(v4, e4)) + val actualResult = graph.findShortestPathDijkstra(v0, v4) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `direct path should be the shortest in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 5) + val e1 = graph.addEdge(v1, v2, 5) + val e2 = graph.addEdge(v0, v2, 10) + + val expectedResult = listOf(Pair(v2, e2)) + val actualResult = graph.findShortestPathDijkstra(v0, v2) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has multiple paths and equal weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + + val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2)) + val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3)) + + val actualResult = graph.findShortestPathDijkstra(v0, v3) + + Assertions.assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has single edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e0 = graph.addEdge(v0, v1, 5) + + val expectedResult = listOf(Pair(v1, e0)) + val actualResult = graph.findShortestPathDijkstra(v0, v1) + + assertEquals(expectedResult, actualResult) + } + + @Disabled("Dijkstra's algorithm doesn't work with negative weights") + @Test + fun `if graph has negative weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, -1) + val e1 = graph.addEdge(v1, v2, -2) + val e2 = graph.addEdge(v2, v3, -3) + + val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1), Pair(v3, e2)) + val actualResult = graph.findShortestPathDijkstra(v0, v3) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `graph has multiple equal shortest paths`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + val e4 = graph.addEdge(v3, v4, 1) + + val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2), Pair(v4, e4)) + val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3), Pair(v4, e4)) + val actualResult = graph.findShortestPathDijkstra(v0, v4) + + Assertions.assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has a cycle`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v1, v2, 2) + val e2 = graph.addEdge(v2, v0, 3) + + val actualResult = graph.findShortestPathDijkstra(v0, v2) + val expectedResult = listOf(Pair(v2, e2)) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if all the edges have zero weight in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1)) + val actualResult = graph.findShortestPathDijkstra(v0, v2) + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No path should be returned`() { + + @Test + fun `no path exists in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v0, v1, 10) + graph.addEdge(v1, v2, 20) + + val actualResult = graph.findShortestPathDijkstra(v0, v3) + assertEquals(actualResult, null) + } + + @Test + fun `if start and end vertices are the same`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v1, v2, 2) + graph.addEdge(v2, v0, 2) + + val actualResult = graph.findShortestPathDijkstra(v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if graph has single vertex`() { + val v0 = graph.addVertex(0) + + val actualResult = graph.findShortestPathDijkstra(v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if path is in other way (not how edges were set)`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val actualResult = graph.findShortestPathDijkstra(v2, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + } + } } diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt index 10bd9bbd..5c037195 100644 --- a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt @@ -2,12 +2,11 @@ package model import model.abstractGraph.Edge import model.abstractGraph.Vertex -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import util.setup import util.setupWeightedUndirected class WeightedUndirectedGraphTest { @@ -104,7 +103,194 @@ class WeightedUndirectedGraphTest { } @Nested - inner class FindShortestPathDijkstraTest {} + inner class FindShortestPathDijkstraTest { + @Nested + inner class `Normal path should be returned`() { + @Test + fun `all is as usual, should return default`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 10) + val e1 = graph.addEdge(v0, v4, 100) + val e2 = graph.addEdge(v0, v3, 30) + val e3 = graph.addEdge(v1, v2, 2) + val e4 = graph.addEdge(v2, v4, 10) + val e5 = graph.addEdge(v3, v2, 20) + val e6 = graph.addEdge(v3, v4, 60) + + val expectedResult = listOf, Edge>>(Pair(v1, e0), Pair(v2, e3), Pair(v4, e4)) + val actualResult = graph.findShortestPathDijkstra(v0, v4) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has multiple paths and equal weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + + val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2)) + val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3)) + + val actualResult = graph.findShortestPathDijkstra(v0, v3) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has single edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e0 = graph.addEdge(v0, v1, 5) + + val expectedResult = listOf(Pair(v1, e0)) + val actualResult = graph.findShortestPathDijkstra(v0, v1) + + assertEquals(expectedResult, actualResult) + } + + @Disabled("Dijkstra's algorithm doesn't work with negative weights") + @Test + fun `if graph has negative weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, -1) + val e1 = graph.addEdge(v1, v2, -2) + val e2 = graph.addEdge(v2, v3, -3) + + val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1), Pair(v3, e2)) + val actualResult = graph.findShortestPathDijkstra(v0, v3) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `graph has multiple equal shortest paths`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + val e4 = graph.addEdge(v3, v4, 1) + + val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2), Pair(v4, e4)) + val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3), Pair(v4, e4)) + val actualResult = graph.findShortestPathDijkstra(v0, v4) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has a cycle`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v1, v2, 2) + val e2 = graph.addEdge(v2, v0, 3) + + val actualResult = graph.findShortestPathDijkstra(v0, v2) + val expectedResult = listOf(Pair(v2, e2)) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if path is in other way (not how edges were set)`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(Pair(v1, e1), Pair(v0, e0)) + val actualResult = graph.findShortestPathDijkstra(v2, v0) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if all the edges have zero weight in undirected graph`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1)) + val actualResult = graph.findShortestPathDijkstra(v0, v2) + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No path should be returned`() { + @Test + fun `no path exists in undirected graph`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v1, v2, 2) + + val actualResult = graph.findShortestPathDijkstra(v0, v3) + + assertEquals(actualResult, null) + } + + @Test + fun `if start and end vertices are the same`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v1, v2, 2) + + val actualResult = graph.findShortestPathDijkstra(v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if graph has single vertex`() { + val v0 = graph.addVertex(0) + + val actualResult = graph.findShortestPathDijkstra(v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + } + } @Nested inner class FindMinSpanningTreeTest { From 6044fdee3911d48fd021abd5e04e5738bb60c1b0 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Tue, 21 May 2024 22:37:01 +0300 Subject: [PATCH 54/77] test: implement tests for Kosaraju's SCC search #37 * fix: change return types to sets, fix the logic * test: add tests for kosaraju's SCC search * fix: replace adjacency map iteration on 'getNeighbours' call * test: add more test cases * refactor: remove annoying lines * fix: get rid of adjacency map calls * test: add side-effect check test cases --- app/src/main/kotlin/model/DirectedGraph.kt | 50 +-- .../test/kotlin/model/DirectedGraphTest.kt | 340 +++++++++++++++++- 2 files changed, 367 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 86543711..545eb4ce 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -12,7 +12,7 @@ open class DirectedGraph : Graph() { if (vertex1 !in vertices || vertex2 !in vertices) throw IllegalArgumentException( "One of the vertices (${vertex1.id}, ${vertex1.data}) and " + - "(${vertex2.id}, ${vertex2.data}) isn't in the graph" + "(${vertex2.id}, ${vertex2.data}) isn't in the graph" ) // Don't do anything if the edge is already in the graph @@ -30,7 +30,7 @@ open class DirectedGraph : Graph() { override fun removeEdge(edgeToRemove: Edge): Edge { if (edgeToRemove !in edges) throw NoSuchElementException( "Edge between vertices (${edgeToRemove.vertex1.id}, ${edgeToRemove.vertex1.data}) and " + - "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) isn't in the graph" + "(${edgeToRemove.vertex2.id}, ${edgeToRemove.vertex2.data}) isn't in the graph" ) val vertex1 = edgeToRemove.vertex1 @@ -54,51 +54,59 @@ open class DirectedGraph : Graph() { } // SCC - Strongly Connected Components (by Kosaraju) - fun findSCC(): ArrayList>> { + fun findSCC(): MutableSet>> { val visited = mutableMapOf, Boolean>().withDefault { false } val stack = ArrayDeque>() val component = arrayListOf>() - val sccList: ArrayList>> = arrayListOf() + val sccList: MutableSet>> = mutableSetOf() fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { visited[srcVertex] = true componentList.add(srcVertex) - adjacencyMap[srcVertex]?.forEach { vertex2 -> - if (visited[vertex2] != null && visited[vertex2] != true) { + getNeighbours(srcVertex).forEach { vertex2 -> + if (visited[vertex2] != true) { auxiliaryDFS(vertex2, componentList) } } stack.add(srcVertex) } - for (vertex in vertices) { - if (visited[vertex] != null && visited[vertex] != true) auxiliaryDFS(vertex, component) + vertices.forEach { vertex -> + if (visited[vertex] != true) auxiliaryDFS(vertex, component) } - reverseGraph() + val reversedEdgesMap = reverseEdgesMap() visited.clear() component.clear() + fun reverseDFS(vertex: Vertex, componentList: MutableSet>) { + visited[vertex] = true + componentList.add(vertex) + reversedEdgesMap[vertex]?.forEach { vertex2 -> + if (visited[vertex2] != true) { + reverseDFS(vertex2, componentList) + } + } + } + while (stack.isNotEmpty()) { val vertex = stack.removeLast() - if (visited[vertex] != null && visited[vertex] != true) { - val currentComponent = arrayListOf>() - auxiliaryDFS(vertex, currentComponent) + if (visited[vertex] != true) { + val currentComponent = mutableSetOf>() + reverseDFS(vertex, currentComponent) sccList.add(currentComponent) } } return sccList } - private fun reverseGraph() { - val reversedAdjacencyMap = mutableMapOf, MutableSet>>() - for (vertex in vertices) { - adjacencyMap[vertex]?.forEach { vertex2 -> - reversedAdjacencyMap[vertex2] = reversedAdjacencyMap[vertex2] ?: mutableSetOf() - reversedAdjacencyMap[vertex2]?.add(vertex) - } + private fun reverseEdgesMap(): Map, MutableSet>> { + val reversedEdgesMap = mutableMapOf, MutableSet>>() + vertices.forEach { reversedEdgesMap[it] = mutableSetOf() } + edges.forEach { edge -> + reversedEdgesMap[edge.vertex2]?.add(edge.vertex1) } - adjacencyMap.clear() - adjacencyMap.putAll(reversedAdjacencyMap) + return reversedEdgesMap } } + diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt index da70c953..17ea2e93 100644 --- a/app/src/test/kotlin/model/DirectedGraphTest.kt +++ b/app/src/test/kotlin/model/DirectedGraphTest.kt @@ -3,7 +3,9 @@ package model import model.abstractGraph.Edge import model.abstractGraph.Vertex import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test import util.annotations.TestAllDirectedGraphs import util.emptyEdgesSet import util.emptyVerticesList @@ -429,12 +431,346 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `non-existing edge should throw an exception`(graph: DirectedGraph) { assertThrows(NoSuchElementException::class.java) { - graph.removeEdge(Edge(Vertex(0,0), Vertex(1, 1))) + graph.removeEdge(Edge(Vertex(0, 0), Vertex(1, 1))) } } } } @Nested - inner class FindSCCTest {} + inner class FindSCCTest { + @Nested + inner class `SCC should return not empty array`() { + @TestAllDirectedGraphs + fun `graph has two connected vertices`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v1) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1, v2)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `complex graph`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v3, v4) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph has multiple SCCs`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v1) + graph.addEdge(v3, v4) + graph.addEdge(v4, v3) + graph.addEdge(v5, v1) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v3, v4), mutableSetOf(v1, v2), mutableSetOf(v5)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with nested cycles`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v3, v4) + graph.addEdge(v4, v5) + graph.addEdge(v5, v6) + graph.addEdge(v6, v4) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with cross connections`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v3, v4) + graph.addEdge(v4, v5) + graph.addEdge(v5, v6) + graph.addEdge(v6, v4) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with disconnected subgraphs`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v1) + graph.addEdge(v3, v4) + graph.addEdge(v4, v3) + graph.addEdge(v5, v6) + graph.addEdge(v6, v5) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1, v2), mutableSetOf(v3, v4), mutableSetOf(v5, v6)) + assertEquals(expectedValue, actualValue) + } + + @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") + @TestAllDirectedGraphs + fun `graph with single vertex cycle`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v3, v3) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3)) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `SCC should return single-element SCCs`() { + @TestAllDirectedGraphs + fun `graph has single vertex`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with multiple disconnected vertices`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1), mutableSetOf(v2), mutableSetOf(v3), mutableSetOf(v4)) + + assertEquals(expectedValue, actualValue) + } + } + @Nested + inner class `Additional edge cases`() { + @TestAllDirectedGraphs + fun `empty graph`() { + val graph = DirectedGraph() + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf>>() + + assertEquals(expectedValue, actualValue) + } + + @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") + @TestAllDirectedGraphs + fun `graph with self-loops`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v1, v1) + graph.addEdge(v2, v2) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1), mutableSetOf(v2)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `linear graph`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v3), mutableSetOf(v2), mutableSetOf(v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with cycles and tail`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v4, v3) + graph.addEdge(v4, v5) + + val actualValue = graph.findSCC() + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4), mutableSetOf(v5)) + + assertEquals(expectedValue, actualValue) + } + } + @Nested + inner class `Side-effects check`() { + @TestAllDirectedGraphs + fun `check vertices in complex graph`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v3, v4) + + val edgesBeforeReverse = graph.getEdges() + val expectedValue = graph.getVertices() + graph.findSCC() + val actualValue = graph.getVertices() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check edges in complex graph`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v3, v4) + + + val expectedValue = graph.getEdges() + + graph.findSCC() + + val actualValue = graph.getEdges() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check edges in graph with cycles and tail`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v4, v3) + graph.addEdge(v4, v5) + + val expectedValue = graph.getEdges() + + graph.findSCC() + + val actualValue = graph.getEdges() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check vertices graph with cycles and tail`() { + val graph = DirectedGraph() + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + graph.addEdge(v3, v1) + graph.addEdge(v4, v3) + graph.addEdge(v4, v5) + + val expectedValue = graph.getVertices() + + graph.findSCC() + + val actualValue = graph.getVertices() + + assertEquals(expectedValue, actualValue) + } + } + } } From 20849c82f4cc23d735aba0d313796a81fc90fd58 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Wed, 22 May 2024 13:48:09 +0300 Subject: [PATCH 55/77] feat: implement Ford-Bellman algorithm and write tests for it #38 * feat: implement tests for Kosaraju's SCC search #37 * fix: change return types to sets, fix the logic * test: add tests for kosaraju's SCC search * fix: replace adjacency map iteration on 'getNeighbours' call * test: add more test cases * refactor: remove annoying lines * fix: get rid of adjacency map calls * test: add side-effect check test cases * feat: add Ford-Bellman algo for directed graphs * fix(Ford-Bellman): add negative cycle processing + return null if path doesn't exist, rename variables * test: add tests for directed Ford-Bellman algo + add util method for creating graph with negative cycle and change order of elements in pair of the algo's return value --------- Co-authored-by: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> --- .../kotlin/model/WeightedDirectedGraph.kt | 56 ++++++ .../kotlin/model/WeightedDirectedGraphTest.kt | 167 ++++++++++++++++++ app/src/test/kotlin/util/setup.kt | 29 +++ 3 files changed, 252 insertions(+) diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 5e530a56..bbcff46a 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -78,4 +78,60 @@ class WeightedDirectedGraph : DirectedGraph() { } return path.reversed() } + + // returns null if path doesn't exist + fun findShortestPathFordBellman(srcVertex: Vertex, destVertex: Vertex): List, Vertex>>? { + val NEG_INF = -1000000 + + val distance = MutableList(vertices.size) { Int.MAX_VALUE } + val predecessor = MutableList?>(vertices.size) { null } + + distance[srcVertex.id] = 0 + + for (i in 0..vertices.size - 1) { + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + getWeight(edge)) { + // distance will equal negative infinity if there is negative cycle + distance[v2.id] = maxOf(distance[v1.id] + getWeight(edge), NEG_INF) + + predecessor[v2.id] = v1 + } + } + } + + // check for negative cycles, determine if path to destVertex exists + for (i in 0..vertices.size - 1) { + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + getWeight(edge)) { + distance[v2.id] = NEG_INF + + } + } + } + + // there is a negative cycle on the way, so path doesn't exist + if (distance[destVertex.id] == NEG_INF) return null + + if (srcVertex == destVertex) return emptyList() + + // reconstruct the path from srcVertex to destVertex + val path: MutableList, Vertex>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val currentPredecessor = predecessor[currentVertex.id] + ?: return null // path doesn't exist + + path.add(getEdge(currentPredecessor, currentVertex) to currentVertex) + + currentVertex = currentPredecessor + } + + return path.reversed() + } } diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt index ae711032..51c01692 100644 --- a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt @@ -4,6 +4,7 @@ import model.abstractGraph.Edge import model.abstractGraph.Vertex import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* +import util.setupDirectedGraphWithCycle import util.setupWeightedDirected class WeightedDirectedGraphTest { @@ -304,4 +305,170 @@ class WeightedDirectedGraphTest { } } } + + @Nested + inner class findShortestPathFordBellmanTest { + @Nested + inner class `Path exists` { + @Test + fun `path between neighbours should consist of one edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val edge = graph.addEdge(v0, v1, 12345) + + val actualValue = graph.findShortestPathFordBellman(v0, v1) + val expectedValue = listOf(edge to v1) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `shortest path should be returned`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val v4 = defaultVertices[4] + + val actualValue = graph.findShortestPathFordBellman(v0, v4) + val expectedValue = listOf( + graph.getEdge(v0, v1) to v1, + graph.getEdge(v1, v2) to v2, + graph.getEdge(v2, v4) to v4 + ) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `path from vertex to itself should exist and be empty`() { + val v0 = graph.addVertex(69) + + val actualValue = graph.findShortestPathFordBellman(v0, v0) + val expectedValue = emptyList, Vertex>>() + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v3 = defaultVertices[3] + val v4 = defaultVertices[4] + + val expectedGraph = graphStructure + + graph.findShortestPathFordBellman(v3, v4) + + val actualGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Path doesn't exist` { + @Test + fun `there is simply no path between vertices`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v5 = defaultVertices[5] + + val actualValue = graph.findShortestPathFordBellman(v1, v5) + val expectedValue = null + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `order of arguments should matter`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v2 = defaultVertices[2] + + val actualValue = graph.findShortestPathFordBellman(v2, v0) + val expectedValue = null + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `there is a negative cycle on the path`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v8 = defaultVertices[8] + + val actualValue = graph.findShortestPathFordBellman(v0, v8) + val expectedValue = null + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `srcVertex is a part of negative cycle`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v6 = defaultVertices[6] + val v8 = defaultVertices[8] + + val actualValue = graph.findShortestPathFordBellman(v6, v8) + val expectedValue = null + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `vertex without outgoing edges shouldn't have any paths`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val v3 = defaultVertices[3] + val v4 = defaultVertices[4] + val v5 = defaultVertices[5] + val v6 = defaultVertices[6] + val v7 = defaultVertices[7] + val v8 = defaultVertices[8] + + assertEquals(null, graph.findShortestPathFordBellman(v8, v0)) + assertEquals(null, graph.findShortestPathFordBellman(v8, v1)) + assertEquals(null, graph.findShortestPathFordBellman(v8, v2)) + assertEquals(null, graph.findShortestPathFordBellman(v8, v3)) + assertEquals(null, graph.findShortestPathFordBellman(v8, v4)) + assertEquals(null, graph.findShortestPathFordBellman(v8, v5)) + assertEquals(null, graph.findShortestPathFordBellman(v8, v6)) + assertEquals(null, graph.findShortestPathFordBellman(v8, v7)) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v8 = defaultVertices[8] + + val expectedGraph = graphStructure + + graph.findShortestPathFordBellman(v8, v1) + + val actualGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + } } diff --git a/app/src/test/kotlin/util/setup.kt b/app/src/test/kotlin/util/setup.kt index 60578fe3..382cf962 100644 --- a/app/src/test/kotlin/util/setup.kt +++ b/app/src/test/kotlin/util/setup.kt @@ -66,5 +66,34 @@ fun setupWeightedUndirected(graph: WeightedUndirectedGraph): Pair): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4, v5, v6, v7, v8) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1, -1), + graph.addEdge(v1, v2, 4), + graph.addEdge(v1, v3, 7), + graph.addEdge(v2, v4, -2), + graph.addEdge(v3, v4, -3), + graph.addEdge(v0, v5, 3), + graph.addEdge(v5, v6, 0), + graph.addEdge(v6, v7, 10), + graph.addEdge(v7, v5, -2000), + graph.addEdge(v7, v8, 2) + ) + return defaultVerticesList to defaultEdgesSet } \ No newline at end of file From 16bee467b41a39eceb352fca5e75bd0e53d46e4e Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Thu, 23 May 2024 11:34:00 +0300 Subject: [PATCH 56/77] feat: implement algorithm for finding key vertices #39 * feat: add getWeightMap() method to every graph in an undirected graph, every edge weight is set to 1 * feat: implement algorithm for finding key vertices * feat: change return type of findKeyVertices() to set of top 20% central vertices * feat: add hasNegativeEdges() method to every graph * feat: discard exception in findKeyVertices(), return null instead --- .../kotlin/model/WeightedDirectedGraph.kt | 8 + .../kotlin/model/WeightedUndirectedGraph.kt | 8 + .../main/kotlin/model/abstractGraph/Graph.kt | 148 ++++++++++++++++++ 3 files changed, 164 insertions(+) diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index bbcff46a..50aa2d05 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -37,6 +37,14 @@ class WeightedDirectedGraph : DirectedGraph() { return weight } + override fun getWeightMap() = weightMap.toMap() + + override fun hasNegativeEdges(): Boolean { + val edgeWeighs = getWeightMap().values + + return edgeWeighs.any { it < 0 } + } + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>>? { val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 39aeffef..86916345 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -38,6 +38,14 @@ class WeightedUndirectedGraph : UndirectedGraph() { return weight } + override fun getWeightMap() = weightMap.toMap() + + override fun hasNegativeEdges(): Boolean { + val edgeWeighs = getWeightMap().values + + return edgeWeighs.any { it < 0 } + } + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>>? { val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/abstractGraph/Graph.kt index dbe7f63c..561c8368 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/abstractGraph/Graph.kt @@ -1,5 +1,11 @@ package model.abstractGraph +import java.util.* +import java.util.ArrayDeque +import kotlin.NoSuchElementException +import kotlin.collections.ArrayList +import kotlin.math.roundToInt + abstract class Graph { protected val vertices: ArrayList> = arrayListOf() protected val edges: MutableSet> = mutableSetOf() @@ -58,6 +64,16 @@ abstract class Graph { fun getVertices() = vertices.toList() + /* In undirected graph, returns a map with every edge as a key and 1 as a value + * In a directed graph, returns copy of weightMap property */ + open fun getWeightMap(): Map, Int> { + val weightMap = mutableMapOf, Int>() + + for (edge in edges) weightMap[edge] = 1 + + return weightMap + } + fun getNeighbours(vertex: Vertex): List> { val neighbours = adjacencyMap[vertex] ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the adjacency map.") @@ -73,4 +89,136 @@ abstract class Graph { } abstract fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge + + open fun hasNegativeEdges() = false + + /* For every vertex, calculates normalized closeness centrality, based on which the key vertices are picked. + * Formula was taken from "Efficient Top-k Closeness Centrality Search" by Paul W. Olsen et al., + * yet an easier algorithm for traversal was chosen. */ + fun findKeyVertices(): Set>? { + val graphSize = vertices.size + + val distanceMap = getWeightMap() + if (this.hasNegativeEdges()) return null + + val centralityMap = mutableMapOf, Double>() + + for (currVertex in vertices) { + val currSumOfDistances = calcSumOfDistancesFromVertex(currVertex, distanceMap, graphSize) + + val reachableNum = findReachableVerticesNumFromVertex(currVertex, graphSize) + + val currCentrality = calcCentralityOfVertex(currSumOfDistances, reachableNum, graphSize) + centralityMap[currVertex] = currCentrality + } + + val keyVertices = pickMostKeyVertices(centralityMap, graphSize) + + return keyVertices + } + + /* Uses modified Dijkstra's algorithm to calculate the sum of all weights (distances) + * of shortest paths from source vertex to every other reachable one */ + private fun calcSumOfDistancesFromVertex( + srcVertex: Vertex, + distanceMap: Map, Int>, + graphSize: Int + ): Int { + val POS_INF = 100_000_000 // to infinity and beyond + + val visited = Array(graphSize) { false } + + val distances = Array(graphSize) { POS_INF } + distances[srcVertex.id] = 0 + + // stores pairs of vertices and total distances to them, sorted by distances ascending + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }) + priorityQueue.add(srcVertex to distances[srcVertex.id]) + + while (priorityQueue.isNotEmpty()) { + val (currVertex, currDistance) = priorityQueue.poll() + + if (visited[currVertex.id]) continue + + visited[currVertex.id] = true + + val neighbours = getNeighbours(currVertex) + for (neighbour in neighbours) { + val edgeToNeighbour = getEdge(currVertex, neighbour) + + val edgeToNeighbourDistance = distanceMap[edgeToNeighbour] ?: POS_INF + + val totalDistanceToNeighbour = currDistance + edgeToNeighbourDistance + + if (totalDistanceToNeighbour < distances[neighbour.id]) { + distances[neighbour.id] = totalDistanceToNeighbour + priorityQueue.add(neighbour to totalDistanceToNeighbour) + } + } + } + + for (i in distances.indices) { + if (distances[i] == POS_INF) distances[i] = 0 + } + + val sum = distances.sum() + + return sum + } + + private fun findReachableVerticesNumFromVertex(vertex: Vertex, graphSize: Int): Int { + var reachableVerticesNum = 0 + + val verticesToVisit = ArrayDeque>() + verticesToVisit.add(vertex) + + val visited = Array(graphSize) { false } + + while (verticesToVisit.isNotEmpty()) { + val vertexToVisit = verticesToVisit.poll() + + if (visited[vertexToVisit.id]) continue + + for (neighbour in getNeighbours(vertexToVisit)) { + verticesToVisit.add(neighbour) + } + + visited[vertexToVisit.id] = true + verticesToVisit.add(vertexToVisit) + + reachableVerticesNum++ + } + + return reachableVerticesNum + } + + private fun calcCentralityOfVertex(sumOfDistances: Int, reachableNum: Int, graphSize: Int): Double { + if (sumOfDistances == 0) return 0.0 + + val centrality = + ((reachableNum - 1) * (reachableNum - 1)) / ((graphSize - 1) * sumOfDistances).toDouble() + + return centrality + } + + private fun pickMostKeyVertices(centralityMap: Map, Double>, graphSize: Int): Set> { + val keyVertices = mutableSetOf>() + + val percent = 0.2 + val keyVerticesNum = (graphSize * percent).roundToInt() // rounds up + + var currKeyVerticesNum = 0 + + val vertexCentralityPairs = centralityMap.toList() + val vertexCentralityPairsSorted = vertexCentralityPairs.sortedByDescending { it.second } + + for ((vertex, _) in vertexCentralityPairsSorted) { + if (currKeyVerticesNum >= keyVerticesNum) break + + keyVertices.add(vertex) + currKeyVerticesNum++ + } + + return keyVertices + } } From 76f69b5b01e2300f3f2f7edf9ddcba4c671d16b1 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Thu, 23 May 2024 17:49:35 +0300 Subject: [PATCH 57/77] feat: implement Johnson's find cycles algorithm and tests for it #40 * feat: implement Johnson find cycle algorithm + change accessing graph fields to calling getter methods * fix: accessing subGraph instead of the whole graph + add forgotten negation * test: fix wrong usage of custom annotation in FindCSSTest * test: add setup method for cycle search test * test: add tests for find cycles algorithm + fix found bugs * refactor: change Dijkstra return value --- app/src/main/kotlin/model/DirectedGraph.kt | 93 ++++++- .../kotlin/model/WeightedDirectedGraph.kt | 16 +- .../kotlin/model/WeightedUndirectedGraph.kt | 6 +- .../test/kotlin/model/DirectedGraphTest.kt | 260 ++++++++++++++---- .../test/kotlin/model/UndirectedGraphTest.kt | 58 ++-- .../kotlin/model/WeightedDirectedGraphTest.kt | 20 +- .../model/WeightedUndirectedGraphTest.kt | 20 +- .../kotlin/model/abstractGraph/GraphTest.kt | 31 +-- app/src/test/kotlin/util/setup.kt | 37 ++- 9 files changed, 399 insertions(+), 142 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index 545eb4ce..cbdc4a12 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -3,6 +3,9 @@ package model import model.abstractGraph.Edge import model.abstractGraph.Graph import model.abstractGraph.Vertex +import kotlin.NoSuchElementException +import kotlin.collections.ArrayDeque +import kotlin.collections.ArrayList open class DirectedGraph : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge { @@ -71,7 +74,7 @@ open class DirectedGraph : Graph() { stack.add(srcVertex) } - vertices.forEach { vertex -> + getVertices().forEach { vertex -> if (visited[vertex] != true) auxiliaryDFS(vertex, component) } @@ -102,11 +105,93 @@ open class DirectedGraph : Graph() { private fun reverseEdgesMap(): Map, MutableSet>> { val reversedEdgesMap = mutableMapOf, MutableSet>>() - vertices.forEach { reversedEdgesMap[it] = mutableSetOf() } - edges.forEach { edge -> + getVertices().forEach { reversedEdgesMap[it] = mutableSetOf() } + getEdges().forEach { edge -> reversedEdgesMap[edge.vertex2]?.add(edge.vertex1) } return reversedEdgesMap } -} + fun findCycles(srcVertex: Vertex): Set, Vertex>>> { + if (getOutgoingEdges(srcVertex).isEmpty()) return emptySet() + + val sccs = findSCC() + val vertexSCC = sccs.find { srcVertex in it } + ?: throw NoSuchElementException("Vertex (${srcVertex.id}, ${srcVertex.data}) isn't in any of the SCCs") + + if (vertexSCC.size == 1) return emptySet() + + // create SCC subgraph + val subGraph = this + for (edge in getEdges()) { + if (edge.vertex1 !in vertexSCC || edge.vertex2 !in vertexSCC) { + subGraph.removeEdge(edge) + } + } + + val blockedSet = mutableSetOf>() + val blockedMap = mutableMapOf, MutableSet>>() + val stack = ArrayDeque>() + val verticesCycles = mutableSetOf>>() + + fun DFSToFindCycles(currentVertex: Vertex): Boolean { + var cycleIsFound = false + + stack.addLast(currentVertex) + blockedSet.add(currentVertex) + + for (neighbour in subGraph.getNeighbours(currentVertex)) { + if (neighbour == srcVertex) { + // cycle is found + stack.addLast(srcVertex) + + val cycleOfVertices = mutableListOf>() + cycleOfVertices.addAll(stack) + verticesCycles.add(cycleOfVertices) + + stack.removeLast() + + cycleIsFound = true + } else if (neighbour !in blockedSet) { + cycleIsFound = DFSToFindCycles(neighbour) || cycleIsFound + } + } + + fun unblock(vertex: Vertex) { + blockedSet.remove(vertex) + blockedMap[vertex]?.forEach { unblock(it) } + blockedMap.remove(vertex) + } + + if (cycleIsFound) unblock(currentVertex) + else { + for (neighbour in subGraph.getNeighbours(currentVertex)) { + blockedMap[neighbour]?.add(currentVertex) + ?: blockedMap.put(neighbour, mutableSetOf(currentVertex)) + } + } + + stack.removeLast() + + return cycleIsFound + } + + DFSToFindCycles(srcVertex) + + val cycles = mutableSetOf, Vertex>>>() + for (verticesCycle in verticesCycles) { + val cycle = mutableListOf, Vertex>>() + + for (i in 0..verticesCycle.size - 2) { + val v1 = verticesCycle[i] + val v2 = verticesCycle[i + 1] + + cycle.add(getEdge(v1, v2) to v2) + } + + cycles.add(cycle) + } + + return cycles + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/WeightedDirectedGraph.kt index 50aa2d05..330b8d88 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedDirectedGraph.kt @@ -45,7 +45,7 @@ class WeightedDirectedGraph : DirectedGraph() { return edgeWeighs.any { it < 0 } } - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>>? { + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Vertex>>? { val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } @@ -73,14 +73,14 @@ class WeightedDirectedGraph : DirectedGraph() { } // Reconstruct the path from srcVertex to destVertex - val path: MutableList, Edge>> = mutableListOf() + val path: MutableList, Vertex>> = mutableListOf() var currentVertex = destVertex while (currentVertex != srcVertex) { val predecessor = predecessorMap[currentVertex] ?: return null // path doesn't exist val currentEdge = getEdge(predecessor, currentVertex) - path.add(Pair(currentVertex, currentEdge)) + path.add(currentEdge to currentVertex) currentVertex = predecessor } @@ -91,12 +91,12 @@ class WeightedDirectedGraph : DirectedGraph() { fun findShortestPathFordBellman(srcVertex: Vertex, destVertex: Vertex): List, Vertex>>? { val NEG_INF = -1000000 - val distance = MutableList(vertices.size) { Int.MAX_VALUE } - val predecessor = MutableList?>(vertices.size) { null } + val distance = MutableList(getVertices().size) { Int.MAX_VALUE } + val predecessor = MutableList?>(getVertices().size) { null } distance[srcVertex.id] = 0 - for (i in 0..vertices.size - 1) { + for (i in 0..getVertices().size - 1) { for (edge in edges) { val v1 = edge.vertex1 val v2 = edge.vertex2 @@ -111,8 +111,8 @@ class WeightedDirectedGraph : DirectedGraph() { } // check for negative cycles, determine if path to destVertex exists - for (i in 0..vertices.size - 1) { - for (edge in edges) { + for (i in 0..getVertices().size - 1) { + for (edge in getEdges()) { val v1 = edge.vertex1 val v2 = edge.vertex2 diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt index 86916345..73668f31 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/WeightedUndirectedGraph.kt @@ -46,7 +46,7 @@ class WeightedUndirectedGraph : UndirectedGraph() { return edgeWeighs.any { it < 0 } } - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Edge>>? { + fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Vertex>>? { val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } val predecessorMap = mutableMapOf, Vertex?>() val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } @@ -74,14 +74,14 @@ class WeightedUndirectedGraph : UndirectedGraph() { } // Reconstruct the path from srcVertex to destVertex - val path: MutableList, Edge>> = mutableListOf() + val path: MutableList, Vertex>> = mutableListOf() var currentVertex = destVertex while (currentVertex != srcVertex) { val predecessor = predecessorMap[currentVertex] ?: return null // path doesn't exist val currentEdge = getEdge(predecessor, currentVertex) - path.add(Pair(currentVertex, currentEdge)) + path.add(currentEdge to currentVertex) currentVertex = predecessor } diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt index 17ea2e93..073fb226 100644 --- a/app/src/test/kotlin/model/DirectedGraphTest.kt +++ b/app/src/test/kotlin/model/DirectedGraphTest.kt @@ -5,11 +5,11 @@ import model.abstractGraph.Vertex import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test import util.annotations.TestAllDirectedGraphs import util.emptyEdgesSet import util.emptyVerticesList -import util.setup +import util.setupAbstractGraph +import util.setupGraphForFindingCycles class DirectedGraphTest { @Nested @@ -18,7 +18,7 @@ class DirectedGraphTest { inner class `Edge is in the graph` { @TestAllDirectedGraphs fun `edge should be returned`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -34,7 +34,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `graph shouldn't change`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v2 = defaultVerticesList[2] @@ -53,7 +53,7 @@ class DirectedGraphTest { inner class `Edge isn't in the graph` { @TestAllDirectedGraphs fun `order of arguments should matter`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -79,7 +79,7 @@ class DirectedGraphTest { inner class `Vertex is in the graph` { @TestAllDirectedGraphs fun `neighbours should be returned`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -96,7 +96,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `graph shouldn't change`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -127,7 +127,7 @@ class DirectedGraphTest { inner class `Vertex is in the graph` { @TestAllDirectedGraphs fun `outgoing edges should be returned`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -144,7 +144,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `graph shouldn't change`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v4 = defaultVerticesList[4] @@ -177,7 +177,7 @@ class DirectedGraphTest { inner class `Vertices are different` { @TestAllDirectedGraphs fun `Added edge should be returned`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -191,7 +191,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `Edge should be added to graph`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val defaultEdgesSet = graphStructure.second @@ -208,7 +208,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `one vertex has to be added to the other's adjacency map value`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -227,7 +227,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `edge has to be added to first vertex's outgoing edges map value`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -246,7 +246,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `adding already existing edge shouldn't change anything`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -270,7 +270,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `second vertex's map values shouldn't change`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -293,7 +293,7 @@ class DirectedGraphTest { inner class `Vertices are the same` { @TestAllDirectedGraphs fun `exception should be thrown`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v2 = defaultVerticesList[2] @@ -309,7 +309,7 @@ class DirectedGraphTest { inner class `One of the vertices isn't in the graph` { @TestAllDirectedGraphs fun `first vertex isn't in the graph`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -321,7 +321,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `second vertex isn't in the graph`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -346,7 +346,7 @@ class DirectedGraphTest { inner class `Edge is in the graph` { @TestAllDirectedGraphs fun `removed edge should be returned`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -362,7 +362,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `edge should be removed from graph`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val defaultEdgesSet = graphStructure.second @@ -380,7 +380,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `second vertex should be removed from first's adjacency map value`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -397,7 +397,7 @@ class DirectedGraphTest { @TestAllDirectedGraphs fun `edge should be removed from first vertex's outgoing edges map value`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v2 = defaultVerticesList[2] @@ -417,7 +417,7 @@ class DirectedGraphTest { inner class `Edge isn't in the graph` { @TestAllDirectedGraphs fun `wrong order of the arguments should throw an exception`(graph: DirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v3 = defaultVerticesList[3] @@ -440,10 +440,9 @@ class DirectedGraphTest { @Nested inner class FindSCCTest { @Nested - inner class `SCC should return not empty array`() { + inner class `SCC should return not empty array` { @TestAllDirectedGraphs - fun `graph has two connected vertices`() { - val graph = DirectedGraph() + fun `graph has two connected vertices`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) @@ -457,8 +456,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `complex graph`() { - val graph = DirectedGraph() + fun `complex graph`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -476,8 +474,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `graph has multiple SCCs`() { - val graph = DirectedGraph() + fun `graph has multiple SCCs`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -497,8 +494,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `graph with nested cycles`() { - val graph = DirectedGraph() + fun `graph with nested cycles`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -521,8 +517,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `graph with cross connections`() { - val graph = DirectedGraph() + fun `graph with cross connections`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -545,8 +540,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `graph with disconnected subgraphs`() { - val graph = DirectedGraph() + fun `graph with disconnected subgraphs`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -568,8 +562,7 @@ class DirectedGraphTest { @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") @TestAllDirectedGraphs - fun `graph with single vertex cycle`() { - val graph = DirectedGraph() + fun `graph with single vertex cycle`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -587,10 +580,9 @@ class DirectedGraphTest { } @Nested - inner class `SCC should return single-element SCCs`() { + inner class `SCC should return single-element SCCs` { @TestAllDirectedGraphs - fun `graph has single vertex`() { - val graph = DirectedGraph() + fun `graph has single vertex`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val actualValue = graph.findSCC() @@ -600,8 +592,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `graph with multiple disconnected vertices`() { - val graph = DirectedGraph() + fun `graph with multiple disconnected vertices`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -613,11 +604,11 @@ class DirectedGraphTest { assertEquals(expectedValue, actualValue) } } + @Nested inner class `Additional edge cases`() { @TestAllDirectedGraphs - fun `empty graph`() { - val graph = DirectedGraph() + fun `empty graph`(graph: DirectedGraph) { val actualValue = graph.findSCC() val expectedValue = mutableSetOf>>() @@ -627,8 +618,7 @@ class DirectedGraphTest { @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") @TestAllDirectedGraphs - fun `graph with self-loops`() { - val graph = DirectedGraph() + fun `graph with self-loops`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) @@ -642,8 +632,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `linear graph`() { - val graph = DirectedGraph() + fun `linear graph`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -658,8 +647,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `graph with cycles and tail`() { - val graph = DirectedGraph() + fun `graph with cycles and tail`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -678,11 +666,11 @@ class DirectedGraphTest { assertEquals(expectedValue, actualValue) } } + @Nested - inner class `Side-effects check`() { + inner class `Side-effects check` { @TestAllDirectedGraphs - fun `check vertices in complex graph`() { - val graph = DirectedGraph() + fun `check vertices in complex graph`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -702,8 +690,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `check edges in complex graph`() { - val graph = DirectedGraph() + fun `check edges in complex graph`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -725,8 +712,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `check edges in graph with cycles and tail`() { - val graph = DirectedGraph() + fun `check edges in graph with cycles and tail`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -749,8 +735,7 @@ class DirectedGraphTest { } @TestAllDirectedGraphs - fun `check vertices graph with cycles and tail`() { - val graph = DirectedGraph() + fun `check vertices graph with cycles and tail`(graph: DirectedGraph) { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) @@ -773,4 +758,159 @@ class DirectedGraphTest { } } } + +// [[(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@1bab8268, model.abstractGraph.Vertex@44a59da3)], +// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6986852, model.abstractGraph.Vertex@42a15bdc), (model.abstractGraph.Edge@704deff2, model.abstractGraph.Vertex@44a59da3)], +// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6986852, model.abstractGraph.Vertex@42a15bdc), (model.abstractGraph.Edge@404bbcbd, model.abstractGraph.Vertex@27508c5d), (model.abstractGraph.Edge@658c5a19, model.abstractGraph.Vertex@44a59da3)], +// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6e01f9b0, model.abstractGraph.Vertex@6f6745d6), (model.abstractGraph.Edge@6c61a903, model.abstractGraph.Vertex@27508c5d), (model.abstractGraph.Edge@658c5a19, model.abstractGraph.Vertex@44a59da3)], +// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@a307a8c, model.abstractGraph.Vertex@4f704591), (model.abstractGraph.Edge@2b9ed6da, model.abstractGraph.Vertex@6f6745d6), (model.abstractGraph.Edge@6c61a903, model.abstractGraph.Vertex@27508c5d), (model.abstractGraph.Edge@658c5a19, model.abstractGraph.Vertex@44a59da3)]] + + +// [[(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6986852, model.abstractGraph.Vertex@42a15bdc), (model.abstractGraph.Edge@704deff2, model.abstractGraph.Vertex@44a59da3)], [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@1bab8268, model.abstractGraph.Vertex@44a59da3)]] + @Nested + inner class FindCyclesTest { + @Nested + inner class `There are some cycles` { + @TestAllDirectedGraphs + fun `all cycles should be returned`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val e01 = graph.addEdge(v0, v1) + val e07 = graph.addEdge(v0, v7) + val e04 = graph.addEdge(v0, v4) + val e18 = graph.addEdge(v1, v8) + val e12 = graph.addEdge(v1, v2) + val e20 = graph.addEdge(v2, v0) + val e21 = graph.addEdge(v2, v1) + val e25 = graph.addEdge(v2, v5) + val e23 = graph.addEdge(v2, v3) + val e53 = graph.addEdge(v5, v3) + val e34 = graph.addEdge(v3, v4) + val e41 = graph.addEdge(v4, v1) + val e78 = graph.addEdge(v7, v8) + val e87 = graph.addEdge(v8, v7) + + val expectedCycle1 = listOf( + e12 to v2, + e21 to v1 + ) + + val expectedCycle2 = listOf( + e12 to v2, + e20 to v0, + e01 to v1 + ) + + val expectedCycle3 = listOf( + e12 to v2, + e20 to v0, + e04 to v4, + e41 to v1 + ) + + val expectedCycle4 = listOf( + e12 to v2, + e23 to v3, + e34 to v4, + e41 to v1 + ) + + val expectedCycle5 = listOf( + e12 to v2, + e25 to v5, + e53 to v3, + e34 to v4, + e41 to v1 + ) + + val actualValue = graph.findCycles(v1) + val expectedValue = setOf( + expectedCycle1, + expectedCycle2, + expectedCycle3, + expectedCycle4, + expectedCycle5 + ) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `SCC of 2 vertices should have one cycle`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e12 = graph.addEdge(v1, v2) + val e21 = graph.addEdge(v2, v1) + val e23 = graph.addEdge(v2, v3) + + val actualValue = graph.findCycles(v1) + val expectedValue = setOf( + listOf( + e12 to v2, + e21 to v1 + ) + ) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `There are no cycles` { + @TestAllDirectedGraphs + fun `vertex without outgoing edges shouldn't have cycles`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01 = graph.addEdge(v0, v1) + + val actualValue = graph.findCycles(v1) + val expectedValue = emptySet, Vertex>>>() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `SCC of 1 vertex shouldn't have cycles`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01 = graph.addEdge(v0, v1) + + val actualValue = graph.findCycles(v0) + val expectedValue = emptySet, Vertex>>>() + + assertEquals(expectedValue, actualValue) + } + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01 = graph.addEdge(v0, v1) + val e10 = graph.addEdge(v1, v0) + + val expectedGraph = graph.getVertices() to graph.getEdges().toSet() + + graph.findCycles(v1) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + } + } } diff --git a/app/src/test/kotlin/model/UndirectedGraphTest.kt b/app/src/test/kotlin/model/UndirectedGraphTest.kt index c13b62a1..fe1d5938 100644 --- a/app/src/test/kotlin/model/UndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/UndirectedGraphTest.kt @@ -6,9 +6,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Nested import util.annotations.TestAllUndirectedGraphs -import util.emptyEdgesSet -import util.emptyVerticesList -import util.setup +import util.setupAbstractGraph class UndirectedGraphTest { @Nested @@ -17,7 +15,7 @@ class UndirectedGraphTest { inner class `Edge is in the graph` { @TestAllUndirectedGraphs fun `edge should be returned`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -25,15 +23,15 @@ class UndirectedGraphTest { val newEdge = graph.addEdge(v0, v2) - val actualValue = newEdge - val expectedValue = graph.getEdge(v0, v2) + val actualValue = graph.getEdge(v0, v2) + val expectedValue = newEdge assertEquals(expectedValue, actualValue) } @TestAllUndirectedGraphs fun `order of the arguments shouldn't matter`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -44,7 +42,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `graph shouldn't change`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v2 = defaultVerticesList[2] @@ -76,7 +74,7 @@ class UndirectedGraphTest { inner class `Vertex is in the graph` { @TestAllUndirectedGraphs fun `neighbours should be returned`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -93,7 +91,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `graph shouldn't change`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -124,7 +122,7 @@ class UndirectedGraphTest { inner class `Vertex is in the graph` { @TestAllUndirectedGraphs fun `outgoing edges should be returned`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -145,7 +143,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `graph shouldn't change`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v4 = defaultVerticesList[4] @@ -178,7 +176,7 @@ class UndirectedGraphTest { inner class `Vertices are different` { @TestAllUndirectedGraphs fun `Added edge should be returned`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -192,7 +190,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `Edge should be added to graph`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val defaultEdgesSet = graphStructure.second @@ -209,7 +207,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `vertices have to be added to each other's adjacency map values`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -232,7 +230,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `edge has to be added to both vertices' outgoing edges map values`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -261,7 +259,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `adding already existing edge shouldn't change graph`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -277,7 +275,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `adding already existing edge shouldn't change adjacency map`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -297,7 +295,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `adding already existing edge shouldn't change outgoing edges map`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -317,7 +315,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `adding edge with reversed arguments shouldn't change graph`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -333,7 +331,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `adding edge with reversed arguments shouldn't change adjacency map`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -353,7 +351,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `adding edge with reversed arguments shouldn't change outgoing edges map`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -376,7 +374,7 @@ class UndirectedGraphTest { inner class `Vertices are the same` { @TestAllUndirectedGraphs fun `exception should be thrown`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v2 = defaultVerticesList[2] @@ -392,7 +390,7 @@ class UndirectedGraphTest { inner class `One of the vertices isn't in the graph` { @TestAllUndirectedGraphs fun `first vertex isn't in the graph`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -404,7 +402,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `second vertex isn't in the graph`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -429,7 +427,7 @@ class UndirectedGraphTest { inner class `Edge is in the graph` { @TestAllUndirectedGraphs fun `removed edge should be returned`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -445,7 +443,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `order of the arguments shouldn't matter`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v1 = defaultVerticesList[1] @@ -461,7 +459,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `edge should be removed from graph`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val defaultEdgesSet = graphStructure.second @@ -479,7 +477,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `vertices should be removed from each other's adjacency map values`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] @@ -503,7 +501,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `edge should be removed from vertices' outgoing edges map values`(graph: UndirectedGraph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val v0 = defaultVerticesList[0] diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt index 51c01692..38b67a10 100644 --- a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt @@ -120,7 +120,7 @@ class WeightedDirectedGraphTest { val e5 = graph.addEdge(v3, v2, 20) val e6 = graph.addEdge(v3, v4, 60) - val expectedResult = listOf, Edge>>(Pair(v1, e0), Pair(v2, e3), Pair(v4, e4)) + val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) val actualResult = graph.findShortestPathDijkstra(v0, v4) assertEquals(expectedResult, actualResult) @@ -137,7 +137,7 @@ class WeightedDirectedGraphTest { val e1 = graph.addEdge(v1, v2, 5) val e2 = graph.addEdge(v0, v2, 10) - val expectedResult = listOf(Pair(v2, e2)) + val expectedResult = listOf(e2 to v2) val actualResult = graph.findShortestPathDijkstra(v0, v2) assertEquals(expectedResult, actualResult) } @@ -154,8 +154,8 @@ class WeightedDirectedGraphTest { val e2 = graph.addEdge(v1, v3, 1) val e3 = graph.addEdge(v2, v3, 1) - val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2)) - val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3)) + val expectedResult1 = listOf(e0 to v1, e2 to v3) + val expectedResult2 = listOf(e1 to v2, e3 to v3) val actualResult = graph.findShortestPathDijkstra(v0, v3) @@ -169,7 +169,7 @@ class WeightedDirectedGraphTest { val e0 = graph.addEdge(v0, v1, 5) - val expectedResult = listOf(Pair(v1, e0)) + val expectedResult = listOf(e0 to v1) val actualResult = graph.findShortestPathDijkstra(v0, v1) assertEquals(expectedResult, actualResult) @@ -187,7 +187,7 @@ class WeightedDirectedGraphTest { val e1 = graph.addEdge(v1, v2, -2) val e2 = graph.addEdge(v2, v3, -3) - val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1), Pair(v3, e2)) + val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) val actualResult = graph.findShortestPathDijkstra(v0, v3) assertEquals(expectedResult, actualResult) @@ -207,8 +207,8 @@ class WeightedDirectedGraphTest { val e3 = graph.addEdge(v2, v3, 1) val e4 = graph.addEdge(v3, v4, 1) - val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2), Pair(v4, e4)) - val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3), Pair(v4, e4)) + val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) + val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) val actualResult = graph.findShortestPathDijkstra(v0, v4) Assertions.assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) @@ -226,7 +226,7 @@ class WeightedDirectedGraphTest { val e2 = graph.addEdge(v2, v0, 3) val actualResult = graph.findShortestPathDijkstra(v0, v2) - val expectedResult = listOf(Pair(v2, e2)) + val expectedResult = listOf(e2 to v2) assertEquals(expectedResult, actualResult) } @@ -241,7 +241,7 @@ class WeightedDirectedGraphTest { val e0 = graph.addEdge(v0, v1, 0) val e1 = graph.addEdge(v1, v2, 0) - val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1)) + val expectedResult = listOf(e0 to v1, e1 to v2) val actualResult = graph.findShortestPathDijkstra(v0, v2) assertEquals(expectedResult, actualResult) } diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt index 5c037195..546b0299 100644 --- a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt @@ -122,7 +122,7 @@ class WeightedUndirectedGraphTest { val e5 = graph.addEdge(v3, v2, 20) val e6 = graph.addEdge(v3, v4, 60) - val expectedResult = listOf, Edge>>(Pair(v1, e0), Pair(v2, e3), Pair(v4, e4)) + val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) val actualResult = graph.findShortestPathDijkstra(v0, v4) assertEquals(expectedResult, actualResult) @@ -140,8 +140,8 @@ class WeightedUndirectedGraphTest { val e2 = graph.addEdge(v1, v3, 1) val e3 = graph.addEdge(v2, v3, 1) - val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2)) - val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3)) + val expectedResult1 = listOf(e0 to v1, e2 to v3) + val expectedResult2 = listOf(e1 to v2, e3 to v3) val actualResult = graph.findShortestPathDijkstra(v0, v3) @@ -155,7 +155,7 @@ class WeightedUndirectedGraphTest { val e0 = graph.addEdge(v0, v1, 5) - val expectedResult = listOf(Pair(v1, e0)) + val expectedResult = listOf(e0 to v1) val actualResult = graph.findShortestPathDijkstra(v0, v1) assertEquals(expectedResult, actualResult) @@ -173,7 +173,7 @@ class WeightedUndirectedGraphTest { val e1 = graph.addEdge(v1, v2, -2) val e2 = graph.addEdge(v2, v3, -3) - val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1), Pair(v3, e2)) + val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) val actualResult = graph.findShortestPathDijkstra(v0, v3) assertEquals(expectedResult, actualResult) @@ -193,8 +193,8 @@ class WeightedUndirectedGraphTest { val e3 = graph.addEdge(v2, v3, 1) val e4 = graph.addEdge(v3, v4, 1) - val expectedResult1 = listOf(Pair(v1, e0), Pair(v3, e2), Pair(v4, e4)) - val expectedResult2 = listOf(Pair(v2, e1), Pair(v3, e3), Pair(v4, e4)) + val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) + val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) val actualResult = graph.findShortestPathDijkstra(v0, v4) assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) @@ -212,7 +212,7 @@ class WeightedUndirectedGraphTest { val e2 = graph.addEdge(v2, v0, 3) val actualResult = graph.findShortestPathDijkstra(v0, v2) - val expectedResult = listOf(Pair(v2, e2)) + val expectedResult = listOf(e2 to v2) assertEquals(expectedResult, actualResult) } @@ -227,7 +227,7 @@ class WeightedUndirectedGraphTest { val e0 = graph.addEdge(v0, v1, 0) val e1 = graph.addEdge(v1, v2, 0) - val expectedResult = listOf(Pair(v1, e1), Pair(v0, e0)) + val expectedResult = listOf(e1 to v1, e0 to v0) val actualResult = graph.findShortestPathDijkstra(v2, v0) assertEquals(expectedResult, actualResult) @@ -243,7 +243,7 @@ class WeightedUndirectedGraphTest { val e0 = graph.addEdge(v0, v1, 0) val e1 = graph.addEdge(v1, v2, 0) - val expectedResult = listOf(Pair(v1, e0), Pair(v2, e1)) + val expectedResult = listOf(e0 to v1, e1 to v2) val actualResult = graph.findShortestPathDijkstra(v0, v2) assertEquals(expectedResult, actualResult) diff --git a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt index 83810f21..01fd66c7 100644 --- a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt +++ b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt @@ -3,8 +3,7 @@ package model.abstractGraph import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import util.annotations.TestAllGraphTypes -import util.setup -import util.emptyVerticesList +import util.setupAbstractGraph import util.emptyEdgesSet import util.emptyGraph @@ -15,7 +14,7 @@ class GraphTest { inner class `Graph is not empty` { @TestAllGraphTypes fun `non-empty list of vertices should be returned`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val actualList = graph.getVertices() @@ -26,7 +25,7 @@ class GraphTest { @TestAllGraphTypes fun `graph should not change`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) graph.getVertices() @@ -65,7 +64,7 @@ class GraphTest { inner class `Graph is not empty` { @TestAllGraphTypes fun `non-empty list of edges should be returned`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultEdgesSet = graphStructure.second val actualSet = graph.getEdges().toSet() @@ -76,7 +75,7 @@ class GraphTest { @TestAllGraphTypes fun `graph should not change`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) graph.getEdges() @@ -120,7 +119,7 @@ class GraphTest { @TestAllGraphTypes fun `vertex should be added to graph`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val defaultEdgesSet = graphStructure.second @@ -139,7 +138,7 @@ class GraphTest { inner class `Vertex is in the graph` { @TestAllGraphTypes fun `removed vertex should be returned`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val returnedVertex = graph.removeVertex(defaultVerticesList[2]) @@ -149,7 +148,7 @@ class GraphTest { @TestAllGraphTypes fun `vertex added after removal should have right id`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first graph.removeVertex(defaultVerticesList[3]) @@ -162,7 +161,7 @@ class GraphTest { inner class `Vertex is last` { @TestAllGraphTypes fun `vertex should be removed from vertices list`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val removedVertex = graph.removeVertex(defaultVerticesList[4]) @@ -175,7 +174,7 @@ class GraphTest { @TestAllGraphTypes fun `incident edges should be removed`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val defaultEdgesSet = graphStructure.second @@ -201,7 +200,7 @@ class GraphTest { inner class `Vertex isn't last` { @TestAllGraphTypes fun `last added vertex should be moved to removed vertex's place`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val oldV0 = defaultVerticesList[0] @@ -229,7 +228,7 @@ class GraphTest { @TestAllGraphTypes fun `last added vertex's incident edges should change`(graph: Graph) { - val graphStructure = setup(graph) + val graphStructure = setupAbstractGraph(graph) val defaultVerticesList = graphStructure.first val oldV0 = defaultVerticesList[0] @@ -271,7 +270,7 @@ class GraphTest { @TestAllGraphTypes fun `removing non-existing vertex from a non-empty graph should cause exception`(graph: Graph) { - setup(graph) + setupAbstractGraph(graph) assertThrows(NoSuchElementException::class.java) { graph.removeVertex(Vertex(1904,-360)) @@ -280,7 +279,7 @@ class GraphTest { @TestAllGraphTypes fun `removing vertex with wrong id should cause exception`(graph: Graph) { - setup(graph) + setupAbstractGraph(graph) assertThrows(NoSuchElementException::class.java) { graph.removeVertex(Vertex(6,3)) @@ -289,7 +288,7 @@ class GraphTest { @TestAllGraphTypes fun `removing vertex with wrong data should cause exception`(graph: Graph) { - setup(graph) + setupAbstractGraph(graph) assertThrows(NoSuchElementException::class.java) { graph.removeVertex(Vertex(0,35)) diff --git a/app/src/test/kotlin/util/setup.kt b/app/src/test/kotlin/util/setup.kt index 382cf962..967cc8d6 100644 --- a/app/src/test/kotlin/util/setup.kt +++ b/app/src/test/kotlin/util/setup.kt @@ -1,12 +1,13 @@ package util +import model.DirectedGraph import model.abstractGraph.Edge import model.abstractGraph.Graph import model.abstractGraph.Vertex import model.WeightedUndirectedGraph import model.WeightedDirectedGraph -fun setup(graph: Graph): Pair>, Set>> { +fun setupAbstractGraph(graph: Graph): Pair>, Set>> { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) @@ -95,5 +96,39 @@ fun setupDirectedGraphWithCycle(graph: WeightedDirectedGraph): Pair): Pair>, Set>> { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val defaultVerticesList = listOf(v0, v1, v2, v3, v4, v5, v6, v7, v8) + + val defaultEdgesSet = setOf( + graph.addEdge(v0, v1), + graph.addEdge(v0, v7), + graph.addEdge(v0, v4), + graph.addEdge(v1, v8), + graph.addEdge(v1, v6), + graph.addEdge(v1, v2), + graph.addEdge(v2, v0), + graph.addEdge(v2, v1), + graph.addEdge(v2, v5), + graph.addEdge(v2, v3), + graph.addEdge(v5, v3), + graph.addEdge(v3, v4), + graph.addEdge(v4, v1), + graph.addEdge(v7, v8), + graph.addEdge(v8, v7) + ) + return defaultVerticesList to defaultEdgesSet } \ No newline at end of file From 9b7c2f7843822c014b023e5672027189407095b5 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Fri, 24 May 2024 10:50:29 +0300 Subject: [PATCH 58/77] feat: enforce graph visualisation, add T-FDP layout #42 * feat: add class for graph creation * fix: upload draft implementations of new methods * feat: implement 'add edges' and 'vertices' * feat: throw 'graphType' parameter for FAQ * feat: call representation after 'add' operations * fix: add border dependency on 'scaleFactor' * feat: create rough force-directed layout * feat: add vertex ID display * feat: modify some buttons + apply layout after changes * feat: write draft t-FDP layout algo * refactor: formatter + some small syntax changes * fix: resolve conflicts after rebase * fix: rename 'showId' variable, resolve issue with edges directions * fix: change edge layer * fix: remove debug lines * fix: correct connection between UI and graphVM * fix: add 'updateRequired' for graph update handle * feat: handle 'updateRequired' + implement 'add Edge' * refactor: apply plugin, add glitter * refactor: move tab handler to a separate file * feat: add zoom control buttons * refactor: clean up the MainScreen file * feat: handle resize after zoom * feat: add id display and zoom handle * refactor: remove unnecessary scale factors * feat: improve scale handling, add borders for scale rate * refactor: add new lines at the end, remove init * refactor: remove commented lines --- app/src/main/kotlin/Main.kt | 2 +- app/src/main/kotlin/view/MainScreen.kt | 136 ++---------------- app/src/main/kotlin/view/graph/EdgeView.kt | 18 ++- app/src/main/kotlin/view/graph/GraphView.kt | 60 +++++++- app/src/main/kotlin/view/graph/VertexView.kt | 72 ++++++---- .../kotlin/view/tabScreen/FileControlTab.kt | 2 +- .../main/kotlin/view/tabScreen/GeneralTab.kt | 42 ++++-- .../kotlin/view/tabScreen/SelectTabRow.kt | 5 +- .../main/kotlin/view/tabScreen/TabHandler.kt | 70 +++++++++ .../{ => utils}/CreateGraphDialogWindow.kt | 12 +- app/src/main/kotlin/view/utils/FAQBox.kt | 69 +++++++++ .../view/utils/ImportGraphDialogWindow.kt | 2 + .../{ => utils}/SelectInitDialogWindow.kt | 12 +- app/src/main/kotlin/view/utils/ZoomBox.kt | 37 +++++ .../kotlin/viewmodel/MainScreenViewModel.kt | 17 ++- .../viewmodel/graph/CreateGraphViewModel.kt | 1 + .../kotlin/viewmodel/graph/EdgeViewModel.kt | 39 ++--- .../kotlin/viewmodel/graph/GraphViewModel.kt | 73 +++++----- .../main/kotlin/viewmodel/graph/TFDPLayout.kt | 62 ++++++++ .../kotlin/viewmodel/graph/VertexViewModel.kt | 13 +- 20 files changed, 489 insertions(+), 255 deletions(-) create mode 100644 app/src/main/kotlin/view/tabScreen/TabHandler.kt rename app/src/main/kotlin/view/{ => utils}/CreateGraphDialogWindow.kt (97%) create mode 100644 app/src/main/kotlin/view/utils/FAQBox.kt create mode 100644 app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt rename app/src/main/kotlin/view/{ => utils}/SelectInitDialogWindow.kt (89%) create mode 100644 app/src/main/kotlin/view/utils/ZoomBox.kt create mode 100644 app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt diff --git a/app/src/main/kotlin/Main.kt b/app/src/main/kotlin/Main.kt index e7c4d7ff..14759776 100644 --- a/app/src/main/kotlin/Main.kt +++ b/app/src/main/kotlin/Main.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import java.awt.Dimension -import view.SelectInitDialogWindow +import view.utils.SelectInitDialogWindow @Composable @Preview diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt index 2f232fe5..d02db404 100644 --- a/app/src/main/kotlin/view/MainScreen.kt +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -1,142 +1,34 @@ package view import MyAppTheme -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.material.TabRowDefaults.tabIndicatorOffset -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp import view.graph.GraphView -import view.tabScreen.AnalyzeTab -import view.tabScreen.FileControlTab -import view.tabScreen.GeneralTab -import view.tabScreen.SelectTabRow +import view.tabScreen.TabHandler +import view.utils.FAQBox +import view.utils.ZoomBox import viewmodel.MainScreenViewModel - -@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Composable fun MainScreen(viewmodel: MainScreenViewModel) { MyAppTheme { // state for hover effect - var isHovered by remember { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } + val scale = remember { mutableStateOf(1f) } Row { - // Column with tabs and content - Column( - modifier = - Modifier.width(360.dp) - .background(color = MaterialTheme.colors.surface) - .fillMaxHeight() - .clip(shape = RoundedCornerShape(10.dp)) - ) { - // Tab row - val pageState = rememberPagerState(pageCount = { 3 }) - val coroutineScope = rememberCoroutineScope() - val tabs = listOf("General", "Analyze", "Save & Load") - - TabRow( - selectedTabIndex = pageState.currentPage, - contentColor = MaterialTheme.colors.surface, - backgroundColor = MaterialTheme.colors.secondary, - divider = {}, // to remove divider between - indicator = { tabPositions -> - TabRowDefaults.Indicator( - modifier = - Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), - height = 0.dp - ) - }, - modifier = Modifier.height(50.dp) - ) { - tabs.forEachIndexed { index, title -> - SelectTabRow(pageState, index, coroutineScope, title) - } - } - - // Content corresponding to the selected tab - HorizontalPager(state = pageState, userScrollEnabled = true) { - Column( - modifier = - Modifier.width(360.dp) - .background(MaterialTheme.colors.background) - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - ) { - when (pageState.currentPage) { - 0 -> GeneralTab(viewmodel.graphViewModel) - 1 -> AnalyzeTab(viewmodel.graphViewModel) - 2 -> FileControlTab(viewmodel.graphViewModel) - } - } - } - } - Surface(modifier = Modifier.fillMaxSize()) { GraphView(viewmodel.graphViewModel) } + TabHandler(viewmodel) + Surface(modifier = Modifier.fillMaxSize()) { GraphView(viewmodel.graphViewModel, scale) } } // Hoverable box over the existing Surface - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.TopEnd - ) { + FAQBox(interactionSource, viewmodel) - Surface( - modifier = Modifier - .padding(top = 20.dp) - .width(220.dp).height(50.dp) - .background(if (isHovered) Color.LightGray else Color.Gray) - .hoverable(interactionSource = interactionSource) - ) { - if (isHovered) { - Box( - modifier = Modifier.width(200.dp), - contentAlignment = Alignment.CenterEnd - ) { - Text( - text = viewmodel.graphViewModel.graphType.value.replace(" ", "\nData type: "), - color = Color.Black - ) - } - } else { - Image( - painterResource("drawable/question.png"), - contentDescription = "Question Mark Icon", - modifier = Modifier.size(10.dp).padding(start = 50.dp) - ) - } - } - - // Observe the interaction source to change the hover state - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { interaction -> - when (interaction) { - is HoverInteraction.Enter -> { - isHovered = true - } - - is HoverInteraction.Exit -> { - isHovered = false - } - } - } - } - } + ZoomBox(scale) } } diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt index 3286eeb7..1a76cb7e 100644 --- a/app/src/main/kotlin/view/graph/EdgeView.kt +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -8,28 +8,34 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.zIndex import viewmodel.WindowViewModel import viewmodel.graph.EdgeViewModel +import kotlin.math.max +import kotlin.math.min @Composable -fun EdgeView(viewModel: EdgeViewModel) { +fun EdgeView(viewModel: EdgeViewModel, scale: Float) { val windowVM = WindowViewModel() windowVM.SetCurrentDimensions() - val firstVertexCenter = viewModel.calculateFirstVertexCenter() - val secondVertexCenter = viewModel.calculateSecondVertexCenter() + val maxStrokeWidth = 12f// TODO: move to shared const file + val minStrokeWidth = 4f + + val firstVertexCenter = viewModel.calculateFirstVertexCenter(scale) + val secondVertexCenter = viewModel.calculateSecondVertexCenter(scale) val firstVertexCenterX = firstVertexCenter.first val firstVertexCenterY = firstVertexCenter.second val secondVertexCenterX = secondVertexCenter.first val secondVertexCenterY = secondVertexCenter.second - val arrowPoints = viewModel.calculateArrowPoints() + val arrowPoints = viewModel.calculateArrowPoints(scale) - Canvas(modifier = Modifier.fillMaxSize()) { + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( color = Color.LightGray, - strokeWidth = 5f, + strokeWidth = (5f * scale).coerceIn(minStrokeWidth, maxStrokeWidth), start = Offset( firstVertexCenterX.toPx(), diff --git a/app/src/main/kotlin/view/graph/GraphView.kt b/app/src/main/kotlin/view/graph/GraphView.kt index a92d81a3..3ff48f86 100644 --- a/app/src/main/kotlin/view/graph/GraphView.kt +++ b/app/src/main/kotlin/view/graph/GraphView.kt @@ -1,17 +1,63 @@ package view.graph import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.gestures.panBy +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.zoomBy +import androidx.compose.foundation.layout.* +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import viewmodel.graph.GraphViewModel @Composable -fun GraphView(viewModel: GraphViewModel) { - Box(modifier = Modifier.fillMaxSize().background(Color.White)) { - viewModel.verticesVM.forEach { v -> VertexView(v) } - viewModel.edgesVM.forEach { e -> EdgeView(e) } +fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState) { + val offset = remember { mutableStateOf(Offset.Zero) } + val coroutineScope = rememberCoroutineScope { Dispatchers.Default } + + val updateRequired = remember { derivedStateOf { viewModel.updateIsRequired } } + // val density = LocalDensity.current.density + + val transformationState = rememberTransformableState { zoomChange, offsetChange, _ -> + currentScaleState.value *= zoomChange + offset.value += offsetChange + } + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .pointerInput(Unit) { + detectTransformGestures { _, pan, _, _ -> + coroutineScope.launch { + // update the transformation state using the gesture values + transformationState.panBy(pan) + } + } + } + .graphicsLayer( + scaleX = currentScaleState.value, + scaleY = currentScaleState.value, + translationX = offset.value.x, + translationY = offset.value.y + ) + ) { + if (updateRequired.value.value) { + viewModel.applyForceDirectedLayout(740.0, 650.0) + } + viewModel.verticesVM.forEach { v -> VertexView(v, currentScaleState.value) } + viewModel.edgesVM.forEach { e -> EdgeView(e, currentScaleState.value) } + + viewModel.updateIsRequired.value = false } } diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt index 7e96c511..432300e3 100644 --- a/app/src/main/kotlin/view/graph/VertexView.kt +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -7,55 +7,75 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import androidx.compose.ui.zIndex +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import viewmodel.WindowViewModel import viewmodel.graph.VertexViewModel +import kotlin.math.min +import kotlin.ranges.coerceIn @Composable -fun VertexView(viewModel: VertexViewModel) { - val coroutineScope = rememberCoroutineScope() +fun VertexView(viewModel: VertexViewModel, scale: Float) { + val coroutineScope = rememberCoroutineScope { Dispatchers.Default } val windowVM = WindowViewModel() windowVM.SetCurrentDimensions() + val density = LocalDensity.current.density + + val maxRadius = 35.dp // TODO: move to shared const file + val minRadius = 7.dp + + val adjustedX = (viewModel.x.value) + val adjustedY = (viewModel.y.value) + val adjustedRadius = (viewModel.radius * scale).coerceIn(minRadius, maxRadius) Box( - modifier = - Modifier.offset { - IntOffset(viewModel.x.value.roundToPx(), viewModel.y.value.roundToPx()) - } - .size(viewModel.radius * 2) - .background( - if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, - shape = CircleShape - ) - .clip(CircleShape) - .pointerInput(Unit) { - coroutineScope.launch { - detectDragGestures { change, dragAmount -> - viewModel.onDrag( - DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()), - windowVM - ) - change.consume() - } - detectTapGestures( - onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } + modifier = Modifier + .offset { IntOffset(adjustedX.roundToPx(), adjustedY.roundToPx()) } + .size(adjustedRadius * 2) + .background( + if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, + shape = CircleShape + ) + .clip(CircleShape) + .pointerInput(Unit) { + coroutineScope.launch { + detectDragGestures { change, dragAmount -> + viewModel.onDrag( + DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()), + windowVM, density, scale ) + change.consume() } - }, + } + detectTapGestures( + onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } + ) + }, contentAlignment = Alignment.Center ) { if (viewModel.dataVisible.value) { Text(modifier = Modifier.align(Alignment.Center), text = viewModel.getVertexData) } + if (viewModel.idVisible.value) { + Text( + modifier = Modifier.align(Alignment.Center).zIndex(3f), + text = viewModel.getVertexID.toString(), + color = Color.Black, + style = MaterialTheme.typography.body1.copy(fontSize = 20.sp) + ) + } } } diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index 6d371d27..4cd181cc 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -8,6 +8,6 @@ import androidx.compose.ui.Modifier import viewmodel.graph.GraphViewModel @Composable -fun FileControlTab(graphVM: GraphViewModel) { +fun FileControlTab(graphVM: GraphViewModel? = null) { Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 3rd tab") } } diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index caa5be71..af6b4f16 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -18,13 +18,12 @@ import viewmodel.graph.GraphViewModel @Composable fun GeneralTab(graphVM: GraphViewModel) { - var showDialog by remember { mutableStateOf(false) } + var showVertexAddDialog by remember { mutableStateOf(false) } var vertexData by remember { mutableStateOf("") } var errorMessage by remember { mutableStateOf("") } var connectVertexId by remember { mutableStateOf("") } var firstVertexId by remember { mutableStateOf("") } var secondVertexId by remember { mutableStateOf("") } - var displayId by remember { mutableStateOf(false) } var secondVertexData by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { @@ -56,7 +55,7 @@ fun GeneralTab(graphVM: GraphViewModel) { Column(modifier = Modifier.width(120.dp).fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize().height(70.dp), - onClick = { if (vertexData.isNotEmpty()) showDialog = true }, + onClick = { if (vertexData.isNotEmpty()) showVertexAddDialog = true }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { Text("add") @@ -110,8 +109,20 @@ fun GeneralTab(graphVM: GraphViewModel) { Column(modifier = Modifier.width(110.dp).fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize().height(70.dp), - onClick = {}, - // TODO add edge + onClick = { + if (graphVM.graph.getVertices() + .any { it.id == firstVertexId.toInt() } && graphVM.graph.getVertices() + .any { it.id == secondVertexId.toInt() } + ) { + graphVM.addEdge(firstVertexId.toInt(), secondVertexId.toInt()) + + graphVM.updateIsRequired.value = true + secondVertexId = "" + firstVertexId = "" + } else { + // TODO: show error window + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { Text("add") @@ -121,14 +132,13 @@ fun GeneralTab(graphVM: GraphViewModel) { Row( modifier = Modifier.fillMaxWidth().padding(5.dp).clickable { - displayId = !displayId + graphVM.showVerticesID.value = !graphVM.showVerticesID.value }, verticalAlignment = Alignment.CenterVertically ) { Checkbox( - checked = displayId, - onCheckedChange = { displayId = it }, - // TODO display ids + checked = graphVM.showVerticesID.value, + onCheckedChange = { graphVM.showVerticesID.value = it }, colors = CheckboxDefaults.colors( checkedColor = MaterialTheme.colors.primary, @@ -136,14 +146,14 @@ fun GeneralTab(graphVM: GraphViewModel) { ) ) Text( - text = "Checkbox Text", + text = "Show ID", modifier = Modifier.padding(start = 10.dp, bottom = 3.dp).align(Alignment.CenterVertically) ) } } - if (showDialog) { + if (showVertexAddDialog) { Dialog(onDismissRequest = {}) { vertexData = "" @@ -181,9 +191,11 @@ fun GeneralTab(graphVM: GraphViewModel) { val secondId = graphVM.addVertex(secondVertexData) graphVM.addEdge(firstId, secondId) - showDialog = false + graphVM.updateIsRequired.value = true + + showVertexAddDialog = false errorMessage = "" - connectVertexId = "" + secondVertexData = "" } } @@ -228,7 +240,9 @@ fun GeneralTab(graphVM: GraphViewModel) { val firstId = graphVM.addVertex(vertexData) graphVM.addEdge(firstId, connectVertexId.toInt()) - showDialog = false + graphVM.updateIsRequired.value = true + + showVertexAddDialog = false errorMessage = "" connectVertexId = "" } diff --git a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt index 6a7693ad..82983543 100644 --- a/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt +++ b/app/src/main/kotlin/view/tabScreen/SelectTabRow.kt @@ -11,12 +11,14 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Tab import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -24,9 +26,10 @@ import kotlinx.coroutines.launch fun SelectTabRow( currentPageState: PagerState, index: Int, - coroutineScope: CoroutineScope, title: String ) { + val coroutineScope = rememberCoroutineScope { Dispatchers.Default } + Tab( selected = currentPageState.currentPage == index, onClick = { coroutineScope.launch { currentPageState.animateScrollToPage(index) } }, diff --git a/app/src/main/kotlin/view/tabScreen/TabHandler.kt b/app/src/main/kotlin/view/tabScreen/TabHandler.kt new file mode 100644 index 00000000..be4607ed --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/TabHandler.kt @@ -0,0 +1,70 @@ +package view.tabScreen + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import viewmodel.MainScreenViewModel + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TabHandler(viewmodel: MainScreenViewModel) { + Column( + modifier = + Modifier.width(360.dp) + .background(color = MaterialTheme.colors.surface) + .fillMaxHeight() + .clip(shape = RoundedCornerShape(10.dp)) + ) { + val pageState = rememberPagerState(initialPage = 0, pageCount = { 3 }) + val tabs = listOf("General", "Analyze", "Save & Load") + + TabRow( + selectedTabIndex = pageState.currentPage, + contentColor = MaterialTheme.colors.surface, + backgroundColor = MaterialTheme.colors.secondary, + divider = {}, // to remove divider between + indicator = { tabPositions -> + TabRowDefaults.Indicator( + modifier = + Modifier.tabIndicatorOffset(tabPositions[pageState.currentPage]), + height = 0.dp + ) + }, + modifier = Modifier.height(50.dp) + ) { + tabs.forEachIndexed { index, title -> + SelectTabRow(pageState, index, title) + } + } + + // Content corresponding to the selected tab + HorizontalPager(state = pageState, userScrollEnabled = true) { + Column( + modifier = + Modifier.width(360.dp) + .background(MaterialTheme.colors.background) + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + when (pageState.currentPage) { + 0 -> GeneralTab(viewmodel.graphViewModel) + 1 -> AnalyzeTab(viewmodel.graphViewModel) + 2 -> FileControlTab(viewmodel.graphViewModel) + } + } + } + } +} diff --git a/app/src/main/kotlin/view/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt similarity index 97% rename from app/src/main/kotlin/view/CreateGraphDialogWindow.kt rename to app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt index b7450cd5..287cbec9 100644 --- a/app/src/main/kotlin/view/CreateGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt @@ -1,4 +1,4 @@ -package view +package view.utils import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -32,7 +32,7 @@ fun CreateGraphDialogWindow(viewModel: CreateGraphViewModel) { ) { Column( modifier = - Modifier.background(Color.White).padding(16.dp).width(700.dp).height(390.dp) + Modifier.background(Color.White).padding(16.dp).width(700.dp).height(290.dp) ) { Text( "Create Graph", @@ -40,13 +40,13 @@ fun CreateGraphDialogWindow(viewModel: CreateGraphViewModel) { fontSize = 20.sp, modifier = Modifier.padding(bottom = 10.dp) ) - Row(modifier = Modifier.fillMaxWidth().height(300.dp)) { + Row(modifier = Modifier.fillMaxWidth().height(200.dp)) { Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { Text("Select stored data:") Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - val radioOptions = listOf("Integer", "UInteger", "String", "Boolean") + val radioOptions = listOf("Integer", "UInteger", "String") Column(modifier = Modifier.width(220.dp)) { radioOptions.forEachIndexed { index, option -> @@ -198,8 +198,8 @@ fun onCreateGraphClicked( } val graphStructure = when (orientationIndex) { - 0 -> CreateGraphViewModel.GraphStructure.Directed - 1 -> CreateGraphViewModel.GraphStructure.Undirected + 0 -> CreateGraphViewModel.GraphStructure.Undirected + 1 -> CreateGraphViewModel.GraphStructure.Directed else -> CreateGraphViewModel.GraphStructure.Directed // default to directed } diff --git a/app/src/main/kotlin/view/utils/FAQBox.kt b/app/src/main/kotlin/view/utils/FAQBox.kt new file mode 100644 index 00000000..598184b6 --- /dev/null +++ b/app/src/main/kotlin/view/utils/FAQBox.kt @@ -0,0 +1,69 @@ +package view.utils + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import viewmodel.MainScreenViewModel + +@Composable +fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScreenViewModel) { + var isHovered by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.TopEnd + ) { + + Surface( + modifier = Modifier + .padding(top = 20.dp) + .width(220.dp).height(50.dp) + .background(if (isHovered) Color.LightGray else Color.Gray) + .hoverable(interactionSource = interactionSource) + ) { + if (isHovered) { + Box( + modifier = Modifier.width(200.dp), + contentAlignment = Alignment.CenterEnd + ) { + Text( + text = viewmodel.graphViewModel.graphType.value.replace(" ", "\nData type: "), + color = Color.Black + ) + } + } else { + Image( + painterResource("drawable/question.png"), + contentDescription = "Question Mark Icon", + modifier = Modifier.size(10.dp).padding(start = 50.dp) + ) + } + } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is HoverInteraction.Enter -> { + isHovered = true + } + + is HoverInteraction.Exit -> { + isHovered = false + } + } + } + } + } +} diff --git a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt new file mode 100644 index 00000000..7dbe4aa9 --- /dev/null +++ b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt @@ -0,0 +1,2 @@ +package view.utils + diff --git a/app/src/main/kotlin/view/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt similarity index 89% rename from app/src/main/kotlin/view/SelectInitDialogWindow.kt rename to app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt index 27c70d75..581b960a 100644 --- a/app/src/main/kotlin/view/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt @@ -1,4 +1,4 @@ -package view +package view.utils import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -21,6 +21,7 @@ class SelectInitDialogWindow( ) { var showGraphDialog by mutableStateOf(false) var showCreateGraphDialog by mutableStateOf(false) + var showImportTab by mutableStateOf(false) @Composable fun GraphInitDialogWindow( @@ -65,7 +66,10 @@ class SelectInitDialogWindow( Button(modifier = Modifier.width(145.dp).height(50.dp), colors = ButtonDefaults.buttonColors(Color.Blue), - onClick = { showGraphDialog = false } + onClick = { + showGraphDialog = false + showImportTab = true + } ) { Text("Import", color = Color.White) } @@ -77,5 +81,9 @@ class SelectInitDialogWindow( if (showCreateGraphDialog) { CreateGraphDialogWindow(CreateGraphViewModel()) } + +// if (showImportTab) { +// ImportGraphDialogWindow() +// } } } diff --git a/app/src/main/kotlin/view/utils/ZoomBox.kt b/app/src/main/kotlin/view/utils/ZoomBox.kt new file mode 100644 index 00000000..d5ea6a9e --- /dev/null +++ b/app/src/main/kotlin/view/utils/ZoomBox.kt @@ -0,0 +1,37 @@ +package view.utils + +import androidx.compose.foundation.layout.* +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ZoomBox(currentScale: MutableState) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Bottom + ) { + FloatingActionButton(onClick = { + currentScale.value = (1.1f * currentScale.value).coerceIn(0.7f, 1.9f) // TODO: move to const + }) { + Text("+") + } + Spacer(modifier = Modifier.height(8.dp)) + FloatingActionButton(onClick = { + currentScale.value = (currentScale.value / 1.1f).coerceIn(0.7f, 1.9f) + }) { + Text("-") + } + } + } +} diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 6a3be3a9..a9291591 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -1,24 +1,23 @@ package viewmodel +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import model.abstractGraph.Graph import viewmodel.graph.GraphViewModel -import viewmodel.graph.TestRepresentation +import viewmodel.graph.TFDPLayout class MainScreenViewModel(graph: Graph, currentGraphType: String) { val showVerticesData = mutableStateOf(false) val showVerticesIds = mutableStateOf(false) - val isGraphDirected = mutableStateOf(false) val graphType = mutableStateOf(currentGraphType) - val graphViewModel = GraphViewModel(graph, showVerticesIds, showVerticesData, graphType, isGraphDirected) - - init { // here will be a placement-function call - TestRepresentation().place(740.0, 650.0, graphViewModel.verticesVM) + fun setDirectionState(currentGraphType: String): MutableState { + if (currentGraphType.contains("Directed")) return mutableStateOf(true) + return mutableStateOf(false) } - // fun setEdgeColor - // - // fun setVerticesColor() + val graphViewModel = + GraphViewModel(graph, showVerticesIds, showVerticesData, graphType, setDirectionState(currentGraphType)) } diff --git a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt index ed5d0536..a03d38e7 100644 --- a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt @@ -42,6 +42,7 @@ class CreateGraphViewModel { "WeightedDirectedGraph UInt")) is GraphType.String -> MainScreen(MainScreenViewModel(WeightedDirectedGraph(), "WeightedDirectedGraph String")) + } } diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index dd110b50..f731925f 100644 --- a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -4,40 +4,41 @@ import androidx.compose.runtime.State import androidx.compose.ui.unit.Dp import kotlin.math.sqrt +// TODO: move to a seperate const file const val ARROW_SIZE = 20f const val ARROW_DEPTH = 2.5f const val SQRT_3 = 1.732f class EdgeViewModel( - private val firstVertex: VertexViewModel, - private val secondVertex: VertexViewModel, + val firstVertex: VertexViewModel, + val secondVertex: VertexViewModel, private val isDirected: State ) { private val radius = firstVertex.radius - internal fun calculateFirstVertexCenter(): Pair { - val x = firstVertex.x.value + radius - val y = firstVertex.y.value + radius + internal fun calculateFirstVertexCenter(scale: Float): Pair { + val x = firstVertex.x.value + radius * scale + val y = firstVertex.y.value + radius * scale return Pair(x, y) } - internal fun calculateSecondVertexCenter(): Pair { - val x = secondVertex.x.value + radius - val y = secondVertex.y.value + radius + internal fun calculateSecondVertexCenter(scale: Float): Pair { + val x = secondVertex.x.value + radius * scale + val y = secondVertex.y.value + radius * scale return Pair(x, y) } - internal fun calculateArrowPoints(): List> { + internal fun calculateArrowPoints(scale: Float): List> { if (!isDirected.value) return listOf() - val firstVertexCenterX = calculateFirstVertexCenter().first - val firstVertexCenterY = calculateFirstVertexCenter().second + val firstVertexCenterX = calculateFirstVertexCenter(scale).first + val firstVertexCenterY = calculateFirstVertexCenter(scale).second - val secondVertexCenterX = calculateSecondVertexCenter().first - val secondVertexCenterY = calculateSecondVertexCenter().second + val secondVertexCenterX = calculateSecondVertexCenter(scale).first + val secondVertexCenterY = calculateSecondVertexCenter(scale).second val vectorX = secondVertexCenterX - firstVertexCenterX val vectorY = secondVertexCenterY - firstVertexCenterY @@ -54,14 +55,14 @@ class EdgeViewModel( val bX = normedVectorX * SQRT_3 / 2 + normedVectorY * 1 / 2 val bY = -normedVectorX * 1 / 2 + normedVectorY * SQRT_3 / 2 - val arrowEndPointX = secondVertexCenterX - normedVectorX * (radius.value - ARROW_DEPTH) - val arrowEndPointY = secondVertexCenterY - normedVectorY * (radius.value - ARROW_DEPTH) + val arrowEndPointX = secondVertexCenterX - normedVectorX * (radius.value - ARROW_DEPTH) * scale + val arrowEndPointY = secondVertexCenterY - normedVectorY * (radius.value - ARROW_DEPTH) * scale - val arrowLeftPointX = arrowEndPointX - aX * ARROW_SIZE - val arrowLeftPointY = arrowEndPointY - aY * ARROW_SIZE + val arrowLeftPointX = arrowEndPointX - aX * ARROW_SIZE * scale + val arrowLeftPointY = arrowEndPointY - aY * ARROW_SIZE * scale - val arrowRightPointX = arrowEndPointX - bX * ARROW_SIZE - val arrowRightPointY = arrowEndPointY - bY * ARROW_SIZE + val arrowRightPointX = arrowEndPointX - bX * ARROW_SIZE * scale + val arrowRightPointY = arrowEndPointY - bY * ARROW_SIZE * scale return listOf( Pair(arrowEndPointX, arrowEndPointY), diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 866cdcf7..07409676 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -1,40 +1,42 @@ package viewmodel.graph -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State +import androidx.compose.runtime.* import model.abstractGraph.Edge import model.abstractGraph.Graph +import model.abstractGraph.Vertex class GraphViewModel( - private val graph: Graph, - private val showIds: State, + val graph: Graph, private val showVerticesData: State, + var showVerticesID: MutableState, val graphType: MutableState, private val isDirected: State, ) { - private var _verticesViewModels = - graph.getVertices().associateWith { vertex -> - VertexViewModel( - dataVisible = showVerticesData, - idVisible = showIds, - vertex = vertex, - ) - }.toMutableMap() + val updateIsRequired = mutableStateOf(false) + + var _verticesViewModels = mutableMapOf, VertexViewModel>() + var _edgeViewModels = mutableMapOf, EdgeViewModel>() - private var _edgeViewModels: Map, EdgeViewModel> = - graph.getEdges().associateWith { edge -> - val firstVertex: VertexViewModel = - _verticesViewModels[edge.vertex1] - ?: throw NoSuchElementException("No such View Model, with mentioned edges") + fun updateEdgeViewModels(edge: Edge) { + val firstVertex: VertexViewModel = + _verticesViewModels[edge.vertex1] + ?: throw NoSuchElementException("No such View Model, with mentioned edges") - val secondVertex: VertexViewModel = - _verticesViewModels[edge.vertex2] - ?: throw NoSuchElementException("No such View Model, with mentioned edges") + val secondVertex: VertexViewModel = + _verticesViewModels[edge.vertex2] + ?: throw NoSuchElementException("No such View Model, with mentioned edges") - EdgeViewModel(firstVertex, secondVertex, isDirected) - }.toMutableMap() + _edgeViewModels[edge] = EdgeViewModel(firstVertex, secondVertex, isDirected) + } + fun updateVertexViewModels(vertex: Vertex) { + _verticesViewModels[vertex] = VertexViewModel( + dataVisible = showVerticesData, + idVisible = showVerticesID, + vertex = vertex, + ) + } fun checkVertexById(id: Int): Boolean { return _verticesViewModels.keys.any { it.id == id } @@ -42,13 +44,9 @@ class GraphViewModel( fun addVertex(data: String): Int { val newVertex = graph.addVertex(data as D) - _verticesViewModels[newVertex] = - VertexViewModel( - dataVisible = showVerticesData, - idVisible = showIds, - vertex = newVertex, - ) - TestRepresentation().place(740.0, 650.0, verticesVM) + + updateVertexViewModels(newVertex) + return newVertex.id } @@ -62,16 +60,17 @@ class GraphViewModel( ?: throw NoSuchElementException("No ViewModel found for vertex1") val secondVertexVM = _verticesViewModels[secondVertex] ?: throw NoSuchElementException("No ViewModel found for vertex2") - val edge = graph.addEdge(firstVertexVM.vertex, secondVertexVM.vertex) - _edgeViewModels = _edgeViewModels.toMutableMap().apply { - this[edge] = EdgeViewModel(firstVertexVM, secondVertexVM, isDirected) - } + val newEdge = graph.addEdge(firstVertexVM.vertex, secondVertexVM.vertex) + + updateEdgeViewModels(newEdge) } - val verticesVM: Collection> - get() = _verticesViewModels.values + fun applyForceDirectedLayout(width: Double, height: Double) { + val layout = TFDPLayout() + layout.place(width, height, verticesVM) + } - val edgesVM: Collection> - get() = _edgeViewModels.values + val verticesVM: List> get() = _verticesViewModels.values.toList() + val edgesVM: List> get() = _edgeViewModels.values.toList() } diff --git a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt new file mode 100644 index 00000000..4de351c3 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt @@ -0,0 +1,62 @@ +package viewmodel.graph + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import kotlin.math.pow +import kotlin.math.sqrt +import androidx.compose.ui.platform.LocalDensity + +class TFDPLayout( + private val longRangeAttractionConstant: Float = 0.001f, // strength of attractive force (long-range) - B + private val nearAttractionConstant: Float = 16.0f, // strength of attractive t-force (near) - A + private val repulsiveConstant: Float = 2.0f, // extent and magnitude of the repulsive t-force that + // controls the longest distance of neighbors in the layout - Y +) { + fun place(width: Double, height: Double, vertices: Collection>) { +// val forces = Array(vertices.size) { mk.ndarray(mk[0.0, 0.0]) } // TODO + val forces = Array(vertices.size) { Pair(0f, 0f) } + var k = 0 + + vertices.onEach { + val vi = it + var forceX = 0f + var forceY = 0f + + for (vj in vertices) { // repulsive forces + if (vi == vj) continue + + val dx = vi.x.value - vj.x.value + val dy = vi.y.value - vj.y.value + + val distance = sqrt(dx.value * dx.value + dy.value * dy.value) + + val repulsion = (distance) / (1 + distance * distance).pow(repulsiveConstant) + + forceX -= dx.value / distance * repulsion + forceY -= dy.value / distance * repulsion + + val attraction = + longRangeAttractionConstant * (distance + ((nearAttractionConstant * distance) / (1 + distance * distance))) + + forceX += dx.value / distance * attraction + forceY += dy.value / distance * attraction + } + forces[k] = Pair(forceX, forceY) + k++ + } + + k = 0 + vertices.onEach { // update positions + val vi = it + vi.x.value += forces[k].first.dp + vi.y.value += forces[k].second.dp + k++ + } + + for (vi in vertices) { // check borders + vi.x.value = vi.x.value.coerceIn(0.dp, (width.toFloat() - 360 - vi.radius.value * 2).dp) + vi.y.value = vi.y.value.coerceIn(0.dp, (height.toFloat() - vi.radius.value * 2).dp) + println("${vi.x.value} ${vi.y.value}") + } + } +} diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index 23d4a0fe..7f2399e3 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -1,5 +1,6 @@ package viewmodel.graph +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf @@ -15,17 +16,17 @@ class VertexViewModel( var dataVisible: State, var idVisible: State, val vertex: Vertex, - val radius: Dp = 30.dp, + val radius: Dp = 20.dp, ) { var isSelected = mutableStateOf(false) val getVertexData get() = vertex.data.toString() - fun onDrag(dragAmount: DpOffset, currentWindowVM: WindowViewModel) { + fun onDrag(dragAmount: DpOffset, currentWindowVM: WindowViewModel, density: Float, scale: Float) { - val maxX = currentWindowVM.getWidth / 2 - 360.dp - radius * 2 - val maxY = currentWindowVM.getHeight / 2 - radius * 2 + val maxX = currentWindowVM.getWidth - 360.dp - radius * 2 + val maxY = currentWindowVM.getHeight - radius * 2 // calculate the new position after dragging val newX = (x.value + dragAmount.x).coerceIn(0.dp, maxX) @@ -35,4 +36,8 @@ class VertexViewModel( x.value = newX y.value = newY } + + val getVertexID + get() = vertex.id } + From 78cd8e08b88787e3050194b260f1beec3b44ab5b Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Fri, 24 May 2024 17:35:15 +0300 Subject: [PATCH 59/77] test: implement tests for key vertices finding algorithm #41 * test: implement tests for key vertices finding algorithm * refactor: apply same code style to tests --- .../test/kotlin/model/DirectedGraphTest.kt | 270 ++++++++++-------- .../test/kotlin/model/UndirectedGraphTest.kt | 58 ++-- .../kotlin/model/WeightedDirectedGraphTest.kt | 147 +++++++--- .../model/WeightedUndirectedGraphTest.kt | 124 ++++++-- .../kotlin/model/abstractGraph/GraphTest.kt | 30 +- 5 files changed, 413 insertions(+), 216 deletions(-) diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt index 073fb226..9db57d2a 100644 --- a/app/src/test/kotlin/model/DirectedGraphTest.kt +++ b/app/src/test/kotlin/model/DirectedGraphTest.kt @@ -9,7 +9,6 @@ import util.annotations.TestAllDirectedGraphs import util.emptyEdgesSet import util.emptyVerticesList import util.setupAbstractGraph -import util.setupGraphForFindingCycles class DirectedGraphTest { @Nested @@ -462,10 +461,12 @@ class DirectedGraphTest { val v3 = graph.addVertex(3) val v4 = graph.addVertex(4) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v3, v4) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } val actualValue = graph.findSCC() val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4)) @@ -481,11 +482,13 @@ class DirectedGraphTest { val v4 = graph.addVertex(4) val v5 = graph.addVertex(5) - graph.addEdge(v1, v2) - graph.addEdge(v2, v1) - graph.addEdge(v3, v4) - graph.addEdge(v4, v3) - graph.addEdge(v5, v1) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v1) + addEdge(v3, v4) + addEdge(v4, v3) + addEdge(v5, v1) + } val actualValue = graph.findSCC() val expectedValue = mutableSetOf(mutableSetOf(v3, v4), mutableSetOf(v1, v2), mutableSetOf(v5)) @@ -502,13 +505,15 @@ class DirectedGraphTest { val v5 = graph.addVertex(5) val v6 = graph.addVertex(6) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v3, v4) - graph.addEdge(v4, v5) - graph.addEdge(v5, v6) - graph.addEdge(v6, v4) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + addEdge(v4, v5) + addEdge(v5, v6) + addEdge(v6, v4) + } val actualValue = graph.findSCC() val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) @@ -525,13 +530,15 @@ class DirectedGraphTest { val v5 = graph.addVertex(5) val v6 = graph.addVertex(6) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v3, v4) - graph.addEdge(v4, v5) - graph.addEdge(v5, v6) - graph.addEdge(v6, v4) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + addEdge(v4, v5) + addEdge(v5, v6) + addEdge(v6, v4) + } val actualValue = graph.findSCC() val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) @@ -548,12 +555,14 @@ class DirectedGraphTest { val v5 = graph.addVertex(5) val v6 = graph.addVertex(6) - graph.addEdge(v1, v2) - graph.addEdge(v2, v1) - graph.addEdge(v3, v4) - graph.addEdge(v4, v3) - graph.addEdge(v5, v6) - graph.addEdge(v6, v5) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v1) + addEdge(v3, v4) + addEdge(v4, v3) + addEdge(v5, v6) + addEdge(v6, v5) + } val actualValue = graph.findSCC() val expectedValue = mutableSetOf(mutableSetOf(v1, v2), mutableSetOf(v3, v4), mutableSetOf(v5, v6)) @@ -567,10 +576,12 @@ class DirectedGraphTest { val v2 = graph.addVertex(2) val v3 = graph.addVertex(3) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v3, v3) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v3) + } val actualValue = graph.findSCC() val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3)) @@ -599,7 +610,13 @@ class DirectedGraphTest { val v4 = graph.addVertex(4) val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1), mutableSetOf(v2), mutableSetOf(v3), mutableSetOf(v4)) + val expectedValue = + mutableSetOf( + mutableSetOf(v1), + mutableSetOf(v2), + mutableSetOf(v3), + mutableSetOf(v4) + ) assertEquals(expectedValue, actualValue) } @@ -654,11 +671,13 @@ class DirectedGraphTest { val v4 = graph.addVertex(4) val v5 = graph.addVertex(5) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v4, v3) - graph.addEdge(v4, v5) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } val actualValue = graph.findSCC() val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4), mutableSetOf(v5)) @@ -676,12 +695,13 @@ class DirectedGraphTest { val v3 = graph.addVertex(3) val v4 = graph.addVertex(4) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v3, v4) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } - val edgesBeforeReverse = graph.getEdges() val expectedValue = graph.getVertices() graph.findSCC() val actualValue = graph.getVertices() @@ -696,16 +716,15 @@ class DirectedGraphTest { val v3 = graph.addVertex(3) val v4 = graph.addVertex(4) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v3, v4) - + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } val expectedValue = graph.getEdges() - graph.findSCC() - val actualValue = graph.getEdges() assertEquals(expectedValue, actualValue) @@ -719,16 +738,16 @@ class DirectedGraphTest { val v4 = graph.addVertex(4) val v5 = graph.addVertex(5) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v4, v3) - graph.addEdge(v4, v5) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } val expectedValue = graph.getEdges() - graph.findSCC() - val actualValue = graph.getEdges() assertEquals(expectedValue, actualValue) @@ -742,16 +761,16 @@ class DirectedGraphTest { val v4 = graph.addVertex(4) val v5 = graph.addVertex(5) - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - graph.addEdge(v3, v1) - graph.addEdge(v4, v3) - graph.addEdge(v4, v5) + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } val expectedValue = graph.getVertices() - graph.findSCC() - val actualValue = graph.getVertices() assertEquals(expectedValue, actualValue) @@ -759,14 +778,52 @@ class DirectedGraphTest { } } -// [[(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@1bab8268, model.abstractGraph.Vertex@44a59da3)], -// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6986852, model.abstractGraph.Vertex@42a15bdc), (model.abstractGraph.Edge@704deff2, model.abstractGraph.Vertex@44a59da3)], -// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6986852, model.abstractGraph.Vertex@42a15bdc), (model.abstractGraph.Edge@404bbcbd, model.abstractGraph.Vertex@27508c5d), (model.abstractGraph.Edge@658c5a19, model.abstractGraph.Vertex@44a59da3)], -// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6e01f9b0, model.abstractGraph.Vertex@6f6745d6), (model.abstractGraph.Edge@6c61a903, model.abstractGraph.Vertex@27508c5d), (model.abstractGraph.Edge@658c5a19, model.abstractGraph.Vertex@44a59da3)], -// [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@a307a8c, model.abstractGraph.Vertex@4f704591), (model.abstractGraph.Edge@2b9ed6da, model.abstractGraph.Vertex@6f6745d6), (model.abstractGraph.Edge@6c61a903, model.abstractGraph.Vertex@27508c5d), (model.abstractGraph.Edge@658c5a19, model.abstractGraph.Vertex@44a59da3)]] + @Nested + inner class FindKeyVerticesTest { + @Nested + inner class `One vertex is picked over another`() { + @TestAllDirectedGraphs + fun `if it can reach more vertices`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + addEdge(v1, v2) + } + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + + @TestAllDirectedGraphs + fun `if it can reach other vertices with fewer edges`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + } + } -// [[(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@6986852, model.abstractGraph.Vertex@42a15bdc), (model.abstractGraph.Edge@704deff2, model.abstractGraph.Vertex@44a59da3)], [(model.abstractGraph.Edge@1acaf3d, model.abstractGraph.Vertex@27e47833), (model.abstractGraph.Edge@1bab8268, model.abstractGraph.Vertex@44a59da3)]] @Nested inner class FindCyclesTest { @Nested @@ -798,47 +855,21 @@ class DirectedGraphTest { val e78 = graph.addEdge(v7, v8) val e87 = graph.addEdge(v8, v7) - val expectedCycle1 = listOf( - e12 to v2, - e21 to v1 - ) - - val expectedCycle2 = listOf( - e12 to v2, - e20 to v0, - e01 to v1 - ) - - val expectedCycle3 = listOf( - e12 to v2, - e20 to v0, - e04 to v4, - e41 to v1 - ) - - val expectedCycle4 = listOf( - e12 to v2, - e23 to v3, - e34 to v4, - e41 to v1 - ) - - val expectedCycle5 = listOf( - e12 to v2, - e25 to v5, - e53 to v3, - e34 to v4, - e41 to v1 - ) + val expectedCycle1 = listOf(e12 to v2, e21 to v1) + val expectedCycle2 = listOf(e12 to v2, e20 to v0, e01 to v1) + val expectedCycle3 = listOf(e12 to v2, e20 to v0, e04 to v4, e41 to v1) + val expectedCycle4 = listOf(e12 to v2, e23 to v3, e34 to v4, e41 to v1) + val expectedCycle5 = listOf(e12 to v2, e25 to v5, e53 to v3, e34 to v4, e41 to v1) val actualValue = graph.findCycles(v1) - val expectedValue = setOf( - expectedCycle1, - expectedCycle2, - expectedCycle3, - expectedCycle4, - expectedCycle5 - ) + val expectedValue = + setOf( + expectedCycle1, + expectedCycle2, + expectedCycle3, + expectedCycle4, + expectedCycle5 + ) assertEquals(expectedValue, actualValue) } @@ -856,12 +887,7 @@ class DirectedGraphTest { val e23 = graph.addEdge(v2, v3) val actualValue = graph.findCycles(v1) - val expectedValue = setOf( - listOf( - e12 to v2, - e21 to v1 - ) - ) + val expectedValue = setOf(listOf(e12 to v2, e21 to v1)) assertEquals(expectedValue, actualValue) } @@ -874,7 +900,7 @@ class DirectedGraphTest { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - val e01 = graph.addEdge(v0, v1) + graph.addEdge(v0, v1) val actualValue = graph.findCycles(v1) val expectedValue = emptySet, Vertex>>>() @@ -887,7 +913,7 @@ class DirectedGraphTest { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - val e01 = graph.addEdge(v0, v1) + graph.addEdge(v0, v1) val actualValue = graph.findCycles(v0) val expectedValue = emptySet, Vertex>>>() @@ -901,13 +927,11 @@ class DirectedGraphTest { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - val e01 = graph.addEdge(v0, v1) - val e10 = graph.addEdge(v1, v0) + graph.addEdge(v0, v1) + graph.addEdge(v1, v0) val expectedGraph = graph.getVertices() to graph.getEdges().toSet() - graph.findCycles(v1) - val actualGraph = graph.getVertices() to graph.getEdges().toSet() assertEquals(expectedGraph, actualGraph) diff --git a/app/src/test/kotlin/model/UndirectedGraphTest.kt b/app/src/test/kotlin/model/UndirectedGraphTest.kt index fe1d5938..809d62cc 100644 --- a/app/src/test/kotlin/model/UndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/UndirectedGraphTest.kt @@ -132,11 +132,7 @@ class UndirectedGraphTest { val v4 = defaultVerticesList[4] val actualValue = graph.getOutgoingEdges(v3).toSet() - val expectedValue = setOf( - graph.getEdge(v3, v4), - graph.getEdge(v3, v1), - graph.getEdge(v3, v2) - ) + val expectedValue = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1), graph.getEdge(v3, v2)) assertEquals(expectedValue, actualValue) } @@ -245,13 +241,13 @@ class UndirectedGraphTest { val expectedEdges1 = setOf(graph.getEdge(v0, v1), graph.getEdge(v0, v3)) val actualEdges2 = graph.getOutgoingEdges(v3).toSet() - val expectedEdges2 = setOf( - graph.getEdge(v3, v0), - graph.getEdge(v3, v1), - graph.getEdge(v3, v2), - graph.getEdge(v3, v4) - ) - + val expectedEdges2 = + setOf( + graph.getEdge(v3, v0), + graph.getEdge(v3, v1), + graph.getEdge(v3, v2), + graph.getEdge(v3, v4) + ) assertEquals(expectedEdges1, actualEdges1) assertEquals(expectedEdges2, actualEdges2) @@ -514,11 +510,12 @@ class UndirectedGraphTest { graph.removeEdge(edgeToRemove) val actualEdges1 = graph.getOutgoingEdges(v1).toSet() - val expectedEdges1 = setOf( - graph.getEdge(v1, v0), - graph.getEdge(v1, v3), - graph.getEdge(v1, v4), - ) + val expectedEdges1 = + setOf( + graph.getEdge(v1, v0), + graph.getEdge(v1, v3), + graph.getEdge(v1, v4), + ) val actualEdges2 = graph.getOutgoingEdges(v2).toSet() val expectedEdges2 = setOf(graph.getEdge(v2, v3)) @@ -533,7 +530,7 @@ class UndirectedGraphTest { @TestAllUndirectedGraphs fun `non-existing edge should throw an exception`(graph: UndirectedGraph) { assertThrows(NoSuchElementException::class.java) { - graph.removeEdge(Edge(Vertex(0,0), Vertex(1, 1))) + graph.removeEdge(Edge(Vertex(0, 0), Vertex(1, 1))) } } } @@ -691,4 +688,29 @@ class UndirectedGraphTest { } } } + + @Nested + inner class FindKeyVerticesTest { + @Nested + inner class `One vertex is picked over another`() { + @TestAllUndirectedGraphs + fun `if it can reach other vertices with fewer edges`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + } + } } diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt index 38b67a10..3d2fac95 100644 --- a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt @@ -94,9 +94,7 @@ class WeightedDirectedGraphTest { graph.removeEdge(edge) - assertThrows(NoSuchElementException::class.java) { - graph.getWeight(edge) - } + assertThrows(NoSuchElementException::class.java) { graph.getWeight(edge) } } } @@ -159,7 +157,7 @@ class WeightedDirectedGraphTest { val actualResult = graph.findShortestPathDijkstra(v0, v3) - Assertions.assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) } @Test @@ -211,7 +209,7 @@ class WeightedDirectedGraphTest { val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) val actualResult = graph.findShortestPathDijkstra(v0, v4) - Assertions.assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) } @Test @@ -249,7 +247,6 @@ class WeightedDirectedGraphTest { @Nested inner class `No path should be returned`() { - @Test fun `no path exists in directed graph`() { val graph = WeightedDirectedGraph() @@ -271,9 +268,11 @@ class WeightedDirectedGraphTest { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) - graph.addEdge(v0, v1, 1) - graph.addEdge(v1, v2, 2) - graph.addEdge(v2, v0, 2) + graph.apply { + addEdge(v0, v1, 1) + addEdge(v1, v2, 2) + addEdge(v2, v0, 2) + } val actualResult = graph.findShortestPathDijkstra(v0, v0) @@ -296,8 +295,8 @@ class WeightedDirectedGraphTest { val v1 = graph.addVertex(1) val v2 = graph.addVertex(2) - val e0 = graph.addEdge(v0, v1, 0) - val e1 = graph.addEdge(v1, v2, 0) + graph.addEdge(v0, v1, 0) + graph.addEdge(v1, v2, 0) val actualResult = graph.findShortestPathDijkstra(v2, v0) @@ -306,6 +305,89 @@ class WeightedDirectedGraphTest { } } + @Nested + inner class FindKeyVerticesTest { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + addEdge(v2, v3, 1) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if its sum of distances to other vertices is less`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 2) + addEdge(v2, v3, 2) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `Returns null`() { + @Test + fun `if graph has negative edges`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v0, v2, -1) + + val actualResult = graph.findKeyVertices() + + assertNull(actualResult) + } + } + } + @Nested inner class findShortestPathFordBellmanTest { @Nested @@ -333,11 +415,12 @@ class WeightedDirectedGraphTest { val v4 = defaultVertices[4] val actualValue = graph.findShortestPathFordBellman(v0, v4) - val expectedValue = listOf( - graph.getEdge(v0, v1) to v1, - graph.getEdge(v1, v2) to v2, - graph.getEdge(v2, v4) to v4 - ) + val expectedValue = + listOf( + graph.getEdge(v0, v1) to v1, + graph.getEdge(v1, v2) to v2, + graph.getEdge(v2, v4) to v4 + ) assertEquals(expectedValue, actualValue) } @@ -361,9 +444,7 @@ class WeightedDirectedGraphTest { val v4 = defaultVertices[4] val expectedGraph = graphStructure - graph.findShortestPathFordBellman(v3, v4) - val actualGraph = graphStructure assertEquals(expectedGraph, actualGraph) @@ -381,9 +462,8 @@ class WeightedDirectedGraphTest { val v5 = defaultVertices[5] val actualValue = graph.findShortestPathFordBellman(v1, v5) - val expectedValue = null - assertEquals(expectedValue, actualValue) + assertNull(actualValue) } @Test @@ -395,9 +475,8 @@ class WeightedDirectedGraphTest { val v2 = defaultVertices[2] val actualValue = graph.findShortestPathFordBellman(v2, v0) - val expectedValue = null - assertEquals(expectedValue, actualValue) + assertNull(actualValue) } @Test @@ -409,9 +488,8 @@ class WeightedDirectedGraphTest { val v8 = defaultVertices[8] val actualValue = graph.findShortestPathFordBellman(v0, v8) - val expectedValue = null - assertEquals(expectedValue, actualValue) + assertNull(actualValue) } @Test @@ -423,9 +501,8 @@ class WeightedDirectedGraphTest { val v8 = defaultVertices[8] val actualValue = graph.findShortestPathFordBellman(v6, v8) - val expectedValue = null - assertEquals(expectedValue, actualValue) + assertNull(actualValue) } @Test @@ -443,14 +520,14 @@ class WeightedDirectedGraphTest { val v7 = defaultVertices[7] val v8 = defaultVertices[8] - assertEquals(null, graph.findShortestPathFordBellman(v8, v0)) - assertEquals(null, graph.findShortestPathFordBellman(v8, v1)) - assertEquals(null, graph.findShortestPathFordBellman(v8, v2)) - assertEquals(null, graph.findShortestPathFordBellman(v8, v3)) - assertEquals(null, graph.findShortestPathFordBellman(v8, v4)) - assertEquals(null, graph.findShortestPathFordBellman(v8, v5)) - assertEquals(null, graph.findShortestPathFordBellman(v8, v6)) - assertEquals(null, graph.findShortestPathFordBellman(v8, v7)) + assertNull(graph.findShortestPathFordBellman(v8, v0)) + assertNull(graph.findShortestPathFordBellman(v8, v1)) + assertNull(graph.findShortestPathFordBellman(v8, v2)) + assertNull(graph.findShortestPathFordBellman(v8, v3)) + assertNull(graph.findShortestPathFordBellman(v8, v4)) + assertNull(graph.findShortestPathFordBellman(v8, v5)) + assertNull(graph.findShortestPathFordBellman(v8, v6)) + assertNull(graph.findShortestPathFordBellman(v8, v7)) } @Test @@ -462,9 +539,7 @@ class WeightedDirectedGraphTest { val v8 = defaultVertices[8] val expectedGraph = graphStructure - graph.findShortestPathFordBellman(v8, v1) - val actualGraph = graphStructure assertEquals(expectedGraph, actualGraph) diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt index 546b0299..e55a84bf 100644 --- a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt @@ -264,7 +264,7 @@ class WeightedUndirectedGraphTest { val actualResult = graph.findShortestPathDijkstra(v0, v3) - assertEquals(actualResult, null) + assertNull(actualResult) } @Test @@ -301,10 +301,10 @@ class WeightedUndirectedGraphTest { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - graph.addEdge(v0, v1, 5) - val lightEdge = graph.addEdge(v0, v1, 3) + val e01Heavy = graph.addEdge(v0, v1, 5) + val e01Light = graph.addEdge(v0, v1, 3) - val expectedReturn = listOf(lightEdge) + val expectedReturn = listOf(e01Light) val actualReturn = graph.findMinSpanningTree() assertEquals(expectedReturn, actualReturn) @@ -315,10 +315,10 @@ class WeightedUndirectedGraphTest { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - graph.addEdge(v0, v1, -5) - val lightEdge = graph.addEdge(v0, v1, -10) + val e01Heavy = graph.addEdge(v0, v1, -5) + val e01Light = graph.addEdge(v0, v1, -10) - val expectedReturn = listOf(lightEdge) + val expectedReturn = listOf(e01Light) val actualReturn = graph.findMinSpanningTree() assertEquals(expectedReturn, actualReturn) @@ -329,10 +329,10 @@ class WeightedUndirectedGraphTest { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - graph.addEdge(v0, v1, 5) - val zeroEdge = graph.addEdge(v0, v1, 0) + val e01Pos = graph.addEdge(v0, v1, 5) + val e01Zero = graph.addEdge(v0, v1, 0) - val expectedReturn = listOf(zeroEdge) + val expectedReturn = listOf(e01Zero) val actualReturn = graph.findMinSpanningTree() assertEquals(expectedReturn, actualReturn) @@ -343,11 +343,11 @@ class WeightedUndirectedGraphTest { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - graph.addEdge(v0, v1, 5) - graph.addEdge(v0, v1, 0) - val negativeEdge = graph.addEdge(v0, v1, -5) + val e01Pos = graph.addEdge(v0, v1, 5) + val e01Zero = graph.addEdge(v0, v1, 0) + val e01Neg = graph.addEdge(v0, v1, -5) - val expectedReturn = listOf(negativeEdge) + val expectedReturn = listOf(e01Neg) val actualReturn = graph.findMinSpanningTree() assertEquals(expectedReturn, actualReturn) @@ -367,7 +367,7 @@ class WeightedUndirectedGraphTest { val e12 = graph.addEdge(v1, v2, 1) val e23 = graph.addEdge(v2, v3, 1) - val cycleEdge30 = graph.addEdge(v3, v0, 5) + val e30 = graph.addEdge(v3, v0, 5) val expectedReturn = setOf(e01, e12, e23) val actualReturn = graph.findMinSpanningTree().toSet() @@ -386,12 +386,12 @@ class WeightedUndirectedGraphTest { val v3 = graph.addVertex(3) val v4 = graph.addVertex(4) - val e01 = graph.addEdge(v0, v1, 1) - val e02 = graph.addEdge(v0, v2, 10) + val eva01 = graph.addEdge(v0, v1, 1) + val eva02 = graph.addEdge(v0, v2, 10) val e23 = graph.addEdge(v2, v3, 0) val e24 = graph.addEdge(v2, v4, -20) - val expectedResult = setOf(e24, e23, e01, e02) + val expectedResult = setOf(e24, e23, eva01, eva02) val actualResult = graph.findMinSpanningTree().toSet() assertEquals(expectedResult, actualResult) @@ -410,10 +410,8 @@ class WeightedUndirectedGraphTest { @Test fun `if graph has no edges`() { - graph.apply { - addVertex(0) - addVertex(1) - } + graph.addVertex(0) + graph.addVertex(1) val expectedResult = listOf>() val actualResult = graph.findMinSpanningTree() @@ -422,4 +420,86 @@ class WeightedUndirectedGraphTest { } } } + + @Nested + inner class FindKeyVerticesTest { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if its sum of distances to other vertices is less`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 2) + addEdge(v2, v3, 2) + } + + val expectedResult = setOf(v0) + val actualResult = graph.findKeyVertices() + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `Returns null`() { + @Test + fun `if graph has negative edges`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v0, v2, -1) + + val actualResult = graph.findKeyVertices() + + assertNull(actualResult) + } + } + } } diff --git a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt index 01fd66c7..c69f96e3 100644 --- a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt +++ b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt @@ -3,9 +3,9 @@ package model.abstractGraph import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import util.annotations.TestAllGraphTypes -import util.setupAbstractGraph import util.emptyEdgesSet import util.emptyGraph +import util.setupAbstractGraph class GraphTest { @Nested @@ -218,12 +218,7 @@ class GraphTest { val newV2 = newVertices[2] val newV3 = newVertices[3] - assertTrue( - newV0 == oldV0 && - newV1 == oldV1 && - newV2.id == 2 && newV2.data == 4 && - newV3 == oldV3 - ) + assertTrue(newV0 == oldV0 && newV1 == oldV1 && newV2.id == 2 && newV2.data == 4 && newV3 == oldV3) } @TestAllGraphTypes @@ -247,12 +242,13 @@ class GraphTest { val newV3 = newVertices[3] val actualEdges = graph.getEdges().toSet() - val expectedEdges = setOf( - graph.getEdge(newV0, newV1), - graph.getEdge(newV3, newV2), - graph.getEdge(newV2, newV1), - graph.getEdge(newV3, newV1) - ) + val expectedEdges = + setOf( + graph.getEdge(newV0, newV1), + graph.getEdge(newV3, newV2), + graph.getEdge(newV2, newV1), + graph.getEdge(newV3, newV1) + ) assertEquals(expectedEdges, actualEdges) } @@ -264,7 +260,7 @@ class GraphTest { @TestAllGraphTypes fun `removing vertex from an empty graph should cause exception`(graph: Graph) { assertThrows(NoSuchElementException::class.java) { - graph.removeVertex(Vertex(0,0)) + graph.removeVertex(Vertex(0, 0)) } } @@ -273,7 +269,7 @@ class GraphTest { setupAbstractGraph(graph) assertThrows(NoSuchElementException::class.java) { - graph.removeVertex(Vertex(1904,-360)) + graph.removeVertex(Vertex(1904, -360)) } } @@ -282,7 +278,7 @@ class GraphTest { setupAbstractGraph(graph) assertThrows(NoSuchElementException::class.java) { - graph.removeVertex(Vertex(6,3)) + graph.removeVertex(Vertex(6, 3)) } } @@ -291,7 +287,7 @@ class GraphTest { setupAbstractGraph(graph) assertThrows(NoSuchElementException::class.java) { - graph.removeVertex(Vertex(0,35)) + graph.removeVertex(Vertex(0, 35)) } } } From 368a54fc41e5ac5b61a7b61969e88abcd4a9ff08 Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Fri, 24 May 2024 22:28:19 +0300 Subject: [PATCH 60/77] feat: enhance FAQ hoverable icon #43 * fix: resolve merge conflict * refactor: formatter + some small syntax changes * feat: change question mark icon from pdf to svg format * feat: change color of FAQ, make it more smooth * feat: change FAQ text color to black --------- Co-authored-by: qruty <64466788+qrutyy@users.noreply.github.com> --- .../kotlin/view/ImportGraphDialogWindow.kt | 2 + .../main/kotlin/view/tabScreen/GeneralTab.kt | 10 +++ app/src/main/kotlin/view/utils/FAQBox.kt | 61 ++++++++++++------ .../view/utils/SelectInitDialogWindow.kt | 2 + .../kotlin/viewmodel/MainScreenViewModel.kt | 1 - .../kotlin/viewmodel/graph/GraphViewModel.kt | 6 +- app/src/main/resources/drawable/question.png | Bin 17551 -> 0 bytes app/src/main/resources/drawable/question.svg | 1 + 8 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 app/src/main/kotlin/view/ImportGraphDialogWindow.kt delete mode 100644 app/src/main/resources/drawable/question.png create mode 100644 app/src/main/resources/drawable/question.svg diff --git a/app/src/main/kotlin/view/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/ImportGraphDialogWindow.kt new file mode 100644 index 00000000..8d8a2bb6 --- /dev/null +++ b/app/src/main/kotlin/view/ImportGraphDialogWindow.kt @@ -0,0 +1,2 @@ +package view + diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index af6b4f16..168def0b 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import viewmodel.WindowViewModel import viewmodel.graph.GraphViewModel @Composable @@ -25,6 +26,7 @@ fun GeneralTab(graphVM: GraphViewModel) { var firstVertexId by remember { mutableStateOf("") } var secondVertexId by remember { mutableStateOf("") } var secondVertexData by remember { mutableStateOf("") } + var changesWereMade by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { Row(modifier = Modifier.height(0.dp)) {} @@ -254,4 +256,12 @@ fun GeneralTab(graphVM: GraphViewModel) { } } } + + if (changesWereMade) { + changesWereMade = false + val currentWindowVM = WindowViewModel() + currentWindowVM.SetCurrentDimensions() + + graphVM.applyForceDirectedLayout(currentWindowVM.getWidth.value.toDouble(), currentWindowVM.getHeight.value.toDouble()) + } } diff --git a/app/src/main/kotlin/view/utils/FAQBox.kt b/app/src/main/kotlin/view/utils/FAQBox.kt index 598184b6..554a3a8e 100644 --- a/app/src/main/kotlin/view/utils/FAQBox.kt +++ b/app/src/main/kotlin/view/utils/FAQBox.kt @@ -1,11 +1,14 @@ package view.utils +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.* @@ -13,11 +16,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import viewmodel.MainScreenViewModel @Composable -fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScreenViewModel) { +fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScreenViewModel) { var isHovered by remember { mutableStateOf(false) } Box( @@ -26,28 +31,48 @@ fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScreen contentAlignment = Alignment.TopEnd ) { + val iconSize = 40.dp + val paddingSize = 5.dp + + val textBoxHeight = 58.dp + val textBoxWidth = 230.dp + + if (isHovered) { + Surface( + color = Color.White, + shape = RoundedCornerShape(15.dp), + border = BorderStroke(3.dp, MaterialTheme.colors.secondary), + modifier = + Modifier + .height(textBoxHeight + paddingSize) + .width(textBoxWidth + paddingSize) + .padding(paddingSize) + ) { + Text( + text = viewmodel.graphViewModel.graphType.value.replace(" ", "\nData type: "), + fontSize = 16.sp, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + Surface( - modifier = Modifier - .padding(top = 20.dp) - .width(220.dp).height(50.dp) - .background(if (isHovered) Color.LightGray else Color.Gray) + color = Color.Transparent, + modifier = + Modifier + .width(iconSize + paddingSize) + .height(iconSize + paddingSize) + .padding(paddingSize) + .background(Color.Transparent) .hoverable(interactionSource = interactionSource) ) { - if (isHovered) { - Box( - modifier = Modifier.width(200.dp), - contentAlignment = Alignment.CenterEnd - ) { - Text( - text = viewmodel.graphViewModel.graphType.value.replace(" ", "\nData type: "), - color = Color.Black - ) - } - } else { + if (!isHovered) { Image( - painterResource("drawable/question.png"), + painterResource("drawable/question.svg"), contentDescription = "Question Mark Icon", - modifier = Modifier.size(10.dp).padding(start = 50.dp) + modifier = Modifier.size(iconSize + paddingSize), + alignment = Alignment.TopEnd ) } } diff --git a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt index 581b960a..3aea24d0 100644 --- a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import model.abstractGraph.Graph +import view.tabScreen.FileControlTab import viewmodel.graph.CreateGraphViewModel diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index a9291591..37cc0afd 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -17,7 +17,6 @@ class MainScreenViewModel(graph: Graph, currentGraphType: String) { return mutableStateOf(false) } - val graphViewModel = GraphViewModel(graph, showVerticesIds, showVerticesData, graphType, setDirectionState(currentGraphType)) } diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 07409676..375ac890 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -1,6 +1,8 @@ package viewmodel.graph -import androidx.compose.runtime.* +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import model.abstractGraph.Edge import model.abstractGraph.Graph import model.abstractGraph.Vertex @@ -10,7 +12,7 @@ class GraphViewModel( private val showVerticesData: State, var showVerticesID: MutableState, val graphType: MutableState, - private val isDirected: State, + private val isDirected: State ) { val updateIsRequired = mutableStateOf(false) diff --git a/app/src/main/resources/drawable/question.png b/app/src/main/resources/drawable/question.png deleted file mode 100644 index ca4e343a1ac8fe3bef2114b77be17e66c9bc448b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17551 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 e-KG~w+W!Yt0x)vk69tX{0000 \ No newline at end of file From 8d83704925a96a4d135921c0f00109cbe56d0aef Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Sat, 25 May 2024 20:08:42 +0300 Subject: [PATCH 61/77] fix: delete invisible borders #44 * fix: delete maxX and maxY from vertex view model * refactor: delete redundant parameters and comments * fix: delete extra parameters from function invocation --- app/src/main/kotlin/view/graph/VertexView.kt | 3 +-- .../main/kotlin/viewmodel/graph/VertexViewModel.kt | 12 +++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt index 432300e3..ed94a873 100644 --- a/app/src/main/kotlin/view/graph/VertexView.kt +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -54,8 +54,7 @@ fun VertexView(viewModel: VertexViewModel, scale: Float) { coroutineScope.launch { detectDragGestures { change, dragAmount -> viewModel.onDrag( - DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()), - windowVM, density, scale + DpOffset(dragAmount.x.toDp(), dragAmount.y.toDp()) ) change.consume() } diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index 7f2399e3..ae44f97d 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -23,16 +23,10 @@ class VertexViewModel( val getVertexData get() = vertex.data.toString() - fun onDrag(dragAmount: DpOffset, currentWindowVM: WindowViewModel, density: Float, scale: Float) { + fun onDrag(dragAmount: DpOffset) { + val newX = x.value + dragAmount.x + val newY = y.value + dragAmount.y - val maxX = currentWindowVM.getWidth - 360.dp - radius * 2 - val maxY = currentWindowVM.getHeight - radius * 2 - - // calculate the new position after dragging - val newX = (x.value + dragAmount.x).coerceIn(0.dp, maxX) - val newY = (y.value + dragAmount.y).coerceIn(0.dp, maxY) - - // update the position x.value = newX y.value = newY } From 3c4cc44555ed80d3e98227e375d62f0dcd67f150 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Mon, 27 May 2024 22:11:33 +0300 Subject: [PATCH 62/77] fix(findCycles): wrong copying of graph #45 * test: change test for checking if graph didn't change * fix(findCycles): copying of graph --- app/src/main/kotlin/model/DirectedGraph.kt | 37 ++++++++++++++----- .../test/kotlin/model/DirectedGraphTest.kt | 27 ++++++++++++-- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/DirectedGraph.kt index cbdc4a12..0b3417c8 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/DirectedGraph.kt @@ -122,13 +122,27 @@ open class DirectedGraph : Graph() { if (vertexSCC.size == 1) return emptySet() // create SCC subgraph - val subGraph = this + val subGraph = DirectedGraph() + + // map to restore original vertices + val verticesCopiesMap = mutableMapOf, Vertex>() + + for (originalVertex in vertexSCC) { + val vertexCopy = subGraph.addVertex(originalVertex.data) + verticesCopiesMap[vertexCopy] = originalVertex + } + for (edge in getEdges()) { - if (edge.vertex1 !in vertexSCC || edge.vertex2 !in vertexSCC) { - subGraph.removeEdge(edge) + if (edge.vertex1 in vertexSCC && edge.vertex2 in vertexSCC) { + val vertex1copy = verticesCopiesMap.filterValues { it == edge.vertex1 }.keys.toList()[0] + val vertex2copy = verticesCopiesMap.filterValues { it == edge.vertex2 }.keys.toList()[0] + + subGraph.addEdge(vertex1copy, vertex2copy) } } + val copyOfSrcVertex = verticesCopiesMap.filterValues { it == srcVertex }.keys.toList()[0] + val blockedSet = mutableSetOf>() val blockedMap = mutableMapOf, MutableSet>>() val stack = ArrayDeque>() @@ -141,9 +155,9 @@ open class DirectedGraph : Graph() { blockedSet.add(currentVertex) for (neighbour in subGraph.getNeighbours(currentVertex)) { - if (neighbour == srcVertex) { + if (neighbour == copyOfSrcVertex) { // cycle is found - stack.addLast(srcVertex) + stack.addLast(copyOfSrcVertex) val cycleOfVertices = mutableListOf>() cycleOfVertices.addAll(stack) @@ -153,6 +167,7 @@ open class DirectedGraph : Graph() { cycleIsFound = true } else if (neighbour !in blockedSet) { + // next iteration cycleIsFound = DFSToFindCycles(neighbour) || cycleIsFound } } @@ -176,15 +191,19 @@ open class DirectedGraph : Graph() { return cycleIsFound } - DFSToFindCycles(srcVertex) + DFSToFindCycles(copyOfSrcVertex) val cycles = mutableSetOf, Vertex>>>() for (verticesCycle in verticesCycles) { + val originalVerticesCycle = mutableListOf>() + for (vertex in verticesCycle) originalVerticesCycle += verticesCopiesMap[vertex] + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the vertices map") + val cycle = mutableListOf, Vertex>>() - for (i in 0..verticesCycle.size - 2) { - val v1 = verticesCycle[i] - val v2 = verticesCycle[i + 1] + for (i in 0..originalVerticesCycle.size - 2) { + val v1 = originalVerticesCycle[i] + val v2 = originalVerticesCycle[i + 1] cycle.add(getEdge(v1, v2) to v2) } diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt index 9db57d2a..de471208 100644 --- a/app/src/test/kotlin/model/DirectedGraphTest.kt +++ b/app/src/test/kotlin/model/DirectedGraphTest.kt @@ -926,12 +926,33 @@ class DirectedGraphTest { fun `graph shouldn't change`(graph: DirectedGraph) { val v0 = graph.addVertex(0) val v1 = graph.addVertex(1) - - graph.addEdge(v0, v1) - graph.addEdge(v1, v0) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val e01 = graph.addEdge(v0, v1) + val e07 = graph.addEdge(v0, v7) + val e04 = graph.addEdge(v0, v4) + val e18 = graph.addEdge(v1, v8) + val e12 = graph.addEdge(v1, v2) + val e20 = graph.addEdge(v2, v0) + val e21 = graph.addEdge(v2, v1) + val e25 = graph.addEdge(v2, v5) + val e23 = graph.addEdge(v2, v3) + val e53 = graph.addEdge(v5, v3) + val e34 = graph.addEdge(v3, v4) + val e41 = graph.addEdge(v4, v1) + val e78 = graph.addEdge(v7, v8) + val e87 = graph.addEdge(v8, v7) val expectedGraph = graph.getVertices() to graph.getEdges().toSet() + graph.findCycles(v1) + val actualGraph = graph.getVertices() to graph.getEdges().toSet() assertEquals(expectedGraph, actualGraph) From f1de99ef8f3df0f5f83aaffe4a157c6b38ec9d80 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Mon, 27 May 2024 23:04:38 +0300 Subject: [PATCH 63/77] test: enhance app with integration test draft #46 * build: add new dependencies for int. tests * test: implement first integration test * feat: add getter for edge's direction type * refactor: move graphs to a separate package in module * feat: add test-tags * fix: add tag for 'not hoverable' FAQ * refactor: edit test name --- app/build.gradle.kts | 31 ++++++--- .../model/{ => graphs}/DirectedGraph.kt | 8 +-- .../model/{ => graphs}/UndirectedGraph.kt | 8 +-- .../{ => graphs}/WeightedDirectedGraph.kt | 6 +- .../{ => graphs}/WeightedUndirectedGraph.kt | 6 +- .../model/{ => graphs}/abstractGraph/Edge.kt | 2 +- .../model/{ => graphs}/abstractGraph/Graph.kt | 2 +- .../{ => graphs}/abstractGraph/Vertex.kt | 2 +- app/src/main/kotlin/view/utils/FAQBox.kt | 9 ++- .../view/utils/SelectInitDialogWindow.kt | 2 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 2 +- .../viewmodel/graph/CreateGraphViewModel.kt | 44 ++++++++----- .../kotlin/viewmodel/graph/EdgeViewModel.kt | 2 + .../kotlin/viewmodel/graph/GraphViewModel.kt | 6 +- .../kotlin/viewmodel/graph/VertexViewModel.kt | 2 +- .../kotlin/integration/IntegrationTest.kt | 63 +++++++++++++++++++ .../test/kotlin/model/DirectedGraphTest.kt | 5 +- .../test/kotlin/model/UndirectedGraphTest.kt | 5 +- .../kotlin/model/WeightedDirectedGraphTest.kt | 6 +- .../model/WeightedUndirectedGraphTest.kt | 5 +- .../kotlin/model/abstractGraph/GraphTest.kt | 2 + .../AllGraphTypesProvider.kt | 8 +-- ...htedAndUnweightedDirectedGraphsProvider.kt | 4 +- ...edAndUnweightedUndirectedGraphsProvider.kt | 4 +- app/src/test/kotlin/util/emptyGraphs.kt | 12 ++-- app/src/test/kotlin/util/setup.kt | 12 ++-- 26 files changed, 179 insertions(+), 79 deletions(-) rename app/src/main/kotlin/model/{ => graphs}/DirectedGraph.kt (98%) rename app/src/main/kotlin/model/{ => graphs}/UndirectedGraph.kt (96%) rename app/src/main/kotlin/model/{ => graphs}/WeightedDirectedGraph.kt (98%) rename app/src/main/kotlin/model/{ => graphs}/WeightedUndirectedGraph.kt (97%) rename app/src/main/kotlin/model/{ => graphs}/abstractGraph/Edge.kt (80%) rename app/src/main/kotlin/model/{ => graphs}/abstractGraph/Graph.kt (99%) rename app/src/main/kotlin/model/{ => graphs}/abstractGraph/Vertex.kt (55%) create mode 100644 app/src/test/kotlin/integration/IntegrationTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72a4fda8..482324ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,23 +1,34 @@ plugins { - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.compose) + kotlin("jvm") version "1.9.22" + id("org.jetbrains.compose") version "1.6.2" +} + +repositories { + google() // to ensure dependencies like androidx.annotation can be resolved. + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } dependencies { implementation(compose.desktop.currentOs) - implementation(libs.koin.core) + implementation("io.insert-koin:koin-core:3.4.2") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") + + implementation("org.xerial:sqlite-jdbc:3.41.2.1") + implementation("org.slf4j:slf4j-api:1.7.36") + implementation("ch.qos.logback:logback-classic:1.4.11") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + testImplementation("org.jetbrains.compose.ui:ui-test-junit4:1.6.2") } + compose.desktop { application { mainClass = "MainKt" } } - -tasks.test { - useJUnitPlatform() -} diff --git a/app/src/main/kotlin/model/DirectedGraph.kt b/app/src/main/kotlin/model/graphs/DirectedGraph.kt similarity index 98% rename from app/src/main/kotlin/model/DirectedGraph.kt rename to app/src/main/kotlin/model/graphs/DirectedGraph.kt index 0b3417c8..56ef496c 100644 --- a/app/src/main/kotlin/model/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/DirectedGraph.kt @@ -1,8 +1,8 @@ -package model +package model.graphs -import model.abstractGraph.Edge -import model.abstractGraph.Graph -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex import kotlin.NoSuchElementException import kotlin.collections.ArrayDeque import kotlin.collections.ArrayList diff --git a/app/src/main/kotlin/model/UndirectedGraph.kt b/app/src/main/kotlin/model/graphs/UndirectedGraph.kt similarity index 96% rename from app/src/main/kotlin/model/UndirectedGraph.kt rename to app/src/main/kotlin/model/graphs/UndirectedGraph.kt index c5040d40..c13a01f8 100644 --- a/app/src/main/kotlin/model/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/UndirectedGraph.kt @@ -1,9 +1,9 @@ -package model +package model.graphs import kotlin.math.min -import model.abstractGraph.Edge -import model.abstractGraph.Graph -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex open class UndirectedGraph : Graph() { override fun addEdge(vertex1: Vertex, vertex2: Vertex): Edge { diff --git a/app/src/main/kotlin/model/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt similarity index 98% rename from app/src/main/kotlin/model/WeightedDirectedGraph.kt rename to app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt index 330b8d88..91985d34 100644 --- a/app/src/main/kotlin/model/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt @@ -1,7 +1,7 @@ -package model +package model.graphs -import model.abstractGraph.Edge -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex import java.util.* import kotlin.NoSuchElementException diff --git a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt similarity index 97% rename from app/src/main/kotlin/model/WeightedUndirectedGraph.kt rename to app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt index 73668f31..c1fd58ee 100644 --- a/app/src/main/kotlin/model/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt @@ -1,7 +1,7 @@ -package model +package model.graphs -import model.abstractGraph.Edge -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex import java.util.* import kotlin.NoSuchElementException diff --git a/app/src/main/kotlin/model/abstractGraph/Edge.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Edge.kt similarity index 80% rename from app/src/main/kotlin/model/abstractGraph/Edge.kt rename to app/src/main/kotlin/model/graphs/abstractGraph/Edge.kt index 139b87da..d8344eef 100644 --- a/app/src/main/kotlin/model/abstractGraph/Edge.kt +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Edge.kt @@ -1,4 +1,4 @@ -package model.abstractGraph +package model.graphs.abstractGraph class Edge(val vertex1: Vertex, val vertex2: Vertex) { fun isIncident(vertex: Vertex) = (vertex1 == vertex || vertex2 == vertex) diff --git a/app/src/main/kotlin/model/abstractGraph/Graph.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt similarity index 99% rename from app/src/main/kotlin/model/abstractGraph/Graph.kt rename to app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt index 561c8368..8a1ceeff 100644 --- a/app/src/main/kotlin/model/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt @@ -1,4 +1,4 @@ -package model.abstractGraph +package model.graphs.abstractGraph import java.util.* import java.util.ArrayDeque diff --git a/app/src/main/kotlin/model/abstractGraph/Vertex.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Vertex.kt similarity index 55% rename from app/src/main/kotlin/model/abstractGraph/Vertex.kt rename to app/src/main/kotlin/model/graphs/abstractGraph/Vertex.kt index 721ce29a..d8770fe7 100644 --- a/app/src/main/kotlin/model/abstractGraph/Vertex.kt +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Vertex.kt @@ -1,3 +1,3 @@ -package model.abstractGraph +package model.graphs.abstractGraph class Vertex(var id: Int, val data: D) diff --git a/app/src/main/kotlin/view/utils/FAQBox.kt b/app/src/main/kotlin/view/utils/FAQBox.kt index 554a3a8e..a488caf3 100644 --- a/app/src/main/kotlin/view/utils/FAQBox.kt +++ b/app/src/main/kotlin/view/utils/FAQBox.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -27,7 +28,8 @@ fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScree Box( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .testTag("FAQBox"), contentAlignment = Alignment.TopEnd ) { @@ -47,12 +49,14 @@ fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScree .height(textBoxHeight + paddingSize) .width(textBoxWidth + paddingSize) .padding(paddingSize) + .testTag("FAQBoxHovered") ) { Text( text = viewmodel.graphViewModel.graphType.value.replace(" ", "\nData type: "), fontSize = 16.sp, color = Color.Black, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.testTag("HoveredText") ) } } @@ -66,6 +70,7 @@ fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScree .padding(paddingSize) .background(Color.Transparent) .hoverable(interactionSource = interactionSource) + .testTag("FAQBoxNotHovered") ) { if (!isHovered) { Image( diff --git a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt index 3aea24d0..dd276733 100644 --- a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import model.abstractGraph.Graph +import model.graphs.abstractGraph.Graph import view.tabScreen.FileControlTab import viewmodel.graph.CreateGraphViewModel diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 37cc0afd..f6ea6e50 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -3,7 +3,7 @@ package viewmodel import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import model.abstractGraph.Graph +import model.graphs.abstractGraph.Graph import viewmodel.graph.GraphViewModel import viewmodel.graph.TFDPLayout diff --git a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt index a03d38e7..666c3b81 100644 --- a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt @@ -1,10 +1,10 @@ package viewmodel.graph import androidx.compose.runtime.Composable -import model.DirectedGraph -import model.UndirectedGraph -import model.WeightedDirectedGraph -import model.WeightedUndirectedGraph +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph import view.MainScreen import viewmodel.MainScreenViewModel @@ -36,11 +36,14 @@ class CreateGraphViewModel { when (graphStructure) { is GraphStructure.Directed -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel(WeightedDirectedGraph(), + is GraphType.Integer -> MainScreen(MainScreenViewModel( + WeightedDirectedGraph(), "WeightedDirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel(WeightedDirectedGraph(), + is GraphType.UInteger -> MainScreen(MainScreenViewModel( + WeightedDirectedGraph(), "WeightedDirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel(WeightedDirectedGraph(), + is GraphType.String -> MainScreen(MainScreenViewModel( + WeightedDirectedGraph(), "WeightedDirectedGraph String")) } @@ -48,11 +51,14 @@ class CreateGraphViewModel { is GraphStructure.Undirected -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel(WeightedUndirectedGraph(), + is GraphType.Integer -> MainScreen(MainScreenViewModel( + WeightedUndirectedGraph(), "WeightedUndirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel(WeightedUndirectedGraph(), + is GraphType.UInteger -> MainScreen(MainScreenViewModel( + WeightedUndirectedGraph(), "WeightedUndirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel(WeightedUndirectedGraph(), + is GraphType.String -> MainScreen(MainScreenViewModel( + WeightedUndirectedGraph(), "WeightedUndirectedGraph String") ) } @@ -64,22 +70,28 @@ class CreateGraphViewModel { when (graphStructure) { is GraphStructure.Directed -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel(DirectedGraph(), + is GraphType.Integer -> MainScreen(MainScreenViewModel( + DirectedGraph(), "DirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel(DirectedGraph(), + is GraphType.UInteger -> MainScreen(MainScreenViewModel( + DirectedGraph(), "DirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel(DirectedGraph(), + is GraphType.String -> MainScreen(MainScreenViewModel( + DirectedGraph(), "DirectedGraph String")) } } is GraphStructure.Undirected -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel(UndirectedGraph(), + is GraphType.Integer -> MainScreen(MainScreenViewModel( + UndirectedGraph(), "UndirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel(UndirectedGraph(), + is GraphType.UInteger -> MainScreen(MainScreenViewModel( + UndirectedGraph(), "UndirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel(UndirectedGraph(), + is GraphType.String -> MainScreen(MainScreenViewModel( + UndirectedGraph(), "UndirectedGraph String")) } } diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index f731925f..ad1e1d79 100644 --- a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -15,6 +15,8 @@ class EdgeViewModel( private val isDirected: State ) { + fun isDirected() = isDirected.value + private val radius = firstVertex.radius internal fun calculateFirstVertexCenter(scale: Float): Pair { diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 375ac890..a32878ca 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -3,9 +3,9 @@ package viewmodel.graph import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import model.abstractGraph.Edge -import model.abstractGraph.Graph -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex class GraphViewModel( val graph: Graph, diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index ae44f97d..95ec261d 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Vertex import viewmodel.WindowViewModel class VertexViewModel( diff --git a/app/src/test/kotlin/integration/IntegrationTest.kt b/app/src/test/kotlin/integration/IntegrationTest.kt new file mode 100644 index 00000000..ffbd1a0f --- /dev/null +++ b/app/src/test/kotlin/integration/IntegrationTest.kt @@ -0,0 +1,63 @@ +package integration + +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import kotlinx.coroutines.runBlocking +import model.graphs.UndirectedGraph +import org.junit.Rule +import org.junit.Test +import view.utils.FAQBox +import viewmodel.MainScreenViewModel + + +class IntegrationTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `graph is undirected, check its view and FAQ button`() { + // I'm not sure if we can consider this as an integration test + // Imho it's an integration test, due to UI's connection with VM + there is more than 2 components (view/vm, or front/back) + + // SETUP + val interactionSource = MutableInteractionSource() + val viewmodel = MainScreenViewModel(UndirectedGraph(),"UndirectedGraph") + + composeTestRule.setContent { + FAQBox(interactionSource, viewmodel) + } + + // UI TEST + + // Verify initial state (not hovered) + composeTestRule.onNodeWithTag("FAQBoxNotHovered").assertExists() + + // Simulate hover enter by changing the state manually + runBlocking { + interactionSource.tryEmit(HoverInteraction.Enter()) + } + + // Verify hovered state + composeTestRule.onNodeWithTag("FAQBoxHovered").assertExists() + composeTestRule.onNodeWithTag("FAQBoxHovered").assertExists() + composeTestRule.onNodeWithTag("HoveredText").assertTextEquals("UndirectedGraph") + + composeTestRule.onNodeWithTag("") + + // CHECK VM + val edgeViewModels = viewmodel.graphViewModel._edgeViewModels + val allEdgesDirected = edgeViewModels.all { it.value.isDirected() } + + assert(allEdgesDirected) { "Not all edges are directed" } + } + + @Test + fun `check import from DB and save with other name`() { + + } +} diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt index de471208..4012a6dd 100644 --- a/app/src/test/kotlin/model/DirectedGraphTest.kt +++ b/app/src/test/kotlin/model/DirectedGraphTest.kt @@ -1,7 +1,8 @@ package model -import model.abstractGraph.Edge -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import model.graphs.DirectedGraph import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Nested diff --git a/app/src/test/kotlin/model/UndirectedGraphTest.kt b/app/src/test/kotlin/model/UndirectedGraphTest.kt index 809d62cc..f4ecb5fe 100644 --- a/app/src/test/kotlin/model/UndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/UndirectedGraphTest.kt @@ -1,7 +1,8 @@ package model -import model.abstractGraph.Edge -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import model.graphs.UndirectedGraph import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Nested diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt index 3d2fac95..a22bfce7 100644 --- a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt @@ -1,7 +1,9 @@ package model -import model.abstractGraph.Edge -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* import util.setupDirectedGraphWithCycle diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt index e55a84bf..1830b4e8 100644 --- a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt @@ -1,7 +1,8 @@ package model -import model.abstractGraph.Edge -import model.abstractGraph.Vertex +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import model.graphs.WeightedUndirectedGraph import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled diff --git a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt index c69f96e3..af4851f5 100644 --- a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt +++ b/app/src/test/kotlin/model/abstractGraph/GraphTest.kt @@ -1,5 +1,7 @@ package model.abstractGraph +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import util.annotations.TestAllGraphTypes diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt index eb2744ed..71368b65 100644 --- a/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt +++ b/app/src/test/kotlin/util/annotations/argumentProviders/AllGraphTypesProvider.kt @@ -1,9 +1,9 @@ package util.annotations.argumentProviders -import model.DirectedGraph -import model.UndirectedGraph -import model.WeightedDirectedGraph -import model.WeightedUndirectedGraph +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.ArgumentsProvider diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt index 740607c7..fe552ba8 100644 --- a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt +++ b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedDirectedGraphsProvider.kt @@ -1,7 +1,7 @@ package util.annotations.argumentProviders -import model.DirectedGraph -import model.WeightedDirectedGraph +import model.graphs.DirectedGraph +import model.graphs.WeightedDirectedGraph import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.ArgumentsProvider diff --git a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt index 98e5c043..d765e8e1 100644 --- a/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt +++ b/app/src/test/kotlin/util/annotations/argumentProviders/WeightedAndUnweightedUndirectedGraphsProvider.kt @@ -1,7 +1,7 @@ package util.annotations.argumentProviders -import model.UndirectedGraph -import model.WeightedUndirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedUndirectedGraph import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.ArgumentsProvider diff --git a/app/src/test/kotlin/util/emptyGraphs.kt b/app/src/test/kotlin/util/emptyGraphs.kt index 1d3af414..fe9cb4e0 100644 --- a/app/src/test/kotlin/util/emptyGraphs.kt +++ b/app/src/test/kotlin/util/emptyGraphs.kt @@ -1,11 +1,11 @@ package util -import model.DirectedGraph -import model.UndirectedGraph -import model.WeightedDirectedGraph -import model.WeightedUndirectedGraph -import model.abstractGraph.Edge -import model.abstractGraph.Vertex +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex val emptyDirectedGraph = DirectedGraph() val emptyUndirectedGraph = UndirectedGraph() diff --git a/app/src/test/kotlin/util/setup.kt b/app/src/test/kotlin/util/setup.kt index 967cc8d6..cead670d 100644 --- a/app/src/test/kotlin/util/setup.kt +++ b/app/src/test/kotlin/util/setup.kt @@ -1,11 +1,11 @@ package util -import model.DirectedGraph -import model.abstractGraph.Edge -import model.abstractGraph.Graph -import model.abstractGraph.Vertex -import model.WeightedUndirectedGraph -import model.WeightedDirectedGraph +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import model.graphs.WeightedUndirectedGraph +import model.graphs.WeightedDirectedGraph fun setupAbstractGraph(graph: Graph): Pair>, Set>> { val v0 = graph.addVertex(0) From a4cca715555f773e3e39c88e72e98823013b370e Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Tue, 28 May 2024 12:28:25 +0300 Subject: [PATCH 64/77] feat: add basic file control tab fields and buttons #48 * fix: small typo * fix: correct graph types picked by user, change default types * feat: add text field for graph name, save and load buttons * feat: add base dialog windows which can be dismissed * feat: introduce more variables * feat: add dropdown menu for database choosing * feat: add button and dialog to choose file when json is chosen --- .../kotlin/view/tabScreen/FileControlTab.kt | 193 +++++++++++++++++- .../main/kotlin/view/tabScreen/GeneralTab.kt | 12 +- .../view/utils/CreateGraphDialogWindow.kt | 10 +- 3 files changed, 203 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index 4cd181cc..ae24a215 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -1,13 +1,196 @@ package view.tabScreen -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import viewmodel.graph.GraphViewModel +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.window.Dialog +import java.awt.FileDialog +import java.awt.Frame +@OptIn(ExperimentalMaterialApi::class) @Composable -fun FileControlTab(graphVM: GraphViewModel? = null) { - Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 3rd tab") } +fun FileControlTab(graphVM: GraphViewModel) { + var showSaveDialog by remember { mutableStateOf(false) } + var showLoadDialog by remember { mutableStateOf(false) } + var graphName by remember { mutableStateOf("") } + var showEnterPathField by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { + Row(modifier = Modifier.height(0.dp)) {} + + val rowHeight = 75.dp + val fieldHeight = 70.dp + + val borderPadding = 10.dp + val horizontalGap = 20.dp + + val tabWidth = 360.dp + val fieldWidth = (tabWidth / 2) - horizontalGap + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(borderPadding) + ) { + Column(modifier = Modifier.width(tabWidth).fillMaxHeight(), Arrangement.Center) { + TextField( + value = graphName, + onValueChange = { graphName = it }, + modifier = Modifier + .fillMaxWidth() + .height(fieldHeight) + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Graph name", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + var expanded by remember { mutableStateOf(false) } + + val databases = arrayOf("SQLite", "Neo4j", "JSON") + var selectedDatabase by remember { mutableStateOf(databases[0])} + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.width(fieldWidth).fillMaxHeight() + ) { + TextField( + value = selectedDatabase, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + databases.forEach { db -> + DropdownMenuItem( + modifier = Modifier, + onClick = { + selectedDatabase = db + expanded = false + } + ) { + Text(text = db) + } + } + } + } + + if (selectedDatabase == "JSON") { + val fileDialog = FileDialog(null as Frame?, "Select File to Open") + fileDialog.mode = FileDialog.LOAD + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { + fileDialog.isVisible = true + }, + colors = ButtonDefaults.buttonColors(Color.White) + ) { + Text("Select File") + } + } + } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.width(fieldWidth).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { showSaveDialog = true }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Save") + } + } + Column(modifier = Modifier.width(fieldWidth).fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { showLoadDialog = true }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Load") + } + } + } + } + + + val dialogueHeight = 300.dp + val dialogueWidth = 400.dp + val padding = 14.dp + + if (showSaveDialog) { + Dialog( + onDismissRequest = { + showSaveDialog = false + } + ) { + Column( + modifier = + Modifier + .background(Color.White) + .padding(padding) + .width(dialogueWidth) + .height(dialogueHeight) + ) {} + } + } + + if (showLoadDialog) { + Dialog( + onDismissRequest = { + showLoadDialog = false + } + ) { + Column( + modifier = + Modifier + .background(Color.White) + .padding(padding) + .width(dialogueWidth) + .height(dialogueHeight) + ) {} + } + } } diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index 168def0b..b13ca93a 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -156,12 +156,20 @@ fun GeneralTab(graphVM: GraphViewModel) { } if (showVertexAddDialog) { - Dialog(onDismissRequest = {}) { + Dialog( + onDismissRequest = { + showVertexAddDialog = false + } + ) { vertexData = "" Column( modifier = - Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) + Modifier + .background(Color.White) + .padding(16.dp) + .width(350.dp) + .height(200.dp) ) { if (graphVM.verticesVM.isEmpty()) { Text("Input data of second vertex to create and connect with") diff --git a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt index 287cbec9..525cfa7b 100644 --- a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt @@ -115,7 +115,7 @@ fun CreateGraphDialogWindow(viewModel: CreateGraphViewModel) { } Column(modifier = Modifier.width(250.dp).height(200.dp)) { - Text("Select the weightnes*:") + Text("Select the weightiness:") Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} @@ -200,13 +200,13 @@ fun onCreateGraphClicked( val graphStructure = when (orientationIndex) { 0 -> CreateGraphViewModel.GraphStructure.Undirected 1 -> CreateGraphViewModel.GraphStructure.Directed - else -> CreateGraphViewModel.GraphStructure.Directed // default to directed + else -> CreateGraphViewModel.GraphStructure.Undirected // default to undirected } val weight = when (weightnessIndex) { - 0 -> CreateGraphViewModel.Weight.Weighted - 1 -> CreateGraphViewModel.Weight.Unweighted - else -> CreateGraphViewModel.Weight.Weighted // default to weighted + 0 -> CreateGraphViewModel.Weight.Unweighted + 1 -> CreateGraphViewModel.Weight.Weighted + else -> CreateGraphViewModel.Weight.Unweighted // default to unweighted } return viewModel.createGraph(storedData, graphStructure, weight) From 2f6d7699d4b65d098fa89ecd971b032b0f0bddca Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Tue, 28 May 2024 14:00:43 +0300 Subject: [PATCH 65/77] refactor: clean up code, resolve some TODO's #49 * feat: move constants to a separate file * fix: make 'getWeightMap' return mutable map * refactor: rename 'CreateGraphVM' class, edit 'addEdge' * refactor: resolve TODO's * fix: edit name of radius variable --- app/src/main/kotlin/Constants.kt | 13 +++++ .../model/graphs/WeightedDirectedGraph.kt | 2 +- .../model/graphs/WeightedUndirectedGraph.kt | 2 +- .../model/graphs/abstractGraph/Graph.kt | 2 +- app/src/main/kotlin/view/graph/EdgeView.kt | 12 ++--- app/src/main/kotlin/view/graph/VertexView.kt | 20 ++++---- .../view/utils/CreateGraphDialogWindow.kt | 26 +++++----- .../view/utils/SelectInitDialogWindow.kt | 6 +-- app/src/main/kotlin/view/utils/ZoomBox.kt | 5 +- .../kotlin/viewmodel/graph/EdgeViewModel.kt | 8 ++-- .../kotlin/viewmodel/graph/GraphViewModel.kt | 4 +- ...aphViewModel.kt => SetupGraphViewModel.kt} | 2 +- .../main/kotlin/viewmodel/graph/TFDPLayout.kt | 5 +- .../viewmodel/graph/TestRepresentation.kt | 47 ------------------- 14 files changed, 56 insertions(+), 98 deletions(-) create mode 100644 app/src/main/kotlin/Constants.kt rename app/src/main/kotlin/viewmodel/graph/{CreateGraphViewModel.kt => SetupGraphViewModel.kt} (99%) delete mode 100644 app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt diff --git a/app/src/main/kotlin/Constants.kt b/app/src/main/kotlin/Constants.kt new file mode 100644 index 00000000..8d8d4850 --- /dev/null +++ b/app/src/main/kotlin/Constants.kt @@ -0,0 +1,13 @@ +import androidx.compose.ui.unit.dp + +const val scaleFactor: Float = 1.1f + +const val ARROW_SIZE = 20f +const val ARROW_DEPTH = 2.5f +const val SQRT_3 = 1.732f + +val maxVertexRadius = 35.dp +val minVertexRadius = 7.dp + +val maxEdgeStrokeWidth = 12f +val minEdgeStrokeWidth = 4f \ No newline at end of file diff --git a/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt index 91985d34..1b6170bf 100644 --- a/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt @@ -37,7 +37,7 @@ class WeightedDirectedGraph : DirectedGraph() { return weight } - override fun getWeightMap() = weightMap.toMap() + override fun getWeightMap() = weightMap.toMutableMap() override fun hasNegativeEdges(): Boolean { val edgeWeighs = getWeightMap().values diff --git a/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt index c1fd58ee..6913ea64 100644 --- a/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt @@ -38,7 +38,7 @@ class WeightedUndirectedGraph : UndirectedGraph() { return weight } - override fun getWeightMap() = weightMap.toMap() + override fun getWeightMap() = weightMap.toMutableMap() override fun hasNegativeEdges(): Boolean { val edgeWeighs = getWeightMap().values diff --git a/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt index 8a1ceeff..2a8ca133 100644 --- a/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt @@ -66,7 +66,7 @@ abstract class Graph { /* In undirected graph, returns a map with every edge as a key and 1 as a value * In a directed graph, returns copy of weightMap property */ - open fun getWeightMap(): Map, Int> { + open fun getWeightMap(): MutableMap, Int> { val weightMap = mutableMapOf, Int>() for (edge in edges) weightMap[edge] = 1 diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt index 1a76cb7e..6a259f41 100644 --- a/app/src/main/kotlin/view/graph/EdgeView.kt +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -9,19 +9,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.zIndex +import maxEdgeStrokeWidth +import minEdgeStrokeWidth import viewmodel.WindowViewModel import viewmodel.graph.EdgeViewModel -import kotlin.math.max -import kotlin.math.min + @Composable fun EdgeView(viewModel: EdgeViewModel, scale: Float) { val windowVM = WindowViewModel() windowVM.SetCurrentDimensions() - val maxStrokeWidth = 12f// TODO: move to shared const file - val minStrokeWidth = 4f - val firstVertexCenter = viewModel.calculateFirstVertexCenter(scale) val secondVertexCenter = viewModel.calculateSecondVertexCenter(scale) @@ -35,7 +33,7 @@ fun EdgeView(viewModel: EdgeViewModel, scale: Float) { Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( color = Color.LightGray, - strokeWidth = (5f * scale).coerceIn(minStrokeWidth, maxStrokeWidth), + strokeWidth = (5f * scale).coerceIn(minEdgeStrokeWidth, maxEdgeStrokeWidth), start = Offset( firstVertexCenterX.toPx(), @@ -73,4 +71,4 @@ fun EdgeView(viewModel: EdgeViewModel, scale: Float) { ) } } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt index ed94a873..2153b1af 100644 --- a/app/src/main/kotlin/view/graph/VertexView.kt +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -14,32 +14,28 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.* +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import maxVertexRadius +import minVertexRadius import viewmodel.WindowViewModel import viewmodel.graph.VertexViewModel -import kotlin.math.min -import kotlin.ranges.coerceIn @Composable fun VertexView(viewModel: VertexViewModel, scale: Float) { val coroutineScope = rememberCoroutineScope { Dispatchers.Default } val windowVM = WindowViewModel() windowVM.SetCurrentDimensions() - val density = LocalDensity.current.density - val maxRadius = 35.dp // TODO: move to shared const file - val minRadius = 7.dp - - val adjustedX = (viewModel.x.value) - val adjustedY = (viewModel.y.value) - val adjustedRadius = (viewModel.radius * scale).coerceIn(minRadius, maxRadius) + val adjustedX = viewModel.x.value + val adjustedY = viewModel.y.value + val adjustedRadius = (viewModel.radius * scale).coerceIn(minVertexRadius, maxVertexRadius) Box( modifier = Modifier diff --git a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt index 525cfa7b..0a07ecd6 100644 --- a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt @@ -14,10 +14,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import viewmodel.graph.CreateGraphViewModel +import viewmodel.graph.SetupGraphViewModel @Composable -fun CreateGraphDialogWindow(viewModel: CreateGraphViewModel) { +fun CreateGraphDialogWindow(viewModel: SetupGraphViewModel) { var closeDialog = remember { mutableStateOf(false) } val selectedStoredDataIndex = remember { mutableStateOf(0) } @@ -185,28 +185,28 @@ fun CreateGraphDialogWindow(viewModel: CreateGraphViewModel) { @Composable fun onCreateGraphClicked( - viewModel: CreateGraphViewModel, + viewModel: SetupGraphViewModel, storedDataIndex: Int, orientationIndex: Int, weightnessIndex: Int ) { val storedData = when (storedDataIndex) { - 0 -> CreateGraphViewModel.GraphType.Integer - 1 -> CreateGraphViewModel.GraphType.UInteger - 2 -> CreateGraphViewModel.GraphType.String - else -> CreateGraphViewModel.GraphType.Integer // default to integer + 0 -> SetupGraphViewModel.GraphType.Integer + 1 -> SetupGraphViewModel.GraphType.UInteger + 2 -> SetupGraphViewModel.GraphType.String + else -> SetupGraphViewModel.GraphType.Integer // default to integer } val graphStructure = when (orientationIndex) { - 0 -> CreateGraphViewModel.GraphStructure.Undirected - 1 -> CreateGraphViewModel.GraphStructure.Directed - else -> CreateGraphViewModel.GraphStructure.Undirected // default to undirected + 0 -> SetupGraphViewModel.GraphStructure.Undirected + 1 -> SetupGraphViewModel.GraphStructure.Directed + else -> SetupGraphViewModel.GraphStructure.Undirected // default to undirected } val weight = when (weightnessIndex) { - 0 -> CreateGraphViewModel.Weight.Unweighted - 1 -> CreateGraphViewModel.Weight.Weighted - else -> CreateGraphViewModel.Weight.Unweighted // default to unweighted + 0 -> SetupGraphViewModel.Weight.Unweighted + 1 -> SetupGraphViewModel.Weight.Weighted + else -> SetupGraphViewModel.Weight.Unweighted // default to unweighted } return viewModel.createGraph(storedData, graphStructure, weight) diff --git a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt index dd276733..c95114d4 100644 --- a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt @@ -13,9 +13,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import model.graphs.abstractGraph.Graph -import view.tabScreen.FileControlTab -import viewmodel.graph.CreateGraphViewModel +import viewmodel.graph.SetupGraphViewModel class SelectInitDialogWindow( @@ -81,7 +79,7 @@ class SelectInitDialogWindow( } if (showCreateGraphDialog) { - CreateGraphDialogWindow(CreateGraphViewModel()) + CreateGraphDialogWindow(SetupGraphViewModel()) } // if (showImportTab) { diff --git a/app/src/main/kotlin/view/utils/ZoomBox.kt b/app/src/main/kotlin/view/utils/ZoomBox.kt index d5ea6a9e..601c6cae 100644 --- a/app/src/main/kotlin/view/utils/ZoomBox.kt +++ b/app/src/main/kotlin/view/utils/ZoomBox.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import scaleFactor @Composable fun ZoomBox(currentScale: MutableState) { @@ -22,13 +23,13 @@ fun ZoomBox(currentScale: MutableState) { verticalArrangement = Arrangement.Bottom ) { FloatingActionButton(onClick = { - currentScale.value = (1.1f * currentScale.value).coerceIn(0.7f, 1.9f) // TODO: move to const + currentScale.value = (scaleFactor * currentScale.value).coerceIn(0.7f, 1.9f) }) { Text("+") } Spacer(modifier = Modifier.height(8.dp)) FloatingActionButton(onClick = { - currentScale.value = (currentScale.value / 1.1f).coerceIn(0.7f, 1.9f) + currentScale.value = (currentScale.value / scaleFactor).coerceIn(0.7f, 1.9f) }) { Text("-") } diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index ad1e1d79..2c3684e0 100644 --- a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -2,13 +2,11 @@ package viewmodel.graph import androidx.compose.runtime.State import androidx.compose.ui.unit.Dp +import ARROW_DEPTH +import ARROW_SIZE +import SQRT_3 import kotlin.math.sqrt -// TODO: move to a seperate const file -const val ARROW_SIZE = 20f -const val ARROW_DEPTH = 2.5f -const val SQRT_3 = 1.732f - class EdgeViewModel( val firstVertex: VertexViewModel, val secondVertex: VertexViewModel, diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index a32878ca..faf8bb58 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -52,7 +52,7 @@ class GraphViewModel( return newVertex.id } - fun addEdge(firstId: Int, secondId: Int) { + fun addEdge(firstId: Int, secondId: Int, weight: Int = 1) { val firstVertex = graph.getVertices().find { it.id == firstId } ?: throw NoSuchElementException("No vertex found with id $firstId") val secondVertex = graph.getVertices().find { it.id == secondId } @@ -64,7 +64,7 @@ class GraphViewModel( ?: throw NoSuchElementException("No ViewModel found for vertex2") val newEdge = graph.addEdge(firstVertexVM.vertex, secondVertexVM.vertex) - + graph.getWeightMap()[newEdge] = weight updateEdgeViewModels(newEdge) } diff --git a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt similarity index 99% rename from app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt rename to app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt index 666c3b81..f1dabae4 100644 --- a/app/src/main/kotlin/viewmodel/graph/CreateGraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt @@ -8,7 +8,7 @@ import model.graphs.WeightedUndirectedGraph import view.MainScreen import viewmodel.MainScreenViewModel -class CreateGraphViewModel { +class SetupGraphViewModel { sealed class GraphType { object Integer : GraphType() object UInteger : GraphType() diff --git a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt index 4de351c3..cc43d8f3 100644 --- a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt +++ b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt @@ -13,7 +13,6 @@ class TFDPLayout( // controls the longest distance of neighbors in the layout - Y ) { fun place(width: Double, height: Double, vertices: Collection>) { -// val forces = Array(vertices.size) { mk.ndarray(mk[0.0, 0.0]) } // TODO val forces = Array(vertices.size) { Pair(0f, 0f) } var k = 0 @@ -54,9 +53,11 @@ class TFDPLayout( } for (vi in vertices) { // check borders + if (vi.x.value > (width.toFloat() - 360 - vi.radius.value * 2).dp) vi.x.value = vi.x.value / 2 + if (vi.y.value > (height.toFloat() - vi.radius.value * 2).dp) vi.y.value = vi.y.value / 2 + vi.x.value = vi.x.value.coerceIn(0.dp, (width.toFloat() - 360 - vi.radius.value * 2).dp) vi.y.value = vi.y.value.coerceIn(0.dp, (height.toFloat() - vi.radius.value * 2).dp) - println("${vi.x.value} ${vi.y.value}") } } } diff --git a/app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt b/app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt deleted file mode 100644 index 175f00a8..00000000 --- a/app/src/main/kotlin/viewmodel/graph/TestRepresentation.kt +++ /dev/null @@ -1,47 +0,0 @@ -package viewmodel.graph - -import androidx.compose.ui.unit.dp -import kotlin.math.cos -import kotlin.math.min -import kotlin.math.sin - -class TestRepresentation() { - - fun place(width: Double, height: Double, vertices: Collection>) { - if (vertices.isEmpty()) { - println("CircularPlacementStrategy.place: there is nothing to place 👐🏻") - return - } - - val center = Pair(width / 2, height / 2) - val angle = 2 * Math.PI / vertices.size - - val sorted = vertices.sortedBy { it.getVertexData } - val first = sorted.first() - var point = Pair(center.first, center.second - min(width, height) / 2) - first.x.value = point.first.dp - first.y.value = point.second.dp - - sorted.drop(1).onEach { - point = point.rotate(center, angle) - it.x.value = point.first.dp - it.y.value = 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 - } -} From e2b4f577c346de30d1bd2d90bc152f7335333693 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Tue, 28 May 2024 14:16:24 +0300 Subject: [PATCH 66/77] feat: add clustering algorithm #47 * feat: add louvain clustering algorithm implementation by jetbrains-research * feat: add class for running Louvain algorithm * test: add tests for clustering * chore: add license for used source code --- LICENSE.md | 195 ++++++++++++++++ .../clustering/LouvainClustering.kt | 68 ++++++ .../clustering/implementation/API.kt | 33 +++ .../clustering/implementation/Community.kt | 100 ++++++++ .../clustering/implementation/Link.kt | 21 ++ .../clustering/implementation/Louvain.kt | 214 ++++++++++++++++++ .../clustering/implementation/Node.kt | 55 +++++ .../clustering/LouvainClusteringTest.kt | 41 ++++ 8 files changed, 727 insertions(+) create mode 100644 app/src/main/kotlin/model/algorithms/clustering/LouvainClustering.kt create mode 100644 app/src/main/kotlin/model/algorithms/clustering/implementation/API.kt create mode 100644 app/src/main/kotlin/model/algorithms/clustering/implementation/Community.kt create mode 100644 app/src/main/kotlin/model/algorithms/clustering/implementation/Link.kt create mode 100644 app/src/main/kotlin/model/algorithms/clustering/implementation/Louvain.kt create mode 100644 app/src/main/kotlin/model/algorithms/clustering/implementation/Node.kt create mode 100644 app/src/test/kotlin/model/algorithms/clustering/LouvainClusteringTest.kt diff --git a/LICENSE.md b/LICENSE.md index 6a15320c..351396e8 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -19,3 +19,198 @@ 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. + +--- + +All files in the package model.algorithms.clustering.implementation are provided under the following license: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2021-2022 JetBrains s.r.o. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/app/src/main/kotlin/model/algorithms/clustering/LouvainClustering.kt b/app/src/main/kotlin/model/algorithms/clustering/LouvainClustering.kt new file mode 100644 index 00000000..d4e1a5b5 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/LouvainClustering.kt @@ -0,0 +1,68 @@ +package model.algorithms.clustering + +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import model.algorithms.clustering.implementation.Link +import model.algorithms.clustering.implementation.getPartition + +class LouvainClustering { + fun findClusters(graph: Graph): Set>> { + if (graph.getVertices().size == 1) return setOf(setOf(graph.getVertices()[0])) + + val links = convertToAPIFormat(graph) + + val result = getPartition(links) + + return convertResult(result, graph) + } + + private fun convertToAPIFormat(graph: Graph): List { + val links = mutableListOf() + + for (edge in graph.getEdges()) { + val id1 = edge.vertex1.id + val id2 = edge.vertex2.id + + val weight: Double = + when (graph) { + is WeightedUndirectedGraph -> graph.getWeight(edge).toDouble() + is WeightedDirectedGraph -> graph.getWeight(edge).toDouble() + else -> 1.0 + } + + links += EdgeLink(id1, id2, weight) + } + + return links + } + + private fun convertResult(resultMap: Map, graph: Graph): Set>> { + val vertices = graph.getVertices() + val communities = mutableSetOf>>() + + var currentCommunityId = 0 + var currentCommunity = mutableSetOf>() + + for (vertexId in resultMap.keys) { + if (resultMap[vertexId] == currentCommunityId) currentCommunity += vertices[vertexId] + else { + communities += currentCommunity + + currentCommunityId++ + currentCommunity = mutableSetOf() + + currentCommunity += vertices[vertexId] + } + } + + return communities + } + + inner class EdgeLink(private val vertex1Id: Int, private val vertex2Id: Int, val weight: Double) : Link { + override fun source() = vertex1Id + override fun target() = vertex2Id + override fun weight() = weight + } +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/API.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/API.kt new file mode 100644 index 00000000..5f6230eb --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/API.kt @@ -0,0 +1,33 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file was changed compared to the source (one public method was deleted) + */ + +package model.algorithms.clustering.implementation + +/** + * Runs Louvain algorithm to approximate the best modularity partition and returns a corresponding mapping of nodes to communities. + * If depth > 0 then algorithm tries to split large communities into smaller ones depth times recursively. + * + * @param depth Number of attempts to split large communities + * @return Map: nodeIndex -> communityIndex + */ +fun getPartition(links: List, depth: Int = 0): Map { + val louvain = Louvain(links) + louvain.optimizeModularity(depth) + return louvain.resultingCommunities() +} + diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Community.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Community.kt new file mode 100644 index 00000000..22e8ef09 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Community.kt @@ -0,0 +1,100 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model.algorithms.clustering.implementation + +import kotlin.math.pow +import kotlin.math.sqrt + +internal fun Community(nodeIndex: Int, graph: List): Community { + val node = graph[nodeIndex] + return Community(node.selfLoopsWeight, node.outDegree, mutableSetOf(nodeIndex)) +} + +internal class Community( + private var selfLoopsWeight: Double = 0.0, + private var outLinksWeight: Double = 0.0, + val nodes: MutableSet = mutableSetOf() +) { + + private fun totalWeightsSum() = selfLoopsWeight + outLinksWeight + + fun addNode(index: Int, nodes: List) { + val node = nodes[index] + node.incidentLinks.forEach { link -> + if (link.to in this.nodes) { + selfLoopsWeight += 2 * link.weight + outLinksWeight -= link.weight + } else { + outLinksWeight += link.weight + } + } + selfLoopsWeight += node.selfLoopsWeight + this.nodes.add(index) + } + + fun removeNode(index: Int, nodes: List): Boolean { + val node = nodes[index] + node.incidentLinks.forEach { link -> + if (link.to in this.nodes) { + selfLoopsWeight -= 2 * link.weight + outLinksWeight += link.weight + } else { + outLinksWeight -= link.weight + } + } + this.nodes.remove(index) + selfLoopsWeight -= node.selfLoopsWeight + return this.nodes.size == 0 + } + + fun modularityChangeIfNodeAdded(node: Node, graphWeight: Double): Double = + (1 / graphWeight) * (weightsToNode(node) - totalWeightsSum() * node.degree() / (2 * graphWeight)) + + private fun weightsToNode(node: Node): Double = node.incidentLinks.filter { it.to in nodes }.sumOf { it.weight } + + fun computeModularity(graphWeight: Double): Double = (selfLoopsWeight / (2 * graphWeight)) - (totalWeightsSum() / (2 * graphWeight)).pow(2) + + fun toLouvainNode(nodes: List): Node { + val newIndex = nodes[this.nodes.first()].community + val consumedNodes = this.nodes.flatMap { nodes[it].originalNodes }.toSet() + var newSelfLoopsWeight = 0.0 + + val incidentLinksMap = mutableMapOf() + this.nodes.forEach { nodeIndex -> + newSelfLoopsWeight += nodes[nodeIndex].selfLoopsWeight + nodes[nodeIndex].incidentLinks.forEach { link -> + val toNewNode = nodes[link.to].community + if (toNewNode != newIndex) { + if (toNewNode in incidentLinksMap) { + incidentLinksMap[toNewNode] = incidentLinksMap[toNewNode]!! + link.weight + } else { + incidentLinksMap[toNewNode] = link.weight + } + } else { + newSelfLoopsWeight += link.weight + } + } + } + val links = incidentLinksMap.map { InternalLink(it.key, it.value) } + + return Node(newIndex, consumedNodes, links, newSelfLoopsWeight) + } + + /** + * If communities size is less than sqrt(2 * graphWeight) then merging it with another one will always increase modularity. + * Hence, if community size is greater than sqrt(2 * graphWeight), it might actually consist of several smaller communities. + */ + fun overResolutionLimit(graphWeight: Double): Boolean = selfLoopsWeight >= sqrt(2 * graphWeight) +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Link.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Link.kt new file mode 100644 index 00000000..284c5971 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Link.kt @@ -0,0 +1,21 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model.algorithms.clustering.implementation + +interface Link { + fun source(): Int + fun target(): Int + fun weight(): Double +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Louvain.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Louvain.kt new file mode 100644 index 00000000..973df394 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Louvain.kt @@ -0,0 +1,214 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model.algorithms.clustering.implementation + +/** + * Class that encapsulates the Louvain algorithm. + */ +internal class Louvain( + private val links: List +) { + private var communities: MutableMap + private var nodes: List = emptyList() + private var graphWeight: Double + private val originalNodesNumber: Int + + init { + buildNodesFromLinks() + originalNodesNumber = nodes.size + communities = nodes.withIndex().associate { it.index to Community(it.index, nodes) }.toMutableMap() + graphWeight = computeGraphWeight() + } + + private fun buildNodesFromLinks() { + val nodeIndices = links.flatMap { listOf(it.source(), it.target()) }.distinct().sorted() + val mutableNodes = nodeIndices + .withIndex() + .associateBy({ it.value }, { MutableNode(it.index, setOf(it.value)) }) + links.forEach { link -> + if (link.source() == link.target()) { + mutableNodes[link.source()]!!.selfLoopsWeight += 2 * link.weight() + } else { + val newSource = mutableNodes[link.source()]!!.community + val newTarget = mutableNodes[link.target()]!!.community + mutableNodes[link.source()]!!.incidentLinks.add(InternalLink(newTarget, link.weight())) + mutableNodes[link.target()]!!.incidentLinks.add(InternalLink(newSource, link.weight())) + } + } + nodes = mutableNodes.values.map { it.toNode() } + } + + private fun computeGraphWeight() = + nodes.sumOf { n -> n.incidentLinks.sumOf { l -> l.weight } + n.selfLoopsWeight } / 2 + + private fun aggregateCommunities() { + // re-index communities in nodes + communities.values.withIndex().forEach { (newIndex, community) -> + community.nodes.forEach { nodeIndex -> + nodes[nodeIndex].community = newIndex + } + } + + val newNodes = communities.values.map { it.toLouvainNode(nodes) } + val newCommunities = + newNodes.withIndex().associateBy({ it.index }, { Community(it.index, nodes) }).toMutableMap() + + nodes = newNodes + communities = newCommunities + } + + private fun moveNode(nodeIndex: Int, node: Node, toCommunityIndex: Int) { + val from = communities[node.community]!! + if (from.removeNode(nodeIndex, nodes)) { + communities.remove(node.community) + } + node.community = toCommunityIndex + communities[toCommunityIndex]!!.addNode(nodeIndex, nodes) + } + + private fun computeCostOfMovingOut(index: Int, node: Node): Double { + val theCommunity = communities[node.community]!! + theCommunity.removeNode(index, nodes) + val cost = theCommunity.modularityChangeIfNodeAdded(node, graphWeight) + theCommunity.addNode(index, nodes) + return cost + } + + /** + * Step I of the algorithm: + * For each node i evaluate the gain in modularity if node i is moved to the community of one of its neighbors j. + * Then move node i in the community for which the modularity gain is the largest, but only if this gain is positive. + * This process is applied to all nodes until no further improvement can be achieved, completing Step I. + * @see optimizeModularity + */ + private fun findLocalMaxModularityPartition() { + var repeat = true + while (repeat) { + repeat = false + for ((i, node) in nodes.withIndex()) { + var bestCommunity = node.community + var maxDeltaM = 0.0 + val costOfMovingOut = computeCostOfMovingOut(i, node) + for (communityIndex in node.neighbourCommunities(nodes)) { + if (communityIndex == node.community) { + continue + } + val toCommunity = communities[communityIndex]!! + val deltaM = toCommunity.modularityChangeIfNodeAdded(node, graphWeight) - costOfMovingOut + if (deltaM > maxDeltaM) { + bestCommunity = communityIndex + maxDeltaM = deltaM + } + } + if (bestCommunity != node.community) { + moveNode(i, node, bestCommunity) + repeat = true + } + } + } + } + + fun computeModularity() = communities.values.sumOf { it.computeModularity(graphWeight) } + + fun optimizeModularity(depth: Int = 0) { + var bestModularity = computeModularity() + var bestCommunities = communities + var bestNodes = nodes + do { + val from = communities.size + findLocalMaxModularityPartition() + aggregateCommunities() + val newModularity = computeModularity() + if (newModularity > bestModularity) { + bestModularity = newModularity + bestCommunities = communities + bestNodes = nodes + } + } while (communities.size != from) + communities = bestCommunities + nodes = bestNodes + if (communities.size != 1 && depth != 0) { + refine(depth) + } + } + + fun resultingCommunities(): Map { + val communitiesMap = mutableMapOf() + communities.forEach { (communityIndex, community) -> + community.nodes.forEach { nodeIndex -> + val node = nodes[nodeIndex] + node.originalNodes.forEach { + communitiesMap[it] = communityIndex + } + } + } + return communitiesMap + } + + fun assignCommunities(communitiesMap: Map) { + communities.clear() + buildNodesFromLinks() + buildCommunitiesFromMap(communitiesMap) + } + + private fun buildCommunitiesFromMap(communitiesMap: Map) { + // create all necessary communities + for (entry in communitiesMap) { + val communityIndex = entry.value + if (communityIndex !in communities.keys) { + communities[communityIndex] = Community() + } + } + + val nodeIndicesMap = communitiesMap.keys.sorted().withIndex().associateBy({ it.value }, { it.index }) + + // distribute the nodes among communities + for (entry in communitiesMap) { + val nodeIndex = nodeIndicesMap[entry.key]!! + val communityIndex = entry.value + + nodes[nodeIndex].community = -1 + communities[communityIndex]!!.addNode(nodeIndex, nodes) + nodes[nodeIndex].community = communityIndex + } + } + + private fun refine(depth: Int = 0) { + var communitiesMap = resultingCommunities() + var resultingCommunitiesNumber = communitiesMap.values.distinct().size + links + .filter { communitiesMap[it.source()] == communitiesMap[it.target()] } + .groupBy({ communitiesMap[it.source()]!! }, { it }) + .filter { communities[it.key]!!.overResolutionLimit(graphWeight) } + .forEach { (communityIndex, links) -> + val thisLouvain = Louvain(links) + thisLouvain.optimizeModularity(depth - 1) + val thisMap = thisLouvain.resultingCommunities() + val reindex = reIndexMap(thisMap, communityIndex, resultingCommunitiesNumber) + communitiesMap = communitiesMap + reindex + resultingCommunitiesNumber = communitiesMap.values.distinct().size + } + assignCommunities(communitiesMap) + } + + private fun reIndexMap(theMap: Map, saveIndex: Int, startFrom: Int) = theMap + .mapValues { (_, communityIndex) -> + if (communityIndex == 0) { + saveIndex + } else { + communityIndex + startFrom - 1 + } + } +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/implementation/Node.kt b/app/src/main/kotlin/model/algorithms/clustering/implementation/Node.kt new file mode 100644 index 00000000..a7ec080d --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/clustering/implementation/Node.kt @@ -0,0 +1,55 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model.algorithms.clustering.implementation + +/** + * Simple link representation used inside Node and Louvain algorithm. + */ +internal class InternalLink( + val to: Int, + val weight: Double +) + +/** + * @param community Index of a community to which the node has been assigned. + * @param selfLoopsWeight Weight of all links that start and end at originalNodes making up this node * 2. + */ +internal sealed class BaseNode( + var community: Int, + val originalNodes: Set, + open val incidentLinks: List, + open val selfLoopsWeight: Double +) { + fun neighbourCommunities(nodes: List) = incidentLinks.map { nodes[it.to].community }.distinct().filter { it != community } +} + +internal class Node( + community: Int, + originalNodes: Set, + incidentLinks: List, + selfLoopsWeight: Double = 0.0 +) : BaseNode(community, originalNodes, incidentLinks, selfLoopsWeight) { + val outDegree = incidentLinks.sumOf { it.weight } + fun degree() = outDegree + selfLoopsWeight +} + +internal class MutableNode( + community: Int, + originalNodes: Set, + override val incidentLinks: MutableList = mutableListOf(), + override var selfLoopsWeight: Double = 0.0 +) : BaseNode(community, originalNodes, incidentLinks, selfLoopsWeight) { + fun toNode(): Node = Node(community, originalNodes, incidentLinks) +} diff --git a/app/src/test/kotlin/model/algorithms/clustering/LouvainClusteringTest.kt b/app/src/test/kotlin/model/algorithms/clustering/LouvainClusteringTest.kt new file mode 100644 index 00000000..b15060aa --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/clustering/LouvainClusteringTest.kt @@ -0,0 +1,41 @@ +package model.algorithms.clustering + +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import util.annotations.TestAllGraphTypes +import util.setupAbstractGraph + +class LouvainClusteringTest { + val louvain = LouvainClustering() + + @TestAllGraphTypes + fun `graph of 1 vertex should have one community`(graph: Graph) { + val v0 = graph.addVertex(0) + + val actualValue = louvain.findClusters(graph) + val expectedValue = setOf(setOf(v0)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllGraphTypes + fun `empty graph should have no communities`(graph: Graph) { + val actualValue = louvain.findClusters(graph) + val expectedValue = emptySet>>() + + assertEquals(expectedValue, actualValue) + } + + @TestAllGraphTypes + fun `graph doesn't change`(graph: Graph) { + val graphStructure = setupAbstractGraph(graph) + + louvain.findClusters(graph) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } +} From c3ab6111df0d0c9c766a6447ff9d7614a3c8100d Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Wed, 29 May 2024 10:11:43 +0300 Subject: [PATCH 67/77] feat: bolster SQLite management, improve UI and error handling #50 * refactor: correct packages * fix: remove erase data variable * feat: implement DB creation and saving * feat: add draft UI for file-control tab * feat: implement general function to display error windows * feat: set up the process of graph import, MSVM-VM-SQL connection * feat: implement dialog window for graph import * feat: implement DB handling. TOFIX: import * feat: add auxiliary queries for SQLite * fix: remove coroutines and launch ef., due to composable conflict * refactor: remove redundant lines, cleared up a little * fix: edit layout coefficients * feat: add draft SQLite DB * fix: display the FAQ and zoom * test: enhance app with integration test draft #46 * build: add new dependencies for int. tests * test: implement first integration test * feat: add getter for edge's direction type * refactor: move graphs to a separate package in module * feat: add test-tags * fix: add tag for 'not hoverable' FAQ * refactor: edit test name * feat: add basic file control tab fields and buttons #48 * fix: small typo * fix: correct graph types picked by user, change default types * feat: add text field for graph name, save and load buttons * feat: add base dialog windows which can be dismissed * feat: introduce more variables * feat: add dropdown menu for database choosing * feat: add button and dialog to choose file when json is chosen * refactor: clean up code, resolve some TODO's #49 * feat: move constants to a separate file * fix: make 'getWeightMap' return mutable map * refactor: rename 'CreateGraphVM' class, edit 'addEdge' * refactor: resolve TODO's * fix: edit name of radius variable * feat: add error message handle * fix: edit button title, change ui layout * feat: add SQLite save+import integration into UI * fix: update queries, due to error with id * feat: add graph name update and deletion * fix: remove line, that ban MainScreen update * feat: handle EditDB window * feat: implement EditDB window * refactor: correct dimensions and indices * build: remove vulnerable dependencies --------- Co-authored-by: Daniel Vlasenco --- app/build.gradle.kts | 4 +- app/database/my_graph_database.db | Bin 0 -> 20480 bytes .../kotlin/model/io/sql/SQLDatabaseModule.kt | 337 ++++++++++++++++++ app/src/main/kotlin/model/io/sql/queries.txt | 58 +++ app/src/main/kotlin/view/MainScreen.kt | 13 +- .../kotlin/view/tabScreen/FileControlTab.kt | 100 +++++- .../main/kotlin/view/tabScreen/GeneralTab.kt | 26 +- .../view/utils/CreateGraphDialogWindow.kt | 34 +- .../main/kotlin/view/utils/EditDBWindow.kt | 165 +++++++++ app/src/main/kotlin/view/utils/ErrorWindow.kt | 53 +++ .../view/utils/ImportGraphDialogWindow.kt | 104 ++++++ .../view/utils/SelectInitDialogWindow.kt | 6 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 15 +- .../kotlin/viewmodel/graph/GraphViewModel.kt | 4 +- .../viewmodel/graph/SetupGraphViewModel.kt | 104 +++++- .../main/kotlin/viewmodel/graph/TFDPLayout.kt | 8 +- .../kotlin/viewmodel/graph/VertexViewModel.kt | 7 +- 17 files changed, 964 insertions(+), 74 deletions(-) create mode 100644 app/database/my_graph_database.db create mode 100644 app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt create mode 100644 app/src/main/kotlin/model/io/sql/queries.txt create mode 100644 app/src/main/kotlin/view/utils/EditDBWindow.kt create mode 100644 app/src/main/kotlin/view/utils/ErrorWindow.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 482324ee..0456a5ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,9 +17,9 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") - implementation("org.xerial:sqlite-jdbc:3.41.2.1") + implementation("org.xerial:sqlite-jdbc:3.41.2.2") implementation("org.slf4j:slf4j-api:1.7.36") - implementation("ch.qos.logback:logback-classic:1.4.11") + implementation("ch.qos.logback:logback-classic:1.4.12") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3") diff --git a/app/database/my_graph_database.db b/app/database/my_graph_database.db new file mode 100644 index 0000000000000000000000000000000000000000..d24a2287951e612dbd890325ce73faf0abef4d11 GIT binary patch literal 20480 zcmeI2PiP!f9LML+o0<2vyYow$rju>5?KClFV_R`&BcT^-jMKQ7ZraU8%|XK2og|BC z+U#zk1qD+NdJ{oB=}o+d2n9jBh=)Qi#nyudF&9w;6;xDOdh$2(W-uv9wW3hr_sH(| z`+mRQ{NBuG_A)OseQL7N4&3>(%`>&OTU0s}RaG8#T}4sS_%ZO43>^(iCaCHrzmrWX z_s@Ty!7R%>s6ffSp1F>O1`!|vM1Tko0U|&IhyW2F0z}~dLSX$Evz%N`y>O~sduAzE z3!3f5>7bRUF<$YY<{zC2ZR%QKUcGF`8=Yi`wlDy}|0Rq;y`<)~RcIds^q_{aT< zUmo|T-6+_~H|oQw;q@1F%W)j_<=N;8g8D+xioV#EbB{v*_KAl;g69i6UzsfMb}o1> zXe=zY6OIzcon8M)&2$bOQsvdRR+jJ?c&-(!tOm3$v4quV#12sedrg?{aC)Tn=5lYt);9j+;G4St7>yYsfvSrGvu zKm>>Y5g-CYfCvx)B0vO)01+Spw-89FdRCS17>Km4=hWo=lOZfjiRg_Eq3Mn)-|Yzh z|L@wLC~yOQf^XqVxC)oxWB35xg}2~!cm-a76_|&oVHzHThoA`e!9j4q24>&1f3d%_ zH|)>t%h-Sh5g-CYfCvx)B0vO)01+SpM1Tkof&Y$x<(i7hg%JDz8g$$c`$MM+&NQBj zgF5evoo4l-!y`D~CqtIbv$4~x9+|YbBSWTU?UfXyb3d%~P5n4$A7S;m;y9S3!d zcZOC7rdb{6a4jZ4W_6PZJF%L`#MaPwI<#EqOzet-*eDeTHA|qwM!MiKt6?K6v6?Vr zi;axXa-lK7;vhEC70la1q{v zci?S!6JCQ?;Uzc^O?VdSxEoN0V=w}FaG?h}fP-S+uz$9{wy)w|z=zn51`!|vM1Tko z0U|&IhyW2F0z`la5P^SAz#L+V3gUszdr=2P!7DZjN4+9y!5j=z-0Qdpb%2MV_%A(x zkzS6oqE{%I{ZU=PJSgi;H=Z8|)$+{!vF2j!i?opHMRm-PqF409KJ?w@=t$vcCW|&_ zCTnGS!l{AlV8kmJ4o)+Mu{RWEbW7PIr7IG1HwrM%=#*N*itf5(vg_EP(b&L gl#Gf{G=odAq?l5SNKA$TnrG-Tsxd`?$5fQeAAPf*@c;k- literal 0 HcmV?d00001 diff --git a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt new file mode 100644 index 00000000..0c3d11c0 --- /dev/null +++ b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt @@ -0,0 +1,337 @@ +package model.io.sql + +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.* +import kotlinx.coroutines.* +import model.graphs.abstractGraph.Graph +import view.MainScreen +import view.utils.ErrorWindow +import view.utils.getGraphVMParameter +import viewmodel.MainScreenViewModel +import viewmodel.graph.GraphViewModel +import viewmodel.graph.SetupGraphViewModel +import java.io.File +import java.sql.* +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +object SQLDatabaseModule { + private const val DB_DIRECTORY = "database" + private const val DB_NAME = "my_graph_database.db" + private const val QUERY_NAME = "queries.txt" + private const val QUERY_DIRECTORY = "src/main/kotlin/model/io/sql/" + private val DB_URL = "jdbc:sqlite:${File(DB_DIRECTORY, DB_NAME).absolutePath}" + + private fun readQueriesFromFile(): String { + val fileContent = File(QUERY_DIRECTORY, QUERY_NAME).readText() + return fileContent.trim() // Trim any leading/trailing whitespace + } + + private val insertQueries = readQueriesFromFile() + + init { + createDatabaseDirectory() + createTables() + } + + private fun createDatabaseDirectory() { + val dbDir = File(DB_DIRECTORY) + if (!dbDir.exists()) { + dbDir.mkdirs() + } + } + + private fun getConnection(): Connection { + return DriverManager.getConnection(DB_URL) + } + + private fun createTables() { + val createTableSQL = insertQueries.split(":")[7].trimIndent() + + val connection = getConnection() + connection.use { conn -> + conn.createStatement().use { statement -> + statement.executeUpdate(createTableSQL) + } + } + } + + + fun insertGraph(graphVM: GraphViewModel, graphName: String, graphType: String) { + val insertGraphSQL = insertQueries.split(":")[0] + val insertEdgesSQL = insertQueries.split(":")[1] + val insertVerticesSQL = insertQueries.split(":")[2] + + getConnection().use { connection -> + connection.autoCommit = false + try { + var graphId: Int + connection.prepareStatement(insertGraphSQL, Statement.RETURN_GENERATED_KEYS).use { statement -> + statement.setString(1, graphName) + statement.setString(2, graphType.split(" ")[0].replace("Graph", "")) + statement.setString(3, graphType.split(" ")[1]) + statement.executeUpdate() + val generatedKeys = statement.generatedKeys + if (generatedKeys.next()) { + graphId = generatedKeys.getInt(1) + } else { + throw SQLException("Failed to insert graph, no ID obtained.") + } + } + + graphVM.graph.getEdges().forEach { edge -> + connection.prepareStatement(insertEdgesSQL).use { statement -> + statement.setInt(1, graphId) + statement.setInt(2, edge.vertex1.id) + statement.setInt(3, edge.vertex2.id) + graphVM.graph.getWeightMap()[edge]?.let { weight -> + statement.setInt(4, weight) + } ?: statement.setNull(4, Types.INTEGER) + statement.executeUpdate() + } + } + + graphVM.graph.getVertices().forEach { vertex -> + connection.prepareStatement(insertVerticesSQL).use { statement -> + statement.setInt(1, graphId) + statement.setString(2, vertex.data.toString()) + statement.executeUpdate() + } + } + + connection.commit() + } catch (ex: SQLException) { + connection.rollback() + throw ex + } finally { + connection.autoCommit = true + } + } + } + + @Composable + fun getGraphNames(graphNames: MutableState>>) { + val selectNamesSQL = insertQueries.split(":")[3] + val showErrorMessage = remember { mutableStateOf(false) } + val errorMessage = remember { mutableStateOf("") } + graphNames.value = arrayListOf() + + try { + getConnection().use { connection -> + connection.prepareStatement(selectNamesSQL).use { statement -> + val resultSet = statement.executeQuery() + while (resultSet.next()) { + graphNames.value.add(Pair(resultSet.getInt("id"), resultSet.getString("name"))) + } + } + } + } catch (e: SQLException) { + showErrorMessage.value = true + errorMessage.value = e.message.toString() + } + if (showErrorMessage.value) { + ErrorWindow(errorMessage.value, { System.exit(-1) }) + } + } + + + @Composable + fun importGraph(graphId: Int) { + val graphVMState = remember { mutableStateOf?>(null) } + val showErrorMessage = remember { mutableStateOf(false) } + val updateIsRequired = remember { mutableStateOf(false) } + var currentGraphSetup: Pair, String>? = null + + try { + val connection = getConnection() + connection.use { + val selectGraphSQL = insertQueries.split(":")[6] + it.prepareStatement(selectGraphSQL).use { statement -> + statement.setInt(1, graphId) + val resultSet = statement.executeQuery() + + if (resultSet.next()) { + currentGraphSetup = importGraphInfo(graphId) + } else { + showErrorMessage.value = true + throw SQLException("Graph with ID $graphId not found.") + } + } + } + + + // Execute side-effect to create graph object + SetupGraphViewModel().createGraphObject( + currentGraphSetup?.first?.first as SetupGraphViewModel.GraphType, + currentGraphSetup?.first?.second as SetupGraphViewModel.GraphStructure, + currentGraphSetup?.first?.third as SetupGraphViewModel.Weight, + graphId, + graphVMState + ) + updateIsRequired.value = true + + } catch (e: SQLException) { + e.printStackTrace() + showErrorMessage.value = true + } + + if (updateIsRequired.value) return importGraphUI(showErrorMessage, graphVMState, graphId) + } + + @Composable + fun importGraphUI( + showErrorMessage: MutableState, + graphVMState: MutableState?>, + graphId: Int + ) { + if (showErrorMessage.value) { + ErrorWindow("Graph with ID $graphId not found.", { System.exit(-1) }) + System.exit(-1) + } + if (graphVMState.value != null) { + graphVMState.value?.updateIsRequired?.value = true + + MainScreen( + MainScreenViewModel( + graphVMState.value?.graph as Graph, + graphVMState.value?.graphType?.value as String, + graphVMState.value + ) + ) + + } else CircularProgressIndicator() + } + + fun updateImportedGraphVM( + graph: Graph, + graphId: Int, + graphVMState: MutableState?> + ): GraphViewModel? { + val selectGraphSQL = insertQueries.split(":")[6] + val selectVerticesSQL = insertQueries.split(":")[4] + val selectEdgesSQL = insertQueries.split(":")[5] + try { + getConnection().use { connection -> + connection.prepareStatement(selectGraphSQL).use { statement -> + statement.setInt(1, graphId) + val resultSet = statement.executeQuery() + if (resultSet.next()) { + val currentGraphSetup = importGraphInfo(graphId) + val graphVMType = + mutableStateOf(currentGraphSetup.first.second.toString() + "Graph" + " " + currentGraphSetup.first.first.toString()) + + val graphVM = GraphViewModel( + graph, + mutableStateOf(false), + mutableStateOf(false), + graphVMType, + mutableStateOf(currentGraphSetup.first.second.toString().contains("Directed")) + ) + + // Fetch vertices + connection.prepareStatement(selectVerticesSQL).use { vertexStatement -> + vertexStatement.setInt(1, graphId) + val vertexResultSet = vertexStatement.executeQuery() + while (vertexResultSet.next()) { + val vertexData = vertexResultSet.getString("data") + graphVM.addVertex(vertexData) + } + } + + // Fetch edges + connection.prepareStatement(selectEdgesSQL).use { edgeStatement -> + edgeStatement.setInt(1, graphId) + val edgeResultSet = edgeStatement.executeQuery() + while (edgeResultSet.next()) { + val vertex1Id = edgeResultSet.getInt("vertex1_id") + val vertex2Id = edgeResultSet.getInt("vertex2_id") + val weight = edgeResultSet.getInt("weight") + + graphVM.addEdge(vertex1Id, vertex2Id, weight) + } + } + graphVMState.value = graphVM + + } else { + throw SQLException("Graph with ID $graphId not found.") + } + } + } + } catch (e: SQLException) { + e.printStackTrace() + } + return graphVMState.value + } + + + private fun importGraphInfo(graphId: Int): Pair, String> { + val selectGraphSQL = insertQueries.split(":")[6] + var graphStructure: Int? + var graphWeight: Int? + var storedValueType: Int? + val graphName: String? + getConnection().use { connection -> + connection.prepareStatement(selectGraphSQL).use { statement -> + statement.setInt(1, graphId) + val resultSet = statement.executeQuery() + graphName = resultSet.getString("name") + val graphType = resultSet.getString("graph_type") + + graphStructure = if (graphType.contains("Undirected")) 0 else 1 + graphWeight = if (graphType.contains("Weighted")) 0 else 1 + + storedValueType = + if (resultSet.getString("stored_value_type") == "Int") 0 + else if (resultSet.getString("stored_value_type") == "UInt") 1 else 2 + } + } + return Pair(graphWeight?.let { + storedValueType?.let { it1 -> + graphStructure?.let { it2 -> + getGraphVMParameter(it1, it2, it) + } + } + } ?: throw NoSuchElementException("No info found about graph with ID = ${graphId}"), + graphName ?: throw NoSuchElementException("Graph with ID = ${graphId} has no name")) + } + + fun deleteGraph(graphId: Int) { + val deleteGraphSQL = insertQueries.split(":")[8] + val deleteGraphEdgesSQL = insertQueries.split(":")[9] + val deleteGraphVerticesSQL = insertQueries.split(":")[10] + try { + getConnection().use { connection -> + connection.prepareStatement(deleteGraphSQL).use { graphstatement -> + graphstatement.setInt(1, graphId) + graphstatement.executeUpdate() + } + connection.prepareStatement(deleteGraphEdgesSQL).use { edgeStatement -> + edgeStatement.setInt(1, graphId) + edgeStatement.executeUpdate() + } + connection.prepareStatement(deleteGraphVerticesSQL).use { vertexStatement -> + vertexStatement.setInt(1, graphId) + vertexStatement.executeUpdate() + } + } + } catch (e: SQLException) { + e.printStackTrace() + } + } + + fun renameGraph(graphId: Int, newGraphName: String) { + val renameGraphSQL = insertQueries.split(":")[11] + try { + getConnection().use { connection -> + connection.prepareStatement(renameGraphSQL).use { graphstatement -> + graphstatement.setString(1, newGraphName) + graphstatement.setInt(2, graphId) + graphstatement.executeUpdate() + } + } + } catch (e: SQLException) { + e.printStackTrace() + } + } +} diff --git a/app/src/main/kotlin/model/io/sql/queries.txt b/app/src/main/kotlin/model/io/sql/queries.txt new file mode 100644 index 00000000..24433cf9 --- /dev/null +++ b/app/src/main/kotlin/model/io/sql/queries.txt @@ -0,0 +1,58 @@ +-- Insert graph query +INSERT INTO graphs(name, graph_type, stored_value_type) VALUES (?, ?, ?): + +-- Insert edges query +INSERT INTO edges(graph_id, vertex1_id, vertex2_id, weight) VALUES (?, ?, ?, ?): + +-- Insert vertices query +INSERT INTO vertices(graph_id, data) VALUES (?, ?): + +-- Select names query +SELECT id, name FROM graphs: + +-- Select vertices query +SELECT id, data FROM vertices WHERE graph_id = ?: + +-- Select edges query +SELECT id, vertex1_id, vertex2_id, weight FROM edges WHERE graph_id = ?: + +-- Select graph query +SELECT id, name, graph_type, stored_value_type FROM graphs WHERE id = ?: + +-- Create all graph tables query +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS graphs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + graph_type TEXT NOT NULL, + stored_value_type TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS edges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + graph_id INTEGER NOT NULL, + vertex1_id INTEGER NOT NULL, + vertex2_id INTEGER NOT NULL, + weight INTEGER, + FOREIGN KEY (graph_id) REFERENCES graphs(id) +); + +CREATE TABLE IF NOT EXISTS vertices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + graph_id INTEGER NOT NULL, + data TEXT, + FOREIGN KEY (graph_id) REFERENCES graphs(id) +);: + +-- Delete from graphs table +DELETE FROM graphs WHERE id = ?: + +-- Delete from edges table +DELETE FROM edges WHERE graph_id = ?: + +-- Delete from vertices table +DELETE FROM vertices WHERE graph_id = ?: + +-- Rename graph in graph table +UPDATE graphs SET name = ? WHERE id = ?: \ No newline at end of file diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt index d02db404..eb2b7a3c 100644 --- a/app/src/main/kotlin/view/MainScreen.kt +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -1,14 +1,18 @@ package view import MyAppTheme +import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp import view.graph.GraphView import view.tabScreen.TabHandler import view.utils.FAQBox @@ -24,11 +28,12 @@ fun MainScreen(viewmodel: MainScreenViewModel) { Row { TabHandler(viewmodel) - Surface(modifier = Modifier.fillMaxSize()) { GraphView(viewmodel.graphViewModel, scale) } + Surface(modifier = Modifier.fillMaxSize()) { + GraphView(viewmodel.graphViewModel, scale) + } } // Hoverable box over the existing Surface FAQBox(interactionSource, viewmodel) - ZoomBox(scale) } } diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index ae24a215..01b4d9f5 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -17,6 +17,10 @@ import viewmodel.graph.GraphViewModel import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.delay +import model.io.sql.SQLDatabaseModule +import view.utils.EditDBWindow +import view.utils.ErrorWindow import java.awt.FileDialog import java.awt.Frame @@ -27,6 +31,12 @@ fun FileControlTab(graphVM: GraphViewModel) { var showLoadDialog by remember { mutableStateOf(false) } var graphName by remember { mutableStateOf("") } var showEnterPathField by remember { mutableStateOf(false) } + var showErrorWindow by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + var showEditDialog by remember { mutableStateOf(false) } + + val databases = arrayOf("SQLite", "Neo4j", "JSON") + var selectedDatabase by remember { mutableStateOf(databases[0]) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { Row(modifier = Modifier.height(0.dp)) {} @@ -47,7 +57,9 @@ fun FileControlTab(graphVM: GraphViewModel) { Column(modifier = Modifier.width(tabWidth).fillMaxHeight(), Arrangement.Center) { TextField( value = graphName, - onValueChange = { graphName = it }, + onValueChange = { newValue -> + graphName = newValue + }, modifier = Modifier .fillMaxWidth() .height(fieldHeight) @@ -74,9 +86,6 @@ fun FileControlTab(graphVM: GraphViewModel) { ) { var expanded by remember { mutableStateOf(false) } - val databases = arrayOf("SQLite", "Neo4j", "JSON") - var selectedDatabase by remember { mutableStateOf(databases[0])} - ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { @@ -128,6 +137,7 @@ fun FileControlTab(graphVM: GraphViewModel) { Text("Select File") } } + } Row( @@ -137,7 +147,10 @@ fun FileControlTab(graphVM: GraphViewModel) { Column(modifier = Modifier.width(fieldWidth).fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize().height(fieldHeight), - onClick = { showSaveDialog = true }, + onClick = { + showSaveDialog = true + graphName = "" + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { Text("Save") @@ -146,13 +159,33 @@ fun FileControlTab(graphVM: GraphViewModel) { Column(modifier = Modifier.width(fieldWidth).fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize().height(fieldHeight), - onClick = { showLoadDialog = true }, + onClick = { + showLoadDialog = true + graphName = "" + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { Text("Load") } } } + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize().height(fieldHeight), + onClick = { + showEditDialog = true + graphName = "" + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Edit DB") + } + } + } } @@ -161,19 +194,39 @@ fun FileControlTab(graphVM: GraphViewModel) { val padding = 14.dp if (showSaveDialog) { - Dialog( - onDismissRequest = { + if (selectedDatabase == "SQLite") { + val existingGraphNamesSQL = remember { mutableStateOf(arrayListOf>()) } + SQLDatabaseModule.getGraphNames(existingGraphNamesSQL) + + if (existingGraphNamesSQL.value.any { it.second == graphName }) { + showErrorWindow = true + errorMessage = "Graph with name: ${graphName} already exists" + graphName = "" showSaveDialog = false + } else { + SQLDatabaseModule.insertGraph(graphVM, graphName, graphVM.graphType.value) + Dialog( + onDismissRequest = { + showSaveDialog = false + } + ) { + Column( + modifier = Modifier + .background(Color.White) + .padding(16.dp) + .width(300.dp) + .height(50.dp) + ) { + Text("Graph '$graphName' loaded successfully!") + } + } + + // Automatically dismiss the dialog after 3 seconds + LaunchedEffect(Unit) { + delay(3000) + showSaveDialog = false + } } - ) { - Column( - modifier = - Modifier - .background(Color.White) - .padding(padding) - .width(dialogueWidth) - .height(dialogueHeight) - ) {} } } @@ -193,4 +246,17 @@ fun FileControlTab(graphVM: GraphViewModel) { ) {} } } + + if (showEditDialog) { + EditDBWindow(selectedDatabase) { showEditDialog = false } + } + + if (showErrorWindow) { + ErrorWindow( + errorMessage + ) { + showErrorWindow = false + errorMessage = "" + } + } } diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index b13ca93a..2c1e9721 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import view.utils.ErrorWindow import viewmodel.WindowViewModel import viewmodel.graph.GraphViewModel @@ -27,6 +28,7 @@ fun GeneralTab(graphVM: GraphViewModel) { var secondVertexId by remember { mutableStateOf("") } var secondVertexData by remember { mutableStateOf("") } var changesWereMade by remember { mutableStateOf(false) } + val showErrorWindow = remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { Row(modifier = Modifier.height(0.dp)) {} @@ -122,7 +124,7 @@ fun GeneralTab(graphVM: GraphViewModel) { secondVertexId = "" firstVertexId = "" } else { - // TODO: show error window + showErrorWindow.value = true } }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) @@ -156,20 +158,10 @@ fun GeneralTab(graphVM: GraphViewModel) { } if (showVertexAddDialog) { - Dialog( - onDismissRequest = { - showVertexAddDialog = false - } - ) { - vertexData = "" - + Dialog(onDismissRequest = {}) { Column( modifier = - Modifier - .background(Color.White) - .padding(16.dp) - .width(350.dp) - .height(200.dp) + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) ) { if (graphVM.verticesVM.isEmpty()) { Text("Input data of second vertex to create and connect with") @@ -206,6 +198,7 @@ fun GeneralTab(graphVM: GraphViewModel) { showVertexAddDialog = false errorMessage = "" secondVertexData = "" + vertexData = "" } } @@ -255,6 +248,7 @@ fun GeneralTab(graphVM: GraphViewModel) { showVertexAddDialog = false errorMessage = "" connectVertexId = "" + vertexData = "" } } ) { @@ -272,4 +266,8 @@ fun GeneralTab(graphVM: GraphViewModel) { graphVM.applyForceDirectedLayout(currentWindowVM.getWidth.value.toDouble(), currentWindowVM.getHeight.value.toDouble()) } -} + + if (showErrorWindow.value) { + ErrorWindow("No such Vertex", { showErrorWindow.value = false }) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt index 0a07ecd6..d1b5a220 100644 --- a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt @@ -18,8 +18,7 @@ import viewmodel.graph.SetupGraphViewModel @Composable fun CreateGraphDialogWindow(viewModel: SetupGraphViewModel) { - - var closeDialog = remember { mutableStateOf(false) } + val closeDialog = remember { mutableStateOf(false) } val selectedStoredDataIndex = remember { mutableStateOf(0) } val selectedOrientationIndex = remember { mutableStateOf(0) } val selectedWeightinessIndex = remember { mutableStateOf(0) } @@ -173,7 +172,7 @@ fun CreateGraphDialogWindow(viewModel: SetupGraphViewModel) { } } if (createGraphClicked.value) { - onCreateGraphClicked( + createGraphFromTypesIndices( viewModel, selectedStoredDataIndex.value, selectedOrientationIndex.value, @@ -184,7 +183,7 @@ fun CreateGraphDialogWindow(viewModel: SetupGraphViewModel) { @Composable -fun onCreateGraphClicked( +fun createGraphFromTypesIndices( viewModel: SetupGraphViewModel, storedDataIndex: Int, orientationIndex: Int, @@ -209,5 +208,28 @@ fun onCreateGraphClicked( else -> SetupGraphViewModel.Weight.Unweighted // default to unweighted } - return viewModel.createGraph(storedData, graphStructure, weight) -} \ No newline at end of file + return viewModel.createGraphAndApplyScreen(storedData, graphStructure, weight) +} + +fun getGraphVMParameter(storedDataType: Int, structureType: Int, weightType: Int): Triple { + val storedData = when (storedDataType) { + 0 -> SetupGraphViewModel.GraphType.Integer + 1 -> SetupGraphViewModel.GraphType.UInteger + 2 -> SetupGraphViewModel.GraphType.String + else -> SetupGraphViewModel.GraphType.Integer // default to integer + } + + val graphStructure = when (structureType) { + 0 -> SetupGraphViewModel.GraphStructure.Undirected + 1 -> SetupGraphViewModel.GraphStructure.Directed + else -> SetupGraphViewModel.GraphStructure.Undirected // default to directed + } + + val weight = when (weightType) { + 0 -> SetupGraphViewModel.Weight.Unweighted + 1 -> SetupGraphViewModel.Weight.Weighted + else -> SetupGraphViewModel.Weight.Unweighted // default to weighted + } + + return Triple(storedData, graphStructure, weight) +} diff --git a/app/src/main/kotlin/view/utils/EditDBWindow.kt b/app/src/main/kotlin/view/utils/EditDBWindow.kt new file mode 100644 index 00000000..764e562c --- /dev/null +++ b/app/src/main/kotlin/view/utils/EditDBWindow.kt @@ -0,0 +1,165 @@ +package view.utils + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import model.io.sql.SQLDatabaseModule + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun EditDBWindow(DBType: String, onDismiss: () -> Unit) { + var showDialog by remember { mutableStateOf(false) } + val graphNamesSQL = remember { mutableStateOf(arrayListOf>()) } + var expanded by remember { mutableStateOf(false) } + var selectedGraphName by remember { mutableStateOf("") } + var selectedGraphID by remember { mutableStateOf(0)} + var updateGraphNames by remember { mutableStateOf(false) } + var graphNameToReplaceWith by remember { mutableStateOf("")} + + if (DBType == "SQLite") { + SQLDatabaseModule.getGraphNames(graphNamesSQL) + if (graphNamesSQL.value.isNotEmpty()) showDialog = true + else ErrorWindow("Database doesn't have any Graphs", {}) + } + + + if (showDialog) { + Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = false, usePlatformDefaultWidth = false)) { + Column( + modifier = Modifier.background(Color.White).padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(450.dp).height(180.dp) + ) { + Text( + "Edit database", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + TextField( + value = selectedGraphName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + graphNamesSQL.value.forEach { db -> + DropdownMenuItem( + modifier = Modifier, + onClick = { + selectedGraphName = db.second + selectedGraphID = db.first + expanded = false + } + ) { + Text(text = db.second) + } + } + } + } + } + Row( + modifier = Modifier + .padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 10.dp) + .fillMaxWidth() + .height(150.dp), + horizontalArrangement = Arrangement.End, + ) { + TextField( + value = graphNameToReplaceWith, + onValueChange = { newValue -> + graphNameToReplaceWith = newValue + }, + modifier = Modifier + .width(200.dp) + .height(50.dp) + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Graph name", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + Spacer(modifier = Modifier.width(15.dp)) + + Button( + modifier = Modifier + .width(50.dp) + .height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + SQLDatabaseModule.deleteGraph(selectedGraphID) + updateGraphNames = true + selectedGraphName = "" + } + ) { + Icon(Icons.Default.Delete, contentDescription = "Delete", tint = Color.White) + } + + Spacer(modifier = Modifier.width(15.dp)) + + Button( + modifier = Modifier + .width(50.dp) + .height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + SQLDatabaseModule.renameGraph(selectedGraphID, graphNameToReplaceWith) + updateGraphNames = true + selectedGraphName = "" + graphNameToReplaceWith = "" + } + ) { + Icon(Icons.Default.Edit, contentDescription = "Rename", tint = Color.White) + } + } + } + } + } + if (updateGraphNames) { + SQLDatabaseModule.getGraphNames(graphNamesSQL) + updateGraphNames = false + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/utils/ErrorWindow.kt b/app/src/main/kotlin/view/utils/ErrorWindow.kt new file mode 100644 index 00000000..7daee609 --- /dev/null +++ b/app/src/main/kotlin/view/utils/ErrorWindow.kt @@ -0,0 +1,53 @@ +package view.utils + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun ErrorWindow(message: String, onDismiss: () -> Unit) { + val closeDialog = remember { mutableStateOf(false) } + + if (!closeDialog.value) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + modifier = Modifier.size(300.dp, 180.dp), + shape = MaterialTheme.shapes.medium, + elevation = 24.dp + ) { + Row(modifier = Modifier.height(100.dp).fillMaxWidth().padding(16.dp)) { + Column(modifier = Modifier, horizontalAlignment = Alignment.Start) { + Text(text = "Error", style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold) + } + } + Row(modifier = Modifier.fillMaxWidth().padding(top = 32.dp)) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween + ) { + + Text(text = message) + Spacer(modifier = Modifier.height(30.dp)) + Button(onClick = { closeDialog.value = true }, modifier = Modifier.align(Alignment.End)) { + Text("Ok") + } + } + } + } + } + } +} + + + diff --git a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt index 7dbe4aa9..4f85e222 100644 --- a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt @@ -1,2 +1,106 @@ package view.utils +import model.io.sql.SQLDatabaseModule +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import viewmodel.graph.GraphViewModel + + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ImportGraphDialogWindow() { + val selectedGraphID = remember { mutableStateOf(0) } + val closeDialog = remember { mutableStateOf(false) } + val expanded = remember { mutableStateOf(false) } + val importFromDBRequired = remember { mutableStateOf(false) } + val selectedGraphName = remember { mutableStateOf("") } + val graphs = remember { mutableStateOf(arrayListOf>()) } + + SQLDatabaseModule.getGraphNames(graphs) + + if (!closeDialog.value) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) + ) { + Row(modifier = Modifier.width(350.dp).height(150.dp)) { + Text( + "Select the database", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Box(modifier = Modifier.width(300.dp).padding(16.dp)) { + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value + } + ) { + TextField( + value = selectedGraphName.value, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded.value) }, + modifier = Modifier + ) + + ExposedDropdownMenu( + expanded = expanded.value, + onDismissRequest = { expanded.value = false } + ) { + graphs.value.forEach { graphName -> + // TODO: fix its layout + DropdownMenuItem( + onClick = { + selectedGraphName.value = graphName.second + selectedGraphID.value = graphName.first + expanded.value = false + } + ) { + Text(text = graphName.second) + } + } + } + } + } + } + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + importFromDBRequired.value = true + expanded.value = false + closeDialog.value = true + } + ) { + Text("Import", color = Color.White) + } + } + } + } + } + if (importFromDBRequired.value) { + return SQLDatabaseModule.importGraph(selectedGraphID.value) + } +} diff --git a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt index c95114d4..a65fdc5e 100644 --- a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt @@ -82,8 +82,8 @@ class SelectInitDialogWindow( CreateGraphDialogWindow(SetupGraphViewModel()) } -// if (showImportTab) { -// ImportGraphDialogWindow() -// } + if (showImportTab) { + ImportGraphDialogWindow() + } } } diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index f6ea6e50..e03fd0a3 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -1,22 +1,23 @@ package viewmodel import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import model.graphs.abstractGraph.Graph import viewmodel.graph.GraphViewModel -import viewmodel.graph.TFDPLayout -class MainScreenViewModel(graph: Graph, currentGraphType: String) { +class MainScreenViewModel( + graph: Graph, + currentGraphType: String, + existingGraphViewModel: GraphViewModel? = null +) { val showVerticesData = mutableStateOf(false) val showVerticesIds = mutableStateOf(false) val graphType = mutableStateOf(currentGraphType) fun setDirectionState(currentGraphType: String): MutableState { - if (currentGraphType.contains("Directed")) return mutableStateOf(true) - return mutableStateOf(false) + return mutableStateOf(currentGraphType.contains("Directed")) } - val graphViewModel = - GraphViewModel(graph, showVerticesIds, showVerticesData, graphType, setDirectionState(currentGraphType)) + var graphViewModel: GraphViewModel = existingGraphViewModel + ?: GraphViewModel(graph, showVerticesIds, showVerticesData, graphType, setDirectionState(currentGraphType)) } diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index faf8bb58..95a8e121 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -8,7 +8,7 @@ import model.graphs.abstractGraph.Graph import model.graphs.abstractGraph.Vertex class GraphViewModel( - val graph: Graph, + val currentGraph: Graph, private val showVerticesData: State, var showVerticesID: MutableState, val graphType: MutableState, @@ -75,4 +75,6 @@ class GraphViewModel( val verticesVM: List> get() = _verticesViewModels.values.toList() val edgesVM: List> get() = _edgeViewModels.values.toList() + + val graph: Graph get() = currentGraph } diff --git a/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt index f1dabae4..43286fc4 100644 --- a/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt @@ -1,6 +1,8 @@ package viewmodel.graph +import model.io.sql.SQLDatabaseModule import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import model.graphs.DirectedGraph import model.graphs.UndirectedGraph import model.graphs.WeightedDirectedGraph @@ -10,23 +12,23 @@ import viewmodel.MainScreenViewModel class SetupGraphViewModel { sealed class GraphType { - object Integer : GraphType() - object UInteger : GraphType() - object String : GraphType() + data object Integer : GraphType() + data object UInteger : GraphType() + data object String : GraphType() } sealed class GraphStructure { - object Directed : GraphStructure() - object Undirected : GraphStructure() + data object Directed : GraphStructure() + data object Undirected : GraphStructure() } sealed class Weight { - object Weighted : Weight() - object Unweighted : Weight() + data object Weighted : Weight() + data object Unweighted : Weight() } @Composable - fun createGraph( + fun createGraphAndApplyScreen( storedData: GraphType, graphStructure: GraphStructure, weight: Weight @@ -45,7 +47,6 @@ class SetupGraphViewModel { is GraphType.String -> MainScreen(MainScreenViewModel( WeightedDirectedGraph(), "WeightedDirectedGraph String")) - } } @@ -59,8 +60,7 @@ class SetupGraphViewModel { "WeightedUndirectedGraph UInt")) is GraphType.String -> MainScreen(MainScreenViewModel( WeightedUndirectedGraph(), - "WeightedUndirectedGraph String") - ) + "WeightedUndirectedGraph String")) } } } @@ -99,4 +99,86 @@ class SetupGraphViewModel { } } } + + @Suppress("UNCHECKED_CAST") + fun createGraphObject( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight, + graphId: Int, + graphVMState: MutableState?> + ) = when (weight) { + is Weight.Weighted -> { + when (graphStructure) { + is GraphStructure.Directed -> { + when (storedData) { + is GraphType.Integer -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedDirectedGraph(), + graphId, graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.UInteger -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedDirectedGraph(), + graphId, graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.String -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedDirectedGraph(), + graphId, graphVMState as MutableState?>) as GraphViewModel? + } + } + } + is GraphStructure.Undirected -> { + when (storedData) { + is GraphType.Integer -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedUndirectedGraph(), + graphId, graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.UInteger -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedUndirectedGraph(), + graphId, graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.String -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedUndirectedGraph(), + graphId, graphVMState as MutableState?>) as GraphViewModel? + } + } + } + } + } + is Weight.Unweighted -> { + when (graphStructure) { + is GraphStructure.Directed -> { + when (storedData) { + is GraphType.Integer -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(DirectedGraph(), graphId, + graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.UInteger -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(DirectedGraph(), graphId, + graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.String -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(DirectedGraph(), graphId, + graphVMState as MutableState?>) as GraphViewModel? + } + } + } + is GraphStructure.Undirected -> { + when (storedData) { + is GraphType.Integer -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(UndirectedGraph(), graphId, + graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.UInteger -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(UndirectedGraph(), graphId, + graphVMState as MutableState?>) as GraphViewModel? + } + is GraphType.String -> { + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(UndirectedGraph(), graphId, + graphVMState as MutableState?>) as GraphViewModel? + } + } + } + } + } + } } diff --git a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt index cc43d8f3..149f590f 100644 --- a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt +++ b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt @@ -7,9 +7,9 @@ import kotlin.math.sqrt import androidx.compose.ui.platform.LocalDensity class TFDPLayout( - private val longRangeAttractionConstant: Float = 0.001f, // strength of attractive force (long-range) - B + private val longRangeAttractionConstant: Float = 0.0001f, // strength of attractive force (long-range) - B private val nearAttractionConstant: Float = 16.0f, // strength of attractive t-force (near) - A - private val repulsiveConstant: Float = 2.0f, // extent and magnitude of the repulsive t-force that + private val repulsiveConstant: Float = 5.0f, // extent and magnitude of the repulsive t-force that // controls the longest distance of neighbors in the layout - Y ) { fun place(width: Double, height: Double, vertices: Collection>) { @@ -47,8 +47,8 @@ class TFDPLayout( k = 0 vertices.onEach { // update positions val vi = it - vi.x.value += forces[k].first.dp - vi.y.value += forces[k].second.dp + vi.x.value += forces[k].first.dp / 2 + vi.y.value += forces[k].second.dp / 2 k++ } diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index 95ec261d..cf1017aa 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -24,11 +24,8 @@ class VertexViewModel( get() = vertex.data.toString() fun onDrag(dragAmount: DpOffset) { - val newX = x.value + dragAmount.x - val newY = y.value + dragAmount.y - - x.value = newX - y.value = newY + x.value += dragAmount.x + y.value += dragAmount.y } val getVertexID From cca76aeeba5ac909473b63f207035ee9bfa40596 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Wed, 29 May 2024 13:01:29 +0300 Subject: [PATCH 68/77] feat: algorithms to separate classes in model and add draft of analyze tab #51 * refactor: move key vertices finder algorithm to separate class * refactor: move SCC and cycles find algorithms to separate classes * refactor: move min spanning tree and bridges finder algorithms to separate classes * refactor: move find shortest pat algorithms to separate class * test: move algorithm tests to separate classes, add test cases for finding shortest path + make internal Dijkstra and Ford-Bellman private * refactor: move graph tests to new graphs package * refactor: change text in general tab buttons * feat: add algorithm choosing in analyze tab * feat: add drafts for algorithms UI --- .../kotlin/model/algorithms/BridgesFinder.kt | 63 ++ .../kotlin/model/algorithms/CyclesFinder.kt | 110 ++ .../model/algorithms/KeyVerticesFinder.kt | 146 +++ .../model/algorithms/MinSpanningTreeFinder.kt | 53 + .../main/kotlin/model/algorithms/SCCFinder.kt | 63 ++ .../model/algorithms/ShortestPathFinder.kt | 180 ++++ ...vainClustering.kt => CommunitiesFinder.kt} | 4 +- .../main/kotlin/model/graphs/DirectedGraph.kt | 160 +-- .../kotlin/model/graphs/UndirectedGraph.kt | 53 - .../model/graphs/WeightedDirectedGraph.kt | 98 -- .../model/graphs/WeightedUndirectedGraph.kt | 89 -- .../model/graphs/abstractGraph/Graph.kt | 139 +-- .../main/kotlin/view/tabScreen/AnalyzeTab.kt | 13 - .../main/kotlin/view/tabScreen/GeneralTab.kt | 15 +- .../main/kotlin/view/tabScreen/TabHandler.kt | 1 + .../view/tabScreen/analyzeTab/AnalyzeTab.kt | 105 ++ .../analyzeTab/algorithmsUI/BridgesUI.kt | 31 + .../analyzeTab/algorithmsUI/CommunitiesUI.kt | 31 + .../analyzeTab/algorithmsUI/CyclesUI.kt | 31 + .../analyzeTab/algorithmsUI/KeyVerticesUI.kt | 31 + .../analyzeTab/algorithmsUI/LayoutUI.kt | 32 + .../algorithmsUI/MinSpanningTreeUI.kt | 31 + .../analyzeTab/algorithmsUI/SCCUI.kt | 31 + .../analyzeTab/algorithmsUI/ShortestPathUI.kt | 31 + .../test/kotlin/model/DirectedGraphTest.kt | 962 ------------------ .../kotlin/model/WeightedDirectedGraphTest.kt | 551 ---------- .../model/WeightedUndirectedGraphTest.kt | 506 --------- .../model/algorithms/BridgesFinderTest.kt | 162 +++ .../model/algorithms/CyclesFinderTest.kt | 147 +++ .../model/algorithms/KeyVerticesFinderTest.kt | 262 +++++ .../algorithms/MinSpanningTreeFinderTest.kt | 145 +++ .../kotlin/model/algorithms/SCCFinderTest.kt | 352 +++++++ .../algorithms/ShortestPathFinderTest.kt | 636 ++++++++++++ ...teringTest.kt => CommunitiesFinderTest.kt} | 10 +- .../kotlin/model/graphs/DirectedGraphTest.kt | 437 ++++++++ .../model/{ => graphs}/UndirectedGraphTest.kt | 181 +--- .../model/graphs/WeightedDirectedGraphTest.kt | 99 ++ .../graphs/WeightedUndirectedGraphTest.kt | 103 ++ .../{ => graphs}/abstractGraph/GraphTest.kt | 2 +- 39 files changed, 3338 insertions(+), 2758 deletions(-) create mode 100644 app/src/main/kotlin/model/algorithms/BridgesFinder.kt create mode 100644 app/src/main/kotlin/model/algorithms/CyclesFinder.kt create mode 100644 app/src/main/kotlin/model/algorithms/KeyVerticesFinder.kt create mode 100644 app/src/main/kotlin/model/algorithms/MinSpanningTreeFinder.kt create mode 100644 app/src/main/kotlin/model/algorithms/SCCFinder.kt create mode 100644 app/src/main/kotlin/model/algorithms/ShortestPathFinder.kt rename app/src/main/kotlin/model/algorithms/clustering/{LouvainClustering.kt => CommunitiesFinder.kt} (95%) delete mode 100644 app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt create mode 100644 app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt delete mode 100644 app/src/test/kotlin/model/DirectedGraphTest.kt delete mode 100644 app/src/test/kotlin/model/WeightedDirectedGraphTest.kt delete mode 100644 app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt create mode 100644 app/src/test/kotlin/model/algorithms/BridgesFinderTest.kt create mode 100644 app/src/test/kotlin/model/algorithms/CyclesFinderTest.kt create mode 100644 app/src/test/kotlin/model/algorithms/KeyVerticesFinderTest.kt create mode 100644 app/src/test/kotlin/model/algorithms/MinSpanningTreeFinderTest.kt create mode 100644 app/src/test/kotlin/model/algorithms/SCCFinderTest.kt create mode 100644 app/src/test/kotlin/model/algorithms/ShortestPathFinderTest.kt rename app/src/test/kotlin/model/algorithms/clustering/{LouvainClusteringTest.kt => CommunitiesFinderTest.kt} (82%) create mode 100644 app/src/test/kotlin/model/graphs/DirectedGraphTest.kt rename app/src/test/kotlin/model/{ => graphs}/UndirectedGraphTest.kt (77%) create mode 100644 app/src/test/kotlin/model/graphs/WeightedDirectedGraphTest.kt create mode 100644 app/src/test/kotlin/model/graphs/WeightedUndirectedGraphTest.kt rename app/src/test/kotlin/model/{ => graphs}/abstractGraph/GraphTest.kt (99%) diff --git a/app/src/main/kotlin/model/algorithms/BridgesFinder.kt b/app/src/main/kotlin/model/algorithms/BridgesFinder.kt new file mode 100644 index 00000000..fe68d98a --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/BridgesFinder.kt @@ -0,0 +1,63 @@ +package model.algorithms + +import model.graphs.UndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import kotlin.math.min + +class BridgesFinder { + fun findBridges(graph: UndirectedGraph): List> { + val vertices = graph.getVertices() + val bridges = mutableListOf>() + + val graphSize = vertices.size + + val discoveryTime = MutableList(graphSize) { -1 } + val minDiscoveryTime = MutableList(graphSize) { -1 } + + val visitedList = MutableList(graphSize) { false } + val parentList = MutableList(graphSize) { -1 } + + var iterationCount = 0 + + fun doDFSToFindBridgesFromVertex(vertex: Vertex) { + visitedList[vertex.id] = true + + iterationCount++ + discoveryTime[vertex.id] = iterationCount + minDiscoveryTime[vertex.id] = iterationCount + + for (neighbour in graph.getNeighbours(vertex)) { + if (neighbour.id == parentList[vertex.id]) continue + + if (visitedList[neighbour.id]) { + minDiscoveryTime[vertex.id] = + min(minDiscoveryTime[vertex.id], discoveryTime[neighbour.id]) + + continue + } + + parentList[neighbour.id] = vertex.id + + doDFSToFindBridgesFromVertex(neighbour) + + minDiscoveryTime[vertex.id] = + min(minDiscoveryTime[vertex.id], minDiscoveryTime[neighbour.id]) + + if (minDiscoveryTime[neighbour.id] > discoveryTime[vertex.id]) { + val bridgeFound = graph.getEdge(vertex, neighbour) + bridges.add(bridgeFound) + } + } + } + + for (v in vertices) { + if (visitedList[v.id]) continue + + doDFSToFindBridgesFromVertex(v) + } + + return bridges + } +} diff --git a/app/src/main/kotlin/model/algorithms/CyclesFinder.kt b/app/src/main/kotlin/model/algorithms/CyclesFinder.kt new file mode 100644 index 00000000..50a2fa70 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/CyclesFinder.kt @@ -0,0 +1,110 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex + +class CyclesFinder { + fun findCycles(graph: DirectedGraph, srcVertex: Vertex): Set, Vertex>>> { + if (graph.getOutgoingEdges(srcVertex).isEmpty()) return emptySet() + + val SCCFinder = SCCFinder() + val sccs = SCCFinder.findSCC(graph) + val vertexSCC = sccs.find { srcVertex in it } + ?: throw NoSuchElementException("Vertex (${srcVertex.id}, ${srcVertex.data}) isn't in any of the SCCs") + + if (vertexSCC.size == 1) return emptySet() + + // create SCC subgraph + val subGraph = DirectedGraph() + + // map to restore original vertices + val verticesCopiesMap = mutableMapOf, Vertex>() + + for (originalVertex in vertexSCC) { + val vertexCopy = subGraph.addVertex(originalVertex.data) + verticesCopiesMap[vertexCopy] = originalVertex + } + + for (edge in graph.getEdges()) { + if (edge.vertex1 in vertexSCC && edge.vertex2 in vertexSCC) { + val vertex1copy = verticesCopiesMap.filterValues { it == edge.vertex1 }.keys.toList()[0] + val vertex2copy = verticesCopiesMap.filterValues { it == edge.vertex2 }.keys.toList()[0] + + subGraph.addEdge(vertex1copy, vertex2copy) + } + } + + val copyOfSrcVertex = verticesCopiesMap.filterValues { it == srcVertex }.keys.toList()[0] + + val blockedSet = mutableSetOf>() + val blockedMap = mutableMapOf, MutableSet>>() + val stack = ArrayDeque>() + val verticesCycles = mutableSetOf>>() + + fun DFSToFindCycles(currentVertex: Vertex): Boolean { + var cycleIsFound = false + + stack.addLast(currentVertex) + blockedSet.add(currentVertex) + + for (neighbour in subGraph.getNeighbours(currentVertex)) { + if (neighbour == copyOfSrcVertex) { + // cycle is found + stack.addLast(copyOfSrcVertex) + + val cycleOfVertices = mutableListOf>() + cycleOfVertices.addAll(stack) + verticesCycles.add(cycleOfVertices) + + stack.removeLast() + + cycleIsFound = true + } else if (neighbour !in blockedSet) { + // next iteration + cycleIsFound = DFSToFindCycles(neighbour) || cycleIsFound + } + } + + fun unblock(vertex: Vertex) { + blockedSet.remove(vertex) + blockedMap[vertex]?.forEach { unblock(it) } + blockedMap.remove(vertex) + } + + if (cycleIsFound) unblock(currentVertex) + else { + for (neighbour in subGraph.getNeighbours(currentVertex)) { + blockedMap[neighbour]?.add(currentVertex) + ?: blockedMap.put(neighbour, mutableSetOf(currentVertex)) + } + } + + stack.removeLast() + + return cycleIsFound + } + + DFSToFindCycles(copyOfSrcVertex) + + val cycles = mutableSetOf, Vertex>>>() + for (verticesCycle in verticesCycles) { + val originalVerticesCycle = mutableListOf>() + for (vertex in verticesCycle) originalVerticesCycle += verticesCopiesMap[vertex] + ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the vertices map") + + val cycle = mutableListOf, Vertex>>() + + for (i in 0..originalVerticesCycle.size - 2) { + val v1 = originalVerticesCycle[i] + val v2 = originalVerticesCycle[i + 1] + + cycle.add(graph.getEdge(v1, v2) to v2) + } + + cycles.add(cycle) + } + + return cycles + } +} diff --git a/app/src/main/kotlin/model/algorithms/KeyVerticesFinder.kt b/app/src/main/kotlin/model/algorithms/KeyVerticesFinder.kt new file mode 100644 index 00000000..a73bfa4a --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/KeyVerticesFinder.kt @@ -0,0 +1,146 @@ +package model.algorithms + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import java.util.* +import java.util.ArrayDeque +import kotlin.math.roundToInt + +class KeyVerticesFinder { + /** + * For every vertex, calculates normalized closeness centrality, based on which the key vertices are picked. + * Formula was taken from "Efficient Top-k Closeness Centrality Search" by Paul W. Olsen et al., + * yet an easier algorithm for traversal was chosen. + */ + fun findKeyVertices(graph: Graph): Set>? { + val vertices = graph.getVertices() + val graphSize = vertices.size + + val distanceMap = graph.getWeightMap() + if (graph.hasNegativeEdges()) return null + + val centralityMap = mutableMapOf, Double>() + + for (currVertex in vertices) { + val currSumOfDistances = calcSumOfDistancesFromVertex(graph, currVertex, distanceMap, graphSize) + + val reachableNum = findReachableVerticesNumFromVertex(graph, currVertex, graphSize) + + val currCentrality = calcCentralityOfVertex(currSumOfDistances, reachableNum, graphSize) + centralityMap[currVertex] = currCentrality + } + + val keyVertices = pickMostKeyVertices(centralityMap, graphSize) + + return keyVertices + } + + /** + * Uses modified Dijkstra's algorithm to calculate the sum of all weights (distances) + * of shortest paths from source vertex to every other reachable one + */ + private fun calcSumOfDistancesFromVertex( + graph: Graph, + srcVertex: Vertex, + distanceMap: Map, Int>, + graphSize: Int + ): Int { + val POS_INF = 100_000_000 // to infinity and beyond + + val visited = Array(graphSize) { false } + + val distances = Array(graphSize) { POS_INF } + distances[srcVertex.id] = 0 + + // stores pairs of vertices and total distances to them, sorted by distances ascending + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }) + priorityQueue.add(srcVertex to distances[srcVertex.id]) + + while (priorityQueue.isNotEmpty()) { + val (currVertex, currDistance) = priorityQueue.poll() + + if (visited[currVertex.id]) continue + + visited[currVertex.id] = true + + val neighbours = graph.getNeighbours(currVertex) + for (neighbour in neighbours) { + val edgeToNeighbour = graph.getEdge(currVertex, neighbour) + + val edgeToNeighbourDistance = distanceMap[edgeToNeighbour] ?: POS_INF + + val totalDistanceToNeighbour = currDistance + edgeToNeighbourDistance + + if (totalDistanceToNeighbour < distances[neighbour.id]) { + distances[neighbour.id] = totalDistanceToNeighbour + priorityQueue.add(neighbour to totalDistanceToNeighbour) + } + } + } + + for (i in distances.indices) { + if (distances[i] == POS_INF) distances[i] = 0 + } + + val sum = distances.sum() + + return sum + } + + private fun findReachableVerticesNumFromVertex(graph: Graph, vertex: Vertex, graphSize: Int): Int { + var reachableVerticesNum = 0 + + val verticesToVisit = ArrayDeque>() + verticesToVisit.add(vertex) + + val visited = Array(graphSize) { false } + + while (verticesToVisit.isNotEmpty()) { + val vertexToVisit = verticesToVisit.poll() + + if (visited[vertexToVisit.id]) continue + + for (neighbour in graph.getNeighbours(vertexToVisit)) { + verticesToVisit.add(neighbour) + } + + visited[vertexToVisit.id] = true + verticesToVisit.add(vertexToVisit) + + reachableVerticesNum++ + } + + return reachableVerticesNum + } + + private fun calcCentralityOfVertex(sumOfDistances: Int, reachableNum: Int, graphSize: Int): Double { + if (sumOfDistances == 0) return 0.0 + + val centrality = + ((reachableNum - 1) * (reachableNum - 1)) / ((graphSize - 1) * sumOfDistances).toDouble() + + return centrality + } + + private fun pickMostKeyVertices(centralityMap: Map, Double>, graphSize: Int): Set> { + val keyVertices = mutableSetOf>() + + val percent = 0.2 + val keyVerticesNum = (graphSize * percent).roundToInt() // rounds up + + var currKeyVerticesNum = 0 + + val vertexCentralityPairs = centralityMap.toList() + val vertexCentralityPairsSorted = vertexCentralityPairs.sortedByDescending { it.second } + + for ((vertex, _) in vertexCentralityPairsSorted) { + if (currKeyVerticesNum >= keyVerticesNum) break + + keyVertices.add(vertex) + currKeyVerticesNum++ + } + + return keyVertices + } +} diff --git a/app/src/main/kotlin/model/algorithms/MinSpanningTreeFinder.kt b/app/src/main/kotlin/model/algorithms/MinSpanningTreeFinder.kt new file mode 100644 index 00000000..50887c84 --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/MinSpanningTreeFinder.kt @@ -0,0 +1,53 @@ +package model.algorithms + +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge + +class MinSpanningTreeFinder { + fun findMinSpanningTree(graph: WeightedUndirectedGraph): List> { + val graphSize = graph.getVertices().size + + // set each vertex parent to be itself and each vertex rank to 0 + val parentIdList = Array(graphSize) { i: Int -> i } + val rankList = Array(graphSize) { 0 } + + fun findRootIdByVertexId(vId: Int): Int { + if (parentIdList[vId] == vId) return vId + + parentIdList[vId] = findRootIdByVertexId(parentIdList[vId]) + + return parentIdList[vId] + } + + fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { + val rootId1 = findRootIdByVertexId(vId1) + val rootId2 = findRootIdByVertexId(vId2) + + if (rootId1 == rootId2) return + + if (rankList[rootId1] < rankList[rootId2]) { + parentIdList[rootId1] = rootId2 + } else { + parentIdList[rootId2] = rootId1 + if (rankList[rootId1] == rankList[rootId2]) rankList[rootId1]++ + } + } + + val edgeWeightPairs = graph.getWeightMap().toList() + val sortedEdgeWeightPairs = edgeWeightPairs.sortedBy { it.second } + + val chosenEdges = mutableListOf>() + + for ((edge, _) in sortedEdgeWeightPairs) { + val id1 = edge.vertex1.id + val id2 = edge.vertex2.id + + if (findRootIdByVertexId(id1) != findRootIdByVertexId(id2)) { + uniteTwoTreesByVerticesIds(id1, id2) + chosenEdges.add(edge) + } + } + + return chosenEdges + } +} diff --git a/app/src/main/kotlin/model/algorithms/SCCFinder.kt b/app/src/main/kotlin/model/algorithms/SCCFinder.kt new file mode 100644 index 00000000..0b341ded --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/SCCFinder.kt @@ -0,0 +1,63 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex + +class SCCFinder { + // SCC - Strongly Connected Components (by Kosaraju) + fun findSCC(graph: DirectedGraph): MutableSet>> { + val visited = mutableMapOf, Boolean>().withDefault { false } + val stack = ArrayDeque>() + val component = arrayListOf>() + val sccList: MutableSet>> = mutableSetOf() + + fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { + visited[srcVertex] = true + componentList.add(srcVertex) + graph.getNeighbours(srcVertex).forEach { vertex2 -> + if (visited[vertex2] != true) { + auxiliaryDFS(vertex2, componentList) + } + } + stack.add(srcVertex) + } + + graph.getVertices().forEach { vertex -> + if (visited[vertex] != true) auxiliaryDFS(vertex, component) + } + + val reversedEdgesMap = reverseEdgesMap(graph) + visited.clear() + component.clear() + + fun reverseDFS(vertex: Vertex, componentList: MutableSet>) { + visited[vertex] = true + componentList.add(vertex) + reversedEdgesMap[vertex]?.forEach { vertex2 -> + if (visited[vertex2] != true) { + reverseDFS(vertex2, componentList) + } + } + } + + while (stack.isNotEmpty()) { + val vertex = stack.removeLast() + if (visited[vertex] != true) { + val currentComponent = mutableSetOf>() + reverseDFS(vertex, currentComponent) + sccList.add(currentComponent) + } + } + return sccList + } + + private fun reverseEdgesMap(graph: DirectedGraph): Map, MutableSet>> { + val reversedEdgesMap = mutableMapOf, MutableSet>>() + graph.getVertices().forEach { reversedEdgesMap[it] = mutableSetOf() } + graph.getEdges().forEach { edge -> + reversedEdgesMap[edge.vertex2]?.add(edge.vertex1) + } + return reversedEdgesMap + } +} diff --git a/app/src/main/kotlin/model/algorithms/ShortestPathFinder.kt b/app/src/main/kotlin/model/algorithms/ShortestPathFinder.kt new file mode 100644 index 00000000..28f4bdff --- /dev/null +++ b/app/src/main/kotlin/model/algorithms/ShortestPathFinder.kt @@ -0,0 +1,180 @@ +package model.algorithms + +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import java.util.* + +class ShortestPathFinder { + fun findShortestPath(graph: Graph, srcVertex: Vertex, destVertex: Vertex): + List, Vertex>>? { + if (graph.hasNegativeEdges()) { + // there is no shortest path in undirected graphs with negative edges + if (graph is WeightedUndirectedGraph) return null + else return findShortestPathFordBellman(graph as WeightedDirectedGraph, srcVertex, destVertex) + } + + if (graph is WeightedDirectedGraph) return findShortestPathDijkstra(graph, srcVertex, destVertex) + if (graph is WeightedUndirectedGraph) return findShortestPathDijkstra(graph, srcVertex, destVertex) + + // if graph isn't weighted + return null + } + + private fun findShortestPathDijkstra( + graph: WeightedDirectedGraph, + srcVertex: Vertex, + destVertex: Vertex + ): List, Vertex>>? { + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } + val visited = mutableSetOf, Int>>() + + if (srcVertex == destVertex) return emptyList() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (currentVertex, currentDistance) = priorityQueue.poll() + if (visited.add(currentVertex to currentDistance)) { + graph.getNeighbours(currentVertex).forEach { adjacent -> + val currentEdge = graph.getEdge(currentVertex, adjacent) // Ensure correct edge direction + + val totalDist = currentDistance + graph.getWeight(currentEdge) + + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = currentVertex // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, Vertex>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + ?: return null // path doesn't exist + + val currentEdge = graph.getEdge(predecessor, currentVertex) + path.add(currentEdge to currentVertex) + + currentVertex = predecessor + } + return path.reversed() + } + + private fun findShortestPathDijkstra( + graph: WeightedUndirectedGraph, + srcVertex: Vertex, + destVertex: Vertex + ): List, Vertex>>? { + val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } + val predecessorMap = mutableMapOf, Vertex?>() + val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } + val visited = mutableSetOf, Int>>() + + if (srcVertex == destVertex) return emptyList() + + distanceMap[srcVertex] = 0 + + while (priorityQueue.isNotEmpty()) { + val (currentVertex, currentDistance) = priorityQueue.poll() + if (visited.add(currentVertex to currentDistance)) { + graph.getNeighbours(currentVertex).forEach { adjacent -> + val currentEdge = graph.getEdge(currentVertex, adjacent) // Ensure correct edge direction + + val totalDist = currentDistance + graph.getWeight(currentEdge) + + if (totalDist < distanceMap.getValue(adjacent)) { + distanceMap[adjacent] = totalDist + predecessorMap[adjacent] = currentVertex // Update predecessor + priorityQueue.add(adjacent to totalDist) + } + } + } + } + + // Reconstruct the path from srcVertex to destVertex + val path: MutableList, Vertex>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val predecessor = predecessorMap[currentVertex] + ?: return null // path doesn't exist + + val currentEdge = graph.getEdge(predecessor, currentVertex) + path.add(currentEdge to currentVertex) + + currentVertex = predecessor + } + return path.reversed() + } + + // returns null if path doesn't exist + private fun findShortestPathFordBellman( + graph: WeightedDirectedGraph, + srcVertex: Vertex, + destVertex: Vertex + ): List, Vertex>>? { + val NEG_INF = -1000000 + + val vertices = graph.getVertices() + val edges = graph.getEdges() + + val distance = MutableList(vertices.size) { Int.MAX_VALUE } + val predecessor = MutableList?>(vertices.size) { null } + + distance[srcVertex.id] = 0 + + for (i in 0..vertices.size - 1) { + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + graph.getWeight(edge)) { + // distance will equal negative infinity if there is negative cycle + distance[v2.id] = maxOf(distance[v1.id] + graph.getWeight(edge), NEG_INF) + + predecessor[v2.id] = v1 + } + } + } + + // check for negative cycles, determine if path to destVertex exists + for (i in 0..vertices.size - 1) { + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + graph.getWeight(edge)) { + distance[v2.id] = NEG_INF + + } + } + } + + // there is a negative cycle on the way, so path doesn't exist + if (distance[destVertex.id] == NEG_INF) return null + + if (srcVertex == destVertex) return emptyList() + + // reconstruct the path from srcVertex to destVertex + val path: MutableList, Vertex>> = mutableListOf() + var currentVertex = destVertex + while (currentVertex != srcVertex) { + val currentPredecessor = predecessor[currentVertex.id] + ?: return null // path doesn't exist + + path.add(graph.getEdge(currentPredecessor, currentVertex) to currentVertex) + + currentVertex = currentPredecessor + } + + return path.reversed() + } +} diff --git a/app/src/main/kotlin/model/algorithms/clustering/LouvainClustering.kt b/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt similarity index 95% rename from app/src/main/kotlin/model/algorithms/clustering/LouvainClustering.kt rename to app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt index d4e1a5b5..b64994fc 100644 --- a/app/src/main/kotlin/model/algorithms/clustering/LouvainClustering.kt +++ b/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt @@ -7,8 +7,8 @@ import model.graphs.abstractGraph.Vertex import model.algorithms.clustering.implementation.Link import model.algorithms.clustering.implementation.getPartition -class LouvainClustering { - fun findClusters(graph: Graph): Set>> { +class CommunitiesFinder { + fun findCommunity(graph: Graph): Set>> { if (graph.getVertices().size == 1) return setOf(setOf(graph.getVertices()[0])) val links = convertToAPIFormat(graph) diff --git a/app/src/main/kotlin/model/graphs/DirectedGraph.kt b/app/src/main/kotlin/model/graphs/DirectedGraph.kt index 56ef496c..206f35e0 100644 --- a/app/src/main/kotlin/model/graphs/DirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/DirectedGraph.kt @@ -55,162 +55,4 @@ open class DirectedGraph : Graph() { return edge } - - // SCC - Strongly Connected Components (by Kosaraju) - fun findSCC(): MutableSet>> { - val visited = mutableMapOf, Boolean>().withDefault { false } - val stack = ArrayDeque>() - val component = arrayListOf>() - val sccList: MutableSet>> = mutableSetOf() - - fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { - visited[srcVertex] = true - componentList.add(srcVertex) - getNeighbours(srcVertex).forEach { vertex2 -> - if (visited[vertex2] != true) { - auxiliaryDFS(vertex2, componentList) - } - } - stack.add(srcVertex) - } - - getVertices().forEach { vertex -> - if (visited[vertex] != true) auxiliaryDFS(vertex, component) - } - - val reversedEdgesMap = reverseEdgesMap() - visited.clear() - component.clear() - - fun reverseDFS(vertex: Vertex, componentList: MutableSet>) { - visited[vertex] = true - componentList.add(vertex) - reversedEdgesMap[vertex]?.forEach { vertex2 -> - if (visited[vertex2] != true) { - reverseDFS(vertex2, componentList) - } - } - } - - while (stack.isNotEmpty()) { - val vertex = stack.removeLast() - if (visited[vertex] != true) { - val currentComponent = mutableSetOf>() - reverseDFS(vertex, currentComponent) - sccList.add(currentComponent) - } - } - return sccList - } - - private fun reverseEdgesMap(): Map, MutableSet>> { - val reversedEdgesMap = mutableMapOf, MutableSet>>() - getVertices().forEach { reversedEdgesMap[it] = mutableSetOf() } - getEdges().forEach { edge -> - reversedEdgesMap[edge.vertex2]?.add(edge.vertex1) - } - return reversedEdgesMap - } - - fun findCycles(srcVertex: Vertex): Set, Vertex>>> { - if (getOutgoingEdges(srcVertex).isEmpty()) return emptySet() - - val sccs = findSCC() - val vertexSCC = sccs.find { srcVertex in it } - ?: throw NoSuchElementException("Vertex (${srcVertex.id}, ${srcVertex.data}) isn't in any of the SCCs") - - if (vertexSCC.size == 1) return emptySet() - - // create SCC subgraph - val subGraph = DirectedGraph() - - // map to restore original vertices - val verticesCopiesMap = mutableMapOf, Vertex>() - - for (originalVertex in vertexSCC) { - val vertexCopy = subGraph.addVertex(originalVertex.data) - verticesCopiesMap[vertexCopy] = originalVertex - } - - for (edge in getEdges()) { - if (edge.vertex1 in vertexSCC && edge.vertex2 in vertexSCC) { - val vertex1copy = verticesCopiesMap.filterValues { it == edge.vertex1 }.keys.toList()[0] - val vertex2copy = verticesCopiesMap.filterValues { it == edge.vertex2 }.keys.toList()[0] - - subGraph.addEdge(vertex1copy, vertex2copy) - } - } - - val copyOfSrcVertex = verticesCopiesMap.filterValues { it == srcVertex }.keys.toList()[0] - - val blockedSet = mutableSetOf>() - val blockedMap = mutableMapOf, MutableSet>>() - val stack = ArrayDeque>() - val verticesCycles = mutableSetOf>>() - - fun DFSToFindCycles(currentVertex: Vertex): Boolean { - var cycleIsFound = false - - stack.addLast(currentVertex) - blockedSet.add(currentVertex) - - for (neighbour in subGraph.getNeighbours(currentVertex)) { - if (neighbour == copyOfSrcVertex) { - // cycle is found - stack.addLast(copyOfSrcVertex) - - val cycleOfVertices = mutableListOf>() - cycleOfVertices.addAll(stack) - verticesCycles.add(cycleOfVertices) - - stack.removeLast() - - cycleIsFound = true - } else if (neighbour !in blockedSet) { - // next iteration - cycleIsFound = DFSToFindCycles(neighbour) || cycleIsFound - } - } - - fun unblock(vertex: Vertex) { - blockedSet.remove(vertex) - blockedMap[vertex]?.forEach { unblock(it) } - blockedMap.remove(vertex) - } - - if (cycleIsFound) unblock(currentVertex) - else { - for (neighbour in subGraph.getNeighbours(currentVertex)) { - blockedMap[neighbour]?.add(currentVertex) - ?: blockedMap.put(neighbour, mutableSetOf(currentVertex)) - } - } - - stack.removeLast() - - return cycleIsFound - } - - DFSToFindCycles(copyOfSrcVertex) - - val cycles = mutableSetOf, Vertex>>>() - for (verticesCycle in verticesCycles) { - val originalVerticesCycle = mutableListOf>() - for (vertex in verticesCycle) originalVerticesCycle += verticesCopiesMap[vertex] - ?: throw NoSuchElementException("Vertex (${vertex.id}, ${vertex.data}) isn't in the vertices map") - - val cycle = mutableListOf, Vertex>>() - - for (i in 0..originalVerticesCycle.size - 2) { - val v1 = originalVerticesCycle[i] - val v2 = originalVerticesCycle[i + 1] - - cycle.add(getEdge(v1, v2) to v2) - } - - cycles.add(cycle) - } - - return cycles - } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/model/graphs/UndirectedGraph.kt b/app/src/main/kotlin/model/graphs/UndirectedGraph.kt index c13a01f8..47a5d361 100644 --- a/app/src/main/kotlin/model/graphs/UndirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/UndirectedGraph.kt @@ -61,57 +61,4 @@ open class UndirectedGraph : Graph() { return edge } - - fun findBridges(): List> { - val bridges = mutableListOf>() - - val graphSize = vertices.size - - val discoveryTime = MutableList(graphSize) { -1 } - val minDiscoveryTime = MutableList(graphSize) { -1 } - - val visitedList = MutableList(graphSize) { false } - val parentList = MutableList(graphSize) { -1 } - - var iterationCount = 0 - - fun doDFSToFindBridgesFromVertex(vertex: Vertex) { - visitedList[vertex.id] = true - - iterationCount++ - discoveryTime[vertex.id] = iterationCount - minDiscoveryTime[vertex.id] = iterationCount - - for (neighbour in getNeighbours(vertex)) { - if (neighbour.id == parentList[vertex.id]) continue - - if (visitedList[neighbour.id]) { - minDiscoveryTime[vertex.id] = - min(minDiscoveryTime[vertex.id], discoveryTime[neighbour.id]) - - continue - } - - parentList[neighbour.id] = vertex.id - - doDFSToFindBridgesFromVertex(neighbour) - - minDiscoveryTime[vertex.id] = - min(minDiscoveryTime[vertex.id], minDiscoveryTime[neighbour.id]) - - if (minDiscoveryTime[neighbour.id] > discoveryTime[vertex.id]) { - val bridgeFound = getEdge(vertex, neighbour) - bridges.add(bridgeFound) - } - } - } - - for (v in vertices) { - if (visitedList[v.id]) continue - - doDFSToFindBridgesFromVertex(v) - } - - return bridges - } } diff --git a/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt index 1b6170bf..11e46262 100644 --- a/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/WeightedDirectedGraph.kt @@ -44,102 +44,4 @@ class WeightedDirectedGraph : DirectedGraph() { return edgeWeighs.any { it < 0 } } - - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Vertex>>? { - val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } - val predecessorMap = mutableMapOf, Vertex?>() - val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } - val visited = mutableSetOf, Int>>() - - if (srcVertex == destVertex) return emptyList() - - distanceMap[srcVertex] = 0 - - while (priorityQueue.isNotEmpty()) { - val (currentVertex, currentDistance) = priorityQueue.poll() - if (visited.add(currentVertex to currentDistance)) { - getNeighbours(currentVertex).forEach { adjacent -> - val currentEdge = getEdge(currentVertex, adjacent) // Ensure correct edge direction - - val totalDist = currentDistance + getWeight(currentEdge) - - if (totalDist < distanceMap.getValue(adjacent)) { - distanceMap[adjacent] = totalDist - predecessorMap[adjacent] = currentVertex // Update predecessor - priorityQueue.add(adjacent to totalDist) - } - } - } - } - - // Reconstruct the path from srcVertex to destVertex - val path: MutableList, Vertex>> = mutableListOf() - var currentVertex = destVertex - while (currentVertex != srcVertex) { - val predecessor = predecessorMap[currentVertex] - ?: return null // path doesn't exist - - val currentEdge = getEdge(predecessor, currentVertex) - path.add(currentEdge to currentVertex) - - currentVertex = predecessor - } - return path.reversed() - } - - // returns null if path doesn't exist - fun findShortestPathFordBellman(srcVertex: Vertex, destVertex: Vertex): List, Vertex>>? { - val NEG_INF = -1000000 - - val distance = MutableList(getVertices().size) { Int.MAX_VALUE } - val predecessor = MutableList?>(getVertices().size) { null } - - distance[srcVertex.id] = 0 - - for (i in 0..getVertices().size - 1) { - for (edge in edges) { - val v1 = edge.vertex1 - val v2 = edge.vertex2 - - if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + getWeight(edge)) { - // distance will equal negative infinity if there is negative cycle - distance[v2.id] = maxOf(distance[v1.id] + getWeight(edge), NEG_INF) - - predecessor[v2.id] = v1 - } - } - } - - // check for negative cycles, determine if path to destVertex exists - for (i in 0..getVertices().size - 1) { - for (edge in getEdges()) { - val v1 = edge.vertex1 - val v2 = edge.vertex2 - - if (distance[v1.id] != Int.MAX_VALUE && distance[v2.id] > distance[v1.id] + getWeight(edge)) { - distance[v2.id] = NEG_INF - - } - } - } - - // there is a negative cycle on the way, so path doesn't exist - if (distance[destVertex.id] == NEG_INF) return null - - if (srcVertex == destVertex) return emptyList() - - // reconstruct the path from srcVertex to destVertex - val path: MutableList, Vertex>> = mutableListOf() - var currentVertex = destVertex - while (currentVertex != srcVertex) { - val currentPredecessor = predecessor[currentVertex.id] - ?: return null // path doesn't exist - - path.add(getEdge(currentPredecessor, currentVertex) to currentVertex) - - currentVertex = currentPredecessor - } - - return path.reversed() - } } diff --git a/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt index 6913ea64..a1d6bb27 100644 --- a/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt +++ b/app/src/main/kotlin/model/graphs/WeightedUndirectedGraph.kt @@ -45,93 +45,4 @@ class WeightedUndirectedGraph : UndirectedGraph() { return edgeWeighs.any { it < 0 } } - - fun findShortestPathDijkstra(srcVertex: Vertex, destVertex: Vertex): List, Vertex>>? { - val distanceMap = mutableMapOf, Int>().withDefault { Int.MAX_VALUE } - val predecessorMap = mutableMapOf, Vertex?>() - val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }).apply { add(srcVertex to 0) } - val visited = mutableSetOf, Int>>() - - if (srcVertex == destVertex) return emptyList() - - distanceMap[srcVertex] = 0 - - while (priorityQueue.isNotEmpty()) { - val (currentVertex, currentDistance) = priorityQueue.poll() - if (visited.add(currentVertex to currentDistance)) { - getNeighbours(currentVertex).forEach { adjacent -> - val currentEdge = getEdge(adjacent, currentVertex) - - val totalDist = currentDistance + getWeight(currentEdge) - - if (totalDist < distanceMap.getValue(adjacent)) { - distanceMap[adjacent] = totalDist - predecessorMap[adjacent] = currentVertex // Update predecessor - priorityQueue.add(adjacent to totalDist) - } - } - } - } - - // Reconstruct the path from srcVertex to destVertex - val path: MutableList, Vertex>> = mutableListOf() - var currentVertex = destVertex - while (currentVertex != srcVertex) { - val predecessor = predecessorMap[currentVertex] - ?: return null // path doesn't exist - - val currentEdge = getEdge(predecessor, currentVertex) - path.add(currentEdge to currentVertex) - - currentVertex = predecessor - } - return path.reversed() - } - - fun findMinSpanningTree(): List> { - val graphSize = vertices.size - - // set each vertex parent to be itself and each vertex rank to 0 - val parentIdList = Array(graphSize) { i: Int -> i } - val rankList = Array(graphSize) { 0 } - - fun findRootIdByVertexId(vId: Int): Int { - if (parentIdList[vId] == vId) return vId - - parentIdList[vId] = findRootIdByVertexId(parentIdList[vId]) - - return parentIdList[vId] - } - - fun uniteTwoTreesByVerticesIds(vId1: Int, vId2: Int) { - val rootId1 = findRootIdByVertexId(vId1) - val rootId2 = findRootIdByVertexId(vId2) - - if (rootId1 == rootId2) return - - if (rankList[rootId1] < rankList[rootId2]) { - parentIdList[rootId1] = rootId2 - } else { - parentIdList[rootId2] = rootId1 - if (rankList[rootId1] == rankList[rootId2]) rankList[rootId1]++ - } - } - - val edgeWeightPairs = weightMap.toList() - val sortedEdgeWeightPairs = edgeWeightPairs.sortedBy { it.second } - - val chosenEdges = mutableListOf>() - - for ((edge, _) in sortedEdgeWeightPairs) { - val id1 = edge.vertex1.id - val id2 = edge.vertex2.id - - if (findRootIdByVertexId(id1) != findRootIdByVertexId(id2)) { - uniteTwoTreesByVerticesIds(id1, id2) - chosenEdges.add(edge) - } - } - - return chosenEdges - } } diff --git a/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt index 2a8ca133..c3e1fb0c 100644 --- a/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt +++ b/app/src/main/kotlin/model/graphs/abstractGraph/Graph.kt @@ -1,10 +1,7 @@ package model.graphs.abstractGraph -import java.util.* -import java.util.ArrayDeque import kotlin.NoSuchElementException import kotlin.collections.ArrayList -import kotlin.math.roundToInt abstract class Graph { protected val vertices: ArrayList> = arrayListOf() @@ -64,8 +61,10 @@ abstract class Graph { fun getVertices() = vertices.toList() - /* In undirected graph, returns a map with every edge as a key and 1 as a value - * In a directed graph, returns copy of weightMap property */ + /** + * In unweighted graph, returns a map with every edge as a key and 1 as a value + * In a weighted graph, returns copy of weightMap property + */ open fun getWeightMap(): MutableMap, Int> { val weightMap = mutableMapOf, Int>() @@ -91,134 +90,4 @@ abstract class Graph { abstract fun getEdge(vertex1: Vertex, vertex2: Vertex): Edge open fun hasNegativeEdges() = false - - /* For every vertex, calculates normalized closeness centrality, based on which the key vertices are picked. - * Formula was taken from "Efficient Top-k Closeness Centrality Search" by Paul W. Olsen et al., - * yet an easier algorithm for traversal was chosen. */ - fun findKeyVertices(): Set>? { - val graphSize = vertices.size - - val distanceMap = getWeightMap() - if (this.hasNegativeEdges()) return null - - val centralityMap = mutableMapOf, Double>() - - for (currVertex in vertices) { - val currSumOfDistances = calcSumOfDistancesFromVertex(currVertex, distanceMap, graphSize) - - val reachableNum = findReachableVerticesNumFromVertex(currVertex, graphSize) - - val currCentrality = calcCentralityOfVertex(currSumOfDistances, reachableNum, graphSize) - centralityMap[currVertex] = currCentrality - } - - val keyVertices = pickMostKeyVertices(centralityMap, graphSize) - - return keyVertices - } - - /* Uses modified Dijkstra's algorithm to calculate the sum of all weights (distances) - * of shortest paths from source vertex to every other reachable one */ - private fun calcSumOfDistancesFromVertex( - srcVertex: Vertex, - distanceMap: Map, Int>, - graphSize: Int - ): Int { - val POS_INF = 100_000_000 // to infinity and beyond - - val visited = Array(graphSize) { false } - - val distances = Array(graphSize) { POS_INF } - distances[srcVertex.id] = 0 - - // stores pairs of vertices and total distances to them, sorted by distances ascending - val priorityQueue = PriorityQueue, Int>>(compareBy { it.second }) - priorityQueue.add(srcVertex to distances[srcVertex.id]) - - while (priorityQueue.isNotEmpty()) { - val (currVertex, currDistance) = priorityQueue.poll() - - if (visited[currVertex.id]) continue - - visited[currVertex.id] = true - - val neighbours = getNeighbours(currVertex) - for (neighbour in neighbours) { - val edgeToNeighbour = getEdge(currVertex, neighbour) - - val edgeToNeighbourDistance = distanceMap[edgeToNeighbour] ?: POS_INF - - val totalDistanceToNeighbour = currDistance + edgeToNeighbourDistance - - if (totalDistanceToNeighbour < distances[neighbour.id]) { - distances[neighbour.id] = totalDistanceToNeighbour - priorityQueue.add(neighbour to totalDistanceToNeighbour) - } - } - } - - for (i in distances.indices) { - if (distances[i] == POS_INF) distances[i] = 0 - } - - val sum = distances.sum() - - return sum - } - - private fun findReachableVerticesNumFromVertex(vertex: Vertex, graphSize: Int): Int { - var reachableVerticesNum = 0 - - val verticesToVisit = ArrayDeque>() - verticesToVisit.add(vertex) - - val visited = Array(graphSize) { false } - - while (verticesToVisit.isNotEmpty()) { - val vertexToVisit = verticesToVisit.poll() - - if (visited[vertexToVisit.id]) continue - - for (neighbour in getNeighbours(vertexToVisit)) { - verticesToVisit.add(neighbour) - } - - visited[vertexToVisit.id] = true - verticesToVisit.add(vertexToVisit) - - reachableVerticesNum++ - } - - return reachableVerticesNum - } - - private fun calcCentralityOfVertex(sumOfDistances: Int, reachableNum: Int, graphSize: Int): Double { - if (sumOfDistances == 0) return 0.0 - - val centrality = - ((reachableNum - 1) * (reachableNum - 1)) / ((graphSize - 1) * sumOfDistances).toDouble() - - return centrality - } - - private fun pickMostKeyVertices(centralityMap: Map, Double>, graphSize: Int): Set> { - val keyVertices = mutableSetOf>() - - val percent = 0.2 - val keyVerticesNum = (graphSize * percent).roundToInt() // rounds up - - var currKeyVerticesNum = 0 - - val vertexCentralityPairs = centralityMap.toList() - val vertexCentralityPairsSorted = vertexCentralityPairs.sortedByDescending { it.second } - - for ((vertex, _) in vertexCentralityPairsSorted) { - if (currKeyVerticesNum >= keyVerticesNum) break - - keyVertices.add(vertex) - currKeyVerticesNum++ - } - - return keyVertices - } } diff --git a/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt b/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt deleted file mode 100644 index c6d65cf4..00000000 --- a/app/src/main/kotlin/view/tabScreen/AnalyzeTab.kt +++ /dev/null @@ -1,13 +0,0 @@ -package view.tabScreen - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import viewmodel.graph.GraphViewModel - -@Composable -fun AnalyzeTab(graphVM: GraphViewModel) { - Column(modifier = Modifier.fillMaxSize()) { Text("hahahhahh 2nd tab") } -} diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index 2c1e9721..549b2fc4 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog @@ -62,7 +63,10 @@ fun GeneralTab(graphVM: GraphViewModel) { onClick = { if (vertexData.isNotEmpty()) showVertexAddDialog = true }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("add") + Text( + "add\nvertex", + textAlign = TextAlign.Center + ) } } } @@ -79,7 +83,7 @@ fun GeneralTab(graphVM: GraphViewModel) { textStyle = TextStyle(fontSize = 12.sp), label = { Text( - "1 edge ID", + "1 vertex ID", style = MaterialTheme.typography.body1.copy(fontSize = 12.sp), color = Color.Gray ) @@ -98,7 +102,7 @@ fun GeneralTab(graphVM: GraphViewModel) { modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), label = { Text( - "2 edge ID", + "2 vertex ID", style = MaterialTheme.typography.body1.copy(fontSize = 12.sp), color = Color.Gray ) @@ -129,7 +133,10 @@ fun GeneralTab(graphVM: GraphViewModel) { }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("add") + Text( + "add\nedge", + textAlign = TextAlign.Center + ) } } } diff --git a/app/src/main/kotlin/view/tabScreen/TabHandler.kt b/app/src/main/kotlin/view/tabScreen/TabHandler.kt index be4607ed..98952546 100644 --- a/app/src/main/kotlin/view/tabScreen/TabHandler.kt +++ b/app/src/main/kotlin/view/tabScreen/TabHandler.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import view.tabScreen.analyzeTab.AnalyzeTab import viewmodel.MainScreenViewModel @OptIn(ExperimentalFoundationApi::class) diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt new file mode 100644 index 00000000..b4c5ff81 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt @@ -0,0 +1,105 @@ +package view.tabScreen.analyzeTab + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import view.tabScreen.analyzeTab.algorithmsUI.* +import viewmodel.graph.GraphViewModel + +val rowHeight = 75.dp +val borderPadding = 10.dp +val horizontalGap = 20.dp + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AnalyzeTab(graphVM: GraphViewModel) { + val algorithms = arrayOf( + "Layout", + "Communities", + "Key vertices", + "Shortest path", + "Cycles", + "Bridges", + "SCC", + "Min spanning tree" + ) + + var selectedAlgorithm by remember { mutableStateOf(algorithms[0]) } + + Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { + Row(modifier = Modifier.height(0.dp)) {} + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column( + modifier = Modifier.width(95.dp).fillMaxHeight(), + Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Algorithm:", + textAlign = TextAlign.Center, + modifier = Modifier.padding(start = 7.dp) + ) + } + Column(modifier = Modifier.width(225.dp).fillMaxHeight(), Arrangement.Center) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.width(225.dp).fillMaxHeight() + ) { + TextField( + value = selectedAlgorithm, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + algorithms.forEach { algorithm -> + DropdownMenuItem( + modifier = Modifier, + onClick = { + selectedAlgorithm = algorithm + expanded = false + } + ) { + Text(text = algorithm) + } + } + } + } + } + + } + + when (selectedAlgorithm) { + "Layout" -> { LayoutUI(graphVM) } + "Communities" -> { CommunitiesUI(graphVM) } + "Key vertices" -> { KeyVerticesUI(graphVM) } + "Shortest path" -> { ShortestPathUI(graphVM) } + "Cycles" -> { CyclesUI(graphVM) } + "Bridges" -> { BridgesUI(graphVM) } + "SCC" -> { SCCUI(graphVM) } + "Min spanning tree" -> { MinSpanningTreeUI(graphVM) } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt new file mode 100644 index 00000000..af869b4b --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt @@ -0,0 +1,31 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + +@Composable +fun BridgesUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Find bridges") + } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt new file mode 100644 index 00000000..ce7b443a --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt @@ -0,0 +1,31 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + +@Composable +fun CommunitiesUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Find communities") + } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt new file mode 100644 index 00000000..a92d46b4 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt @@ -0,0 +1,31 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + +@Composable +fun CyclesUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Find cycles") + } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt new file mode 100644 index 00000000..5c0e7bd9 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt @@ -0,0 +1,31 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + +@Composable +fun KeyVerticesUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Find key vertices") + } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt new file mode 100644 index 00000000..db9da685 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt @@ -0,0 +1,32 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + + +@Composable +fun LayoutUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Apply layout") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt new file mode 100644 index 00000000..c837325f --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt @@ -0,0 +1,31 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + +@Composable +fun MinSpanningTreeUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Find min spanning tree") + } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt new file mode 100644 index 00000000..9c3614e9 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt @@ -0,0 +1,31 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + +@Composable +fun SCCUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Find SCCs") + } + } + } +} diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt new file mode 100644 index 00000000..7471c0f5 --- /dev/null +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt @@ -0,0 +1,31 @@ +package view.tabScreen.analyzeTab.algorithmsUI + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import viewmodel.graph.GraphViewModel + +@Composable +fun ShortestPathUI(graphVM: GraphViewModel) { + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = {}, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Find Shortest path") + } + } + } +} diff --git a/app/src/test/kotlin/model/DirectedGraphTest.kt b/app/src/test/kotlin/model/DirectedGraphTest.kt deleted file mode 100644 index 4012a6dd..00000000 --- a/app/src/test/kotlin/model/DirectedGraphTest.kt +++ /dev/null @@ -1,962 +0,0 @@ -package model - -import model.graphs.abstractGraph.Edge -import model.graphs.abstractGraph.Vertex -import model.graphs.DirectedGraph -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Nested -import util.annotations.TestAllDirectedGraphs -import util.emptyEdgesSet -import util.emptyVerticesList -import util.setupAbstractGraph - -class DirectedGraphTest { - @Nested - inner class GetEdgeTest { - @Nested - inner class `Edge is in the graph` { - @TestAllDirectedGraphs - fun `edge should be returned`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v2 = defaultVerticesList[2] - - val newEdge = graph.addEdge(v0, v2) - - val actualValue = newEdge - val expectedValue = graph.getEdge(v0, v2) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph shouldn't change`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v2 = defaultVerticesList[2] - val v3 = defaultVerticesList[3] - - graph.getEdge(v2, v3) - - val actualGraph = graph.getVertices() to graph.getEdges().toSet() - val expectedGraph = graphStructure - - assertEquals(expectedGraph, actualGraph) - } - } - - @Nested - inner class `Edge isn't in the graph` { - @TestAllDirectedGraphs - fun `order of arguments should matter`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v1 = defaultVerticesList[1] - - assertThrows(NoSuchElementException::class.java) { - graph.getEdge(v1, v0) - } - } - - @TestAllDirectedGraphs - fun `trying to get non-existent edge should throw an exception`(graph: DirectedGraph) { - assertThrows(NoSuchElementException::class.java) { - graph.getEdge(Vertex(2, 12), Vertex(85, 6)) - } - } - } - } - - @Nested - inner class GetNeighboursTest { - @Nested - inner class `Vertex is in the graph` { - @TestAllDirectedGraphs - fun `neighbours should be returned`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v1 = defaultVerticesList[1] - val v2 = defaultVerticesList[2] - val v3 = defaultVerticesList[3] - val v4 = defaultVerticesList[4] - - val actualValue = graph.getNeighbours(v3).toSet() - val expectedValue = setOf(v4, v1) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph shouldn't change`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - - graph.getNeighbours(v0) - - val actualGraph = graph.getVertices() to graph.getEdges().toSet() - val expectedGraph = graphStructure - - assertEquals(expectedGraph, actualGraph) - } - } - - @Nested - inner class `Vertex isn't in the graph` { - @TestAllDirectedGraphs - fun `exception should be thrown`(graph: DirectedGraph) { - assertThrows(NoSuchElementException::class.java) { - graph.getNeighbours(Vertex(2201, 2006)) - } - } - } - } - - @Nested - inner class GetOutgoingEdgesTest { - @Nested - inner class `Vertex is in the graph` { - @TestAllDirectedGraphs - fun `outgoing edges should be returned`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v1 = defaultVerticesList[1] - val v2 = defaultVerticesList[2] - val v3 = defaultVerticesList[3] - val v4 = defaultVerticesList[4] - - val actualValue = graph.getOutgoingEdges(v3).toSet() - val expectedValue = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph shouldn't change`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v4 = defaultVerticesList[4] - - graph.getOutgoingEdges(v4) - - val actualGraph = graph.getVertices() to graph.getEdges().toSet() - val expectedGraph = graphStructure - - assertEquals(expectedGraph, actualGraph) - } - } - - @Nested - inner class `Vertex isn't in the graph` { - @TestAllDirectedGraphs - fun `exception should be thrown`(graph: DirectedGraph) { - assertThrows(NoSuchElementException::class.java) { - graph.getOutgoingEdges(Vertex(2611, 2005)) - } - } - } - } - - @Nested - inner class AddEdgeTest { - @Nested - inner class `Two vertices are in the graph` { - @Nested - inner class `Vertices are different` { - @TestAllDirectedGraphs - fun `Added edge should be returned`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v4 = defaultVerticesList[4] - - val actualValue = graph.addEdge(v0, v4) - val expectedValue = graph.getEdge(v0, v4) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `Edge should be added to graph`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - val defaultEdgesSet = graphStructure.second - - val v0 = defaultVerticesList[0] - val v4 = defaultVerticesList[4] - - val newEdge = graph.addEdge(v4, v0) - - val actualEdges = graph.getEdges().toSet() - val expectedEdges = defaultEdgesSet + newEdge - - assertEquals(expectedEdges, actualEdges) - } - - @TestAllDirectedGraphs - fun `one vertex has to be added to the other's adjacency map value`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v1 = defaultVerticesList[1] - val v2 = defaultVerticesList[2] - val v3 = defaultVerticesList[3] - val v4 = defaultVerticesList[4] - - graph.addEdge(v0, v2) - - val actualVertices = graph.getNeighbours(v0).toSet() - val expectedVertices = setOf(v1, v2) - - assertEquals(expectedVertices, actualVertices) - } - - @TestAllDirectedGraphs - fun `edge has to be added to first vertex's outgoing edges map value`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v1 = defaultVerticesList[1] - val v2 = defaultVerticesList[2] - val v3 = defaultVerticesList[3] - val v4 = defaultVerticesList[4] - - graph.addEdge(v3, v0) - - val actualEdges = graph.getOutgoingEdges(v3).toSet() - val expectedEdges = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1), graph.getEdge(v3, v0)) - - assertEquals(expectedEdges, actualEdges) - } - - @TestAllDirectedGraphs - fun `adding already existing edge shouldn't change anything`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v1 = defaultVerticesList[1] - val v4 = defaultVerticesList[4] - - val expectedNeighbours = graph.getNeighbours(v4).toSet() - val expectedOutgoingEdges = graph.getOutgoingEdges(v4).toSet() - - graph.addEdge(v4, v1) - - val actualNeighbours = graph.getNeighbours(v4).toSet() - val actualOutgoingEdges = graph.getOutgoingEdges(v4).toSet() - - val expectedGraph = graphStructure - val actualGraph = graph.getVertices() to graph.getEdges().toSet() - - assertEquals(expectedGraph, actualGraph) - assertEquals(expectedNeighbours, actualNeighbours) - assertEquals(expectedOutgoingEdges, actualOutgoingEdges) - } - - @TestAllDirectedGraphs - fun `second vertex's map values shouldn't change`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - val v3 = defaultVerticesList[3] - - val expectedNeighbours = graph.getNeighbours(v0).toSet() - val expectedOutgoingEdges = graph.getOutgoingEdges(v0).toSet() - - graph.addEdge(v3, v0) - - val actualNeighbours = graph.getNeighbours(v0).toSet() - val actualOutgoingEdges = graph.getOutgoingEdges(v0).toSet() - - assertEquals(expectedNeighbours, actualNeighbours) - assertEquals(expectedOutgoingEdges, actualOutgoingEdges) - } - } - - @Nested - inner class `Vertices are the same` { - @TestAllDirectedGraphs - fun `exception should be thrown`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v2 = defaultVerticesList[2] - - assertThrows(IllegalArgumentException::class.java) { - graph.addEdge(v2, v2) - } - } - } - } - - @Nested - inner class `One of the vertices isn't in the graph` { - @TestAllDirectedGraphs - fun `first vertex isn't in the graph`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - - assertThrows(IllegalArgumentException::class.java) { - graph.addEdge(Vertex(2210, 2005), v0) - } - } - - @TestAllDirectedGraphs - fun `second vertex isn't in the graph`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v0 = defaultVerticesList[0] - - assertThrows(IllegalArgumentException::class.java) { - graph.addEdge(v0, Vertex(2510, 1917)) - } - } - - @TestAllDirectedGraphs - fun `both vertices aren't in the graph`(graph: DirectedGraph) { - assertThrows(IllegalArgumentException::class.java) { - graph.addEdge(Vertex(3010, 1978), Vertex(1002, 1982)) - } - } - } - } - - @Nested - inner class RemoveEdgeTest { - @Nested - inner class `Edge is in the graph` { - @TestAllDirectedGraphs - fun `removed edge should be returned`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v1 = defaultVerticesList[1] - val v3 = defaultVerticesList[3] - - val edgeToRemove = graph.getEdge(v3, v1) - - val actualValue = graph.removeEdge(edgeToRemove) - val expectedValue = edgeToRemove - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `edge should be removed from graph`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - val defaultEdgesSet = graphStructure.second - - val v1 = defaultVerticesList[1] - val v4 = defaultVerticesList[4] - - val edgeToRemove = graph.getEdge(v4, v1) - graph.removeEdge(edgeToRemove) - - val actualEdges = graph.getEdges().toSet() - val expectedEdges = defaultEdgesSet - edgeToRemove - - assertEquals(expectedEdges, actualEdges) - } - - @TestAllDirectedGraphs - fun `second vertex should be removed from first's adjacency map value`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v1 = defaultVerticesList[1] - val v2 = defaultVerticesList[2] - - val edgeToRemove = graph.getEdge(v1, v2) - graph.removeEdge(edgeToRemove) - - val actualVertices = graph.getNeighbours(v1).toSet() - val expectedVertices = emptyVerticesList.toSet() - - assertEquals(expectedVertices, actualVertices) - } - - @TestAllDirectedGraphs - fun `edge should be removed from first vertex's outgoing edges map value`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v2 = defaultVerticesList[2] - val v3 = defaultVerticesList[3] - - val edgeToRemove = graph.getEdge(v2, v3) - graph.removeEdge(edgeToRemove) - - val actualEdges = graph.getOutgoingEdges(v2).toSet() - val expectedEdges = emptyEdgesSet - - assertEquals(expectedEdges, actualEdges) - } - } - - @Nested - inner class `Edge isn't in the graph` { - @TestAllDirectedGraphs - fun `wrong order of the arguments should throw an exception`(graph: DirectedGraph) { - val graphStructure = setupAbstractGraph(graph) - val defaultVerticesList = graphStructure.first - - val v3 = defaultVerticesList[3] - val v4 = defaultVerticesList[4] - - assertThrows(NoSuchElementException::class.java) { - graph.removeEdge(graph.getEdge(v4, v3)) - } - } - - @TestAllDirectedGraphs - fun `non-existing edge should throw an exception`(graph: DirectedGraph) { - assertThrows(NoSuchElementException::class.java) { - graph.removeEdge(Edge(Vertex(0, 0), Vertex(1, 1))) - } - } - } - } - - @Nested - inner class FindSCCTest { - @Nested - inner class `SCC should return not empty array` { - @TestAllDirectedGraphs - fun `graph has two connected vertices`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - graph.addEdge(v1, v2) - graph.addEdge(v2, v1) - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1, v2)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `complex graph`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v3, v4) - } - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph has multiple SCCs`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v1) - addEdge(v3, v4) - addEdge(v4, v3) - addEdge(v5, v1) - } - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v3, v4), mutableSetOf(v1, v2), mutableSetOf(v5)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph with nested cycles`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - val v6 = graph.addVertex(6) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v3, v4) - addEdge(v4, v5) - addEdge(v5, v6) - addEdge(v6, v4) - } - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph with cross connections`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - val v6 = graph.addVertex(6) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v3, v4) - addEdge(v4, v5) - addEdge(v5, v6) - addEdge(v6, v4) - } - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph with disconnected subgraphs`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - val v6 = graph.addVertex(6) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v1) - addEdge(v3, v4) - addEdge(v4, v3) - addEdge(v5, v6) - addEdge(v6, v5) - } - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1, v2), mutableSetOf(v3, v4), mutableSetOf(v5, v6)) - assertEquals(expectedValue, actualValue) - } - - @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") - @TestAllDirectedGraphs - fun `graph with single vertex cycle`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v3, v3) - } - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3)) - - assertEquals(expectedValue, actualValue) - } - } - - @Nested - inner class `SCC should return single-element SCCs` { - @TestAllDirectedGraphs - fun `graph has single vertex`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph with multiple disconnected vertices`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - val actualValue = graph.findSCC() - val expectedValue = - mutableSetOf( - mutableSetOf(v1), - mutableSetOf(v2), - mutableSetOf(v3), - mutableSetOf(v4) - ) - - assertEquals(expectedValue, actualValue) - } - } - - @Nested - inner class `Additional edge cases`() { - @TestAllDirectedGraphs - fun `empty graph`(graph: DirectedGraph) { - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf>>() - - assertEquals(expectedValue, actualValue) - } - - @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") - @TestAllDirectedGraphs - fun `graph with self-loops`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - graph.addEdge(v1, v1) - graph.addEdge(v2, v2) - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1), mutableSetOf(v2)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `linear graph`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.addEdge(v1, v2) - graph.addEdge(v2, v3) - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v3), mutableSetOf(v2), mutableSetOf(v1)) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `graph with cycles and tail`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v4, v3) - addEdge(v4, v5) - } - - val actualValue = graph.findSCC() - val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4), mutableSetOf(v5)) - - assertEquals(expectedValue, actualValue) - } - } - - @Nested - inner class `Side-effects check` { - @TestAllDirectedGraphs - fun `check vertices in complex graph`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v3, v4) - } - - val expectedValue = graph.getVertices() - graph.findSCC() - val actualValue = graph.getVertices() - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `check edges in complex graph`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v3, v4) - } - - val expectedValue = graph.getEdges() - graph.findSCC() - val actualValue = graph.getEdges() - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `check edges in graph with cycles and tail`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v4, v3) - addEdge(v4, v5) - } - - val expectedValue = graph.getEdges() - graph.findSCC() - val actualValue = graph.getEdges() - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `check vertices graph with cycles and tail`(graph: DirectedGraph) { - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v1) - addEdge(v4, v3) - addEdge(v4, v5) - } - - val expectedValue = graph.getVertices() - graph.findSCC() - val actualValue = graph.getVertices() - - assertEquals(expectedValue, actualValue) - } - } - } - - @Nested - inner class FindKeyVerticesTest { - @Nested - inner class `One vertex is picked over another`() { - @TestAllDirectedGraphs - fun `if it can reach more vertices`(graph: DirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v1) - addEdge(v0, v2) - addEdge(v0, v3) - addEdge(v1, v2) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - - @TestAllDirectedGraphs - fun `if it can reach other vertices with fewer edges`(graph: DirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v0, v2) - addEdge(v0, v3) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - } - } - - @Nested - inner class FindCyclesTest { - @Nested - inner class `There are some cycles` { - @TestAllDirectedGraphs - fun `all cycles should be returned`(graph: DirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - val v6 = graph.addVertex(6) - val v7 = graph.addVertex(7) - val v8 = graph.addVertex(8) - - val e01 = graph.addEdge(v0, v1) - val e07 = graph.addEdge(v0, v7) - val e04 = graph.addEdge(v0, v4) - val e18 = graph.addEdge(v1, v8) - val e12 = graph.addEdge(v1, v2) - val e20 = graph.addEdge(v2, v0) - val e21 = graph.addEdge(v2, v1) - val e25 = graph.addEdge(v2, v5) - val e23 = graph.addEdge(v2, v3) - val e53 = graph.addEdge(v5, v3) - val e34 = graph.addEdge(v3, v4) - val e41 = graph.addEdge(v4, v1) - val e78 = graph.addEdge(v7, v8) - val e87 = graph.addEdge(v8, v7) - - val expectedCycle1 = listOf(e12 to v2, e21 to v1) - val expectedCycle2 = listOf(e12 to v2, e20 to v0, e01 to v1) - val expectedCycle3 = listOf(e12 to v2, e20 to v0, e04 to v4, e41 to v1) - val expectedCycle4 = listOf(e12 to v2, e23 to v3, e34 to v4, e41 to v1) - val expectedCycle5 = listOf(e12 to v2, e25 to v5, e53 to v3, e34 to v4, e41 to v1) - - val actualValue = graph.findCycles(v1) - val expectedValue = - setOf( - expectedCycle1, - expectedCycle2, - expectedCycle3, - expectedCycle4, - expectedCycle5 - ) - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `SCC of 2 vertices should have one cycle`(graph: DirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e01 = graph.addEdge(v0, v1) - val e12 = graph.addEdge(v1, v2) - val e21 = graph.addEdge(v2, v1) - val e23 = graph.addEdge(v2, v3) - - val actualValue = graph.findCycles(v1) - val expectedValue = setOf(listOf(e12 to v2, e21 to v1)) - - assertEquals(expectedValue, actualValue) - } - } - - @Nested - inner class `There are no cycles` { - @TestAllDirectedGraphs - fun `vertex without outgoing edges shouldn't have cycles`(graph: DirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - graph.addEdge(v0, v1) - - val actualValue = graph.findCycles(v1) - val expectedValue = emptySet, Vertex>>>() - - assertEquals(expectedValue, actualValue) - } - - @TestAllDirectedGraphs - fun `SCC of 1 vertex shouldn't have cycles`(graph: DirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - graph.addEdge(v0, v1) - - val actualValue = graph.findCycles(v0) - val expectedValue = emptySet, Vertex>>>() - - assertEquals(expectedValue, actualValue) - } - } - - @TestAllDirectedGraphs - fun `graph shouldn't change`(graph: DirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - val v6 = graph.addVertex(6) - val v7 = graph.addVertex(7) - val v8 = graph.addVertex(8) - - val e01 = graph.addEdge(v0, v1) - val e07 = graph.addEdge(v0, v7) - val e04 = graph.addEdge(v0, v4) - val e18 = graph.addEdge(v1, v8) - val e12 = graph.addEdge(v1, v2) - val e20 = graph.addEdge(v2, v0) - val e21 = graph.addEdge(v2, v1) - val e25 = graph.addEdge(v2, v5) - val e23 = graph.addEdge(v2, v3) - val e53 = graph.addEdge(v5, v3) - val e34 = graph.addEdge(v3, v4) - val e41 = graph.addEdge(v4, v1) - val e78 = graph.addEdge(v7, v8) - val e87 = graph.addEdge(v8, v7) - - val expectedGraph = graph.getVertices() to graph.getEdges().toSet() - - graph.findCycles(v1) - - val actualGraph = graph.getVertices() to graph.getEdges().toSet() - - assertEquals(expectedGraph, actualGraph) - } - } -} diff --git a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt deleted file mode 100644 index a22bfce7..00000000 --- a/app/src/test/kotlin/model/WeightedDirectedGraphTest.kt +++ /dev/null @@ -1,551 +0,0 @@ -package model - -import model.graphs.abstractGraph.Edge -import model.graphs.abstractGraph.Vertex -import model.graphs.WeightedDirectedGraph -import model.graphs.WeightedUndirectedGraph -import org.junit.jupiter.api.* -import org.junit.jupiter.api.Assertions.* -import util.setupDirectedGraphWithCycle -import util.setupWeightedDirected - -class WeightedDirectedGraphTest { - private lateinit var graph: WeightedDirectedGraph - - @BeforeEach - fun init() { - graph = WeightedDirectedGraph() - } - - @Nested - inner class GetWeightTest { - @Nested - inner class `Edge is in the graph` { - @Test - fun `edge's weight should be returned`() { - val graphStructure = setupWeightedDirected(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v3 = defaultVertices[3] - val edge = graph.getEdge(v3, v1) - - val actualValue = graph.getWeight(edge) - val expectedValue = 3 - - assertEquals(expectedValue, actualValue) - } - - @Test - fun `graph shouldn't change`() { - val graphStructure = setupWeightedDirected(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v4 = defaultVertices[3] - val edge = graph.getEdge(v4, v1) - - graph.getWeight(edge) - - val actualGraph = graph.getVertices() to graph.getEdges().toSet() - val expectedGraph = graphStructure - - assertEquals(expectedGraph, actualGraph) - } - } - - @Nested - inner class `Edge isn't in the graph` { - @Test - fun `exception should be thrown`() { - assertThrows(NoSuchElementException::class.java) { - graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) - } - } - } - } - - // most of the functionality is tested in the DirectedGraphTest class, - // as weighted graphs call super methods inside their methods - @Nested - inner class AddEdgeTest { - @Test - fun `added edge's weight should be added to weight map`() { - val v0 = graph.addVertex(30) - val v1 = graph.addVertex(31) - - val newEdge = graph.addEdge(v0, v1, 62) - - val actualValue = graph.getWeight(newEdge) - val expectedValue = 62 - - assertEquals(expectedValue, actualValue) - } - } - - @Nested - inner class RemoveEdgeTest { - @Test - fun `removed edge should be removed from the weight map`() { - val graphStructure = setupWeightedDirected(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v2 = defaultVertices[2] - val edge = graph.getEdge(v1, v2) - - graph.removeEdge(edge) - - assertThrows(NoSuchElementException::class.java) { graph.getWeight(edge) } - } - } - - @Nested - inner class FindShortestPathDijkstraTest { - @Nested - inner class `Normal path should be returned`() { - @Test - fun `all is as usual, should return default`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - val e0 = graph.addEdge(v0, v1, 10) - val e1 = graph.addEdge(v0, v4, 100) - val e2 = graph.addEdge(v0, v3, 30) - val e3 = graph.addEdge(v1, v2, 2) - val e4 = graph.addEdge(v2, v4, 10) - val e5 = graph.addEdge(v3, v2, 20) - val e6 = graph.addEdge(v3, v4, 60) - - val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) - val actualResult = graph.findShortestPathDijkstra(v0, v4) - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `direct path should be the shortest in directed graph`() { - val graph = WeightedDirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val e0 = graph.addEdge(v0, v1, 5) - val e1 = graph.addEdge(v1, v2, 5) - val e2 = graph.addEdge(v0, v2, 10) - - val expectedResult = listOf(e2 to v2) - val actualResult = graph.findShortestPathDijkstra(v0, v2) - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if graph has multiple paths and equal weights`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e0 = graph.addEdge(v0, v1, 1) - val e1 = graph.addEdge(v0, v2, 1) - val e2 = graph.addEdge(v1, v3, 1) - val e3 = graph.addEdge(v2, v3, 1) - - val expectedResult1 = listOf(e0 to v1, e2 to v3) - val expectedResult2 = listOf(e1 to v2, e3 to v3) - - val actualResult = graph.findShortestPathDijkstra(v0, v3) - - assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) - } - - @Test - fun `if graph has single edge`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - val e0 = graph.addEdge(v0, v1, 5) - - val expectedResult = listOf(e0 to v1) - val actualResult = graph.findShortestPathDijkstra(v0, v1) - - assertEquals(expectedResult, actualResult) - } - - @Disabled("Dijkstra's algorithm doesn't work with negative weights") - @Test - fun `if graph has negative weights`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e0 = graph.addEdge(v0, v1, -1) - val e1 = graph.addEdge(v1, v2, -2) - val e2 = graph.addEdge(v2, v3, -3) - - val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) - val actualResult = graph.findShortestPathDijkstra(v0, v3) - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `graph has multiple equal shortest paths`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - val e0 = graph.addEdge(v0, v1, 1) - val e1 = graph.addEdge(v0, v2, 1) - val e2 = graph.addEdge(v1, v3, 1) - val e3 = graph.addEdge(v2, v3, 1) - val e4 = graph.addEdge(v3, v4, 1) - - val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) - val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) - val actualResult = graph.findShortestPathDijkstra(v0, v4) - - assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) - } - - @Test - fun `if graph has a cycle`() { - val graph = WeightedUndirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val e0 = graph.addEdge(v0, v1, 1) - val e1 = graph.addEdge(v1, v2, 2) - val e2 = graph.addEdge(v2, v0, 3) - - val actualResult = graph.findShortestPathDijkstra(v0, v2) - val expectedResult = listOf(e2 to v2) - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if all the edges have zero weight in directed graph`() { - val graph = WeightedDirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val e0 = graph.addEdge(v0, v1, 0) - val e1 = graph.addEdge(v1, v2, 0) - - val expectedResult = listOf(e0 to v1, e1 to v2) - val actualResult = graph.findShortestPathDijkstra(v0, v2) - assertEquals(expectedResult, actualResult) - } - } - - @Nested - inner class `No path should be returned`() { - @Test - fun `no path exists in directed graph`() { - val graph = WeightedDirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.addEdge(v0, v1, 10) - graph.addEdge(v1, v2, 20) - - val actualResult = graph.findShortestPathDijkstra(v0, v3) - assertEquals(actualResult, null) - } - - @Test - fun `if start and end vertices are the same`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - graph.apply { - addEdge(v0, v1, 1) - addEdge(v1, v2, 2) - addEdge(v2, v0, 2) - } - - val actualResult = graph.findShortestPathDijkstra(v0, v0) - - actualResult?.isEmpty()?.let { assertTrue(it) } - } - - @Test - fun `if graph has single vertex`() { - val v0 = graph.addVertex(0) - - val actualResult = graph.findShortestPathDijkstra(v0, v0) - - actualResult?.isEmpty()?.let { assertTrue(it) } - } - - @Test - fun `if path is in other way (not how edges were set)`() { - val graph = WeightedDirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - graph.addEdge(v0, v1, 0) - graph.addEdge(v1, v2, 0) - - val actualResult = graph.findShortestPathDijkstra(v2, v0) - - actualResult?.isEmpty()?.let { assertTrue(it) } - } - } - } - - @Nested - inner class FindKeyVerticesTest { - @Nested - inner class `One vertex is picked over another`() { - @Test - fun `if it can reach more vertices`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v1, 1) - addEdge(v0, v2, 1) - addEdge(v0, v3, 1) - addEdge(v1, v2, 1) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if it can reach other vertices with fewer edges`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v2, 1) - addEdge(v0, v3, 1) - addEdge(v1, v2, 1) - addEdge(v2, v3, 1) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if its sum of distances to other vertices is less`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v2, 1) - addEdge(v0, v3, 1) - addEdge(v1, v2, 2) - addEdge(v2, v3, 2) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - } - - @Nested - inner class `Returns null`() { - @Test - fun `if graph has negative edges`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - graph.addEdge(v0, v1, 1) - graph.addEdge(v0, v2, -1) - - val actualResult = graph.findKeyVertices() - - assertNull(actualResult) - } - } - } - - @Nested - inner class findShortestPathFordBellmanTest { - @Nested - inner class `Path exists` { - @Test - fun `path between neighbours should consist of one edge`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val edge = graph.addEdge(v0, v1, 12345) - - val actualValue = graph.findShortestPathFordBellman(v0, v1) - val expectedValue = listOf(edge to v1) - - assertEquals(expectedValue, actualValue) - } - - @Test - fun `shortest path should be returned`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v0 = defaultVertices[0] - val v1 = defaultVertices[1] - val v2 = defaultVertices[2] - val v4 = defaultVertices[4] - - val actualValue = graph.findShortestPathFordBellman(v0, v4) - val expectedValue = - listOf( - graph.getEdge(v0, v1) to v1, - graph.getEdge(v1, v2) to v2, - graph.getEdge(v2, v4) to v4 - ) - - assertEquals(expectedValue, actualValue) - } - - @Test - fun `path from vertex to itself should exist and be empty`() { - val v0 = graph.addVertex(69) - - val actualValue = graph.findShortestPathFordBellman(v0, v0) - val expectedValue = emptyList, Vertex>>() - - assertEquals(expectedValue, actualValue) - } - - @Test - fun `graph shouldn't change`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v3 = defaultVertices[3] - val v4 = defaultVertices[4] - - val expectedGraph = graphStructure - graph.findShortestPathFordBellman(v3, v4) - val actualGraph = graphStructure - - assertEquals(expectedGraph, actualGraph) - } - } - - @Nested - inner class `Path doesn't exist` { - @Test - fun `there is simply no path between vertices`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v5 = defaultVertices[5] - - val actualValue = graph.findShortestPathFordBellman(v1, v5) - - assertNull(actualValue) - } - - @Test - fun `order of arguments should matter`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v0 = defaultVertices[0] - val v2 = defaultVertices[2] - - val actualValue = graph.findShortestPathFordBellman(v2, v0) - - assertNull(actualValue) - } - - @Test - fun `there is a negative cycle on the path`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v0 = defaultVertices[0] - val v8 = defaultVertices[8] - - val actualValue = graph.findShortestPathFordBellman(v0, v8) - - assertNull(actualValue) - } - - @Test - fun `srcVertex is a part of negative cycle`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v6 = defaultVertices[6] - val v8 = defaultVertices[8] - - val actualValue = graph.findShortestPathFordBellman(v6, v8) - - assertNull(actualValue) - } - - @Test - fun `vertex without outgoing edges shouldn't have any paths`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v0 = defaultVertices[0] - val v1 = defaultVertices[1] - val v2 = defaultVertices[2] - val v3 = defaultVertices[3] - val v4 = defaultVertices[4] - val v5 = defaultVertices[5] - val v6 = defaultVertices[6] - val v7 = defaultVertices[7] - val v8 = defaultVertices[8] - - assertNull(graph.findShortestPathFordBellman(v8, v0)) - assertNull(graph.findShortestPathFordBellman(v8, v1)) - assertNull(graph.findShortestPathFordBellman(v8, v2)) - assertNull(graph.findShortestPathFordBellman(v8, v3)) - assertNull(graph.findShortestPathFordBellman(v8, v4)) - assertNull(graph.findShortestPathFordBellman(v8, v5)) - assertNull(graph.findShortestPathFordBellman(v8, v6)) - assertNull(graph.findShortestPathFordBellman(v8, v7)) - } - - @Test - fun `graph shouldn't change`() { - val graphStructure = setupDirectedGraphWithCycle(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v8 = defaultVertices[8] - - val expectedGraph = graphStructure - graph.findShortestPathFordBellman(v8, v1) - val actualGraph = graphStructure - - assertEquals(expectedGraph, actualGraph) - } - } - } -} diff --git a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt deleted file mode 100644 index 1830b4e8..00000000 --- a/app/src/test/kotlin/model/WeightedUndirectedGraphTest.kt +++ /dev/null @@ -1,506 +0,0 @@ -package model - -import model.graphs.abstractGraph.Edge -import model.graphs.abstractGraph.Vertex -import model.graphs.WeightedUndirectedGraph -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import util.setupWeightedUndirected - -class WeightedUndirectedGraphTest { - private lateinit var graph: WeightedUndirectedGraph - - @BeforeEach - fun init() { - graph = WeightedUndirectedGraph() - } - - @Nested - inner class GetWeightTest { - @Nested - inner class `Edge is in the graph` { - @Test - fun `edge's weight should be returned`() { - val graphStructure = setupWeightedUndirected(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v3 = defaultVertices[3] - val edge = graph.getEdge(v3, v1) - - val actualValue = graph.getWeight(edge) - val expectedValue = 3 - - assertEquals(expectedValue, actualValue) - } - - @Test - fun `graph shouldn't change`() { - val graphStructure = setupWeightedUndirected(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v4 = defaultVertices[3] - val edge = graph.getEdge(v4, v1) - - graph.getWeight(edge) - - val actualGraph = graph.getVertices() to graph.getEdges().toSet() - val expectedGraph = graphStructure - - assertEquals(expectedGraph, actualGraph) - } - } - - @Nested - inner class `Edge isn't in the graph` { - @Test - fun `exception should be thrown`() { - assertThrows(NoSuchElementException::class.java) { - graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) - } - } - } - } - - // most of the functionality is tested in the DirectedGraphTest class, - // as weighted graphs call super methods inside their methods - @Nested - inner class AddEdgeTest { - @Test - fun `added edge's weight should be added to weight map`() { - val v0 = graph.addVertex(30) - val v1 = graph.addVertex(31) - - val newEdge = graph.addEdge(v0, v1, 62) - - val actualValue = graph.getWeight(newEdge) - val expectedValue = 62 - - assertEquals(expectedValue, actualValue) - } - } - - @Nested - inner class RemoveEdgeTest { - @Test - fun `removed edge should be removed from the weight map`() { - val graphStructure = setupWeightedUndirected(graph) - val defaultVertices = graphStructure.first - - val v1 = defaultVertices[1] - val v2 = defaultVertices[2] - val edge = graph.getEdge(v1, v2) - - graph.removeEdge(edge) - - assertThrows(NoSuchElementException::class.java) { - graph.getWeight(edge) - } - } - } - - @Nested - inner class FindShortestPathDijkstraTest { - @Nested - inner class `Normal path should be returned`() { - @Test - fun `all is as usual, should return default`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - val e0 = graph.addEdge(v0, v1, 10) - val e1 = graph.addEdge(v0, v4, 100) - val e2 = graph.addEdge(v0, v3, 30) - val e3 = graph.addEdge(v1, v2, 2) - val e4 = graph.addEdge(v2, v4, 10) - val e5 = graph.addEdge(v3, v2, 20) - val e6 = graph.addEdge(v3, v4, 60) - - val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) - val actualResult = graph.findShortestPathDijkstra(v0, v4) - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if graph has multiple paths and equal weights`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e0 = graph.addEdge(v0, v1, 1) - val e1 = graph.addEdge(v0, v2, 1) - val e2 = graph.addEdge(v1, v3, 1) - val e3 = graph.addEdge(v2, v3, 1) - - val expectedResult1 = listOf(e0 to v1, e2 to v3) - val expectedResult2 = listOf(e1 to v2, e3 to v3) - - val actualResult = graph.findShortestPathDijkstra(v0, v3) - - assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) - } - - @Test - fun `if graph has single edge`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - val e0 = graph.addEdge(v0, v1, 5) - - val expectedResult = listOf(e0 to v1) - val actualResult = graph.findShortestPathDijkstra(v0, v1) - - assertEquals(expectedResult, actualResult) - } - - @Disabled("Dijkstra's algorithm doesn't work with negative weights") - @Test - fun `if graph has negative weights`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e0 = graph.addEdge(v0, v1, -1) - val e1 = graph.addEdge(v1, v2, -2) - val e2 = graph.addEdge(v2, v3, -3) - - val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) - val actualResult = graph.findShortestPathDijkstra(v0, v3) - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `graph has multiple equal shortest paths`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - val e0 = graph.addEdge(v0, v1, 1) - val e1 = graph.addEdge(v0, v2, 1) - val e2 = graph.addEdge(v1, v3, 1) - val e3 = graph.addEdge(v2, v3, 1) - val e4 = graph.addEdge(v3, v4, 1) - - val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) - val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) - val actualResult = graph.findShortestPathDijkstra(v0, v4) - - assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) - } - - @Test - fun `if graph has a cycle`() { - val graph = WeightedUndirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val e0 = graph.addEdge(v0, v1, 1) - val e1 = graph.addEdge(v1, v2, 2) - val e2 = graph.addEdge(v2, v0, 3) - - val actualResult = graph.findShortestPathDijkstra(v0, v2) - val expectedResult = listOf(e2 to v2) - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if path is in other way (not how edges were set)`() { - val graph = WeightedUndirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val e0 = graph.addEdge(v0, v1, 0) - val e1 = graph.addEdge(v1, v2, 0) - - val expectedResult = listOf(e1 to v1, e0 to v0) - val actualResult = graph.findShortestPathDijkstra(v2, v0) - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if all the edges have zero weight in undirected graph`() { - val graph = WeightedUndirectedGraph() - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val e0 = graph.addEdge(v0, v1, 0) - val e1 = graph.addEdge(v1, v2, 0) - - val expectedResult = listOf(e0 to v1, e1 to v2) - val actualResult = graph.findShortestPathDijkstra(v0, v2) - - assertEquals(expectedResult, actualResult) - } - } - - @Nested - inner class `No path should be returned`() { - @Test - fun `no path exists in undirected graph`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.addEdge(v0, v1, 1) - graph.addEdge(v1, v2, 2) - - val actualResult = graph.findShortestPathDijkstra(v0, v3) - - assertNull(actualResult) - } - - @Test - fun `if start and end vertices are the same`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - graph.addEdge(v0, v1, 1) - graph.addEdge(v1, v2, 2) - - val actualResult = graph.findShortestPathDijkstra(v0, v0) - - actualResult?.isEmpty()?.let { assertTrue(it) } - } - - @Test - fun `if graph has single vertex`() { - val v0 = graph.addVertex(0) - - val actualResult = graph.findShortestPathDijkstra(v0, v0) - - actualResult?.isEmpty()?.let { assertTrue(it) } - } - } - } - - @Nested - inner class FindMinSpanningTreeTest { - @Nested - inner class `An edge is picked over another`() { - @Test - fun `if it has lesser weight but both have positive`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - val e01Heavy = graph.addEdge(v0, v1, 5) - val e01Light = graph.addEdge(v0, v1, 3) - - val expectedReturn = listOf(e01Light) - val actualReturn = graph.findMinSpanningTree() - - assertEquals(expectedReturn, actualReturn) - } - - @Test - fun `if it has lesser weight but both have negative`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - val e01Heavy = graph.addEdge(v0, v1, -5) - val e01Light = graph.addEdge(v0, v1, -10) - - val expectedReturn = listOf(e01Light) - val actualReturn = graph.findMinSpanningTree() - - assertEquals(expectedReturn, actualReturn) - } - - @Test - fun `if it has zero weight and other has positive`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - val e01Pos = graph.addEdge(v0, v1, 5) - val e01Zero = graph.addEdge(v0, v1, 0) - - val expectedReturn = listOf(e01Zero) - val actualReturn = graph.findMinSpanningTree() - - assertEquals(expectedReturn, actualReturn) - } - - @Test - fun `if it has negative weight and other has positive or zero`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - - val e01Pos = graph.addEdge(v0, v1, 5) - val e01Zero = graph.addEdge(v0, v1, 0) - val e01Neg = graph.addEdge(v0, v1, -5) - - val expectedReturn = listOf(e01Neg) - val actualReturn = graph.findMinSpanningTree() - - assertEquals(expectedReturn, actualReturn) - } - } - - @Nested - inner class `An edge is not picked over another`() { - @Test - fun `if it forms a cycle and has greatest weight in it`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e01 = graph.addEdge(v0, v1, 1) - val e12 = graph.addEdge(v1, v2, 1) - val e23 = graph.addEdge(v2, v3, 1) - - val e30 = graph.addEdge(v3, v0, 5) - - val expectedReturn = setOf(e01, e12, e23) - val actualReturn = graph.findMinSpanningTree().toSet() - - assertEquals(expectedReturn, actualReturn) - } - } - - @Nested - inner class `All edges should be returned`() { - @Test - fun `if graph is a tree`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - val eva01 = graph.addEdge(v0, v1, 1) - val eva02 = graph.addEdge(v0, v2, 10) - val e23 = graph.addEdge(v2, v3, 0) - val e24 = graph.addEdge(v2, v4, -20) - - val expectedResult = setOf(e24, e23, eva01, eva02) - val actualResult = graph.findMinSpanningTree().toSet() - - assertEquals(expectedResult, actualResult) - } - } - - @Nested - inner class `No edge should be returned`() { - @Test - fun `if graph has no vertices`() { - val expectedResult = listOf>() - val actualResult = graph.findMinSpanningTree() - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if graph has no edges`() { - graph.addVertex(0) - graph.addVertex(1) - - val expectedResult = listOf>() - val actualResult = graph.findMinSpanningTree() - - assertEquals(expectedResult, actualResult) - } - } - } - - @Nested - inner class FindKeyVerticesTest { - @Nested - inner class `One vertex is picked over another`() { - @Test - fun `if it can reach more vertices`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v1, 1) - addEdge(v0, v2, 1) - addEdge(v0, v3, 1) - addEdge(v1, v2, 1) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if it can reach other vertices with fewer edges`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v1) - addEdge(v0, v2) - addEdge(v0, v3) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - - @Test - fun `if its sum of distances to other vertices is less`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v2, 1) - addEdge(v0, v3, 1) - addEdge(v1, v2, 2) - addEdge(v2, v3, 2) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - } - - @Nested - inner class `Returns null`() { - @Test - fun `if graph has negative edges`() { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - graph.addEdge(v0, v1, 1) - graph.addEdge(v0, v2, -1) - - val actualResult = graph.findKeyVertices() - - assertNull(actualResult) - } - } - } -} diff --git a/app/src/test/kotlin/model/algorithms/BridgesFinderTest.kt b/app/src/test/kotlin/model/algorithms/BridgesFinderTest.kt new file mode 100644 index 00000000..d508746d --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/BridgesFinderTest.kt @@ -0,0 +1,162 @@ +package model.algorithms.clustering + +import model.algorithms.BridgesFinder +import model.graphs.UndirectedGraph +import model.graphs.abstractGraph.Edge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import util.annotations.TestAllUndirectedGraphs + +class BridgesFinderTest { + val bridgesFinder = BridgesFinder() + + @Nested + inner class `All bridges should be found`() { + @TestAllUndirectedGraphs + fun `if graph has one edge`(graph: UndirectedGraph) { + val vertex0 = graph.addVertex(0) + val vertex1 = graph.addVertex(1) + + val expectedBridges = listOf(graph.addEdge(vertex0, vertex1)) + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if two components are connected via one edge`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v1, v2) + + addEdge(v3, v4) + addEdge(v3, v5) + addEdge(v4, v5) + } + + val expectedBridges = listOf(graph.addEdge(v0, v3)) + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph is chain-like`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e12 = graph.addEdge(v1, v2) + val e23 = graph.addEdge(v2, v3) + + val expectedBridges = setOf(e01, e12, e23) + val actualBridges = bridgesFinder.findBridges(graph).toSet() + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph is star-like`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e02 = graph.addEdge(v0, v2) + val e03 = graph.addEdge(v0, v3) + + val expectedBridges = setOf(e01, e02, e03) + val actualBridges = bridgesFinder.findBridges(graph).toSet() + + assertEquals(expectedBridges, actualBridges) + } + } + + @Nested + inner class `No bridge should be found`() { + @TestAllUndirectedGraphs + fun `if graph has no vertices`(graph: UndirectedGraph) { + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph has no edges`(graph: UndirectedGraph) { + graph.apply { + addVertex(0) + addVertex(1) + addVertex(2) + } + + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if graph is circle-like`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v0, v1) + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v4) + addEdge(v4, v0) + } + + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + + @TestAllUndirectedGraphs + fun `if two components are connected via more than one edge`(graph: UndirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v1, v2) + + addEdge(v3, v4) + addEdge(v3, v5) + addEdge(v4, v5) + } + + graph.addEdge(v0, v3) + graph.addEdge(v1, v4) + + val expectedBridges = listOf>() + val actualBridges = bridgesFinder.findBridges(graph) + + assertEquals(expectedBridges, actualBridges) + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/CyclesFinderTest.kt b/app/src/test/kotlin/model/algorithms/CyclesFinderTest.kt new file mode 100644 index 00000000..5169d2b2 --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/CyclesFinderTest.kt @@ -0,0 +1,147 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import util.annotations.TestAllDirectedGraphs + +class CyclesFinderTest { + val cyclesFinder = CyclesFinder() + + @Nested + inner class `There are some cycles` { + @TestAllDirectedGraphs + fun `all cycles should be returned`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + val e01 = graph.addEdge(v0, v1) + val e07 = graph.addEdge(v0, v7) + val e04 = graph.addEdge(v0, v4) + val e18 = graph.addEdge(v1, v8) + val e12 = graph.addEdge(v1, v2) + val e20 = graph.addEdge(v2, v0) + val e21 = graph.addEdge(v2, v1) + val e25 = graph.addEdge(v2, v5) + val e23 = graph.addEdge(v2, v3) + val e53 = graph.addEdge(v5, v3) + val e34 = graph.addEdge(v3, v4) + val e41 = graph.addEdge(v4, v1) + val e78 = graph.addEdge(v7, v8) + val e87 = graph.addEdge(v8, v7) + + val expectedCycle1 = listOf(e12 to v2, e21 to v1) + val expectedCycle2 = listOf(e12 to v2, e20 to v0, e01 to v1) + val expectedCycle3 = listOf(e12 to v2, e20 to v0, e04 to v4, e41 to v1) + val expectedCycle4 = listOf(e12 to v2, e23 to v3, e34 to v4, e41 to v1) + val expectedCycle5 = listOf(e12 to v2, e25 to v5, e53 to v3, e34 to v4, e41 to v1) + + val actualValue = cyclesFinder.findCycles(graph, v1) + val expectedValue = + setOf( + expectedCycle1, + expectedCycle2, + expectedCycle3, + expectedCycle4, + expectedCycle5 + ) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `SCC of 2 vertices should have one cycle`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1) + val e12 = graph.addEdge(v1, v2) + val e21 = graph.addEdge(v2, v1) + val e23 = graph.addEdge(v2, v3) + + val actualValue = cyclesFinder.findCycles(graph, v1) + val expectedValue = setOf(listOf(e12 to v2, e21 to v1)) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `There are no cycles` { + @TestAllDirectedGraphs + fun `vertex without outgoing edges shouldn't have cycles`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1) + + val actualValue = cyclesFinder.findCycles(graph, v1) + val expectedValue = emptySet, Vertex>>>() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `SCC of 1 vertex shouldn't have cycles`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + graph.addEdge(v0, v1) + + val actualValue = cyclesFinder.findCycles(graph, v0) + val expectedValue = emptySet, Vertex>>>() + + assertEquals(expectedValue, actualValue) + } + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + val v7 = graph.addVertex(7) + val v8 = graph.addVertex(8) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v7) + addEdge(v0, v4) + addEdge(v1, v8) + addEdge(v1, v2) + addEdge(v2, v0) + addEdge(v2, v1) + addEdge(v2, v5) + addEdge(v2, v3) + addEdge(v5, v3) + addEdge(v3, v4) + addEdge(v4, v1) + addEdge(v7, v8) + addEdge(v8, v7) + } + + val expectedGraph = graph.getVertices() to graph.getEdges().toSet() + + cyclesFinder.findCycles(graph, v1) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + } +} + diff --git a/app/src/test/kotlin/model/algorithms/KeyVerticesFinderTest.kt b/app/src/test/kotlin/model/algorithms/KeyVerticesFinderTest.kt new file mode 100644 index 00000000..348feaba --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/KeyVerticesFinderTest.kt @@ -0,0 +1,262 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import util.annotations.TestAllDirectedGraphs +import util.annotations.TestAllUndirectedGraphs + +class KeyVerticesFinderTest { + val keyVerticesFinder = KeyVerticesFinder() + @Nested + inner class `Graph is directed and unweighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val graph = DirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + addEdge(v1, v2) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = DirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + } + + @Nested + inner class `Graph is undirected and unweighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = DirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + } + + @Nested + inner class `Graph is directed and weighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + addEdge(v2, v3, 1) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if its sum of distances to other vertices is less`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 2) + addEdge(v2, v3, 2) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `Returns null`() { + @Test + fun `if graph has negative edges`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v0, v2, -1) + + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertNull(actualResult) + } + } + } + + @Nested + inner class `Graph is undirected and weighted` { + @Nested + inner class `One vertex is picked over another`() { + @Test + fun `if it can reach more vertices`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 1) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if it can reach other vertices with fewer edges`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v1) + addEdge(v0, v2) + addEdge(v0, v3) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if its sum of distances to other vertices is less`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v0, v2, 1) + addEdge(v0, v3, 1) + addEdge(v1, v2, 2) + addEdge(v2, v3, 2) + } + + val expectedResult = setOf(v0) + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `Returns null`() { + @Test + fun `if graph has negative edges`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v0, v2, -1) + + val actualResult = keyVerticesFinder.findKeyVertices(graph) + + assertNull(actualResult) + } + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/MinSpanningTreeFinderTest.kt b/app/src/test/kotlin/model/algorithms/MinSpanningTreeFinderTest.kt new file mode 100644 index 00000000..56aa5d0c --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/MinSpanningTreeFinderTest.kt @@ -0,0 +1,145 @@ +package model.algorithms + +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class MinSpanningTreeFinderTest { + private lateinit var graph: WeightedUndirectedGraph + + @BeforeEach + fun init() { + graph = WeightedUndirectedGraph() + } + + val minSpanningTreeFinder = MinSpanningTreeFinder() + + @Nested + inner class `An edge is picked over another` { + @Test + fun `if it has lesser weight but both have positive`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Heavy = graph.addEdge(v0, v1, 5) + val e01Light = graph.addEdge(v0, v1, 3) + + val expectedReturn = listOf(e01Light) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has lesser weight but both have negative`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Heavy = graph.addEdge(v0, v1, -5) + val e01Light = graph.addEdge(v0, v1, -10) + + val expectedReturn = listOf(e01Light) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has zero weight and other has positive`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Pos = graph.addEdge(v0, v1, 5) + val e01Zero = graph.addEdge(v0, v1, 0) + + val expectedReturn = listOf(e01Zero) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + + @Test + fun `if it has negative weight and other has positive or zero`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e01Pos = graph.addEdge(v0, v1, 5) + val e01Zero = graph.addEdge(v0, v1, 0) + val e01Neg = graph.addEdge(v0, v1, -5) + + val expectedReturn = listOf(e01Neg) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedReturn, actualReturn) + } + } + + @Nested + inner class `An edge is not picked over another` { + @Test + fun `if it forms a cycle and has greatest weight in it`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e01 = graph.addEdge(v0, v1, 1) + val e12 = graph.addEdge(v1, v2, 1) + val e23 = graph.addEdge(v2, v3, 1) + + val e30 = graph.addEdge(v3, v0, 5) + + val expectedReturn = setOf(e01, e12, e23) + val actualReturn = minSpanningTreeFinder.findMinSpanningTree(graph).toSet() + + assertEquals(expectedReturn, actualReturn) + } + } + + @Nested + inner class `All edges should be returned` { + @Test + fun `if graph is a tree`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val eva01 = graph.addEdge(v0, v1, 1) + val eva02 = graph.addEdge(v0, v2, 10) + val e23 = graph.addEdge(v2, v3, 0) + val e24 = graph.addEdge(v2, v4, -20) + + val expectedResult = setOf(e24, e23, eva01, eva02) + val actualResult = minSpanningTreeFinder.findMinSpanningTree(graph).toSet() + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No edge should be returned` { + @Test + fun `if graph has no vertices`() { + val expectedResult = listOf>() + val actualResult = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has no edges`() { + graph.addVertex(0) + graph.addVertex(1) + + val expectedResult = listOf>() + val actualResult = minSpanningTreeFinder.findMinSpanningTree(graph) + + assertEquals(expectedResult, actualResult) + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/SCCFinderTest.kt b/app/src/test/kotlin/model/algorithms/SCCFinderTest.kt new file mode 100644 index 00000000..0076d2ab --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/SCCFinderTest.kt @@ -0,0 +1,352 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested +import util.annotations.TestAllDirectedGraphs + +class SCCFinderTest { + val SCCFinder = SCCFinder() + + @Nested + inner class `SCC should return not empty array` { + @TestAllDirectedGraphs + fun `graph has two connected vertices`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v1) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `complex graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph has multiple SCCs`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v1) + addEdge(v3, v4) + addEdge(v4, v3) + addEdge(v5, v1) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v3, v4), mutableSetOf(v1, v2), mutableSetOf(v5)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with nested cycles`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + addEdge(v4, v5) + addEdge(v5, v6) + addEdge(v6, v4) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with cross connections`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + addEdge(v4, v5) + addEdge(v5, v6) + addEdge(v6, v4) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4, v5, v6)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with disconnected subgraphs`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + val v6 = graph.addVertex(6) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v1) + addEdge(v3, v4) + addEdge(v4, v3) + addEdge(v5, v6) + addEdge(v6, v5) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2), mutableSetOf(v3, v4), mutableSetOf(v5, v6)) + assertEquals(expectedValue, actualValue) + } + + @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") + @TestAllDirectedGraphs + fun `graph with single vertex cycle`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v3) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3)) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `SCC should return single-element SCCs` { + @TestAllDirectedGraphs + fun `graph has single vertex`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with multiple disconnected vertices`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = + mutableSetOf( + mutableSetOf(v1), + mutableSetOf(v2), + mutableSetOf(v3), + mutableSetOf(v4) + ) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `Additional edge cases`() { + @TestAllDirectedGraphs + fun `empty graph`(graph: DirectedGraph) { + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf>>() + + assertEquals(expectedValue, actualValue) + } + + @Disabled("Our model doesn't support edge from vertex to itself, check DirectedGraph.kt") + @TestAllDirectedGraphs + fun `graph with self-loops`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v1, v1) + graph.addEdge(v2, v2) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1), mutableSetOf(v2)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `linear graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v1, v2) + graph.addEdge(v2, v3) + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v3), mutableSetOf(v2), mutableSetOf(v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph with cycles and tail`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } + + val actualValue = SCCFinder.findSCC(graph) + val expectedValue = mutableSetOf(mutableSetOf(v1, v2, v3), mutableSetOf(v4), mutableSetOf(v5)) + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class `Side-effects check` { + @TestAllDirectedGraphs + fun `check vertices in complex graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } + + val expectedValue = graph.getVertices() + SCCFinder.findSCC(graph) + val actualValue = graph.getVertices() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check edges in complex graph`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v3, v4) + } + + val expectedValue = graph.getEdges() + SCCFinder.findSCC(graph) + val actualValue = graph.getEdges() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check edges in graph with cycles and tail`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } + + val expectedValue = graph.getEdges() + SCCFinder.findSCC(graph) + val actualValue = graph.getEdges() + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `check vertices graph with cycles and tail`(graph: DirectedGraph) { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + val v5 = graph.addVertex(5) + + graph.apply { + addEdge(v1, v2) + addEdge(v2, v3) + addEdge(v3, v1) + addEdge(v4, v3) + addEdge(v4, v5) + } + + val expectedValue = graph.getVertices() + SCCFinder.findSCC(graph) + val actualValue = graph.getVertices() + + assertEquals(expectedValue, actualValue) + } + } +} + diff --git a/app/src/test/kotlin/model/algorithms/ShortestPathFinderTest.kt b/app/src/test/kotlin/model/algorithms/ShortestPathFinderTest.kt new file mode 100644 index 00000000..281cc032 --- /dev/null +++ b/app/src/test/kotlin/model/algorithms/ShortestPathFinderTest.kt @@ -0,0 +1,636 @@ +package model.algorithms + +import model.graphs.DirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import util.setupAbstractGraph +import util.setupDirectedGraphWithCycle +import util.setupWeightedUndirected + +class ShortestPathFinderTest { + val shortestPathFinder = ShortestPathFinder() + + @Nested + inner class `Graph is weighted and directed` { + private lateinit var graph: WeightedDirectedGraph + + @BeforeEach + fun init() { + graph = WeightedDirectedGraph() + } + + @Nested + inner class `There are no negative weights (Dijkstra)` { + @Nested + inner class `Normal path should be returned`() { + @Test + fun `all is as usual, should return default`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 10) + val e1 = graph.addEdge(v0, v4, 100) + val e2 = graph.addEdge(v0, v3, 30) + val e3 = graph.addEdge(v1, v2, 2) + val e4 = graph.addEdge(v2, v4, 10) + val e5 = graph.addEdge(v3, v2, 20) + val e6 = graph.addEdge(v3, v4, 60) + + val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `direct path should be the shortest in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 5) + val e1 = graph.addEdge(v1, v2, 5) + val e2 = graph.addEdge(v0, v2, 10) + + val expectedResult = listOf(e2 to v2) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has multiple paths and equal weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3) + val expectedResult2 = listOf(e1 to v2, e3 to v3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has single edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e0 = graph.addEdge(v0, v1, 5) + + val expectedResult = listOf(e0 to v1) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v1) + + assertEquals(expectedResult, actualResult) + } + + @Disabled("Dijkstra's algorithm doesn't work with negative weights") + @Test + fun `if graph has negative weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, -1) + val e1 = graph.addEdge(v1, v2, -2) + val e2 = graph.addEdge(v2, v3, -3) + + val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `graph has multiple equal shortest paths`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + val e4 = graph.addEdge(v3, v4, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) + val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has a cycle`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v1, v2, 2) + val e2 = graph.addEdge(v2, v0, 3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + val expectedResult = listOf(e2 to v2) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if all the edges have zero weight in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(e0 to v1, e1 to v2) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No path should be returned`() { + @Test + fun `no path exists in directed graph`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v0, v1, 10) + graph.addEdge(v1, v2, 20) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + assertEquals(actualResult, null) + } + + @Test + fun `if start and end vertices are the same`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.apply { + addEdge(v0, v1, 1) + addEdge(v1, v2, 2) + addEdge(v2, v0, 2) + } + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if graph has single vertex`() { + val v0 = graph.addVertex(0) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if path is in other way (not how edges were set)`() { + val graph = WeightedDirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 0) + graph.addEdge(v1, v2, 0) + + val actualResult = shortestPathFinder.findShortestPath(graph, v2, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + } + } + + @Nested + inner class `There are negative weights (Ford-Bellman)` { + @Nested + inner class `Path exists` { + @Test + fun `path between neighbours should consist of one edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val edge = graph.addEdge(v0, v1, 12345) + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v1) + val expectedValue = listOf(edge to v1) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `shortest path should be returned`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val v4 = defaultVertices[4] + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v4) + val expectedValue = + listOf( + graph.getEdge(v0, v1) to v1, + graph.getEdge(v1, v2) to v2, + graph.getEdge(v2, v4) to v4 + ) + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `path from vertex to itself should exist and be empty`() { + val v0 = graph.addVertex(69) + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v0) + val expectedValue = emptyList, Vertex>>() + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v3 = defaultVertices[3] + val v4 = defaultVertices[4] + + val expectedGraph = graphStructure + shortestPathFinder.findShortestPath(graph, v3, v4) + val actualGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Path doesn't exist` { + @Test + fun `there is simply no path between vertices`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v5 = defaultVertices[5] + + val actualValue = shortestPathFinder.findShortestPath(graph, v1, v5) + + assertNull(actualValue) + } + + @Test + fun `order of arguments should matter`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v2 = defaultVertices[2] + + val actualValue = shortestPathFinder.findShortestPath(graph, v2, v0) + + assertNull(actualValue) + } + + @Test + fun `there is a negative cycle on the path`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v8 = defaultVertices[8] + + val actualValue = shortestPathFinder.findShortestPath(graph, v0, v8) + + assertNull(actualValue) + } + + @Test + fun `srcVertex is a part of negative cycle`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v6 = defaultVertices[6] + val v8 = defaultVertices[8] + + val actualValue = shortestPathFinder.findShortestPath(graph, v6, v8) + + assertNull(actualValue) + } + + @Test + fun `vertex without outgoing edges shouldn't have any paths`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val v3 = defaultVertices[3] + val v4 = defaultVertices[4] + val v5 = defaultVertices[5] + val v6 = defaultVertices[6] + val v7 = defaultVertices[7] + val v8 = defaultVertices[8] + + assertNull(shortestPathFinder.findShortestPath(graph, v8, v0)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v1)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v2)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v3)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v4)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v5)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v6)) + assertNull(shortestPathFinder.findShortestPath(graph, v8, v7)) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupDirectedGraphWithCycle(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v8 = defaultVertices[8] + + val expectedGraph = graphStructure + shortestPathFinder.findShortestPath(graph, v8, v1) + val actualGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + } + } + + @Nested + inner class `Graph is weighted and undirected` { + private lateinit var graph: WeightedUndirectedGraph + + @BeforeEach + fun init() { + graph = WeightedUndirectedGraph() + } + + @Nested + inner class `There are no negative weights (Dijkstra)` { + @Nested + inner class `Normal path should be returned` { + @Test + fun `all is as usual, should return default`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 10) + val e1 = graph.addEdge(v0, v4, 100) + val e2 = graph.addEdge(v0, v3, 30) + val e3 = graph.addEdge(v1, v2, 2) + val e4 = graph.addEdge(v2, v4, 10) + val e5 = graph.addEdge(v3, v2, 20) + val e6 = graph.addEdge(v3, v4, 60) + + val expectedResult = listOf(e0 to v1, e3 to v2, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if graph has multiple paths and equal weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3) + val expectedResult2 = listOf(e1 to v2, e3 to v3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has single edge`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + + val e0 = graph.addEdge(v0, v1, 5) + + val expectedResult = listOf(e0 to v1) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v1) + + assertEquals(expectedResult, actualResult) + } + + @Disabled("Dijkstra's algorithm doesn't work with negative weights") + @Test + fun `if graph has negative weights`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + val e0 = graph.addEdge(v0, v1, -1) + val e1 = graph.addEdge(v1, v2, -2) + val e2 = graph.addEdge(v2, v3, -3) + + val expectedResult = listOf(e0 to v1, e1 to v2, e2 to v3) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `graph has multiple equal shortest paths`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + val v4 = graph.addVertex(4) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v0, v2, 1) + val e2 = graph.addEdge(v1, v3, 1) + val e3 = graph.addEdge(v2, v3, 1) + val e4 = graph.addEdge(v3, v4, 1) + + val expectedResult1 = listOf(e0 to v1, e2 to v3, e4 to v4) + val expectedResult2 = listOf(e1 to v2, e3 to v3, e4 to v4) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v4) + + assertTrue(actualResult == expectedResult1 || actualResult == expectedResult2) + } + + @Test + fun `if graph has a cycle`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 1) + val e1 = graph.addEdge(v1, v2, 2) + val e2 = graph.addEdge(v2, v0, 3) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + val expectedResult = listOf(e2 to v2) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if path is in other way (not how edges were set)`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(e1 to v1, e0 to v0) + val actualResult = shortestPathFinder.findShortestPath(graph, v2, v0) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `if all the edges have zero weight in undirected graph`() { + val graph = WeightedUndirectedGraph() + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val e0 = graph.addEdge(v0, v1, 0) + val e1 = graph.addEdge(v1, v2, 0) + + val expectedResult = listOf(e0 to v1, e1 to v2) + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v2) + + assertEquals(expectedResult, actualResult) + } + } + + @Nested + inner class `No path should be returned`() { + @Test + fun `no path exists in undirected graph`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + val v3 = graph.addVertex(3) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v1, v2, 2) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v3) + + assertNull(actualResult) + } + + @Test + fun `if start and end vertices are the same`() { + val v0 = graph.addVertex(0) + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + graph.addEdge(v0, v1, 1) + graph.addEdge(v1, v2, 2) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + + @Test + fun `if graph has single vertex`() { + val v0 = graph.addVertex(0) + + val actualResult = shortestPathFinder.findShortestPath(graph, v0, v0) + + actualResult?.isEmpty()?.let { assertTrue(it) } + } + } + } + + @Nested + inner class `There are negative weights` { + @Test + fun `shortest path shouldn't exist (null should be returned)`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[4] + + assertNull(shortestPathFinder.findShortestPath(graph, v1, v4)) + } + } + } + + @Nested + inner class `Graph is unweighted` { + @Test + fun `directed unweighted graphs aren't supported`() { + val graph = DirectedGraph() + val graphStructure = setupAbstractGraph(graph) + val defaultVertices = graphStructure.first + + val v0 = defaultVertices[0] + val v2 = defaultVertices[2] + + assertNull(shortestPathFinder.findShortestPath(graph, v0, v2)) + } + + @Test + fun `undirected unweighted graphs aren't supported`() { + val graph = DirectedGraph() + val graphStructure = setupAbstractGraph(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + + assertNull(shortestPathFinder.findShortestPath(graph, v1, v3)) + } + } +} diff --git a/app/src/test/kotlin/model/algorithms/clustering/LouvainClusteringTest.kt b/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt similarity index 82% rename from app/src/test/kotlin/model/algorithms/clustering/LouvainClusteringTest.kt rename to app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt index b15060aa..87e2c124 100644 --- a/app/src/test/kotlin/model/algorithms/clustering/LouvainClusteringTest.kt +++ b/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt @@ -6,14 +6,14 @@ import org.junit.jupiter.api.Assertions.assertEquals import util.annotations.TestAllGraphTypes import util.setupAbstractGraph -class LouvainClusteringTest { - val louvain = LouvainClustering() +class CommunitiesFinderTest { + val louvain = CommunitiesFinder() @TestAllGraphTypes fun `graph of 1 vertex should have one community`(graph: Graph) { val v0 = graph.addVertex(0) - val actualValue = louvain.findClusters(graph) + val actualValue = louvain.findCommunity(graph) val expectedValue = setOf(setOf(v0)) assertEquals(expectedValue, actualValue) @@ -21,7 +21,7 @@ class LouvainClusteringTest { @TestAllGraphTypes fun `empty graph should have no communities`(graph: Graph) { - val actualValue = louvain.findClusters(graph) + val actualValue = louvain.findCommunity(graph) val expectedValue = emptySet>>() assertEquals(expectedValue, actualValue) @@ -31,7 +31,7 @@ class LouvainClusteringTest { fun `graph doesn't change`(graph: Graph) { val graphStructure = setupAbstractGraph(graph) - louvain.findClusters(graph) + louvain.findCommunity(graph) val actualGraph = graph.getVertices() to graph.getEdges().toSet() val expectedGraph = graphStructure diff --git a/app/src/test/kotlin/model/graphs/DirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/DirectedGraphTest.kt new file mode 100644 index 00000000..ac2254f2 --- /dev/null +++ b/app/src/test/kotlin/model/graphs/DirectedGraphTest.kt @@ -0,0 +1,437 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import util.annotations.TestAllDirectedGraphs +import util.emptyEdgesSet +import util.emptyVerticesList +import util.setupAbstractGraph + +class DirectedGraphTest { + @Nested + inner class GetEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllDirectedGraphs + fun `edge should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v2 = defaultVerticesList[2] + + val newEdge = graph.addEdge(v0, v2) + + val actualValue = newEdge + val expectedValue = graph.getEdge(v0, v2) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + graph.getEdge(v2, v3) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllDirectedGraphs + fun `order of arguments should matter`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(v1, v0) + } + } + + @TestAllDirectedGraphs + fun `trying to get non-existent edge should throw an exception`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getEdge(Vertex(2, 12), Vertex(85, 6)) + } + } + } + } + + @Nested + inner class GetNeighboursTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllDirectedGraphs + fun `neighbours should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getNeighbours(v3).toSet() + val expectedValue = setOf(v4, v1) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + graph.getNeighbours(v0) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getNeighbours(Vertex(2201, 2006)) + } + } + } + } + + @Nested + inner class GetOutgoingEdgesTest { + @Nested + inner class `Vertex is in the graph` { + @TestAllDirectedGraphs + fun `outgoing edges should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + val actualValue = graph.getOutgoingEdges(v3).toSet() + val expectedValue = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1)) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `graph shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v4 = defaultVerticesList[4] + + graph.getOutgoingEdges(v4) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Vertex isn't in the graph` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.getOutgoingEdges(Vertex(2611, 2005)) + } + } + } + } + + @Nested + inner class AddEdgeTest { + @Nested + inner class `Two vertices are in the graph` { + @Nested + inner class `Vertices are different` { + @TestAllDirectedGraphs + fun `Added edge should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val actualValue = graph.addEdge(v0, v4) + val expectedValue = graph.getEdge(v0, v4) + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `Edge should be added to graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v0 = defaultVerticesList[0] + val v4 = defaultVerticesList[4] + + val newEdge = graph.addEdge(v4, v0) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet + newEdge + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `one vertex has to be added to the other's adjacency map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v0, v2) + + val actualVertices = graph.getNeighbours(v0).toSet() + val expectedVertices = setOf(v1, v2) + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllDirectedGraphs + fun `edge has to be added to first vertex's outgoing edges map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + graph.addEdge(v3, v0) + + val actualEdges = graph.getOutgoingEdges(v3).toSet() + val expectedEdges = setOf(graph.getEdge(v3, v4), graph.getEdge(v3, v1), graph.getEdge(v3, v0)) + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `adding already existing edge shouldn't change anything`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val expectedNeighbours = graph.getNeighbours(v4).toSet() + val expectedOutgoingEdges = graph.getOutgoingEdges(v4).toSet() + + graph.addEdge(v4, v1) + + val actualNeighbours = graph.getNeighbours(v4).toSet() + val actualOutgoingEdges = graph.getOutgoingEdges(v4).toSet() + + val expectedGraph = graphStructure + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + + assertEquals(expectedGraph, actualGraph) + assertEquals(expectedNeighbours, actualNeighbours) + assertEquals(expectedOutgoingEdges, actualOutgoingEdges) + } + + @TestAllDirectedGraphs + fun `second vertex's map values shouldn't change`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + val v3 = defaultVerticesList[3] + + val expectedNeighbours = graph.getNeighbours(v0).toSet() + val expectedOutgoingEdges = graph.getOutgoingEdges(v0).toSet() + + graph.addEdge(v3, v0) + + val actualNeighbours = graph.getNeighbours(v0).toSet() + val actualOutgoingEdges = graph.getOutgoingEdges(v0).toSet() + + assertEquals(expectedNeighbours, actualNeighbours) + assertEquals(expectedOutgoingEdges, actualOutgoingEdges) + } + } + + @Nested + inner class `Vertices are the same` { + @TestAllDirectedGraphs + fun `exception should be thrown`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v2, v2) + } + } + } + } + + @Nested + inner class `One of the vertices isn't in the graph` { + @TestAllDirectedGraphs + fun `first vertex isn't in the graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(2210, 2005), v0) + } + } + + @TestAllDirectedGraphs + fun `second vertex isn't in the graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v0 = defaultVerticesList[0] + + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(v0, Vertex(2510, 1917)) + } + } + + @TestAllDirectedGraphs + fun `both vertices aren't in the graph`(graph: DirectedGraph) { + assertThrows(IllegalArgumentException::class.java) { + graph.addEdge(Vertex(3010, 1978), Vertex(1002, 1982)) + } + } + } + } + + @Nested + inner class RemoveEdgeTest { + @Nested + inner class `Edge is in the graph` { + @TestAllDirectedGraphs + fun `removed edge should be returned`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v3, v1) + + val actualValue = graph.removeEdge(edgeToRemove) + val expectedValue = edgeToRemove + + assertEquals(expectedValue, actualValue) + } + + @TestAllDirectedGraphs + fun `edge should be removed from graph`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + val defaultEdgesSet = graphStructure.second + + val v1 = defaultVerticesList[1] + val v4 = defaultVerticesList[4] + + val edgeToRemove = graph.getEdge(v4, v1) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getEdges().toSet() + val expectedEdges = defaultEdgesSet - edgeToRemove + + assertEquals(expectedEdges, actualEdges) + } + + @TestAllDirectedGraphs + fun `second vertex should be removed from first's adjacency map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v1 = defaultVerticesList[1] + val v2 = defaultVerticesList[2] + + val edgeToRemove = graph.getEdge(v1, v2) + graph.removeEdge(edgeToRemove) + + val actualVertices = graph.getNeighbours(v1).toSet() + val expectedVertices = emptyVerticesList.toSet() + + assertEquals(expectedVertices, actualVertices) + } + + @TestAllDirectedGraphs + fun `edge should be removed from first vertex's outgoing edges map value`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v2 = defaultVerticesList[2] + val v3 = defaultVerticesList[3] + + val edgeToRemove = graph.getEdge(v2, v3) + graph.removeEdge(edgeToRemove) + + val actualEdges = graph.getOutgoingEdges(v2).toSet() + val expectedEdges = emptyEdgesSet + + assertEquals(expectedEdges, actualEdges) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @TestAllDirectedGraphs + fun `wrong order of the arguments should throw an exception`(graph: DirectedGraph) { + val graphStructure = setupAbstractGraph(graph) + val defaultVerticesList = graphStructure.first + + val v3 = defaultVerticesList[3] + val v4 = defaultVerticesList[4] + + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(graph.getEdge(v4, v3)) + } + } + + @TestAllDirectedGraphs + fun `non-existing edge should throw an exception`(graph: DirectedGraph) { + assertThrows(NoSuchElementException::class.java) { + graph.removeEdge(Edge(Vertex(0, 0), Vertex(1, 1))) + } + } + } + } +} diff --git a/app/src/test/kotlin/model/UndirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/UndirectedGraphTest.kt similarity index 77% rename from app/src/test/kotlin/model/UndirectedGraphTest.kt rename to app/src/test/kotlin/model/graphs/UndirectedGraphTest.kt index f4ecb5fe..c350f48b 100644 --- a/app/src/test/kotlin/model/UndirectedGraphTest.kt +++ b/app/src/test/kotlin/model/graphs/UndirectedGraphTest.kt @@ -1,8 +1,7 @@ -package model +package model.graphs import model.graphs.abstractGraph.Edge import model.graphs.abstractGraph.Vertex -import model.graphs.UndirectedGraph import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Nested @@ -536,182 +535,4 @@ class UndirectedGraphTest { } } } - - @Nested - inner class FindBridgesTest { - @Nested - inner class `All bridges should be found`() { - @TestAllUndirectedGraphs - fun `if graph has one edge`(graph: UndirectedGraph) { - val vertex0 = graph.addVertex(0) - val vertex1 = graph.addVertex(1) - - val expectedBridges = listOf(graph.addEdge(vertex0, vertex1)) - val actualBridges = graph.findBridges() - - assertEquals(expectedBridges, actualBridges) - } - - @TestAllUndirectedGraphs - fun `if two components are connected via one edge`(graph: UndirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - - graph.apply { - addEdge(v0, v1) - addEdge(v0, v2) - addEdge(v1, v2) - - addEdge(v3, v4) - addEdge(v3, v5) - addEdge(v4, v5) - } - - val expectedBridges = listOf(graph.addEdge(v0, v3)) - val actualBridges = graph.findBridges() - - assertEquals(expectedBridges, actualBridges) - } - - @TestAllUndirectedGraphs - fun `if graph is chain-like`(graph: UndirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e01 = graph.addEdge(v0, v1) - val e12 = graph.addEdge(v1, v2) - val e23 = graph.addEdge(v2, v3) - - val expectedBridges = setOf(e01, e12, e23) - val actualBridges = graph.findBridges().toSet() - - assertEquals(expectedBridges, actualBridges) - } - - @TestAllUndirectedGraphs - fun `if graph is star-like`(graph: UndirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - val e01 = graph.addEdge(v0, v1) - val e02 = graph.addEdge(v0, v2) - val e03 = graph.addEdge(v0, v3) - - val expectedBridges = setOf(e01, e02, e03) - val actualBridges = graph.findBridges().toSet() - - assertEquals(expectedBridges, actualBridges) - } - } - - @Nested - inner class `No bridge should be found`() { - @TestAllUndirectedGraphs - fun `if graph has no vertices`(graph: UndirectedGraph) { - val expectedBridges = listOf>() - val actualBridges = graph.findBridges() - - assertEquals(expectedBridges, actualBridges) - } - - @TestAllUndirectedGraphs - fun `if graph has no edges`(graph: UndirectedGraph) { - graph.apply { - addVertex(0) - addVertex(1) - addVertex(2) - } - - val expectedBridges = listOf>() - val actualBridges = graph.findBridges() - - assertEquals(expectedBridges, actualBridges) - } - - @TestAllUndirectedGraphs - fun `if graph is circle-like`(graph: UndirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - - graph.apply { - addEdge(v0, v1) - addEdge(v1, v2) - addEdge(v2, v3) - addEdge(v3, v4) - addEdge(v4, v0) - } - - val expectedBridges = listOf>() - val actualBridges = graph.findBridges() - - assertEquals(expectedBridges, actualBridges) - } - - @TestAllUndirectedGraphs - fun `if two components are connected via more than one edge`(graph: UndirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - - val v3 = graph.addVertex(3) - val v4 = graph.addVertex(4) - val v5 = graph.addVertex(5) - - graph.apply { - addEdge(v0, v1) - addEdge(v0, v2) - addEdge(v1, v2) - - addEdge(v3, v4) - addEdge(v3, v5) - addEdge(v4, v5) - } - - graph.addEdge(v0, v3) - graph.addEdge(v1, v4) - - val expectedBridges = listOf>() - val actualBridges = graph.findBridges() - - assertEquals(expectedBridges, actualBridges) - } - } - } - - @Nested - inner class FindKeyVerticesTest { - @Nested - inner class `One vertex is picked over another`() { - @TestAllUndirectedGraphs - fun `if it can reach other vertices with fewer edges`(graph: UndirectedGraph) { - val v0 = graph.addVertex(0) - val v1 = graph.addVertex(1) - val v2 = graph.addVertex(2) - val v3 = graph.addVertex(3) - - graph.apply { - addEdge(v0, v1) - addEdge(v0, v2) - addEdge(v0, v3) - } - - val expectedResult = setOf(v0) - val actualResult = graph.findKeyVertices() - - assertEquals(expectedResult, actualResult) - } - } - } } diff --git a/app/src/test/kotlin/model/graphs/WeightedDirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/WeightedDirectedGraphTest.kt new file mode 100644 index 00000000..dab249ae --- /dev/null +++ b/app/src/test/kotlin/model/graphs/WeightedDirectedGraphTest.kt @@ -0,0 +1,99 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import util.setupWeightedDirected + +class WeightedDirectedGraphTest { + private lateinit var graph: WeightedDirectedGraph + + @BeforeEach + fun init() { + graph = WeightedDirectedGraph() + } + + @Nested + inner class GetWeightTest { + @Nested + inner class `Edge is in the graph` { + @Test + fun `edge's weight should be returned`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + val edge = graph.getEdge(v3, v1) + + val actualValue = graph.getWeight(edge) + val expectedValue = 3 + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[3] + val edge = graph.getEdge(v4, v1) + + graph.getWeight(edge) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @Test + fun `exception should be thrown`() { + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) + } + } + } + } + + // most of the functionality is tested in the DirectedGraphTest class, + // as weighted graphs call super methods inside their methods + @Nested + inner class AddEdgeTest { + @Test + fun `added edge's weight should be added to weight map`() { + val v0 = graph.addVertex(30) + val v1 = graph.addVertex(31) + + val newEdge = graph.addEdge(v0, v1, 62) + + val actualValue = graph.getWeight(newEdge) + val expectedValue = 62 + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class RemoveEdgeTest { + @Test + fun `removed edge should be removed from the weight map`() { + val graphStructure = setupWeightedDirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val edge = graph.getEdge(v1, v2) + + graph.removeEdge(edge) + + assertThrows(NoSuchElementException::class.java) { graph.getWeight(edge) } + } + } +} diff --git a/app/src/test/kotlin/model/graphs/WeightedUndirectedGraphTest.kt b/app/src/test/kotlin/model/graphs/WeightedUndirectedGraphTest.kt new file mode 100644 index 00000000..c346dc91 --- /dev/null +++ b/app/src/test/kotlin/model/graphs/WeightedUndirectedGraphTest.kt @@ -0,0 +1,103 @@ +package model.graphs + +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Vertex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import util.setupWeightedUndirected + +class WeightedUndirectedGraphTest { + private lateinit var graph: WeightedUndirectedGraph + + @BeforeEach + fun init() { + graph = WeightedUndirectedGraph() + } + + @Nested + inner class GetWeightTest { + @Nested + inner class `Edge is in the graph` { + @Test + fun `edge's weight should be returned`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v3 = defaultVertices[3] + val edge = graph.getEdge(v3, v1) + + val actualValue = graph.getWeight(edge) + val expectedValue = 3 + + assertEquals(expectedValue, actualValue) + } + + @Test + fun `graph shouldn't change`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v4 = defaultVertices[3] + val edge = graph.getEdge(v4, v1) + + graph.getWeight(edge) + + val actualGraph = graph.getVertices() to graph.getEdges().toSet() + val expectedGraph = graphStructure + + assertEquals(expectedGraph, actualGraph) + } + } + + @Nested + inner class `Edge isn't in the graph` { + @Test + fun `exception should be thrown`() { + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(Edge(Vertex(1505, 2), Vertex(9, 0))) + } + } + } + } + + // most of the functionality is tested in the DirectedGraphTest class, + // as weighted graphs call super methods inside their methods + @Nested + inner class AddEdgeTest { + @Test + fun `added edge's weight should be added to weight map`() { + val v0 = graph.addVertex(30) + val v1 = graph.addVertex(31) + + val newEdge = graph.addEdge(v0, v1, 62) + + val actualValue = graph.getWeight(newEdge) + val expectedValue = 62 + + assertEquals(expectedValue, actualValue) + } + } + + @Nested + inner class RemoveEdgeTest { + @Test + fun `removed edge should be removed from the weight map`() { + val graphStructure = setupWeightedUndirected(graph) + val defaultVertices = graphStructure.first + + val v1 = defaultVertices[1] + val v2 = defaultVertices[2] + val edge = graph.getEdge(v1, v2) + + graph.removeEdge(edge) + + assertThrows(NoSuchElementException::class.java) { + graph.getWeight(edge) + } + } + } +} diff --git a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt b/app/src/test/kotlin/model/graphs/abstractGraph/GraphTest.kt similarity index 99% rename from app/src/test/kotlin/model/abstractGraph/GraphTest.kt rename to app/src/test/kotlin/model/graphs/abstractGraph/GraphTest.kt index af4851f5..df12fbf8 100644 --- a/app/src/test/kotlin/model/abstractGraph/GraphTest.kt +++ b/app/src/test/kotlin/model/graphs/abstractGraph/GraphTest.kt @@ -1,4 +1,4 @@ -package model.abstractGraph +package model.graphs.abstractGraph import model.graphs.abstractGraph.Graph import model.graphs.abstractGraph.Vertex From 948a13c3be2b9ebf9cd3ff0f1eb4f3c5026e35bd Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Wed, 29 May 2024 14:39:38 +0300 Subject: [PATCH 69/77] docs(lic): switch from MIT to GPL-V3 #52 --- LICENSE.md | 695 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 674 insertions(+), 21 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 351396e8..4a3a5801 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,24 +1,677 @@ -MIT License - -Copyright (c) 2024 Mike Gavrilenko, Karim Shakirov, Daniel Vlasenco - -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. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. --- From ca8374b140698201a8504410d53233b98427c7bd Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Thu, 30 May 2024 00:50:33 +0300 Subject: [PATCH 70/77] feat: implement FFT optimisation of T-FDP layout. #53 * feat: implement DB creation and saving * feat: add draft UI for file-control tab * feat: implement general function to display error windows * feat: set up the process of graph import, MSVM-VM-SQL connection * feat: add auxiliary queries for SQLite * fix: remove coroutines and launch ef., due to composable conflict * refactor: remove redundant lines, cleared up a little * test: enhance app with integration test draft #46 * build: add new dependencies for int. tests * test: implement first integration test * feat: add getter for edge's direction type * refactor: move graphs to a separate package in module * feat: add test-tags * fix: add tag for 'not hoverable' FAQ * refactor: edit test name * feat: add basic file control tab fields and buttons #48 * fix: small typo * fix: correct graph types picked by user, change default types * feat: add text field for graph name, save and load buttons * feat: add base dialog windows which can be dismissed * feat: introduce more variables * feat: add dropdown menu for database choosing * feat: add button and dialog to choose file when json is chosen * refactor: clean up code, resolve some TODO's #49 * feat: move constants to a separate file * fix: make 'getWeightMap' return mutable map * refactor: rename 'CreateGraphVM' class, edit 'addEdge' * refactor: resolve TODO's * fix: edit name of radius variable * feat: add error message handle * fix: edit button title, change ui layout * feat: add FFT implementation Made by jnalon. Provide it with GPL-3.0 License * fix: add 'apply layout' call after some changes * feat: implement randomize method * feat: implement T-FDP layout + FFT optimization * fix: remove some unused lines * fix: increase grid partition * fix: remove unnecessary update * fix: clean FFT, change coefficients * feat: add SQLite db, with test graphs --------- Co-authored-by: Daniel Vlasenco --- app/database/my_graph_database.db | Bin 20480 -> 20480 bytes .../main/kotlin/model/FastFourierTransform.kt | 235 ++++++++++++++++++ .../kotlin/model/io/sql/SQLDatabaseModule.kt | 6 +- app/src/main/kotlin/model/io/sql/queries.txt | 2 +- .../kotlin/view/ImportGraphDialogWindow.kt | 2 - app/src/main/kotlin/view/graph/GraphView.kt | 4 +- .../main/kotlin/view/tabScreen/GeneralTab.kt | 9 - app/src/main/kotlin/view/utils/ErrorWindow.kt | 13 +- .../kotlin/viewmodel/graph/GraphViewModel.kt | 11 +- .../viewmodel/graph/SetupGraphViewModel.kt | 202 ++++++++++----- .../main/kotlin/viewmodel/graph/TFDPLayout.kt | 173 ++++++++++--- 11 files changed, 534 insertions(+), 123 deletions(-) create mode 100644 app/src/main/kotlin/model/FastFourierTransform.kt delete mode 100644 app/src/main/kotlin/view/ImportGraphDialogWindow.kt diff --git a/app/database/my_graph_database.db b/app/database/my_graph_database.db index d24a2287951e612dbd890325ce73faf0abef4d11..6ca344b0c7c69b7c2b093a8d16cbba9b11eb4f00 100644 GIT binary patch delta 961 zcmZvaPe>F|9LMMF&dko6b?3Wgy2?8Hr)_Qh@64>Oo6;W&B9JLWd4f2|>RZpF|PAVZNW=_xs+PH*aQbovp32 zJ8_o1+kT2=pI&~5HcUz~3K9zVjCFI z(O7ToMY5$eNs?M3X8fN?v5+FQnavAVQ_H25{cNKCEBOKQ;f7$)WY^tFb#B^S9Hj+x?#`6m zMIH#om`_dwT~e#DD-49LbVzT*o%;vKf|8qctS$GDH%xQS)Vql8I};UtEQmw#71 z9bGntXt5Edan1>BMx-f3ImQ^*~k;Q;RvsE4#p4_7s=bDk5f zrtl*ILjkSBL&{a26{aHd!vf`imJxYSJ0vI&N)yrk^$5;M?Z%FT>=+yWD?08A#zk6tih&Q1jG6#YiWhT$cW$iqn4an==>S)lPF9*NK zy+|6<{hSXIH~Xv>{Mc59e6`d;p?O~&)e*TZ)DroC==(+AC;DDr9koYxyyx0#_uc@# bi!`R^9ow;aCvmf0$Xmroj?moO7QMx9tYf7f delta 957 zcma)&PiPZC6o+?bXJ%*GWPVBin3i^%q_I{!h}o#6mwHgBUi2VX5Co--O{j>bts)5R zO>bHvo)jvG7b&7a5HH1p&`Ys;@E{gJ6fc5`3Z)?UHnTk|EPUVm=Do-4n_qP27u}mZ zZepSLpqqGn`X0I+$I)ih!FxQzEzIHshR|((nbk%**D!oQ3#b0NufPk6WTP+7dwr51 zJIn%6AI=8#*=#U%ex~0OPG+dRQq5JWW98~(Wu`oFs9GzJ6Gv-jE>w|g`m@EPy0hL?DbM|glc zSim)0#YLRODICXPjG%;F$Rh&-ZZ^#i^Tn*2H)h2wH}a{=X1|}4j;plRDoaA9gzk&X z)rvC}#G$8pV`F}gbn3rH_Raa76j7_t9kziBPqo`J&UITHdQ7Dw)tWOuMMf=L!`^z5 zYqO%B_dVYbabTmn{-$AqFU=u&_6`!zW{sBp)GX1u$IMAN$Q>7YPkIM&Ko`SOwKyD>h($RNU8yj&omfy& z6n}{VB|VBxrLb6%{mr_f%-edIYqIVi8=Xsbkq3gRs5dN%3>}j~Y>U7}r$vWFdlRya z03M38O;Zs(0SjZ1v`91|TM6(&(PC2_feA$@5q*1<7NQBs31Fe{Y|6N!5ptK1{0k=N BkU0PV diff --git a/app/src/main/kotlin/model/FastFourierTransform.kt b/app/src/main/kotlin/model/FastFourierTransform.kt new file mode 100644 index 00000000..30a20866 --- /dev/null +++ b/app/src/main/kotlin/model/FastFourierTransform.kt @@ -0,0 +1,235 @@ +package model + +/** + * Copyright (C) 2021 José Alexandre Nalon + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/. + */ + +/************************************************************************************************** + * Fast Fourier Transform -- Kotlin Version + * This version implements Cooley-Tukey algorithm for composite-length sequences. + * + * José Alexandre Nalon + ************************************************************************************************** + * This program can be compiled by issuing the command: + * + * $ kotlinc anyfft.kt + * + * It will generate a file named 'FftKt.class' in the same directory. It can be run by issuing the + * command: + * + * $ kotlin AnyfftKt + * + **************************************************************************************************/ + +/************************************************************************************************** + * Include necessary libraries: + **************************************************************************************************/ +import kotlin.math.* // Math functions; + + +/************************************************************************************************** + * Mini-library to deal with complex numbers. + **************************************************************************************************/ +class Complex(val r: Double, val i: Double) { + + // Constructor: + constructor() : this(0.0, 0.0) {} + + // Add the argument to this, giving the result as a new complex number: + operator fun plus(c: Complex): Complex { + return Complex(r + c.r, i + c.i) + } + + // Subtract the argument from this, giving the result as a new complex number: + operator fun minus(c: Complex): Complex { + return Complex(r - c.r, i - c.i) + } + + // Multiply the argument with this, giving the result as a new complex number: + operator fun times(c: Complex): Complex { + return Complex(r * c.r - i * c.i, r * c.i + i * c.r) + } + + // Multiply with an scalar, giving the reulst as a new complex number: + operator fun times(a: Double): Complex { + return Complex(a * r, a * i) + } + + // Divide this by the argument, giving the result as a new complex number: + operator fun div(a: Double): Complex { + return Complex(r / a, i / a) + } + +} + +// Complex exponential of an angle: +fun Cexp(a: Double): Complex { + return Complex(cos(a), sin(a)) +} + + +/************************************************************************************************** + * Auxiliary Function: complexShow + * Pretty printing of an array of complex numbers, used to inspect results. + * + * Parameters: + * x + * A vector of complex numbers, according to the definition above; + **************************************************************************************************/ +fun complexShow(x: Array) { + for (i in 0..x.size - 1) + println("( " + x[i].r + ", " + x[i].i + ")") +} + + +/************************************************************************************************** + * Auxiliary Function: timeIt + * Measure execution time through repeated calls to a (Fast) Fourier Transform function. + * + * Parameters: + * f + * Function to be called, with the given prototype. The first complex vector is the input + * vector, the second complex vector is the result of the computation, and the integer is the + * number of elements in the vector; + * size + * Number of elements in the vector on which the transform will be applied; + * repeat + * Number of times the function will be called. + * + * Returns: + * The average execution time for that function with a vector of the given size. + **************************************************************************************************/ +fun timeIt(f: (x: Array) -> Array, size: Int, repeat: Int): Double { + val x = Array(size) { i -> Complex(i.toDouble(), 0.0) } + val start = System.currentTimeMillis() + for (j in 1..repeat) { + f(x) + } + return (System.currentTimeMillis() - start).toDouble() / (1000 * repeat).toDouble() +} + + +/************************************************************************************************** + * Function: directFT + * Discrete Fourier Transform directly from the definition, an algorithm that has O(N^2) + * complexity. + * + * Parameters: + * x + * The vector of which the DFT will be computed. Given the nature of the implementation, there + * is no restriction on the size of the vector, although it will almost always be called with a + * power of two size to give a fair comparison; + * + * Returns: + * A complex-number vector of the same size, with the coefficients of the DFT. + **************************************************************************************************/ +fun directFT(x: Array): Array { + // Implement the direct DFT computation + // This is usually O(N^2) but can be acceptable for small N + val N = x.size + val X = Array(N) { Complex(0.0, 0.0) } + for (k in 0 until N) { + for (n in 0 until N) { + val theta = -2.0 * PI * k * n / N + X[k] = X[k] + x[n] * Cexp(theta) + } + } + return X +} + +/************************************************************************************************** + * Function: factor + * Smallest prime factor of a given number. If the argument is prime itself, then it is the + * return value. + * + * Parameters: + * n + * Number to be inspected. + * + * Returns: + * The smallest prime factor, or the number itself if it is already a prime. + **************************************************************************************************/ +fun factor(n: Int): Int { + val rn = ceil(sqrt(n.toDouble())).toInt() // Search up to the square root of the number; + for (i in 2..rn) { + if (n % i == 0) return i // If remainder is zero, a factor is found; + } + return n +} + + +/************************************************************************************************** + * Function: recursiveFFT + * Fast Fourier Transform using a recursive decimation in time algorithm. This has smaller + * complexity than the direct FT, though the exact value is difficult to compute. + * + * Parameters: + * x + * The vector of which the FFT will be computed. Its length must be a composite number, or else + * the computation will be defered to the direct FT, and there will be no efficiency gain. + * + * Returns: + * A complex-number vector of the same size, with the coefficients of the DFT. + **************************************************************************************************/ +fun recursiveFFT(x: Array): Array { + val N = x.size + + val N1 = factor(N) // Smallest prime factor of length + if (N == N1) { + return directFT(x) // Direct transform if length is prime + } else { + val N2 = N / N1 // Decompose in two factors, N1 being prime + + val X = Array(N) { Complex(0.0, 0.0) } // Allocate memory for computation + + val W = Cexp(-2 * PI / N.toDouble()) // Twiddle factor + var Wj = Complex(1.0, 0.0) + for (j in 0 until N1) { // Compute subsequences of size N2 + val xj = Array(N2) { n -> x[n * N1 + j] } // Create the subsequence + val Xj = recursiveFFT(xj) // Compute the DFT of the subsequence + var Wkj = Complex(1.0, 0.0) + for (k in 0 until N) { + X[k] = X[k] + Xj[k % N2] * Wkj // Recombine results + Wkj = Wkj * Wj // Update twiddle factors + } + Wj = Wj * W + } + return X + } +} + + +///************************************************************************************************** +// * Main function: +// **************************************************************************************************/ +//fun main() { +// +// val sizes = listOf(2*3, 2*2*3, 2*3*3, 2*3*5, 2*2*3*3, 2*2*5*5, 2*3*5*7, 2*2*3*3*5*5) +// val repeat: Int = 500 // Number of executions to compute average time; +// +// // Start by printing the table with time comparisons: +// println("+---------+---------+---------+---------+") +// println("| N | N^2 | Direct | Recurs. |") +// println("+---------+---------+---------+---------+") +// +// // Try it with vectors with the given sizes: +// for (n in sizes) { +// +// // Compute the average execution time: +// var dtime = timeIt(::directFT, n, repeat) +// var rtime = timeIt(::recursiveFFT, n, repeat) +// +// // Print the results: +// val results = "| %7d | %7d | %7.4f | %7.4f |".format(n, n*n, dtime, rtime) +// println(results); +// } +// +// println("+---------+---------+---------+---------+") +// +//} \ No newline at end of file diff --git a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt index 0c3d11c0..c41582c2 100644 --- a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt +++ b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt @@ -1,8 +1,10 @@ package model.io.sql import androidx.compose.material.CircularProgressIndicator -import androidx.compose.runtime.* -import kotlinx.coroutines.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import model.graphs.abstractGraph.Graph import view.MainScreen import view.utils.ErrorWindow diff --git a/app/src/main/kotlin/model/io/sql/queries.txt b/app/src/main/kotlin/model/io/sql/queries.txt index 24433cf9..03827dfe 100644 --- a/app/src/main/kotlin/model/io/sql/queries.txt +++ b/app/src/main/kotlin/model/io/sql/queries.txt @@ -55,4 +55,4 @@ DELETE FROM edges WHERE graph_id = ?: DELETE FROM vertices WHERE graph_id = ?: -- Rename graph in graph table -UPDATE graphs SET name = ? WHERE id = ?: \ No newline at end of file +UPDATE graphs SET name = ? WHERE id = ?: diff --git a/app/src/main/kotlin/view/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/ImportGraphDialogWindow.kt deleted file mode 100644 index 8d8a2bb6..00000000 --- a/app/src/main/kotlin/view/ImportGraphDialogWindow.kt +++ /dev/null @@ -1,2 +0,0 @@ -package view - diff --git a/app/src/main/kotlin/view/graph/GraphView.kt b/app/src/main/kotlin/view/graph/GraphView.kt index 3ff48f86..b24d9f90 100644 --- a/app/src/main/kotlin/view/graph/GraphView.kt +++ b/app/src/main/kotlin/view/graph/GraphView.kt @@ -27,7 +27,6 @@ fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState< val coroutineScope = rememberCoroutineScope { Dispatchers.Default } val updateRequired = remember { derivedStateOf { viewModel.updateIsRequired } } - // val density = LocalDensity.current.density val transformationState = rememberTransformableState { zoomChange, offsetChange, _ -> currentScaleState.value *= zoomChange @@ -53,7 +52,8 @@ fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState< ) ) { if (updateRequired.value.value) { - viewModel.applyForceDirectedLayout(740.0, 650.0) + viewModel.randomize(740.0, 650.0) + viewModel.applyForceDirectedLayout(740.0, 650.0, 0.1, 8.0, 1.2) } viewModel.verticesVM.forEach { v -> VertexView(v, currentScaleState.value) } viewModel.edgesVM.forEach { e -> EdgeView(e, currentScaleState.value) } diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index 549b2fc4..6869a440 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -265,15 +265,6 @@ fun GeneralTab(graphVM: GraphViewModel) { } } } - - if (changesWereMade) { - changesWereMade = false - val currentWindowVM = WindowViewModel() - currentWindowVM.SetCurrentDimensions() - - graphVM.applyForceDirectedLayout(currentWindowVM.getWidth.value.toDouble(), currentWindowVM.getHeight.value.toDouble()) - } - if (showErrorWindow.value) { ErrorWindow("No such Vertex", { showErrorWindow.value = false }) } diff --git a/app/src/main/kotlin/view/utils/ErrorWindow.kt b/app/src/main/kotlin/view/utils/ErrorWindow.kt index 7daee609..311ad625 100644 --- a/app/src/main/kotlin/view/utils/ErrorWindow.kt +++ b/app/src/main/kotlin/view/utils/ErrorWindow.kt @@ -1,12 +1,16 @@ package view.utils import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -48,6 +52,3 @@ fun ErrorWindow(message: String, onDismiss: () -> Unit) { } } } - - - diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 95a8e121..436e9d66 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -37,7 +37,7 @@ class GraphViewModel( dataVisible = showVerticesData, idVisible = showVerticesID, vertex = vertex, - ) + ) } fun checkVertexById(id: Int): Boolean { @@ -68,9 +68,14 @@ class GraphViewModel( updateEdgeViewModels(newEdge) } - fun applyForceDirectedLayout(width: Double, height: Double) { + fun applyForceDirectedLayout(width: Double, height: Double, a: Double, b: Double, c: Double) { val layout = TFDPLayout() - layout.place(width, height, verticesVM) + layout.place(width, height, verticesVM, 128, a, b, c) + } + + fun randomize(width: Double, height: Double) { + val layout = TFDPLayout() + layout.randomize(width, height, verticesVM) } val verticesVM: List> get() = _verticesViewModels.values.toList() diff --git a/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt index 43286fc4..570e2715 100644 --- a/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt @@ -38,29 +38,51 @@ class SetupGraphViewModel { when (graphStructure) { is GraphStructure.Directed -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel( - WeightedDirectedGraph(), - "WeightedDirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel( - WeightedDirectedGraph(), - "WeightedDirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel( - WeightedDirectedGraph(), - "WeightedDirectedGraph String")) + is GraphType.Integer -> MainScreen( + MainScreenViewModel( + WeightedDirectedGraph(), + "WeightedDirectedGraph Int" + ) + ) + + is GraphType.UInteger -> MainScreen( + MainScreenViewModel( + WeightedDirectedGraph(), + "WeightedDirectedGraph UInt" + ) + ) + + is GraphType.String -> MainScreen( + MainScreenViewModel( + WeightedDirectedGraph(), + "WeightedDirectedGraph String" + ) + ) } } is GraphStructure.Undirected -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel( - WeightedUndirectedGraph(), - "WeightedUndirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel( - WeightedUndirectedGraph(), - "WeightedUndirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel( - WeightedUndirectedGraph(), - "WeightedUndirectedGraph String")) + is GraphType.Integer -> MainScreen( + MainScreenViewModel( + WeightedUndirectedGraph(), + "WeightedUndirectedGraph Int" + ) + ) + + is GraphType.UInteger -> MainScreen( + MainScreenViewModel( + WeightedUndirectedGraph(), + "WeightedUndirectedGraph UInt" + ) + ) + + is GraphType.String -> MainScreen( + MainScreenViewModel( + WeightedUndirectedGraph(), + "WeightedUndirectedGraph String" + ) + ) } } } @@ -70,29 +92,52 @@ class SetupGraphViewModel { when (graphStructure) { is GraphStructure.Directed -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel( - DirectedGraph(), - "DirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel( - DirectedGraph(), - "DirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel( - DirectedGraph(), - "DirectedGraph String")) + is GraphType.Integer -> MainScreen( + MainScreenViewModel( + DirectedGraph(), + "DirectedGraph Int" + ) + ) + + is GraphType.UInteger -> MainScreen( + MainScreenViewModel( + DirectedGraph(), + "DirectedGraph UInt" + ) + ) + + is GraphType.String -> MainScreen( + MainScreenViewModel( + DirectedGraph(), + "DirectedGraph String" + ) + ) } } is GraphStructure.Undirected -> { when (storedData) { - is GraphType.Integer -> MainScreen(MainScreenViewModel( - UndirectedGraph(), - "UndirectedGraph Int")) - is GraphType.UInteger -> MainScreen(MainScreenViewModel( - UndirectedGraph(), - "UndirectedGraph UInt")) - is GraphType.String -> MainScreen(MainScreenViewModel( - UndirectedGraph(), - "UndirectedGraph String")) + is GraphType.Integer -> MainScreen( + MainScreenViewModel( + UndirectedGraph(), + "UndirectedGraph Int" + ) + ) + + is GraphType.UInteger -> MainScreen( + MainScreenViewModel( + UndirectedGraph(), + "UndirectedGraph UInt" + ) + ) + + is GraphType.String -> + MainScreen( + MainScreenViewModel( + UndirectedGraph(), + "UndirectedGraph String" + ) + ) } } } @@ -101,7 +146,7 @@ class SetupGraphViewModel { } @Suppress("UNCHECKED_CAST") - fun createGraphObject( + fun createGraphObject( storedData: GraphType, graphStructure: GraphStructure, weight: Weight, @@ -113,68 +158,103 @@ class SetupGraphViewModel { is GraphStructure.Directed -> { when (storedData) { is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedDirectedGraph(), - graphId, graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + WeightedDirectedGraph(), + graphId, graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedDirectedGraph(), - graphId, graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + WeightedDirectedGraph(), + graphId, graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedDirectedGraph(), - graphId, graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + WeightedDirectedGraph(), + graphId, graphVMState as MutableState?> + ) as GraphViewModel? } } } + is GraphStructure.Undirected -> { when (storedData) { is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedUndirectedGraph(), - graphId, graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + WeightedUndirectedGraph(), + graphId, graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedUndirectedGraph(), - graphId, graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + WeightedUndirectedGraph(), + graphId, graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(WeightedUndirectedGraph(), - graphId, graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + WeightedUndirectedGraph(), + graphId, graphVMState as MutableState?> + ) as GraphViewModel? } } } } } + is Weight.Unweighted -> { when (graphStructure) { is GraphStructure.Directed -> { when (storedData) { is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(DirectedGraph(), graphId, - graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + DirectedGraph(), graphId, + graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(DirectedGraph(), graphId, - graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + DirectedGraph(), graphId, + graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(DirectedGraph(), graphId, - graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + DirectedGraph(), graphId, + graphVMState as MutableState?> + ) as GraphViewModel? } } } + is GraphStructure.Undirected -> { when (storedData) { is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(UndirectedGraph(), graphId, - graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + UndirectedGraph(), graphId, + graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(UndirectedGraph(), graphId, - graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + UndirectedGraph(), graphId, + graphVMState as MutableState?> + ) as GraphViewModel? } + is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM(UndirectedGraph(), graphId, - graphVMState as MutableState?>) as GraphViewModel? + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + UndirectedGraph(), graphId, + graphVMState as MutableState?> + ) as GraphViewModel? } } } diff --git a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt index 149f590f..c2393d0c 100644 --- a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt +++ b/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt @@ -1,63 +1,162 @@ package viewmodel.graph -import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp +import model.Complex +import model.recursiveFFT import kotlin.math.pow import kotlin.math.sqrt -import androidx.compose.ui.platform.LocalDensity +import kotlin.random.Random -class TFDPLayout( - private val longRangeAttractionConstant: Float = 0.0001f, // strength of attractive force (long-range) - B - private val nearAttractionConstant: Float = 16.0f, // strength of attractive t-force (near) - A - private val repulsiveConstant: Float = 5.0f, // extent and magnitude of the repulsive t-force that - // controls the longest distance of neighbors in the layout - Y -) { - fun place(width: Double, height: Double, vertices: Collection>) { - val forces = Array(vertices.size) { Pair(0f, 0f) } - var k = 0 +class TFDPLayout() { + /** + * longRangeAttractionConstant - strength of attractive force (long-range) - B + * nearAttractionConstant - strength of attractive t-force (near) - A + * repulsiveConstant - extent and magnitude of the repulsive t-force that + controls the longest distance of neighbors in the layout - Y + **/ - vertices.onEach { - val vi = it - var forceX = 0f - var forceY = 0f + fun fft2D(input: Array>): Array> { + val rows = input.size + val cols = input[0].size + val output = Array(rows) { Array(cols) { Complex(0.0, 0.0) } } - for (vj in vertices) { // repulsive forces - if (vi == vj) continue + // Transform each row + for (i in 0 until rows) { + output[i] = recursiveFFT(input[i]) + } + + // Transpose the output matrix + val transposedOutput = Array(cols) { Array(rows) { Complex(0.0, 0.0) } } + for (i in 0 until rows) { + for (j in 0 until cols) { + transposedOutput[j][i] = output[i][j] + } + } + + // Transform each column + for (j in 0 until cols) { + val column = Array(rows) { i -> transposedOutput[j][i] } + val transformedCol = recursiveFFT(column) + for (i in 0 until rows) { + output[i][j] = transformedCol[i] + } + } + + return output + } + + fun ifft2D(input: Array>): Array> { + val rows = input.size + val cols = input[0].size + val output = Array(rows) { Array(cols) { Complex() } } + + // Transform each row + for (i in 0 until rows) { + output[i] = recursiveFFT(input[i]) + } + + // Transpose the output matrix + val transposedOutput = Array(cols) { Array(rows) { Complex() } } + for (i in 0 until rows) { + for (j in 0 until cols) { + transposedOutput[j][i] = output[i][j] + } + } + + // Transform each column + for (j in 0 until cols) { + val column = Array(rows) { i -> transposedOutput[j][i] } + val transformedCol = recursiveFFT(column) + for (i in 0 until rows) { + output[i][j] = transformedCol[i] + } + } + + // Divide by the total number of elements to complete the inverse transformation + val totalElements = rows * cols + for (i in 0 until rows) { + for (j in 0 until cols) { + output[i][j] = output[i][j] / totalElements.toDouble() + } + } + + return output + } + + fun place(width: Double, height: Double, vertices: Collection>, gridSize: Int = 128, longRangeAttractionConstant: Double, nearAttractionConstant: Double, repulsiveConstant: Double) { + val forces = Array(vertices.size) { Pair(0.0, 0.0) } + val grid = Array(gridSize) { Array(gridSize) { Complex(0.0, 0.0) } } + val deltaX = width / gridSize + val deltaY = height / gridSize - val dx = vi.x.value - vj.x.value - val dy = vi.y.value - vj.y.value + // Assign particles to the grid + vertices.forEach { vertex -> + val i = (vertex.x.value / deltaX.dp).toInt().coerceIn(0, gridSize - 1) + val j = (vertex.y.value / deltaY.dp).toInt().coerceIn(0, gridSize - 1) + grid[i][j] = grid[i][j].plus(Complex(1.0, 0.0)) // Add particle mass + } + + // Compute potential using FFT + val potential = fft2D(grid) + // Apply the Green's function in frequency domain + for (i in 0 until gridSize) { + for (j in 0 until gridSize) { + val kx = if (i <= gridSize / 2) i else i - gridSize + val ky = if (j <= gridSize / 2) j else j - gridSize + val kSquared = (kx * kx + ky * ky).toDouble() + if (kSquared != 0.0) { + potential[i][j] = potential[i][j].div(kSquared) + } + } + } + val potentialRealSpace = ifft2D(potential) - val distance = sqrt(dx.value * dx.value + dy.value * dy.value) + // Compute forces from potential + val forceX = Array(gridSize) { Array(gridSize) { 0.0 } } + val forceY = Array(gridSize) { Array(gridSize) { 0.0 } } + for (i in 0 until gridSize) { + for (j in 0 until gridSize) { + val right = potentialRealSpace[(i + 1) % gridSize][j].r - potentialRealSpace[i][j].r + val up = potentialRealSpace[i][(j + 1) % gridSize].r - potentialRealSpace[i][j].r + + val distance = sqrt(deltaX * deltaX + deltaY * deltaY) val repulsion = (distance) / (1 + distance * distance).pow(repulsiveConstant) - forceX -= dx.value / distance * repulsion - forceY -= dy.value / distance * repulsion + forceX[i][j] -= right / deltaX / distance * repulsion + forceY[i][j] -= up / deltaY / distance * repulsion val attraction = longRangeAttractionConstant * (distance + ((nearAttractionConstant * distance) / (1 + distance * distance))) - forceX += dx.value / distance * attraction - forceY += dy.value / distance * attraction + forceX[i][j] -= right / deltaX / distance * attraction + forceY[i][j] -= up / deltaY / distance * attraction } - forces[k] = Pair(forceX, forceY) - k++ } - k = 0 - vertices.onEach { // update positions - val vi = it - vi.x.value += forces[k].first.dp / 2 - vi.y.value += forces[k].second.dp / 2 - k++ + // Interpolate forces back to vertices + vertices.forEachIndexed { index, vertex -> + val i = (vertex.x.value / deltaX.dp).toInt().coerceIn(0, gridSize - 1) + val j = (vertex.y.value / deltaY.dp).toInt().coerceIn(0, gridSize - 1) + val fx = forceX[i][j] + val fy = forceY[i][j] + forces[index] = Pair(fx, fy) + } + + // Update positions + vertices.forEachIndexed { index, vertex -> + vertex.x.value += forces[index].first.dp + vertex.y.value += forces[index].second.dp } + } - for (vi in vertices) { // check borders - if (vi.x.value > (width.toFloat() - 360 - vi.radius.value * 2).dp) vi.x.value = vi.x.value / 2 - if (vi.y.value > (height.toFloat() - vi.radius.value * 2).dp) vi.y.value = vi.y.value / 2 + fun randomize(width: Double, height: Double, vertices: Collection>) { + vertices.forEach { vertex -> + val randomX = Random.nextDouble(0.0, width * 1.5 - 360.0 - vertex.radius.value * 2).toFloat().dp + val randomY = Random.nextDouble(0.0, height * 1.5 - vertex.radius.value * 2).toFloat().dp - vi.x.value = vi.x.value.coerceIn(0.dp, (width.toFloat() - 360 - vi.radius.value * 2).dp) - vi.y.value = vi.y.value.coerceIn(0.dp, (height.toFloat() - vi.radius.value * 2).dp) + vertex.x.value = randomX + vertex.y.value = randomY } } } From cae0fa1a8a24f56d5c9163561490e1bcaa35f00c Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Thu, 30 May 2024 02:04:36 +0300 Subject: [PATCH 71/77] docs: implement readme, add general tab screenshot #55 * docs(readme): add project description * docs(readme): add general tab screenshot * docs(redame): corrct grammatical error --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b2108cc..5712d52b 100644 --- a/README.md +++ b/README.md @@ -1 +1,53 @@ -### TODO +# WUDU* + +Application that lets you create, save, visualise, analyse and modify 4 different types of graphs (see *) + +## Features + +- Support 4 types of graphs (see *) +- Use all in all - 9 algorithms +- Store graphs anywhere you want: SQLite, Neo4J, JSON +- Drag, zoom, replace nodes + +![mainscreen_screenshot](https://github.com/spbu-coding-2023/graphs-graph-2/assets/64466788/9c708a11-dc6e-4cf9-a848-44f21dec7a37) + +## Usage + +Firstly - clone the last release/version of our app. Since our app uses Gradle build system - you can use these commands: + +| Command | Description | +|----------------------------------------|-----------------------------------------------------------------------| +| `./gradlew run` | Runs the application | +| `./gradlew assemble` | Builds without tests | +| `./gradlew test` | Runs the unit and integration tests | + +## Contributing + +If you have found a bug, or want to propose some useful feature for our project, please firstly read our [Contribution Rules][contribute_rules_url] and +do the following: +1. Fork the Project +2. Create your Feature Branch (git checkout -b feat/my-feature) +3. Commit your Changes (git commit -m 'add some feature'), but don't forget to keep the commit style. (We use [Conventional Commits]) +4. Push to the Branch (git push origin feat/my-feature) +5. Open a Pull Request + +## License + +Distributed under the [GPL-3.0 License][repo_license_url]. + +## Authors + +- [Gavrilenko Mike](https://github.com/qrutyy) +- [Shakirov Karim](https://github.com/kar1mgh) +- [Vlasenco Daniel](https://github.com/spisladqo) + +** - Weighted Unweighted Directed Undirected (or like a Vodoo doll ;) ) +_______________________________ + +[*Java gnomik*][java_gnomik_url] + +[Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/ +[repo_license_url]: https://github.com/spbu-coding-2023/graphs-graph-2/blob/main/LICENSE.md +[contribute_rules_url]: https://github.com/spbu-coding-2023/graphs-graph-2/blob/main/CONTRIBUTING.md + +[java_gnomik_url]: https://ibb.co/54hJVd2 From ff30eef922f800dcde004ceb3923f5c847415b34 Mon Sep 17 00:00:00 2001 From: Karim Shakirov Date: Thu, 30 May 2024 02:05:47 +0300 Subject: [PATCH 72/77] feat: make algorithms runnable from UI #56 * feat: add UI for finding cycles and shortest path, make algorithms available only to corresponding graphs * feat: add draft of UI for running layout * feat: add methods that run algorithms in graphVM + update import graph dialog window * feat: add methods that visualize algorithms * feat: make algorithms runnable from UI --- .../main/kotlin/model/algorithms/SCCFinder.kt | 11 +- .../clustering/CommunitiesFinder.kt | 2 +- app/src/main/kotlin/view/MainScreen.kt | 9 +- app/src/main/kotlin/view/graph/EdgeView.kt | 10 +- app/src/main/kotlin/view/graph/GraphView.kt | 5 +- app/src/main/kotlin/view/graph/VertexView.kt | 11 +- .../kotlin/view/tabScreen/FileControlTab.kt | 25 +- .../main/kotlin/view/tabScreen/GeneralTab.kt | 1 + .../main/kotlin/view/tabScreen/TabHandler.kt | 4 +- .../view/tabScreen/analyzeTab/AnalyzeTab.kt | 46 ++-- .../analyzeTab/algorithmsUI/BridgesUI.kt | 17 +- .../analyzeTab/algorithmsUI/CommunitiesUI.kt | 17 +- .../analyzeTab/algorithmsUI/CyclesUI.kt | 91 ++++++- .../analyzeTab/algorithmsUI/KeyVerticesUI.kt | 17 +- .../analyzeTab/algorithmsUI/LayoutUI.kt | 94 +++++-- .../algorithmsUI/MinSpanningTreeUI.kt | 17 +- .../analyzeTab/algorithmsUI/SCCUI.kt | 17 +- .../analyzeTab/algorithmsUI/ShortestPathUI.kt | 101 +++++++- .../view/utils/ImportGraphDialogWindow.kt | 76 +++--- app/src/main/kotlin/view/utils/ToolBox.kt | 57 +++++ app/src/main/kotlin/view/utils/ZoomBox.kt | 38 --- .../kotlin/viewmodel/graph/EdgeViewModel.kt | 4 + .../kotlin/viewmodel/graph/GraphViewModel.kt | 230 ++++++++++++++++-- .../kotlin/viewmodel/graph/VertexViewModel.kt | 5 +- .../clustering/CommunitiesFinderTest.kt | 6 +- 25 files changed, 718 insertions(+), 193 deletions(-) create mode 100644 app/src/main/kotlin/view/utils/ToolBox.kt delete mode 100644 app/src/main/kotlin/view/utils/ZoomBox.kt diff --git a/app/src/main/kotlin/model/algorithms/SCCFinder.kt b/app/src/main/kotlin/model/algorithms/SCCFinder.kt index 0b341ded..0c4b3cf1 100644 --- a/app/src/main/kotlin/model/algorithms/SCCFinder.kt +++ b/app/src/main/kotlin/model/algorithms/SCCFinder.kt @@ -1,16 +1,15 @@ package model.algorithms import model.graphs.DirectedGraph -import model.graphs.abstractGraph.Graph import model.graphs.abstractGraph.Vertex class SCCFinder { // SCC - Strongly Connected Components (by Kosaraju) - fun findSCC(graph: DirectedGraph): MutableSet>> { + fun findSCC(graph: DirectedGraph): Set>> { val visited = mutableMapOf, Boolean>().withDefault { false } val stack = ArrayDeque>() val component = arrayListOf>() - val sccList: MutableSet>> = mutableSetOf() + val SCCs: MutableSet>> = mutableSetOf() fun auxiliaryDFS(srcVertex: Vertex, componentList: ArrayList>) { visited[srcVertex] = true @@ -46,10 +45,11 @@ class SCCFinder { if (visited[vertex] != true) { val currentComponent = mutableSetOf>() reverseDFS(vertex, currentComponent) - sccList.add(currentComponent) + SCCs.add(currentComponent) } } - return sccList + + return SCCs } private fun reverseEdgesMap(graph: DirectedGraph): Map, MutableSet>> { @@ -58,6 +58,7 @@ class SCCFinder { graph.getEdges().forEach { edge -> reversedEdgesMap[edge.vertex2]?.add(edge.vertex1) } + return reversedEdgesMap } } diff --git a/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt b/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt index b64994fc..2312d723 100644 --- a/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt +++ b/app/src/main/kotlin/model/algorithms/clustering/CommunitiesFinder.kt @@ -8,7 +8,7 @@ import model.algorithms.clustering.implementation.Link import model.algorithms.clustering.implementation.getPartition class CommunitiesFinder { - fun findCommunity(graph: Graph): Set>> { + fun findCommunities(graph: Graph): Set>> { if (graph.getVertices().size == 1) return setOf(setOf(graph.getVertices()[0])) val links = convertToAPIFormat(graph) diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt index eb2b7a3c..b31e0497 100644 --- a/app/src/main/kotlin/view/MainScreen.kt +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -1,22 +1,17 @@ package view import MyAppTheme -import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp import view.graph.GraphView import view.tabScreen.TabHandler import view.utils.FAQBox -import view.utils.ZoomBox +import view.utils.ToolBox import viewmodel.MainScreenViewModel @Composable @@ -34,6 +29,6 @@ fun MainScreen(viewmodel: MainScreenViewModel) { } // Hoverable box over the existing Surface FAQBox(interactionSource, viewmodel) - ZoomBox(scale) + ToolBox(viewmodel.graphViewModel, scale) } } diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt index 6a259f41..74a09597 100644 --- a/app/src/main/kotlin/view/graph/EdgeView.kt +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -3,6 +3,9 @@ package view.graph import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -30,9 +33,12 @@ fun EdgeView(viewModel: EdgeViewModel, scale: Float) { val arrowPoints = viewModel.calculateArrowPoints(scale) + val highlightColor by remember { derivedStateOf { viewModel.highlightColor } } + val edgeColor = highlightColor.value + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( - color = Color.LightGray, + color = edgeColor, strokeWidth = (5f * scale).coerceIn(minEdgeStrokeWidth, maxEdgeStrokeWidth), start = Offset( @@ -66,7 +72,7 @@ fun EdgeView(viewModel: EdgeViewModel, scale: Float) { drawPath( path = trianglePath, - color = Color.LightGray, + color = edgeColor, style = Fill ) } diff --git a/app/src/main/kotlin/view/graph/GraphView.kt b/app/src/main/kotlin/view/graph/GraphView.kt index b24d9f90..62557c57 100644 --- a/app/src/main/kotlin/view/graph/GraphView.kt +++ b/app/src/main/kotlin/view/graph/GraphView.kt @@ -26,12 +26,13 @@ fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState< val offset = remember { mutableStateOf(Offset.Zero) } val coroutineScope = rememberCoroutineScope { Dispatchers.Default } - val updateRequired = remember { derivedStateOf { viewModel.updateIsRequired } } + val updateRequired by remember { derivedStateOf { viewModel.updateIsRequired } } val transformationState = rememberTransformableState { zoomChange, offsetChange, _ -> currentScaleState.value *= zoomChange offset.value += offsetChange } + Box( modifier = Modifier .fillMaxSize() @@ -51,7 +52,7 @@ fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState< translationY = offset.value.y ) ) { - if (updateRequired.value.value) { + if (updateRequired.value) { viewModel.randomize(740.0, 650.0) viewModel.applyForceDirectedLayout(740.0, 650.0, 0.1, 8.0, 1.2) } diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt index 2153b1af..3222ea8f 100644 --- a/app/src/main/kotlin/view/graph/VertexView.kt +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -1,6 +1,7 @@ package view.graph import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box @@ -9,8 +10,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import kotlinx.coroutines.Dispatchers @@ -35,12 +36,16 @@ fun VertexView(viewModel: VertexViewModel, scale: Float) { val adjustedX = viewModel.x.value val adjustedY = viewModel.y.value - val adjustedRadius = (viewModel.radius * scale).coerceIn(minVertexRadius, maxVertexRadius) + var adjustedRadius = (viewModel.radius * scale).coerceIn(minVertexRadius, maxVertexRadius) + + val highlightColor by remember { derivedStateOf { viewModel.highlightColor } } + val borderColor = highlightColor.value Box( modifier = Modifier .offset { IntOffset(adjustedX.roundToPx(), adjustedY.roundToPx()) } .size(adjustedRadius * 2) + .border(5.dp, borderColor, CircleShape) .background( if (viewModel.isSelected.value) Color.Yellow else Color.LightGray, shape = CircleShape diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index 01b4d9f5..8b781894 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.delay import model.io.sql.SQLDatabaseModule import view.utils.EditDBWindow import view.utils.ErrorWindow +import view.utils.ImportGraphDialogWindow import java.awt.FileDialog import java.awt.Frame @@ -149,7 +150,6 @@ fun FileControlTab(graphVM: GraphViewModel) { modifier = Modifier.fillMaxSize().height(fieldHeight), onClick = { showSaveDialog = true - graphName = "" }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { @@ -188,11 +188,6 @@ fun FileControlTab(graphVM: GraphViewModel) { } } - - val dialogueHeight = 300.dp - val dialogueWidth = 400.dp - val padding = 14.dp - if (showSaveDialog) { if (selectedDatabase == "SQLite") { val existingGraphNamesSQL = remember { mutableStateOf(arrayListOf>()) } @@ -208,6 +203,7 @@ fun FileControlTab(graphVM: GraphViewModel) { Dialog( onDismissRequest = { showSaveDialog = false + graphName = "" } ) { Column( @@ -217,7 +213,7 @@ fun FileControlTab(graphVM: GraphViewModel) { .width(300.dp) .height(50.dp) ) { - Text("Graph '$graphName' loaded successfully!") + Text("Graph '$graphName' saved successfully!") } } @@ -231,20 +227,7 @@ fun FileControlTab(graphVM: GraphViewModel) { } if (showLoadDialog) { - Dialog( - onDismissRequest = { - showLoadDialog = false - } - ) { - Column( - modifier = - Modifier - .background(Color.White) - .padding(padding) - .width(dialogueWidth) - .height(dialogueHeight) - ) {} - } + ImportGraphDialogWindow() // TODO } if (showEditDialog) { diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index 6869a440..6c26f2d3 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -100,6 +100,7 @@ fun GeneralTab(graphVM: GraphViewModel) { value = secondVertexId, onValueChange = { secondVertexId = it }, modifier = Modifier.fillMaxWidth().height(70.dp).clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 12.sp), label = { Text( "2 vertex ID", diff --git a/app/src/main/kotlin/view/tabScreen/TabHandler.kt b/app/src/main/kotlin/view/tabScreen/TabHandler.kt index 98952546..8a15da53 100644 --- a/app/src/main/kotlin/view/tabScreen/TabHandler.kt +++ b/app/src/main/kotlin/view/tabScreen/TabHandler.kt @@ -22,8 +22,8 @@ import viewmodel.MainScreenViewModel @Composable fun TabHandler(viewmodel: MainScreenViewModel) { Column( - modifier = - Modifier.width(360.dp) + modifier = Modifier + .width(360.dp) .background(color = MaterialTheme.colors.surface) .fillMaxHeight() .clip(shape = RoundedCornerShape(10.dp)) diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt index b4c5ff81..1d0fc7ad 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt @@ -7,6 +7,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph import view.tabScreen.analyzeTab.algorithmsUI.* import viewmodel.graph.GraphViewModel @@ -17,17 +21,30 @@ val horizontalGap = 20.dp @OptIn(ExperimentalMaterialApi::class) @Composable fun AnalyzeTab(graphVM: GraphViewModel) { - val algorithms = arrayOf( + val algorithms = mutableListOf( "Layout", - "Communities", - "Key vertices", - "Shortest path", - "Cycles", - "Bridges", - "SCC", - "Min spanning tree" + "Find communities", + "Find key vertices" ) + if (graphVM.graph is DirectedGraph) { + algorithms += "Find SCCs" + algorithms += "Find cycles" + } + + if (graphVM.graph is UndirectedGraph) { + algorithms += "Find bridges" + } + + if (graphVM.graph is WeightedUndirectedGraph) { + algorithms += "Min spanning tree" + algorithms += "Find shortest path" + } + + if (graphVM.graph is WeightedDirectedGraph) { + algorithms += "Find shortest path" + } + var selectedAlgorithm by remember { mutableStateOf(algorithms[0]) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { @@ -88,17 +105,16 @@ fun AnalyzeTab(graphVM: GraphViewModel) { } } } - } when (selectedAlgorithm) { "Layout" -> { LayoutUI(graphVM) } - "Communities" -> { CommunitiesUI(graphVM) } - "Key vertices" -> { KeyVerticesUI(graphVM) } - "Shortest path" -> { ShortestPathUI(graphVM) } - "Cycles" -> { CyclesUI(graphVM) } - "Bridges" -> { BridgesUI(graphVM) } - "SCC" -> { SCCUI(graphVM) } + "Find communities" -> { CommunitiesUI(graphVM) } + "Find key vertices" -> { KeyVerticesUI(graphVM) } + "Find shortest path" -> { ShortestPathUI(graphVM) } + "Find cycles" -> { CyclesUI(graphVM) } + "Find bridges" -> { BridgesUI(graphVM) } + "Find SCCs" -> { SCCUI(graphVM) } "Min spanning tree" -> { MinSpanningTreeUI(graphVM) } } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt index af869b4b..25372106 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt @@ -6,14 +6,19 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight +import view.utils.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun BridgesUI(graphVM: GraphViewModel) { + val showErrorWindow = remember { mutableStateOf(false) } + Row( modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) @@ -21,11 +26,19 @@ fun BridgesUI(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize(), - onClick = {}, + onClick = { + if (!graphVM.findBridges()) { + showErrorWindow.value = true + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("Find bridges") + Text("Run algorithm") } } } + + if (showErrorWindow.value) { + ErrorWindow("No bridges were found", { showErrorWindow.value = false }) + } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt index ce7b443a..d4de43d2 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt @@ -6,14 +6,19 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight +import view.utils.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun CommunitiesUI(graphVM: GraphViewModel) { + val showErrorWindow = remember { mutableStateOf(false) } + Row( modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) @@ -21,11 +26,19 @@ fun CommunitiesUI(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize(), - onClick = {}, + onClick = { + if (!graphVM.findCommunities()) { + showErrorWindow.value = true + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("Find communities") + Text("Run algorithm") } } } + + if (showErrorWindow.value) { + ErrorWindow("No communities were found", { showErrorWindow.value = false }) + } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt index a92d46b4..7cf08e08 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt @@ -1,19 +1,87 @@ package view.tabScreen.analyzeTab.algorithmsUI import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight +import view.utils.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun CyclesUI(graphVM: GraphViewModel) { + var vertexId by remember { mutableStateOf("") } + val showErrorWindow = remember { mutableStateOf(false) } + val errorMessage = remember { mutableStateOf("") } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + TextField( + value = vertexId, + onValueChange = { vertexId = it }, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Vertex ID", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + } + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = { + if (vertexId.isEmpty()) { + errorMessage.value = "Enter vertex's ID" + showErrorWindow.value = true + } + else if (!vertexId.all { char -> char.isDigit() }) { + errorMessage.value = "ID should be a number" + showErrorWindow.value = true + } + else if (vertexId.toInt() > graphVM.graph.getVertices().size - 1) { + errorMessage.value = "No vertex with ID $vertexId" + showErrorWindow.value = true + } + else if (!graphVM.findCycles(vertexId.toInt())) { + errorMessage.value = "No cycles were found" + showErrorWindow.value = true + } + else { + graphVM.highlighNextCycle() + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Run algorithm") + } + } + } Row( modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) @@ -21,11 +89,20 @@ fun CyclesUI(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize(), - onClick = {}, + onClick = { + if (!graphVM.highlighNextCycle()) { + errorMessage.value = "Please run algorithm first" + showErrorWindow.value = true + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("Find cycles") + Text("Highlight next cycle") } } } + + if (showErrorWindow.value) { + ErrorWindow(errorMessage.value, { showErrorWindow.value = false }) + } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt index 5c0e7bd9..05239aba 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt @@ -6,14 +6,19 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight +import view.utils.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun KeyVerticesUI(graphVM: GraphViewModel) { + val showErrorWindow = remember { mutableStateOf(false) } + Row( modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) @@ -21,11 +26,19 @@ fun KeyVerticesUI(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize(), - onClick = {}, + onClick = { + if (!graphVM.findKeyVertices()) { + showErrorWindow.value = true + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("Find key vertices") + Text("Run algorithm") } } } + + if (showErrorWindow.value) { + ErrorWindow("No key vertices were found", { showErrorWindow.value = false }) + } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt index db9da685..90deaf88 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/LayoutUI.kt @@ -1,12 +1,10 @@ package view.tabScreen.analyzeTab.algorithmsUI import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight @@ -15,18 +13,82 @@ import viewmodel.graph.GraphViewModel @Composable fun LayoutUI(graphVM: GraphViewModel) { - Row( - modifier = Modifier.height(rowHeight).padding(borderPadding), - horizontalArrangement = Arrangement.spacedBy(horizontalGap) + var sliderValue1 by remember { mutableStateOf(0f) } + var sliderValue2 by remember { mutableStateOf(0f) } + var sliderValue3 by remember { mutableStateOf(0f) } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) ) { - Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { - Button( - modifier = Modifier.fillMaxSize(), - onClick = {}, - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) - ) { - Text("Apply layout") - } + Text("Analyze Tab", style = MaterialTheme.typography.h6, modifier = Modifier.padding(bottom = 16.dp)) + + SliderWithValue( + value = sliderValue1, + onValueChange = { sliderValue1 = it }, + label = "Slider 1", + valueRange = 0f..2f, + coefficient = 1f + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SliderWithValue( + value = sliderValue2, + onValueChange = { sliderValue2 = it }, + label = "Slider 2", + valueRange = 1f..30f, + coefficient = 1f + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SliderWithValue( + value = sliderValue3, + onValueChange = { sliderValue3 = it }, + label = "Slider 3", + valueRange = 0.5f..10f, + coefficient = 1f + ) + Button( + modifier = Modifier.fillMaxWidth().height(50.dp), + onClick = {}, +// onClick = { graphVM.applyForceDirectedLayout(740.0, 650.0, sliderValue1.toDouble(), sliderValue2.toDouble(), sliderValue3.toDouble())}) + ) { + Text("Apply layout settings") + } + + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + modifier = Modifier.fillMaxWidth().height(50.dp), + onClick = {} +// onClick = { graphVM.randomize(740.0, 650.0) }) { + ) { + Text("Randomize") } + + } +} + +@Composable +fun SliderWithValue( + value: Float, + onValueChange: (Float) -> Unit, + label: String, + valueRange: ClosedFloatingPointRange, + coefficient: Float +) { + Column { + Text("$label: ${"%.2f".format(value * coefficient)}", style = MaterialTheme.typography.body1) + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = ((valueRange.endInclusive - valueRange.start) / 1000).toInt(), + modifier = Modifier.fillMaxWidth() + ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt index c837325f..847e7576 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt @@ -6,14 +6,19 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight +import view.utils.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun MinSpanningTreeUI(graphVM: GraphViewModel) { + val showErrorWindow = remember { mutableStateOf(false) } + Row( modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) @@ -21,11 +26,19 @@ fun MinSpanningTreeUI(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize(), - onClick = {}, + onClick = { + if (!graphVM.findMinSpanningTree()) { + showErrorWindow.value = true + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("Find min spanning tree") + Text("Run algorithm") } } } + + if (showErrorWindow.value) { + ErrorWindow("No min spanning tree was found", { showErrorWindow.value = false }) + } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt index 9c3614e9..0dfba1ff 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt @@ -6,14 +6,19 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight +import view.utils.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun SCCUI(graphVM: GraphViewModel) { + val showErrorWindow = remember { mutableStateOf(false) } + Row( modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) @@ -21,11 +26,19 @@ fun SCCUI(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize(), - onClick = {}, + onClick = { + if (!graphVM.findSCCs()) { + showErrorWindow.value = true + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("Find SCCs") + Text("Run algorithm") } } } + + if (showErrorWindow.value) { + ErrorWindow("No SCCs were found", { showErrorWindow.value = false }) + } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt index 7471c0f5..f6de6c9d 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt @@ -1,19 +1,75 @@ package view.tabScreen.analyzeTab.algorithmsUI import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight +import view.utils.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun ShortestPathUI(graphVM: GraphViewModel) { + var sourceVertexId by remember { mutableStateOf("") } + var destVertexId by remember { mutableStateOf("") } + val showErrorWindow = remember { mutableStateOf(false) } + val errorMessage = remember { mutableStateOf("") } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.width(160.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = sourceVertexId, + onValueChange = { sourceVertexId = it }, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Source ID", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray, + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + Column(modifier = Modifier.width(160.dp).fillMaxHeight(), Arrangement.Center) { + TextField( + value = destVertexId, + onValueChange = { destVertexId = it }, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + textStyle = TextStyle(fontSize = 14.sp), + label = { + Text( + "Destination ID", + style = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.secondaryVariant + ) + ) + } + } Row( modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) @@ -21,11 +77,42 @@ fun ShortestPathUI(graphVM: GraphViewModel) { Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { Button( modifier = Modifier.fillMaxSize(), - onClick = {}, + onClick = { + if (sourceVertexId.isEmpty() || destVertexId.isEmpty()) { + errorMessage.value = "Enter vertices' IDs" + showErrorWindow.value = true + } + else if ( + !sourceVertexId.all { char -> char.isDigit() } || + !destVertexId.all { char -> char.isDigit() } + ) { + errorMessage.value = "ID should be a number" + showErrorWindow.value = true + } + else if (sourceVertexId == destVertexId) { + errorMessage.value = "Vertices' IDs should be different" + showErrorWindow.value = true + } + else if ( + sourceVertexId.toInt() > graphVM.graph.getVertices().size - 1 || + destVertexId.toInt() > graphVM.graph.getVertices().size - 1 + ) { + errorMessage.value = "No vertex with such ID" + showErrorWindow.value = true + } + else if (!graphVM.findShortestPath(sourceVertexId.toInt(), destVertexId.toInt())) { + errorMessage.value = "Shortest path doesn't exist" + showErrorWindow.value = true + } + }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text("Find Shortest path") + Text("Run algorithm") } } } + + if (showErrorWindow.value) { + ErrorWindow(errorMessage.value, { showErrorWindow.value = false }) + } } diff --git a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt index 4f85e222..6c6639ac 100644 --- a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt @@ -36,50 +36,54 @@ fun ImportGraphDialogWindow() { properties = DialogProperties(usePlatformDefaultWidth = false) ) { Column( - modifier = Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) + modifier = Modifier.background(Color.White) + .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp) ) { - Row(modifier = Modifier.width(350.dp).height(150.dp)) { - Text( - "Select the database", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - modifier = Modifier.padding(bottom = 10.dp) - ) - Box(modifier = Modifier.width(300.dp).padding(16.dp)) { - ExposedDropdownMenuBox( + Text( + "Select the graph:", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + TextField( + value = selectedGraphName.value, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded.value) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) + ExposedDropdownMenu( expanded = expanded.value, - onExpandedChange = { - expanded.value = !expanded.value - } + onDismissRequest = { expanded.value = false } ) { - TextField( - value = selectedGraphName.value, - onValueChange = {}, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded.value) }, - modifier = Modifier - ) - - ExposedDropdownMenu( - expanded = expanded.value, - onDismissRequest = { expanded.value = false } - ) { - graphs.value.forEach { graphName -> - // TODO: fix its layout - DropdownMenuItem( - onClick = { - selectedGraphName.value = graphName.second - selectedGraphID.value = graphName.first - expanded.value = false - } - ) { - Text(text = graphName.second) + graphs.value.forEach { graphName -> + // TODO: fix its layout + DropdownMenuItem( + onClick = { + selectedGraphName.value = graphName.second + selectedGraphID.value = graphName.first + expanded.value = false } + ) { + Text(text = graphName.second) } } } } } + Row( modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), verticalAlignment = Alignment.Bottom, @@ -103,4 +107,4 @@ fun ImportGraphDialogWindow() { if (importFromDBRequired.value) { return SQLDatabaseModule.importGraph(selectedGraphID.value) } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/utils/ToolBox.kt b/app/src/main/kotlin/view/utils/ToolBox.kt new file mode 100644 index 00000000..77752868 --- /dev/null +++ b/app/src/main/kotlin/view/utils/ToolBox.kt @@ -0,0 +1,57 @@ +package view.utils + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import scaleFactor +import view.tabScreen.analyzeTab.horizontalGap +import viewmodel.graph.GraphViewModel + +@Composable +fun ToolBox(graphVM: GraphViewModel, currentScale: MutableState) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Bottom + ) { + FloatingActionButton( + onClick = { + currentScale.value = (scaleFactor * currentScale.value).coerceIn(0.7f, 1.9f) + }, + modifier = Modifier.padding(horizontal = 11.dp) + ) { + Text("+") + } + Spacer(modifier = Modifier.height(8.dp)) + FloatingActionButton(onClick = { + currentScale.value = (currentScale.value / scaleFactor).coerceIn(0.7f, 1.9f) + }, + modifier = Modifier.padding(horizontal = 11.dp) + ) { + Text("-") + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + modifier = Modifier + .width(80.dp) + .height(50.dp) + .clip(shape = RoundedCornerShape(25.dp)), + onClick = { + graphVM.clearGraph() + } + ) { + Text("Clear") + } + } + } +} diff --git a/app/src/main/kotlin/view/utils/ZoomBox.kt b/app/src/main/kotlin/view/utils/ZoomBox.kt deleted file mode 100644 index 601c6cae..00000000 --- a/app/src/main/kotlin/view/utils/ZoomBox.kt +++ /dev/null @@ -1,38 +0,0 @@ -package view.utils - -import androidx.compose.foundation.layout.* -import androidx.compose.material.FloatingActionButton -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import scaleFactor - -@Composable -fun ZoomBox(currentScale: MutableState) { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Bottom - ) { - FloatingActionButton(onClick = { - currentScale.value = (scaleFactor * currentScale.value).coerceIn(0.7f, 1.9f) - }) { - Text("+") - } - Spacer(modifier = Modifier.height(8.dp)) - FloatingActionButton(onClick = { - currentScale.value = (currentScale.value / scaleFactor).coerceIn(0.7f, 1.9f) - }) { - Text("-") - } - } - } -} diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index 2c3684e0..e1154689 100644 --- a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -5,6 +5,8 @@ import androidx.compose.ui.unit.Dp import ARROW_DEPTH import ARROW_SIZE import SQRT_3 +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color import kotlin.math.sqrt class EdgeViewModel( @@ -17,6 +19,8 @@ class EdgeViewModel( private val radius = firstVertex.radius + var highlightColor = mutableStateOf(Color.LightGray) + internal fun calculateFirstVertexCenter(scale: Float): Pair { val x = firstVertex.x.value + radius * scale val y = firstVertex.y.value + radius * scale diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 436e9d66..8878f4f1 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -1,11 +1,11 @@ package viewmodel.graph -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import model.graphs.abstractGraph.Edge -import model.graphs.abstractGraph.Graph -import model.graphs.abstractGraph.Vertex +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import model.graphs.* +import model.graphs.abstractGraph.* +import model.algorithms.* +import model.algorithms.clustering.CommunitiesFinder class GraphViewModel( val currentGraph: Graph, @@ -14,12 +14,16 @@ class GraphViewModel( val graphType: MutableState, private val isDirected: State ) { - val updateIsRequired = mutableStateOf(false) var _verticesViewModels = mutableMapOf, VertexViewModel>() var _edgeViewModels = mutableMapOf, EdgeViewModel>() + val verticesVM: List> get() = _verticesViewModels.values.toList() + val edgesVM: List> get() = _edgeViewModels.values.toList() + + val graph: Graph get() = currentGraph + fun updateEdgeViewModels(edge: Edge) { val firstVertex: VertexViewModel = _verticesViewModels[edge.vertex1] @@ -53,15 +57,13 @@ class GraphViewModel( } fun addEdge(firstId: Int, secondId: Int, weight: Int = 1) { - val firstVertex = graph.getVertices().find { it.id == firstId } - ?: throw NoSuchElementException("No vertex found with id $firstId") - val secondVertex = graph.getVertices().find { it.id == secondId } - ?: throw NoSuchElementException("No vertex found with id $secondId") + val firstVertex = graph.getVertices()[firstId] + val secondVertex = graph.getVertices()[secondId] val firstVertexVM = _verticesViewModels[firstVertex] - ?: throw NoSuchElementException("No ViewModel found for vertex1") + ?: throw NoSuchElementException("No ViewModel found for vertex (${firstVertex.id}, ${firstVertex.data})") val secondVertexVM = _verticesViewModels[secondVertex] - ?: throw NoSuchElementException("No ViewModel found for vertex2") + ?: throw NoSuchElementException("No ViewModel found for vertex (${secondVertex.id}, ${secondVertex.data})") val newEdge = graph.addEdge(firstVertexVM.vertex, secondVertexVM.vertex) graph.getWeightMap()[newEdge] = weight @@ -78,8 +80,204 @@ class GraphViewModel( layout.randomize(width, height, verticesVM) } - val verticesVM: List> get() = _verticesViewModels.values.toList() - val edgesVM: List> get() = _edgeViewModels.values.toList() + fun findCommunities(): Boolean { + val communitiesFinder = CommunitiesFinder() + val communities = communitiesFinder.findCommunities(graph) + if (communities.isEmpty()) return false - val graph: Graph get() = currentGraph + return highlightVerticesSets(communities) + } + + fun findKeyVertices(): Boolean { + val keyVerticesFinder = KeyVerticesFinder() + val keyVertices = keyVerticesFinder.findKeyVertices(graph) + if (keyVertices?.isEmpty() ?: false) return false + + return highlightVertices(keyVertices) + } + + fun findBridges(): Boolean { + val bridgesFinder = BridgesFinder() + if (graph is UndirectedGraph) { + val bridges = bridgesFinder.findBridges(graph as UndirectedGraph) + if (bridges.isEmpty()) return false + + return highlightEdges(bridges.toSet()) + } + + return false + } + + var cycles: List, Vertex>>>? = null + var currentCycleIndex = 0 + + fun findCycles(srcVertexId: Int): Boolean { + val cyclesFinder = CyclesFinder() + if (graph is DirectedGraph) { + val foundCycles = cyclesFinder.findCycles(graph as DirectedGraph, graph.getVertices()[srcVertexId]) + if (foundCycles.isEmpty()) return false + + cycles = foundCycles.toList() + + currentCycleIndex = 0 + if (cycles != null) return true + } + + return false + } + + fun findMinSpanningTree(): Boolean { + val minSpanningTreeFinder = MinSpanningTreeFinder() + if (graph is WeightedUndirectedGraph) { + val minSpanningTree = minSpanningTreeFinder.findMinSpanningTree(graph as WeightedUndirectedGraph) + if (minSpanningTree.isEmpty()) return false + + return highlightEdges(minSpanningTree.toSet()) + } + + return false + } + + fun findSCCs(): Boolean { + val SCCFinder = SCCFinder() + if (graph is DirectedGraph) { + val SCCs = SCCFinder.findSCC(graph as DirectedGraph) + if (SCCs.isEmpty()) return false + + return highlightVerticesSets(SCCs) + } + + return false + } + + fun findShortestPath(srcVertexId: Int, destVertexId: Int): Boolean { + val shortestPathFinder = ShortestPathFinder() + + val src = graph.getVertices()[srcVertexId] + val dest = graph.getVertices()[destVertexId] + + if (graph is WeightedDirectedGraph) { + val shortestPath = shortestPathFinder.findShortestPath(graph as WeightedDirectedGraph, src, dest) + if (shortestPath?.isEmpty() ?: false) return false + + return highlightPath(shortestPath) + } + else if (graph is WeightedUndirectedGraph) { + val shortestPath = shortestPathFinder.findShortestPath(graph as WeightedUndirectedGraph, src, dest) + if (shortestPath?.isEmpty() ?: false) return false + + return highlightPath(shortestPath) + } + + return false + } + + private fun highlightVertices(verticesSet: Set>?): Boolean { + if (verticesSet == null) return false + + clearGraph() + + for (vertex in graph.getVertices()) { + if (vertex in verticesSet) { + _verticesViewModels[vertex]?.highlightColor?.value = Color.Black + } else { + _verticesViewModels[vertex]?.highlightColor?.value = Color.LightGray + } + } + + return true + } + + private fun highlightEdges(edgesSet: Set>?): Boolean { + if (edgesSet == null) return false + + clearGraph() + + for (edge in graph.getEdges()) { + if (edge in edgesSet) { + _edgeViewModels[edge]?.highlightColor?.value = Color.Black + } else { + _edgeViewModels[edge]?.highlightColor?.value = Color.LightGray + } + } + + return true + } + + private fun highlightVerticesSets(verticesSets: Set>>?): Boolean { + if (verticesSets == null) return false + + clearGraph() + + val colors = arrayOf( + Color.Red, + Color.Blue, + Color.Green, + Color.Yellow, + Color.Cyan, + Color.Magenta, + Color.Black, + Color.White, + Color.DarkGray, + Color(0xebab34), + Color(0xaeeb34), + Color(0x5e34eb), + Color(0x8334eb), + Color(0xd834eb), + Color(0xeb34a1), + ) + + var i = 0 + for (verticesSet in verticesSets) { + val currentColor = colors[i] + for (vertex in verticesSet) { + _verticesViewModels[vertex]?.highlightColor?.value = currentColor + } + + i++ + if (i > colors.size - 1) throw ArrayIndexOutOfBoundsException("Only 15 colors supported") + } + + return true + } + + private fun highlightPath(path: List, Vertex>>?): Boolean { + if (path == null) return false + + clearGraph() + + val srcVertex = path.first().first.vertex1 + _verticesViewModels[srcVertex]?.highlightColor?.value = Color.Black + + for (pair in path) { + val edge = pair.first + val vertex = pair.second + + _edgeViewModels[edge]?.highlightColor?.value = Color.Black + _verticesViewModels[vertex]?.highlightColor?.value = Color.Black + } + + return true + } + + fun highlighNextCycle(): Boolean { + var returnValue = false + returnValue = highlightPath(cycles?.get(currentCycleIndex)) + + val size = cycles?.size + ?: return false + + if (++currentCycleIndex > size - 1) currentCycleIndex = 0 + + return returnValue + } + + fun clearGraph() { + for (vertex in graph.getVertices()) { + _verticesViewModels[vertex]?.highlightColor?.value = Color.LightGray + } + for (edge in graph.getEdges()) { + _edgeViewModels[edge]?.highlightColor?.value = Color.LightGray + } + } } diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index cf1017aa..c8f4bfeb 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -1,14 +1,13 @@ package viewmodel.graph -import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import model.graphs.abstractGraph.Vertex -import viewmodel.WindowViewModel class VertexViewModel( var x: MutableState = mutableStateOf(0.dp), @@ -18,6 +17,8 @@ class VertexViewModel( val vertex: Vertex, val radius: Dp = 20.dp, ) { + var highlightColor = mutableStateOf(Color.LightGray) + var isSelected = mutableStateOf(false) val getVertexData diff --git a/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt b/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt index 87e2c124..8d3f2e25 100644 --- a/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt +++ b/app/src/test/kotlin/model/algorithms/clustering/CommunitiesFinderTest.kt @@ -13,7 +13,7 @@ class CommunitiesFinderTest { fun `graph of 1 vertex should have one community`(graph: Graph) { val v0 = graph.addVertex(0) - val actualValue = louvain.findCommunity(graph) + val actualValue = louvain.findCommunities(graph) val expectedValue = setOf(setOf(v0)) assertEquals(expectedValue, actualValue) @@ -21,7 +21,7 @@ class CommunitiesFinderTest { @TestAllGraphTypes fun `empty graph should have no communities`(graph: Graph) { - val actualValue = louvain.findCommunity(graph) + val actualValue = louvain.findCommunities(graph) val expectedValue = emptySet>>() assertEquals(expectedValue, actualValue) @@ -31,7 +31,7 @@ class CommunitiesFinderTest { fun `graph doesn't change`(graph: Graph) { val graphStructure = setupAbstractGraph(graph) - louvain.findCommunity(graph) + louvain.findCommunities(graph) val actualGraph = graph.getVertices() to graph.getEdges().toSet() val expectedGraph = graphStructure From 59de5077688e7632716aca35fdf5f910b33c89a4 Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Fri, 31 May 2024 00:02:56 +0300 Subject: [PATCH 73/77] feat: add authorization and saving to Neo4j #57 * feat: make gradle cleaner * feat: add vertex and edge saving to neo4j * feat: add function to save graph with name to neo4j * feat: add function to clear graph from neo4j, use it in save function * feat: add graph import by name from neo4j * feat: move loaded graph initialization to another function * refactor: remove unneeded exception throw * fix: correct imports * fix: first added vertex containing empty data * fix: no more crash when entering empty id of vertex to connect with * feat: remove independent id's and class generic from Neo4jRepository class * feat: add message when graph was or was not saved to neo4j * fix: bug with fake login into neo4j * fix: error window appearing only once at a lifetime * feat: add neo4j repo handler object to operate with repository * fix: bug with making graph names empty just before save, load or edit * feat: add neo4j load dialog window * feat: remove all neo4j-related stuff from MainScreenVM --- app/build.gradle.kts | 32 ++- .../kotlin/model/io/neo4j/Neo4jRepository.kt | 182 ++++++++++++++++++ .../model/io/neo4j/Neo4jRepositoryHandler.kt | 84 ++++++++ .../kotlin/model/io/sql/SQLDatabaseModule.kt | 3 +- .../kotlin/view/tabScreen/FileControlTab.kt | 65 ++++++- .../main/kotlin/view/tabScreen/GeneralTab.kt | 33 ++-- app/src/main/kotlin/view/utils/ErrorWindow.kt | 48 +++-- .../view/utils/ImportGraphDialogWindow.kt | 2 - .../utils/Neo4jImportGraphDialogWindow.kt | 104 ++++++++++ .../kotlin/view/utils/Neo4jLoginDialog.kt | 155 +++++++++++++++ .../kotlin/viewmodel/MainScreenViewModel.kt | 17 +- .../kotlin/viewmodel/graph/GraphViewModel.kt | 11 +- gradle.properties | 8 +- settings.gradle.kts | 28 +-- 14 files changed, 688 insertions(+), 84 deletions(-) create mode 100644 app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt create mode 100644 app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt create mode 100644 app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt create mode 100644 app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0456a5ee..9a2c1c79 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,12 @@ +val kotlinxCoroutinesVersion: String by project +val neo4jDriverVersion: String by project +val composeVersion: String by project +val junitVersion: String by project +val koinVersion: String by project + plugins { - kotlin("jvm") version "1.9.22" - id("org.jetbrains.compose") version "1.6.2" + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.compose) } repositories { @@ -11,24 +17,30 @@ repositories { dependencies { implementation(compose.desktop.currentOs) - implementation("io.insert-koin:koin-core:3.4.2") + testImplementation("org.jetbrains.compose.ui:ui-test-junit4:$composeVersion") + + implementation("io.insert-koin:koin-core:$koinVersion") + implementation(libs.koin.core) - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$kotlinxCoroutinesVersion") implementation("org.xerial:sqlite-jdbc:3.41.2.2") implementation("org.slf4j:slf4j-api:1.7.36") implementation("ch.qos.logback:logback-classic:1.4.12") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3") + testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") + testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") - testImplementation("org.jetbrains.compose.ui:ui-test-junit4:1.6.2") + implementation("org.neo4j.driver:neo4j-java-driver:$neo4jDriverVersion") } - compose.desktop { application { mainClass = "MainKt" } } + +tasks.test { + useJUnitPlatform() +} diff --git a/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt b/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt new file mode 100644 index 00000000..a0950dfb --- /dev/null +++ b/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt @@ -0,0 +1,182 @@ +package model.io.neo4j + +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import org.neo4j.driver.* +import java.io.Closeable + +const val DIR_LABEL = "POINTS_TO" +const val UNDIR_LABEL = "CONNECTED_TO" + +class Neo4jRepository(uri: String, user: String, password: String) : Closeable { + private val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + private val session = driver.session() + + fun getGraphNames(): List { + val result = session.executeRead { tx -> + tx.run( + "MATCH (v) " + + "RETURN " + + "distinct labels(v) AS label" + ).list() + } + println(result) + + val names = mutableListOf() + for (record in result) { + val name = record["label"].toString().drop(2).dropLast(2) + names.add(name) + } + + return names + } + + fun saveOrReplaceGraph(graph: Graph, name: String, isDirected: Boolean, isWeighted: Boolean) { + clearGraph(name) + + val vertices = graph.getVertices() + val edges = graph.getEdges() + + val weightMap = if (isWeighted) graph.getWeightMap() else null + val edgeLabel = if (isDirected) DIR_LABEL else UNDIR_LABEL + + session.executeWrite { tx -> + for (vertex in vertices) { + val id = vertex.id + + val data = vertex.data.toString() + + tx.run( + "CREATE (:$name {id:$id, data:$data}) " + ) + } + + for (edge in edges) { + val v1 = edge.vertex1 + val v2 = edge.vertex2 + + val id1 = v1.id + val id2 = v2.id + + val data1 = v1.data.toString() + val data2 = v2.data.toString() + + val weight = weightMap?.get(edge) + + tx.run( + "MATCH (v:$name {id:$id1, data:$data1}) " + + "MATCH (u:$name {id:$id2, data:$data2}) " + + "CREATE (v)-[:$edgeLabel {weight:$weight}]->(u) " + ) + } + } + } + + private fun clearGraph(name: String) { + session.executeWrite { tx -> + tx.run( + "MATCH (v:$name)-[e]->(u:$name) " + + "DELETE v, e, u " + ) + } + } + + fun loadGraph(name: String): Graph { + val graphContents = readGraphContents(name) + + val isDirected = hasDirection(graphContents[0]) + val isWeighted = hasWeight(graphContents[0]) + + val graph = initializeGraph(isWeighted, isDirected) + + val graphSize = getStoredGraphSize(name) + + val wasVertexLoaded = MutableList(graphSize) { false } + val addedVerticesList = MutableList(graphSize) { Vertex(-1, "-1") } + + for (content in graphContents) { + val id1 = content["id1"].asInt() + val data1 = content["data1"].toString() + + val id2 = content["id2"].asInt() + val data2 = content["data2"].toString() + + val weightString = content["weight"].toString() + val edgeWeight = if (weightString != "NULL") weightString.toInt() else null + + if (!wasVertexLoaded[id1]) { + wasVertexLoaded[id1] = true + addedVerticesList[id1] = graph.addVertex(data1) + } + if (!wasVertexLoaded[id2]) { + wasVertexLoaded[id2] = true + addedVerticesList[id2] = graph.addVertex(data2) + } + + val v1 = addedVerticesList[id1] + val v2 = addedVerticesList[id2] + + if (edgeWeight == null) { + graph.addEdge(v1, v2) + } else if (isDirected) { + graph as WeightedDirectedGraph + graph.addEdge(v1, v2, edgeWeight) + } else { + graph as WeightedUndirectedGraph + graph.addEdge(v1, v2, edgeWeight) + } + } + + return graph + } + + private fun initializeGraph(isWeighted: Boolean, isDirected: Boolean): Graph { + return if (isWeighted && isDirected) WeightedDirectedGraph() + else if (isWeighted) WeightedUndirectedGraph() + else if (isDirected) DirectedGraph() + else UndirectedGraph() + } + + private fun readGraphContents(name: String) = session.executeRead { tx -> + tx.run( + "MATCH (v:$name)-[e]->(u:$name) " + + "RETURN " + + "v.id AS id1, " + + "v.data AS data1, " + + "u.id AS id2, " + + "u.data AS data2, " + + "e.weight AS weight, " + + "type(e) AS relationType " + ).list() + } + + private fun getStoredGraphSize(name: String): Int { + val result = session.executeRead { tx -> + tx.run( + "MATCH (v:$name) " + + "RETURN count(v) AS size " + ).list() + } + + val size = result[0]["size"].asInt() + + return size + } + + private fun hasWeight(graphRecord: Record): Boolean { + return graphRecord["weight"].toString() != "NULL" + } + + private fun hasDirection(graphRecord: Record): Boolean { + return graphRecord["relationType"].asString() == DIR_LABEL + } + + override fun close() { + session.close() + driver.close() + } +} diff --git a/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt b/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt new file mode 100644 index 00000000..931e7a9e --- /dev/null +++ b/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt @@ -0,0 +1,84 @@ +package model.io.neo4j + +import model.graphs.abstractGraph.Graph +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase + +object Neo4jRepositoryHandler { + var neo4jRepo: Neo4jRepository? = null + private set + + var isRepoInit = false + private set + + fun initRepo(uri: String, user: String, password: String): Boolean { + if (isRepoInit) return true + if (!checkIfCredentialsAreValid(uri, user, password)) return false + + neo4jRepo = Neo4jRepository(uri, user, password) + isRepoInit = true + + return true + } + + fun getNames(): List? { + if (!isRepoInit) return null + + val names = neo4jRepo?.getGraphNames() + + return names + } + + fun saveOrReplace(graph: Graph, name: String, isDirected: Boolean, isWeighted: Boolean): Boolean { + if (!isRepoInit || !isValidNeo4jName(name)) return false + + neo4jRepo?.saveOrReplaceGraph(graph, name, isDirected, isWeighted) + + return true + } + + fun loadGraph(name: String): Graph? { + if (!isRepoInit || !isValidNeo4jName(name)) return null + + val graph = neo4jRepo?.loadGraph(name) + + return graph + } + + fun isValidNeo4jName(name: String): Boolean { + if (name.isEmpty()) return false + for (i in name.indices) { + val isValidChar = when (name[i].code) { + 45 -> { // - + if (i!= 0) true else false + } + in 48..57 -> { // 0-9 + if (i != 0) true else false + } + in 65..90 -> true // A-Z + 95 -> true // _ + in 97..122 -> true // a-z + else -> false + } + + if (isValidChar) continue else return false + } + + return true + } + + private fun checkIfCredentialsAreValid(uri: String, user: String, password: String): Boolean { + try { + val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + driver.session().executeWrite { tx -> + tx.run { + "MATCH (v) RETURN v LIMIT 1" + } + } + } catch (e: Exception) { + return false + } + + return true + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt index c41582c2..60ce2943 100644 --- a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt +++ b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt @@ -228,7 +228,8 @@ object SQLDatabaseModule { mutableStateOf(false), mutableStateOf(false), graphVMType, - mutableStateOf(currentGraphSetup.first.second.toString().contains("Directed")) + mutableStateOf(currentGraphSetup.first.second.toString().contains("Directed")), + mutableStateOf(currentGraphSetup.first.second.toString().contains("Weighted")) ) // Fetch vertices diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index 8b781894..033fffc0 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -18,10 +18,10 @@ import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Dialog import kotlinx.coroutines.delay +import model.io.neo4j.Neo4jRepositoryHandler import model.io.sql.SQLDatabaseModule -import view.utils.EditDBWindow -import view.utils.ErrorWindow -import view.utils.ImportGraphDialogWindow +import view.utils.* +import viewmodel.MainScreenViewModel import java.awt.FileDialog import java.awt.Frame @@ -31,10 +31,10 @@ fun FileControlTab(graphVM: GraphViewModel) { var showSaveDialog by remember { mutableStateOf(false) } var showLoadDialog by remember { mutableStateOf(false) } var graphName by remember { mutableStateOf("") } - var showEnterPathField by remember { mutableStateOf(false) } var showErrorWindow by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf("") } var showEditDialog by remember { mutableStateOf(false) } + var showNeo4jDialog by remember { mutableStateOf(false) } val databases = arrayOf("SQLite", "Neo4j", "JSON") var selectedDatabase by remember { mutableStateOf(databases[0]) } @@ -85,6 +85,7 @@ fun FileControlTab(graphVM: GraphViewModel) { modifier = Modifier.height(rowHeight).padding(borderPadding), horizontalArrangement = Arrangement.spacedBy(horizontalGap) ) { + var expanded by remember { mutableStateOf(false) } ExposedDropdownMenuBox( @@ -96,7 +97,7 @@ fun FileControlTab(graphVM: GraphViewModel) { ) { TextField( value = selectedDatabase, - onValueChange = {}, + onValueChange = { graphName = it }, readOnly = true, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, modifier = Modifier, @@ -138,7 +139,6 @@ fun FileControlTab(graphVM: GraphViewModel) { Text("Select File") } } - } Row( @@ -161,7 +161,6 @@ fun FileControlTab(graphVM: GraphViewModel) { modifier = Modifier.fillMaxSize().height(fieldHeight), onClick = { showLoadDialog = true - graphName = "" }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { @@ -178,7 +177,6 @@ fun FileControlTab(graphVM: GraphViewModel) { modifier = Modifier.fillMaxSize().height(fieldHeight), onClick = { showEditDialog = true - graphName = "" }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { @@ -211,7 +209,41 @@ fun FileControlTab(graphVM: GraphViewModel) { .background(Color.White) .padding(16.dp) .width(300.dp) - .height(50.dp) + .height(100.dp) + ) { + Text("Graph '$graphName' saved successfully!") + } + } + + // Automatically dismiss the dialog after 3 seconds + LaunchedEffect(Unit) { + delay(3000) + showSaveDialog = false + } + } + } else if (selectedDatabase == "Neo4j") { + if (!Neo4jRepositoryHandler.isRepoInit) { + showSaveDialog = false + showNeo4jDialog = true + } else if (!Neo4jRepositoryHandler.isValidNeo4jName(graphName)) { + showSaveDialog = false + showErrorWindow = true + errorMessage = "$graphName is an invalid name." + graphName = "" + } else { + Neo4jRepositoryHandler.saveOrReplace(graphVM.graph, graphName, graphVM.isDirected.value, graphVM.isWeighted.value) + + Dialog( + onDismissRequest = { + showSaveDialog = false + } + ) { + Column( + modifier = Modifier + .background(Color.White) + .padding(16.dp) + .width(300.dp) + .height(100.dp) ) { Text("Graph '$graphName' saved successfully!") } @@ -227,13 +259,26 @@ fun FileControlTab(graphVM: GraphViewModel) { } if (showLoadDialog) { - ImportGraphDialogWindow() // TODO + when (selectedDatabase) { + "SQLite" -> ImportGraphDialogWindow() + "Neo4j" -> { + if (!Neo4jRepositoryHandler.isRepoInit) { + showLoadDialog = false + showNeo4jDialog = true + } + Neo4jImportGraphDialogWindow { showLoadDialog = false } + } + } } if (showEditDialog) { EditDBWindow(selectedDatabase) { showEditDialog = false } } + if (showNeo4jDialog) { + Neo4jLoginDialog { showNeo4jDialog = false } + } + if (showErrorWindow) { ErrorWindow( errorMessage diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index 6c26f2d3..a87f8384 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -63,10 +63,7 @@ fun GeneralTab(graphVM: GraphViewModel) { onClick = { if (vertexData.isNotEmpty()) showVertexAddDialog = true }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text( - "add\nvertex", - textAlign = TextAlign.Center - ) + Text("add") } } } @@ -134,10 +131,7 @@ fun GeneralTab(graphVM: GraphViewModel) { }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) ) { - Text( - "add\nedge", - textAlign = TextAlign.Center - ) + Text("add") } } } @@ -166,7 +160,11 @@ fun GeneralTab(graphVM: GraphViewModel) { } if (showVertexAddDialog) { - Dialog(onDismissRequest = {}) { + Dialog( + onDismissRequest = { + showVertexAddDialog = false + } + ) { Column( modifier = Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) @@ -205,10 +203,9 @@ fun GeneralTab(graphVM: GraphViewModel) { showVertexAddDialog = false errorMessage = "" - secondVertexData = "" vertexData = "" + secondVertexData = "" } - } ) { Text("Connect") @@ -237,16 +234,14 @@ fun GeneralTab(graphVM: GraphViewModel) { onClick = { connectVertexId = connectVertexId.replace("\n", "") - if (!connectVertexId.all { char -> char.isDigit() }) { + if (connectVertexId.isBlank()) { + errorMessage = "Please enter an ID" + } else if (connectVertexId.toIntOrNull() == null) { + errorMessage = "ID must be an integer" + } else if (!connectVertexId.all { char -> char.isDigit() }) { errorMessage = "ID should be a numeric" } else if (!graphVM.checkVertexById(connectVertexId.toInt())) { errorMessage = "There isn't a Vertex with such ID" - } else if (connectVertexId.isBlank()) { - errorMessage = "Please enter an ID" - } else if ( - connectVertexId.isNotBlank() && connectVertexId.toIntOrNull() == null - ) { - errorMessage = "ID must be an integer" } else { val firstId = graphVM.addVertex(vertexData) graphVM.addEdge(firstId, connectVertexId.toInt()) @@ -269,4 +264,4 @@ fun GeneralTab(graphVM: GraphViewModel) { if (showErrorWindow.value) { ErrorWindow("No such Vertex", { showErrorWindow.value = false }) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/view/utils/ErrorWindow.kt b/app/src/main/kotlin/view/utils/ErrorWindow.kt index 311ad625..d652de71 100644 --- a/app/src/main/kotlin/view/utils/ErrorWindow.kt +++ b/app/src/main/kotlin/view/utils/ErrorWindow.kt @@ -17,35 +17,31 @@ import androidx.compose.ui.window.DialogProperties @Composable fun ErrorWindow(message: String, onDismiss: () -> Unit) { - val closeDialog = remember { mutableStateOf(false) } - - if (!closeDialog.value) { - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties(usePlatformDefaultWidth = false) + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + modifier = Modifier.size(300.dp, 180.dp), + shape = MaterialTheme.shapes.medium, + elevation = 24.dp ) { - Surface( - modifier = Modifier.size(300.dp, 180.dp), - shape = MaterialTheme.shapes.medium, - elevation = 24.dp - ) { - Row(modifier = Modifier.height(100.dp).fillMaxWidth().padding(16.dp)) { - Column(modifier = Modifier, horizontalAlignment = Alignment.Start) { - Text(text = "Error", style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold) - } + Row(modifier = Modifier.height(100.dp).fillMaxWidth().padding(16.dp)) { + Column(modifier = Modifier, horizontalAlignment = Alignment.Start) { + Text(text = "Error", style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold) } - Row(modifier = Modifier.fillMaxWidth().padding(top = 32.dp)) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.SpaceBetween - ) { + } + Row(modifier = Modifier.fillMaxWidth().padding(top = 32.dp)) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween + ) { - Text(text = message) - Spacer(modifier = Modifier.height(30.dp)) - Button(onClick = { closeDialog.value = true }, modifier = Modifier.align(Alignment.End)) { - Text("Ok") - } + Text(text = message) + Spacer(modifier = Modifier.height(30.dp)) + Button(onClick = { onDismiss() }, modifier = Modifier.align(Alignment.End).width(100.dp)) { + Text("ok") } } } diff --git a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt index 6c6639ac..52bda2c6 100644 --- a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt @@ -15,8 +15,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import viewmodel.graph.GraphViewModel - @OptIn(ExperimentalMaterialApi::class) @Composable diff --git a/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt new file mode 100644 index 00000000..3d5f91a0 --- /dev/null +++ b/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt @@ -0,0 +1,104 @@ +package view.utils + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import model.io.neo4j.Neo4jRepositoryHandler +import model.io.sql.SQLDatabaseModule + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun Neo4jImportGraphDialogWindow(onDismiss: () -> Unit) { + var expanded by remember { mutableStateOf(false) } + var importFromDBRequired by remember { mutableStateOf(false) } + var selectedGraphName by remember { mutableStateOf("") } + val graphNames = Neo4jRepositoryHandler.getNames() + + Dialog( + onDismissRequest = { + onDismiss() + }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = Modifier.background(Color.White) + .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp) + ) { + Text( + "Select the graph:", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + TextField( + value = selectedGraphName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + println(graphNames) + graphNames?.forEach { name -> + DropdownMenuItem( + onClick = { + selectedGraphName = name + expanded = false + } + ) { + Text(text = name) + } + } + } + } + } + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + importFromDBRequired = true + expanded = false + onDismiss() + } + ) { + Text("Import", color = Color.White) + } + } + } + } + + if (importFromDBRequired) { +// return SQLDatabaseModule.importGraph(selectedGraphID.value) + Neo4jRepositoryHandler.loadGraph(selectedGraphName) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt b/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt new file mode 100644 index 00000000..f81c35ee --- /dev/null +++ b/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt @@ -0,0 +1,155 @@ +package view.utils + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.delay +import model.io.neo4j.Neo4jRepositoryHandler +import viewmodel.MainScreenViewModel + +@Composable +fun Neo4jLoginDialog(onDismiss: () -> Unit) { + var uriInput by remember { mutableStateOf("") } + var userInput by remember { mutableStateOf("") } + var passwordInput by remember { mutableStateOf("") } + + var errorMessage by remember { mutableStateOf("") } + var isLoginSuccessful by remember { mutableStateOf(false) } + + val mainFontSize = 20.sp + val secondaryFontSize = 15.sp + + Dialog( + onDismissRequest = onDismiss + ) { + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(360.dp) + ) { + Text( + text = "Please login to Neo4j AuraDB to use Neo4j:", + fontSize = mainFontSize + ) + + val height = 60.dp + + Spacer(modifier = Modifier.height(15.dp)) + + TextField( + value = uriInput, + onValueChange = { + uriInput = it + errorMessage = "" + }, + modifier = Modifier.fillMaxWidth().height(height), + label = { + Text( + "Uri", + style = MaterialTheme.typography.body1.copy(fontSize = secondaryFontSize), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = Color.White + ) + ) + TextField( + value = userInput, + onValueChange = { + userInput = it + errorMessage = "" + }, + modifier = Modifier.fillMaxWidth().height(height), + label = { + Text( + "Name", + style = MaterialTheme.typography.body1.copy(fontSize = secondaryFontSize), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = Color.White + ) + ) + TextField( + value = passwordInput, + onValueChange = { + passwordInput = it + errorMessage = "" + }, + modifier = Modifier.fillMaxWidth().height(height), + label = { + Text( + "Password", + style = MaterialTheme.typography.body1.copy(fontSize = secondaryFontSize), + color = Color.Gray + ) + }, + colors = + TextFieldDefaults.textFieldColors( + backgroundColor = Color.White + ) + ) + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(30.dp)) + + Button( + modifier = Modifier + .width(250.dp) + .height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + if (Neo4jRepositoryHandler.initRepo(uriInput, userInput, passwordInput)) { + isLoginSuccessful = true + } else { + errorMessage = "Sorry, but this input is invalid." + } + } + ) { + Text("Login", color = Color.White) + } + + if (errorMessage.isNotBlank()) { + Text( + text = errorMessage, + color = Color.Red, + modifier = Modifier.padding(top = 8.dp) + ) + + uriInput = "" + userInput = "" + passwordInput = "" + } + if (isLoginSuccessful) { + Text( + text = "Logged in successfully!", + color = MaterialTheme.colors.primary, + modifier = Modifier.padding(top = 8.dp) + ) + LaunchedEffect(Unit) { + + delay(1500) + onDismiss() + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index e03fd0a3..5e134f45 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -2,7 +2,11 @@ package viewmodel import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import model.graphs.abstractGraph.Graph +import model.io.neo4j.Neo4jRepository +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase import viewmodel.graph.GraphViewModel class MainScreenViewModel( @@ -18,6 +22,17 @@ class MainScreenViewModel( return mutableStateOf(currentGraphType.contains("Directed")) } + fun setWeightinessState(currentGraphType: String): MutableState { + return mutableStateOf(currentGraphType.contains("Weighted")) + } + var graphViewModel: GraphViewModel = existingGraphViewModel - ?: GraphViewModel(graph, showVerticesIds, showVerticesData, graphType, setDirectionState(currentGraphType)) + ?: GraphViewModel( + graph, + showVerticesIds, + showVerticesData, + graphType, + setDirectionState(currentGraphType), + setWeightinessState(currentGraphType) + ) } diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index 8878f4f1..abda5039 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -1,5 +1,12 @@ package viewmodel.graph +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import model.io.neo4j.Neo4jRepository import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import model.graphs.* @@ -12,8 +19,10 @@ class GraphViewModel( private val showVerticesData: State, var showVerticesID: MutableState, val graphType: MutableState, - private val isDirected: State + val isDirected: MutableState, + val isWeighted: MutableState ) { + val updateIsRequired = mutableStateOf(false) var _verticesViewModels = mutableMapOf, VertexViewModel>() diff --git a/gradle.properties b/gradle.properties index 27c0bcb5..82aa302e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,8 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=official -kotlin.version=1.9.22 -compose.version=1.6.2 +kotlinVersion=1.9.22 +composeVersion=1.6.2 +junitVersion=5.8.1 +neo4jDriverVersion=5.6.0 +koinVersion=3.5.3 +kotlinxCoroutinesVersion=1.7.3 diff --git a/settings.gradle.kts b/settings.gradle.kts index d0815cf5..32b11109 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,12 +1,14 @@ -rootProject.name = "graphs-2" -include("app") - +val neo4jDriverVersion: String by settings +val composeVersion: String by settings +val kotlinVersion: String by settings +val junitVersion: String by settings +val koinVersion: String by settings pluginManagement { - repositories { - gradlePluginPortal() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - } + repositories { + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } } dependencyResolutionManagement { @@ -17,11 +19,13 @@ dependencyResolutionManagement { } versionCatalogs { create("libs") { - version("kotlin", "1.9.22") - plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").versionRef("kotlin") - plugin("compose", "org.jetbrains.compose").version("1.6.2") - library("koin-core", "io.insert-koin:koin-core:3.5.3") - library("junit-jupiter", "org.junit.jupiter:junit-jupiter:5.10.2") + plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").version(kotlinVersion) + plugin("compose", "org.jetbrains.compose").version(composeVersion) + library("koin-core", "io.insert-koin:koin-core:$koinVersion") + library("junit-jupiter", "org.junit.jupiter:junit-jupiter:$junitVersion") } } } + +rootProject.name = "graphs-2" +include("app") From f27c62a761625fc6cb326f5cad78d483401d1d17 Mon Sep 17 00:00:00 2001 From: Daniel Vlasenco Date: Sat, 1 Jun 2024 09:54:03 +0300 Subject: [PATCH 74/77] feat: improve on-launch UI, enhance operating with Neo4j #58 * feat: add function to save graph with name to neo4j * feat: add function to clear graph from neo4j, use it in save function * fix: resolve merge conflict * fix: no more crash when entering empty id of vertex to connect with * feat: remove independent id's and class generic from Neo4jRepository class * feat: add message when graph was or was not saved to neo4j * feat: add neo4j repo handler object to operate with repository * fix: bug with making graph names empty just before save, load or edit * feat: add neo4j load dialog window * feat: add on-start import window * feat: apply theme colors to create and import dialog windows * feat: introduce constants for database names * feat: change neo4j repo loadGraph() return value, refactor code * feat: change on-start import message * feat: make Neo4jLoginDialog() composable * refactor: add newlines to ends of some files --- app/src/main/kotlin/Constants.kt | 6 +- .../kotlin/model/io/neo4j/Neo4jRepository.kt | 9 +- .../model/io/neo4j/Neo4jRepositoryHandler.kt | 9 +- .../kotlin/view/tabScreen/FileControlTab.kt | 44 ++- .../view/utils/CreateGraphDialogWindow.kt | 267 +++++++++--------- .../main/kotlin/view/utils/EditDBWindow.kt | 5 +- .../view/utils/ImportGraphDialogWindow.kt | 157 +++++----- .../utils/Neo4jImportGraphDialogWindow.kt | 127 +++++---- .../kotlin/view/utils/Neo4jLoginDialog.kt | 6 +- .../utils/SQLiteImportGraphDialogWindow.kt | 108 +++++++ .../view/utils/SelectInitDialogWindow.kt | 91 +++--- 11 files changed, 490 insertions(+), 339 deletions(-) create mode 100644 app/src/main/kotlin/view/utils/SQLiteImportGraphDialogWindow.kt diff --git a/app/src/main/kotlin/Constants.kt b/app/src/main/kotlin/Constants.kt index 8d8d4850..363e4742 100644 --- a/app/src/main/kotlin/Constants.kt +++ b/app/src/main/kotlin/Constants.kt @@ -10,4 +10,8 @@ val maxVertexRadius = 35.dp val minVertexRadius = 7.dp val maxEdgeStrokeWidth = 12f -val minEdgeStrokeWidth = 4f \ No newline at end of file +val minEdgeStrokeWidth = 4f + +val SQLITE = "SQLite" +val NEO4J = "Neo4j" +val JSON = "JSON" diff --git a/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt b/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt index a0950dfb..d5485e9e 100644 --- a/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt +++ b/app/src/main/kotlin/model/io/neo4j/Neo4jRepository.kt @@ -6,7 +6,9 @@ import model.graphs.WeightedDirectedGraph import model.graphs.WeightedUndirectedGraph import model.graphs.abstractGraph.Graph import model.graphs.abstractGraph.Vertex -import org.neo4j.driver.* +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase +import org.neo4j.driver.Record import java.io.Closeable const val DIR_LABEL = "POINTS_TO" @@ -24,7 +26,6 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeable { "distinct labels(v) AS label" ).list() } - println(result) val names = mutableListOf() for (record in result) { @@ -85,7 +86,7 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeable { } } - fun loadGraph(name: String): Graph { + fun loadGraph(name: String): Triple, Boolean, Boolean> { val graphContents = readGraphContents(name) val isDirected = hasDirection(graphContents[0]) @@ -131,7 +132,7 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeable { } } - return graph + return Triple(graph, isWeighted, isDirected) } private fun initializeGraph(isWeighted: Boolean, isDirected: Boolean): Graph { diff --git a/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt b/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt index 931e7a9e..8de1f654 100644 --- a/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt +++ b/app/src/main/kotlin/model/io/neo4j/Neo4jRepositoryHandler.kt @@ -37,12 +37,15 @@ object Neo4jRepositoryHandler { return true } - fun loadGraph(name: String): Graph? { + fun loadGraph(name: String): Triple, Boolean, Boolean>? { if (!isRepoInit || !isValidNeo4jName(name)) return null - val graph = neo4jRepo?.loadGraph(name) + val graphWithInfo = neo4jRepo?.loadGraph(name) ?: return null + val graph = graphWithInfo.first + val isWeighed = graphWithInfo.second + val isDirected = graphWithInfo.third - return graph + return Triple(graph, isWeighed, isDirected) } fun isValidNeo4jName(name: String): Boolean { diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index 033fffc0..8c883ca9 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -1,5 +1,8 @@ package view.tabScreen +import JSON +import NEO4J +import SQLITE import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -21,7 +24,6 @@ import kotlinx.coroutines.delay import model.io.neo4j.Neo4jRepositoryHandler import model.io.sql.SQLDatabaseModule import view.utils.* -import viewmodel.MainScreenViewModel import java.awt.FileDialog import java.awt.Frame @@ -36,7 +38,7 @@ fun FileControlTab(graphVM: GraphViewModel) { var showEditDialog by remember { mutableStateOf(false) } var showNeo4jDialog by remember { mutableStateOf(false) } - val databases = arrayOf("SQLite", "Neo4j", "JSON") + val databases = arrayOf(SQLITE, NEO4J, JSON) var selectedDatabase by remember { mutableStateOf(databases[0]) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { @@ -126,7 +128,7 @@ fun FileControlTab(graphVM: GraphViewModel) { } } - if (selectedDatabase == "JSON") { + if (selectedDatabase == JSON) { val fileDialog = FileDialog(null as Frame?, "Select File to Open") fileDialog.mode = FileDialog.LOAD Button( @@ -187,7 +189,7 @@ fun FileControlTab(graphVM: GraphViewModel) { } if (showSaveDialog) { - if (selectedDatabase == "SQLite") { + if (selectedDatabase == SQLITE) { val existingGraphNamesSQL = remember { mutableStateOf(arrayListOf>()) } SQLDatabaseModule.getGraphNames(existingGraphNamesSQL) @@ -221,7 +223,7 @@ fun FileControlTab(graphVM: GraphViewModel) { showSaveDialog = false } } - } else if (selectedDatabase == "Neo4j") { + } else if (selectedDatabase == NEO4J) { if (!Neo4jRepositoryHandler.isRepoInit) { showSaveDialog = false showNeo4jDialog = true @@ -260,14 +262,8 @@ fun FileControlTab(graphVM: GraphViewModel) { if (showLoadDialog) { when (selectedDatabase) { - "SQLite" -> ImportGraphDialogWindow() - "Neo4j" -> { - if (!Neo4jRepositoryHandler.isRepoInit) { - showLoadDialog = false - showNeo4jDialog = true - } - Neo4jImportGraphDialogWindow { showLoadDialog = false } - } + SQLITE -> SQLiteImportGraphDialogWindow() + NEO4J -> Neo4jImportGraphDialogWindow { showLoadDialog = false } } } @@ -288,3 +284,25 @@ fun FileControlTab(graphVM: GraphViewModel) { } } } + +private fun isValidNeo4jName(name: String): Boolean { + if (name.isEmpty()) return false + for (i in name.indices) { + val isValidChar = when (name[i].code) { + 45 -> { // - + if (i!= 0) true else false + } + in 48..57 -> { // 0-9 + if (i != 0) true else false + } + in 65..90 -> true // A-Z + 95 -> true // _ + in 97..122 -> true // a-z + else -> false + } + + if (isValidChar) continue else return false + } + + return true +} diff --git a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt index d1b5a220..a09964b0 100644 --- a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt @@ -1,5 +1,6 @@ package view.utils +import MyAppTheme import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -24,160 +25,162 @@ fun CreateGraphDialogWindow(viewModel: SetupGraphViewModel) { val selectedWeightinessIndex = remember { mutableStateOf(0) } val createGraphClicked = remember { mutableStateOf(false) } - if (!closeDialog.value) { - Dialog( - onDismissRequest = {}, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - Column( - modifier = - Modifier.background(Color.White).padding(16.dp).width(700.dp).height(290.dp) + MyAppTheme { + if (!closeDialog.value) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false) ) { - Text( - "Create Graph", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - modifier = Modifier.padding(bottom = 10.dp) - ) - Row(modifier = Modifier.fillMaxWidth().height(200.dp)) { - Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { - Text("Select stored data:") - - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - - val radioOptions = listOf("Integer", "UInteger", "String") - - Column(modifier = Modifier.width(220.dp)) { - radioOptions.forEachIndexed { index, option -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedStoredDataIndex.value = index - } - ) { - RadioButton( - selected = selectedStoredDataIndex.value == index, - onClick = { selectedStoredDataIndex.value = index }, - colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(700.dp).height(290.dp) + ) { + Text( + "Create Graph", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Row(modifier = Modifier.fillMaxWidth().height(200.dp)) { + Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { + Text("Select stored data:") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + val radioOptions = listOf("Integer", "UInteger", "String") + + Column(modifier = Modifier.width(220.dp)) { + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedStoredDataIndex.value = index + } + ) { + RadioButton( + selected = selectedStoredDataIndex.value == index, + onClick = { selectedStoredDataIndex.value = index }, + colors = + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) ) - ) - Spacer(Modifier.width(1.dp)) - Text( - text = option, - style = TextStyle(fontSize = 16.sp), - color = - if (selectedStoredDataIndex.value == index) Color.Black - else Color.Gray - ) + Spacer(Modifier.width(1.dp)) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = + if (selectedStoredDataIndex.value == index) Color.Black + else Color.Gray + ) + } } } } - } - - Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { - Text("Select the orientation:") - - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - val radioOptions = listOf("Undirected", "Directed") - Column(modifier = Modifier.width(220.dp)) { - radioOptions.forEachIndexed { index, option -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedOrientationIndex.value = index - } - ) { - RadioButton( - selected = selectedOrientationIndex.value == index, - onClick = { selectedOrientationIndex.value = index }, - colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary + Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { + Text("Select the orientation:") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + val radioOptions = listOf("Undirected", "Directed") + Column(modifier = Modifier.width(220.dp)) { + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedOrientationIndex.value = index + } + ) { + RadioButton( + selected = selectedOrientationIndex.value == index, + onClick = { selectedOrientationIndex.value = index }, + colors = + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) + ) + Spacer(Modifier.width(1.dp)) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = + if (selectedOrientationIndex.value == index) Color.Black + else Color.Gray ) - ) - Spacer(Modifier.width(1.dp)) - Text( - text = option, - style = TextStyle(fontSize = 16.sp), - color = - if (selectedOrientationIndex.value == index) Color.Black - else Color.Gray - ) + } } } } - } - - Column(modifier = Modifier.width(250.dp).height(200.dp)) { - Text("Select the weightiness:") - - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - Column(modifier = Modifier.width(220.dp)) { - val radioOptions = listOf("Unweighted", "Weighted") - - radioOptions.forEachIndexed { index, option -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedWeightinessIndex.value = index - } - ) { - RadioButton( - selected = selectedWeightinessIndex.value == index, - onClick = { selectedWeightinessIndex.value = index }, - colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary + Column(modifier = Modifier.width(250.dp).height(200.dp)) { + Text("Select the weightiness:") + + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + + Column(modifier = Modifier.width(220.dp)) { + val radioOptions = listOf("Unweighted", "Weighted") + + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { + selectedWeightinessIndex.value = index + } + ) { + RadioButton( + selected = selectedWeightinessIndex.value == index, + onClick = { selectedWeightinessIndex.value = index }, + colors = + RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) ) - ) - Spacer(Modifier.width(1.dp)) - Text( - text = option, - style = TextStyle(fontSize = 16.sp), - color = - if (selectedWeightinessIndex.value == index) Color.Black - else Color.Gray - ) + Spacer(Modifier.width(1.dp)) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = + if (selectedWeightinessIndex.value == index) Color.Black + else Color.Gray + ) + } } } } } - } - - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.End - ) { - Button( - modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), - onClick = { - closeDialog.value = true - createGraphClicked.value = true - } + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End ) { - Text("Apply", color = Color.White) + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + closeDialog.value = true + createGraphClicked.value = true + } + + ) { + Text("Apply", color = Color.White) + } } } } } - } - if (createGraphClicked.value) { - createGraphFromTypesIndices( - viewModel, - selectedStoredDataIndex.value, - selectedOrientationIndex.value, - selectedWeightinessIndex.value - ) + if (createGraphClicked.value) { + createGraphFromTypesIndices( + viewModel, + selectedStoredDataIndex.value, + selectedOrientationIndex.value, + selectedWeightinessIndex.value + ) + } } } diff --git a/app/src/main/kotlin/view/utils/EditDBWindow.kt b/app/src/main/kotlin/view/utils/EditDBWindow.kt index 764e562c..2ddcdc95 100644 --- a/app/src/main/kotlin/view/utils/EditDBWindow.kt +++ b/app/src/main/kotlin/view/utils/EditDBWindow.kt @@ -1,5 +1,6 @@ package view.utils +import SQLITE import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -30,7 +31,7 @@ fun EditDBWindow(DBType: String, onDismiss: () -> Unit) { var updateGraphNames by remember { mutableStateOf(false) } var graphNameToReplaceWith by remember { mutableStateOf("")} - if (DBType == "SQLite") { + if (DBType == SQLITE) { SQLDatabaseModule.getGraphNames(graphNamesSQL) if (graphNamesSQL.value.isNotEmpty()) showDialog = true else ErrorWindow("Database doesn't have any Graphs", {}) @@ -162,4 +163,4 @@ fun EditDBWindow(DBType: String, onDismiss: () -> Unit) { SQLDatabaseModule.getGraphNames(graphNamesSQL) updateGraphNames = false } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt index 52bda2c6..e2daa531 100644 --- a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt @@ -1,108 +1,111 @@ package view.utils -import model.io.sql.SQLDatabaseModule +import JSON +import MyAppTheme +import NEO4J +import SQLITE import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties +import model.io.neo4j.Neo4jRepositoryHandler -@OptIn(ExperimentalMaterialApi::class) @Composable fun ImportGraphDialogWindow() { - val selectedGraphID = remember { mutableStateOf(0) } - val closeDialog = remember { mutableStateOf(false) } - val expanded = remember { mutableStateOf(false) } - val importFromDBRequired = remember { mutableStateOf(false) } - val selectedGraphName = remember { mutableStateOf("") } - val graphs = remember { mutableStateOf(arrayListOf>()) } + var selectedDatabase by remember { mutableStateOf("") } + var importGraphClicked by remember { mutableStateOf(false) } - SQLDatabaseModule.getGraphNames(graphs) + val fontSize = 16.sp + val buttonColor = MaterialTheme.colors.secondary - if (!closeDialog.value) { - Dialog( - onDismissRequest = {}, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - Column( - modifier = Modifier.background(Color.White) - .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp) + MyAppTheme { + if (!importGraphClicked) { + Dialog( + onDismissRequest = {} ) { - Text( - "Select the graph:", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - modifier = Modifier.padding(bottom = 10.dp) - ) - - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), - horizontalArrangement = Arrangement.spacedBy(30.dp) + Column( + modifier = + Modifier.background(Color.White).padding(16.dp).width(300.dp).height(290.dp) ) { - ExposedDropdownMenuBox( - expanded = expanded.value, - onExpandedChange = { - expanded.value = !expanded.value - }, - modifier = Modifier.fillMaxWidth().fillMaxHeight() + Text( + "Import from...", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center ) { - TextField( - value = selectedGraphName.value, - onValueChange = {}, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded.value) }, - modifier = Modifier.fillMaxWidth().fillMaxHeight(), - ) - ExposedDropdownMenu( - expanded = expanded.value, - onDismissRequest = { expanded.value = false } - ) { - graphs.value.forEach { graphName -> - // TODO: fix its layout - DropdownMenuItem( - onClick = { - selectedGraphName.value = graphName.second - selectedGraphID.value = graphName.first - expanded.value = false - } - ) { - Text(text = graphName.second) - } + Button( + modifier = + Modifier.height(60.dp).width(250.dp), + colors = ButtonDefaults.buttonColors(buttonColor), + onClick = { + selectedDatabase = SQLITE + importGraphClicked = true } + ) { + Text(SQLITE, color = Color.White, fontSize = fontSize) } } - } - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.End - ) { - Button( - modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), - onClick = { - importFromDBRequired.value = true - expanded.value = false - closeDialog.value = true + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + Button( + modifier = + Modifier.height(60.dp).width(250.dp), + colors = ButtonDefaults.buttonColors(buttonColor), + onClick = { + selectedDatabase = NEO4J + importGraphClicked = true + } + ) { + Text(NEO4J, color = Color.White, fontSize = fontSize) } + } + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center ) { - Text("Import", color = Color.White) + Button( + modifier = + Modifier.height(60.dp).width(250.dp), + colors = ButtonDefaults.buttonColors(buttonColor), + onClick = { + selectedDatabase = JSON + importGraphClicked = true + } + ) { + Text(JSON, color = Color.White, fontSize = fontSize) + } } } } } + if (importGraphClicked) { + when (selectedDatabase) { + SQLITE -> SQLiteImportGraphDialogWindow() + NEO4J -> Neo4jImportGraphDialogWindow { importGraphClicked = false } + } + } } - if (importFromDBRequired.value) { - return SQLDatabaseModule.importGraph(selectedGraphID.value) - } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt index 3d5f91a0..ece47472 100644 --- a/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt @@ -23,82 +23,87 @@ fun Neo4jImportGraphDialogWindow(onDismiss: () -> Unit) { var selectedGraphName by remember { mutableStateOf("") } val graphNames = Neo4jRepositoryHandler.getNames() - Dialog( - onDismissRequest = { - onDismiss() - }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - Column( - modifier = Modifier.background(Color.White) - .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp) + if (!Neo4jRepositoryHandler.isRepoInit) { + Neo4jLoginDialog { onDismiss() } + } + else { + Dialog( + onDismissRequest = { + onDismiss() + }, + properties = DialogProperties(usePlatformDefaultWidth = false) ) { - Text( - "Select the graph:", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - modifier = Modifier.padding(bottom = 10.dp) - ) - - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), - horizontalArrangement = Arrangement.spacedBy(30.dp) + Column( + modifier = Modifier.background(Color.White) + .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { - expanded = !expanded - }, - modifier = Modifier.fillMaxWidth().fillMaxHeight() + Text( + "Select the graph:", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) ) { - TextField( - value = selectedGraphName, - onValueChange = {}, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier.fillMaxWidth().fillMaxHeight(), - ) - ExposedDropdownMenu( + ExposedDropdownMenuBox( expanded = expanded, - onDismissRequest = { expanded = false } + onExpandedChange = { + expanded = !expanded + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() ) { - println(graphNames) - graphNames?.forEach { name -> - DropdownMenuItem( - onClick = { - selectedGraphName = name - expanded = false + TextField( + value = selectedGraphName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + graphNames?.forEach { name -> + DropdownMenuItem( + onClick = { + selectedGraphName = name + expanded = false + } + ) { + Text(text = name) } - ) { - Text(text = name) } } } } - } - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.End - ) { - Button( - modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), - onClick = { - importFromDBRequired = true - expanded = false - onDismiss() - } + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center ) { - Text("Import", color = Color.White) + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + importFromDBRequired = true + expanded = false + onDismiss() + } + ) { + Text("Import", color = Color.White) + } } } } - } - if (importFromDBRequired) { + if (importFromDBRequired) { // return SQLDatabaseModule.importGraph(selectedGraphID.value) - Neo4jRepositoryHandler.loadGraph(selectedGraphName) + Neo4jRepositoryHandler.loadGraph(selectedGraphName) + } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt b/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt index f81c35ee..5b0cc8bf 100644 --- a/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt +++ b/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt @@ -2,17 +2,19 @@ package view.utils import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.HorizontalAlignmentLine import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import kotlinx.coroutines.delay import model.io.neo4j.Neo4jRepositoryHandler -import viewmodel.MainScreenViewModel @Composable fun Neo4jLoginDialog(onDismiss: () -> Unit) { @@ -152,4 +154,4 @@ fun Neo4jLoginDialog(onDismiss: () -> Unit) { } } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/view/utils/SQLiteImportGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/SQLiteImportGraphDialogWindow.kt new file mode 100644 index 00000000..c3e6b841 --- /dev/null +++ b/app/src/main/kotlin/view/utils/SQLiteImportGraphDialogWindow.kt @@ -0,0 +1,108 @@ +package view.utils + +import model.io.sql.SQLDatabaseModule +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SQLiteImportGraphDialogWindow() { + val selectedGraphID = remember { mutableStateOf(0) } + val closeDialog = remember { mutableStateOf(false) } + val expanded = remember { mutableStateOf(false) } + val importFromDBRequired = remember { mutableStateOf(false) } + val selectedGraphName = remember { mutableStateOf("") } + val graphs = remember { mutableStateOf(arrayListOf>()) } + + SQLDatabaseModule.getGraphNames(graphs) + + if (!closeDialog.value) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = Modifier.background(Color.White) + .padding(top = 16.dp, end = 16.dp, start = 16.dp, bottom = 6.dp).width(350.dp).height(180.dp) + ) { + Text( + "Select the graph:", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value + }, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + TextField( + value = selectedGraphName.value, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded.value) }, + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) + ExposedDropdownMenu( + expanded = expanded.value, + onDismissRequest = { expanded.value = false } + ) { + graphs.value.forEach { graphName -> + // TODO: fix its layout + DropdownMenuItem( + onClick = { + selectedGraphName.value = graphName.second + selectedGraphID.value = graphName.first + expanded.value = false + } + ) { + Text(text = graphName.second) + } + } + } + } + } + + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + importFromDBRequired.value = true + expanded.value = false + closeDialog.value = true + } + ) { + Text("Import", color = Color.White) + } + } + } + } + } + if (importFromDBRequired.value) { + return SQLDatabaseModule.importGraph(selectedGraphID.value) + } +} diff --git a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt index a65fdc5e..170d3340 100644 --- a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt @@ -1,9 +1,11 @@ package view.utils +import MyAppTheme import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -27,63 +29,64 @@ class SelectInitDialogWindow( fun GraphInitDialogWindow( showDialog: Boolean, ) { + MyAppTheme { + DisposableEffect(Unit) { + if (showDialog) { + showGraphDialog = true + } - DisposableEffect(Unit) { - if (showDialog) { - showGraphDialog = true + onDispose { showGraphDialog = false } } - onDispose { showGraphDialog = false } - } - - if (showGraphDialog) { - Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) { - Column( - modifier = Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) - ) { - Text( - "Welcome to WUDU!", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - modifier = Modifier.padding(bottom = 10.dp) - ) - Text("Please select how to initialize the graph") + if (showGraphDialog) { + Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) { + Column( + modifier = Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) + ) { + Text( + "Welcome to WUDU!", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Text("Please select how to initialize the graph") - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} + Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(30.dp) - ) { - Button(modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(Color.Red), - onClick = { - showGraphDialog = false - showCreateGraphDialog = true - }) { - Text("Create", color = Color.White) - } + Row( + modifier = Modifier.padding(10.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(30.dp) + ) { + Button(modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.secondary), + onClick = { + showGraphDialog = false + showCreateGraphDialog = true + }) { + Text("Create", color = Color.White) + } - Button(modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(Color.Blue), - onClick = { - showGraphDialog = false - showImportTab = true + Button(modifier = Modifier.width(145.dp).height(50.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + showGraphDialog = false + showImportTab = true + } + ) { + Text("Import", color = Color.White) } - ) { - Text("Import", color = Color.White) } } } } - } - if (showCreateGraphDialog) { - CreateGraphDialogWindow(SetupGraphViewModel()) - } + if (showCreateGraphDialog) { + CreateGraphDialogWindow(SetupGraphViewModel()) + } - if (showImportTab) { - ImportGraphDialogWindow() + if (showImportTab) { + ImportGraphDialogWindow() + } } } } From a353f9802303d96fa9880c8fdb634fba14d98c70 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:05:50 +0300 Subject: [PATCH 75/77] docs: improve README, make it shorter and more functional #60 * docs: add link to Wiki * docs: add WIP status to JSON * fix: switch to GH link * docs: fix grammar * docs: remove 'All in all' heading --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5712d52b..3a64e44d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ -# WUDU* +# WUDU + +#### - Weighted Unweighted Directed Undirected (or like a Vodoo doll ;) ) + +Application that lets you create, save, visualise, analyse and modify 4 different types of graphs -Application that lets you create, save, visualise, analyse and modify 4 different types of graphs (see *) ## Features -- Support 4 types of graphs (see *) -- Use all in all - 9 algorithms -- Store graphs anywhere you want: SQLite, Neo4J, JSON +- Support 4 types of graphs +- Use all in all - 9 algorithms (SCC, bridges, shortest paths, cycles, layout, communities, etc. see [wiki]) +- Store graphs anywhere you want: SQLite, Neo4J, JSON (WIP) (see [wiki]) - Drag, zoom, replace nodes -![mainscreen_screenshot](https://github.com/spbu-coding-2023/graphs-graph-2/assets/64466788/9c708a11-dc6e-4cf9-a848-44f21dec7a37) +![alt text][main_screen_image] ## Usage @@ -40,14 +43,14 @@ Distributed under the [GPL-3.0 License][repo_license_url]. - [Gavrilenko Mike](https://github.com/qrutyy) - [Shakirov Karim](https://github.com/kar1mgh) - [Vlasenco Daniel](https://github.com/spisladqo) - -** - Weighted Unweighted Directed Undirected (or like a Vodoo doll ;) ) _______________________________ [*Java gnomik*][java_gnomik_url] +[wiki]: https://github.com/spbu-coding-2023/graphs-graph-2/wiki [Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/ [repo_license_url]: https://github.com/spbu-coding-2023/graphs-graph-2/blob/main/LICENSE.md [contribute_rules_url]: https://github.com/spbu-coding-2023/graphs-graph-2/blob/main/CONTRIBUTING.md [java_gnomik_url]: https://ibb.co/54hJVd2 +[main_screen_image]: https://github.com/spbu-coding-2023/graphs-graph-2/assets/64466788/9c708a11-dc6e-4cf9-a848-44f21dec7a37 From 1025dfc19112684fa5add87f713eab2f78b1a717 Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Wed, 12 Jun 2024 00:30:03 +0300 Subject: [PATCH 76/77] fix: resolve review issues, edit SQL module architecture #61 * build: remove koin, clean up dependencies * fix: make main private * refactor: move constants to their files * fix: make app run with our custom theme * fix: switch to 'by remember', where possible * fix: optimise graph creation dialog window * refactor: move util viewmodel functions * refactor: move dialog windows to a separate folder * fix: switch DBType from list to enum * fix: remove whole viewmodel pass in FAQ * refactor: take out RadioColumn, simplify EdgeView * fix: move available algs logic into 'GraphVM' * fix: make a general 'RunAlgoButton' for 'Analyze' tab * refactor: rename 'utils' package to 'components' * fix: update 'SetupGraphVM', make it shorter * fix: make SetupGraphVM an object, rename it * fix: make TFDPLayout an object * refactor: glow up the syntax * refactor: move FFT to viewmodel package * fix: move UI-connect methods from SQL module * fix: reduce box size in 'add vertex' layout * fix: remove old DB's, add a new one * refactor: change imports --- app/build.gradle.kts | 10 - app/database/my_graph_database.db | Bin 20480 -> 20480 bytes app/src/main/kotlin/Constants.kt | 20 +- app/src/main/kotlin/Main.kt | 9 +- .../kotlin/{ColorPalette.kt => MyAppTheme.kt} | 2 +- .../kotlin/model/io/sql/SQLDatabaseModule.kt | 138 +++------ app/src/main/kotlin/model/io/sql/queries.txt | 4 +- app/src/main/kotlin/view/MainScreen.kt | 6 +- .../view/{utils => components}/FAQBox.kt | 7 +- .../kotlin/view/components/RadioColumn.kt | 54 ++++ .../kotlin/view/components/RunAlgoButton.kt | 40 +++ .../view/{utils => components}/ToolBox.kt | 14 +- .../dialogWindows/CreateGraphDialogWindow.kt | 97 +++++++ .../dialogWindows}/EditDBWindow.kt | 21 +- .../dialogWindows}/ErrorWindow.kt | 4 +- .../dialogWindows}/ImportGraphDialogWindow.kt | 6 +- .../Neo4jImportGraphDialogWindow.kt | 3 +- .../dialogWindows}/Neo4jLoginDialog.kt | 5 +- .../SQLiteImportGraphDialogWindow.kt | 50 ++-- .../dialogWindows}/SelectInitDialogWindow.kt | 16 +- app/src/main/kotlin/view/graph/EdgeView.kt | 19 +- app/src/main/kotlin/view/graph/GraphView.kt | 17 +- app/src/main/kotlin/view/graph/VertexView.kt | 12 +- .../kotlin/view/tabScreen/FileControlTab.kt | 49 ++-- .../main/kotlin/view/tabScreen/GeneralTab.kt | 15 +- .../view/tabScreen/analyzeTab/AnalyzeTab.kt | 54 +--- .../analyzeTab/algorithmsUI/BridgesUI.kt | 38 +-- .../analyzeTab/algorithmsUI/CommunitiesUI.kt | 38 +-- .../analyzeTab/algorithmsUI/CyclesUI.kt | 30 +- .../analyzeTab/algorithmsUI/KeyVerticesUI.kt | 38 +-- .../algorithmsUI/MinSpanningTreeUI.kt | 38 +-- .../analyzeTab/algorithmsUI/SCCUI.kt | 38 +-- .../analyzeTab/algorithmsUI/ShortestPathUI.kt | 30 +- .../view/utils/CreateGraphDialogWindow.kt | 238 ---------------- .../FastFourierTransform.kt | 2 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 31 +- .../main/kotlin/viewmodel/SQLiteViewModel.kt | 66 +++++ .../viewmodel/{graph => }/TFDPLayout.kt | 34 ++- .../main/kotlin/viewmodel/WindowViewModel.kt | 29 -- .../kotlin/viewmodel/graph/EdgeViewModel.kt | 35 +-- .../kotlin/viewmodel/graph/GraphViewModel.kt | 125 +++++++-- .../viewmodel/graph/GraphViewModelFactory.kt | 127 +++++++++ .../viewmodel/graph/SetupGraphViewModel.kt | 264 ------------------ .../kotlin/viewmodel/graph/VertexViewModel.kt | 4 + .../kotlin/integration/IntegrationTest.kt | 6 +- build.gradle.kts | 4 +- gradle.properties | 1 - settings.gradle.kts | 2 - 48 files changed, 760 insertions(+), 1130 deletions(-) rename app/src/main/kotlin/{ColorPalette.kt => MyAppTheme.kt} (97%) rename app/src/main/kotlin/view/{utils => components}/FAQBox.kt (92%) create mode 100644 app/src/main/kotlin/view/components/RadioColumn.kt create mode 100644 app/src/main/kotlin/view/components/RunAlgoButton.kt rename app/src/main/kotlin/view/{utils => components}/ToolBox.kt (81%) create mode 100644 app/src/main/kotlin/view/components/dialogWindows/CreateGraphDialogWindow.kt rename app/src/main/kotlin/view/{utils => components/dialogWindows}/EditDBWindow.kt (90%) rename app/src/main/kotlin/view/{utils => components/dialogWindows}/ErrorWindow.kt (94%) rename app/src/main/kotlin/view/{utils => components/dialogWindows}/ImportGraphDialogWindow.kt (95%) rename app/src/main/kotlin/view/{utils => components/dialogWindows}/Neo4jImportGraphDialogWindow.kt (98%) rename app/src/main/kotlin/view/{utils => components/dialogWindows}/Neo4jLoginDialog.kt (96%) rename app/src/main/kotlin/view/{utils => components/dialogWindows}/SQLiteImportGraphDialogWindow.kt (69%) rename app/src/main/kotlin/view/{utils => components/dialogWindows}/SelectInitDialogWindow.kt (89%) delete mode 100644 app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt rename app/src/main/kotlin/{model => viewmodel}/FastFourierTransform.kt (99%) create mode 100644 app/src/main/kotlin/viewmodel/SQLiteViewModel.kt rename app/src/main/kotlin/viewmodel/{graph => }/TFDPLayout.kt (91%) delete mode 100644 app/src/main/kotlin/viewmodel/WindowViewModel.kt create mode 100644 app/src/main/kotlin/viewmodel/graph/GraphViewModelFactory.kt delete mode 100644 app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a2c1c79..b7aba023 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,26 +2,16 @@ val kotlinxCoroutinesVersion: String by project val neo4jDriverVersion: String by project val composeVersion: String by project val junitVersion: String by project -val koinVersion: String by project plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.compose) } -repositories { - google() // to ensure dependencies like androidx.annotation can be resolved. - mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") -} - dependencies { implementation(compose.desktop.currentOs) testImplementation("org.jetbrains.compose.ui:ui-test-junit4:$composeVersion") - implementation("io.insert-koin:koin-core:$koinVersion") - implementation(libs.koin.core) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$kotlinxCoroutinesVersion") diff --git a/app/database/my_graph_database.db b/app/database/my_graph_database.db index 6ca344b0c7c69b7c2b093a8d16cbba9b11eb4f00..ba4584035007905f3eb92a2b6d90d370ebb9d7a8 100644 GIT binary patch delta 846 zcmZXRO=uHA6vtPAcRoZBd`Zu==R*?5s1M@a&c=ici|BBmh;(67_`Dm^=575N~O^Z7Q=8{-yC}(KJYvli5b-%&?l1uz0zYww-|#1Yg7>fsZ{RsRf%|X=I&l6^xZnSc zUTON7+eV89&BlG7SlT>B0Mr-=HPrXAIjySzt-RD$&m_NC1=RC)DPt!ri#uL16vXZvcO zK}~T_6wEnYIjt&%qOdD{@;z%7X@HBOc}g7;=8V>9t>xIT^c7Y~o%Dn~g^=R-`rj^e BgqZ*U delta 846 zcmZ9IKWG#|7{zDzZfED4%kDeH^XZOa&>iyn(^9|q3#+I?MW!xH7}zMzU=K8?Qp!pe_hVDSf|&%e64S-vi`3-rv|tCzTiTpEtT?&8&R#kv=FV# zF~%fCi`8g_%v8=uD0DE)^6m~pKXP!9c6aa_Kk*fx@Bwe}3eWKb4{!&!u!gIsU>>J& ztT{dT&TWV;&J074of4gzOgpbg5ZNNvinwWav?8t+aO2LRJu0!oR-BM(24ghseDuF-9yv4&z>n~ z4y`l|XiS@8pYBs)uO`zz*{@JNM>;sey4@cPM^Hi$eaIsVfnWHB&-jS;7pTKKjgUICE)cc$qNpSJy*7aAp(h2Z~oipZ6*YJK#N9J+0k| z`m&%j$4g-l2GpgvotI1gjMJ&mt(AxJ%CyU5=b}QdwU^rYF9 Unit) { val mycolors = ColorPalette( diff --git a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt index 60ce2943..1f34d828 100644 --- a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt +++ b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt @@ -1,21 +1,17 @@ package model.io.sql import androidx.compose.material.CircularProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import model.graphs.abstractGraph.Graph import view.MainScreen -import view.utils.ErrorWindow -import view.utils.getGraphVMParameter +import view.components.dialogWindows.ErrorWindow import viewmodel.MainScreenViewModel import viewmodel.graph.GraphViewModel -import viewmodel.graph.SetupGraphViewModel +import viewmodel.graph.GraphViewModelFactory +import viewmodel.graph.getGraphVMParameter import java.io.File import java.sql.* -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine +import kotlin.system.exitProcess object SQLDatabaseModule { private const val DB_DIRECTORY = "database" @@ -29,7 +25,7 @@ object SQLDatabaseModule { return fileContent.trim() // Trim any leading/trailing whitespace } - private val insertQueries = readQueriesFromFile() + val insertQueries = readQueriesFromFile() init { createDatabaseDirectory() @@ -58,6 +54,29 @@ object SQLDatabaseModule { } } + fun importGraph( + graphId: Int, currentGraphSetup: Pair, String>? + ): Pair, String>? { + var currentGS = currentGraphSetup + val connection = SQLDatabaseModule.getConnection() + connection.use { + val selectGraphSQL = SQLDatabaseModule.insertQueries.split(":")[6] + it.prepareStatement(selectGraphSQL).use { statement -> + statement.setInt(1, graphId) + val resultSet = statement.executeQuery() + + if (resultSet.next()) { + currentGS = SQLDatabaseModule.importGraphInfo(graphId) + } else { + currentGS = null + throw SQLException("Graph with ID $graphId not found.") + } + } + } + return currentGS + } + fun insertGraph(graphVM: GraphViewModel, graphName: String, graphType: String) { val insertGraphSQL = insertQueries.split(":")[0] @@ -111,11 +130,10 @@ object SQLDatabaseModule { } } - @Composable - fun getGraphNames(graphNames: MutableState>>) { + fun getGraphNames(graphNames: MutableState>>): String? { val selectNamesSQL = insertQueries.split(":")[3] - val showErrorMessage = remember { mutableStateOf(false) } - val errorMessage = remember { mutableStateOf("") } + var showErrorMessage = false + var errorMessage = "" graphNames.value = arrayListOf() try { @@ -128,81 +146,13 @@ object SQLDatabaseModule { } } } catch (e: SQLException) { - showErrorMessage.value = true - errorMessage.value = e.message.toString() - } - if (showErrorMessage.value) { - ErrorWindow(errorMessage.value, { System.exit(-1) }) - } - } - - - @Composable - fun importGraph(graphId: Int) { - val graphVMState = remember { mutableStateOf?>(null) } - val showErrorMessage = remember { mutableStateOf(false) } - val updateIsRequired = remember { mutableStateOf(false) } - var currentGraphSetup: Pair, String>? = null - - try { - val connection = getConnection() - connection.use { - val selectGraphSQL = insertQueries.split(":")[6] - it.prepareStatement(selectGraphSQL).use { statement -> - statement.setInt(1, graphId) - val resultSet = statement.executeQuery() - - if (resultSet.next()) { - currentGraphSetup = importGraphInfo(graphId) - } else { - showErrorMessage.value = true - throw SQLException("Graph with ID $graphId not found.") - } - } - } - - - // Execute side-effect to create graph object - SetupGraphViewModel().createGraphObject( - currentGraphSetup?.first?.first as SetupGraphViewModel.GraphType, - currentGraphSetup?.first?.second as SetupGraphViewModel.GraphStructure, - currentGraphSetup?.first?.third as SetupGraphViewModel.Weight, - graphId, - graphVMState - ) - updateIsRequired.value = true - - } catch (e: SQLException) { - e.printStackTrace() - showErrorMessage.value = true + showErrorMessage = true + errorMessage = e.message.toString() } - - if (updateIsRequired.value) return importGraphUI(showErrorMessage, graphVMState, graphId) - } - - @Composable - fun importGraphUI( - showErrorMessage: MutableState, - graphVMState: MutableState?>, - graphId: Int - ) { - if (showErrorMessage.value) { - ErrorWindow("Graph with ID $graphId not found.", { System.exit(-1) }) - System.exit(-1) + if (showErrorMessage) { + return errorMessage } - if (graphVMState.value != null) { - graphVMState.value?.updateIsRequired?.value = true - - MainScreen( - MainScreenViewModel( - graphVMState.value?.graph as Graph, - graphVMState.value?.graphType?.value as String, - graphVMState.value - ) - ) - - } else CircularProgressIndicator() + return null } fun updateImportedGraphVM( @@ -220,16 +170,16 @@ object SQLDatabaseModule { val resultSet = statement.executeQuery() if (resultSet.next()) { val currentGraphSetup = importGraphInfo(graphId) - val graphVMType = - mutableStateOf(currentGraphSetup.first.second.toString() + "Graph" + " " + currentGraphSetup.first.first.toString()) + val graphVMType = currentGraphSetup.first.second.toString() + + "Graph" + " " + currentGraphSetup.first.first.toString() val graphVM = GraphViewModel( graph, mutableStateOf(false), mutableStateOf(false), graphVMType, - mutableStateOf(currentGraphSetup.first.second.toString().contains("Directed")), - mutableStateOf(currentGraphSetup.first.second.toString().contains("Weighted")) + currentGraphSetup.first.second.toString().contains("Directed"), + currentGraphSetup.first.second.toString().contains("Weighted") ) // Fetch vertices @@ -268,7 +218,7 @@ object SQLDatabaseModule { } - private fun importGraphInfo(graphId: Int): Pair, String> { + private fun importGraphInfo(graphId: Int): Pair, String> { val selectGraphSQL = insertQueries.split(":")[6] var graphStructure: Int? var graphWeight: Int? @@ -295,8 +245,8 @@ object SQLDatabaseModule { getGraphVMParameter(it1, it2, it) } } - } ?: throw NoSuchElementException("No info found about graph with ID = ${graphId}"), - graphName ?: throw NoSuchElementException("Graph with ID = ${graphId} has no name")) + } ?: throw NoSuchElementException("No info found about graph with ID = $graphId"), + graphName ?: throw NoSuchElementException("Graph with ID = $graphId has no name")) } fun deleteGraph(graphId: Int) { diff --git a/app/src/main/kotlin/model/io/sql/queries.txt b/app/src/main/kotlin/model/io/sql/queries.txt index 03827dfe..570452de 100644 --- a/app/src/main/kotlin/model/io/sql/queries.txt +++ b/app/src/main/kotlin/model/io/sql/queries.txt @@ -35,7 +35,9 @@ CREATE TABLE IF NOT EXISTS edges ( vertex1_id INTEGER NOT NULL, vertex2_id INTEGER NOT NULL, weight INTEGER, - FOREIGN KEY (graph_id) REFERENCES graphs(id) + FOREIGN KEY (graph_id) REFERENCES graphs(id), + FOREIGN KEY (vertex1_id) REFERENCES vertices(id), + FOREIGN KEY (vertex2_id) REFERENCES vertices(id) ); CREATE TABLE IF NOT EXISTS vertices ( diff --git a/app/src/main/kotlin/view/MainScreen.kt b/app/src/main/kotlin/view/MainScreen.kt index b31e0497..48bea7d0 100644 --- a/app/src/main/kotlin/view/MainScreen.kt +++ b/app/src/main/kotlin/view/MainScreen.kt @@ -10,8 +10,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import view.graph.GraphView import view.tabScreen.TabHandler -import view.utils.FAQBox -import view.utils.ToolBox +import view.components.FAQBox +import view.components.ToolBox import viewmodel.MainScreenViewModel @Composable @@ -28,7 +28,7 @@ fun MainScreen(viewmodel: MainScreenViewModel) { } } // Hoverable box over the existing Surface - FAQBox(interactionSource, viewmodel) + FAQBox(interactionSource, viewmodel.graphType) ToolBox(viewmodel.graphViewModel, scale) } } diff --git a/app/src/main/kotlin/view/utils/FAQBox.kt b/app/src/main/kotlin/view/components/FAQBox.kt similarity index 92% rename from app/src/main/kotlin/view/utils/FAQBox.kt rename to app/src/main/kotlin/view/components/FAQBox.kt index a488caf3..c0404007 100644 --- a/app/src/main/kotlin/view/utils/FAQBox.kt +++ b/app/src/main/kotlin/view/components/FAQBox.kt @@ -1,4 +1,4 @@ -package view.utils +package view.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image @@ -20,10 +20,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import viewmodel.MainScreenViewModel @Composable -fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScreenViewModel) { +fun FAQBox(interactionSource: MutableInteractionSource, currentGraphType: String) { var isHovered by remember { mutableStateOf(false) } Box( @@ -52,7 +51,7 @@ fun FAQBox(interactionSource: MutableInteractionSource, viewmodel: MainScree .testTag("FAQBoxHovered") ) { Text( - text = viewmodel.graphViewModel.graphType.value.replace(" ", "\nData type: "), + text = currentGraphType, fontSize = 16.sp, color = Color.Black, textAlign = TextAlign.Center, diff --git a/app/src/main/kotlin/view/components/RadioColumn.kt b/app/src/main/kotlin/view/components/RadioColumn.kt new file mode 100644 index 00000000..fe67ef48 --- /dev/null +++ b/app/src/main/kotlin/view/components/RadioColumn.kt @@ -0,0 +1,54 @@ +package view.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun RadioColumn( + selectText: String, + currentDataIndex: MutableState, + radioOptions: List, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text(selectText) + Spacer(modifier = Modifier.height(8.dp)) + Column { + radioOptions.forEachIndexed { index, option -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(vertical = 4.dp) + .fillMaxWidth() + .clickable { currentDataIndex.value = index } + ) { + RadioButton( + selected = currentDataIndex.value == index, + onClick = { currentDataIndex.value = index }, + colors = RadioButtonDefaults.colors( + selectedColor = MaterialTheme.colors.secondary + ) + ) + Text( + text = option, + style = TextStyle(fontSize = 16.sp), + color = if (currentDataIndex.value == index) Color.Black else Color.Gray + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/view/components/RunAlgoButton.kt b/app/src/main/kotlin/view/components/RunAlgoButton.kt new file mode 100644 index 00000000..c897b743 --- /dev/null +++ b/app/src/main/kotlin/view/components/RunAlgoButton.kt @@ -0,0 +1,40 @@ +package view.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import view.tabScreen.analyzeTab.borderPadding +import view.tabScreen.analyzeTab.horizontalGap +import view.tabScreen.analyzeTab.rowHeight +import view.components.dialogWindows.ErrorWindow + +@Composable +fun RunAlgoButton(errorText: String, algoToRun: () -> Boolean) { + var showErrorWindow by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.height(rowHeight).padding(borderPadding), + horizontalArrangement = Arrangement.spacedBy(horizontalGap) + ) { + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { + Button( + modifier = Modifier.fillMaxSize(), + onClick = { + if (!algoToRun()) { + showErrorWindow = true + } + }, + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) + ) { + Text("Run algorithm") + } + } + } + if (showErrorWindow) { + ErrorWindow(errorText) { showErrorWindow = false } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/utils/ToolBox.kt b/app/src/main/kotlin/view/components/ToolBox.kt similarity index 81% rename from app/src/main/kotlin/view/utils/ToolBox.kt rename to app/src/main/kotlin/view/components/ToolBox.kt index 77752868..2ed364a8 100644 --- a/app/src/main/kotlin/view/utils/ToolBox.kt +++ b/app/src/main/kotlin/view/components/ToolBox.kt @@ -1,4 +1,4 @@ -package view.utils +package view.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -9,10 +9,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -import scaleFactor -import view.tabScreen.analyzeTab.horizontalGap import viewmodel.graph.GraphViewModel +const val SCALE_FACTOR: Float = 1.1f + @Composable fun ToolBox(graphVM: GraphViewModel, currentScale: MutableState) { Box( @@ -26,7 +26,7 @@ fun ToolBox(graphVM: GraphViewModel, currentScale: MutableState) { ) { FloatingActionButton( onClick = { - currentScale.value = (scaleFactor * currentScale.value).coerceIn(0.7f, 1.9f) + currentScale.value = (SCALE_FACTOR * currentScale.value).coerceIn(0.7f, 1.9f) }, modifier = Modifier.padding(horizontal = 11.dp) ) { @@ -34,7 +34,7 @@ fun ToolBox(graphVM: GraphViewModel, currentScale: MutableState) { } Spacer(modifier = Modifier.height(8.dp)) FloatingActionButton(onClick = { - currentScale.value = (currentScale.value / scaleFactor).coerceIn(0.7f, 1.9f) + currentScale.value = (currentScale.value / SCALE_FACTOR).coerceIn(0.7f, 1.9f) }, modifier = Modifier.padding(horizontal = 11.dp) ) { @@ -46,9 +46,7 @@ fun ToolBox(graphVM: GraphViewModel, currentScale: MutableState) { .width(80.dp) .height(50.dp) .clip(shape = RoundedCornerShape(25.dp)), - onClick = { - graphVM.clearGraph() - } + onClick = graphVM::clearGraph ) { Text("Clear") } diff --git a/app/src/main/kotlin/view/components/dialogWindows/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/CreateGraphDialogWindow.kt new file mode 100644 index 00000000..e88a9faf --- /dev/null +++ b/app/src/main/kotlin/view/components/dialogWindows/CreateGraphDialogWindow.kt @@ -0,0 +1,97 @@ +package view.components.dialogWindows + +import MyAppTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import view.components.RadioColumn +import viewmodel.graph.GraphViewModelFactory +import viewmodel.graph.createGraphFromTypesIndices + +@Composable +fun CreateGraphDialogWindow(viewModel: GraphViewModelFactory) { + var closeDialog by remember { mutableStateOf(false) } + val selectedStoredDataIndex = remember { mutableStateOf(0) } + val selectedOrientationIndex = remember { mutableStateOf(0) } + val selectedWeightinessIndex = remember { mutableStateOf(0) } + var createGraphClicked by remember { mutableStateOf(false) } + + MyAppTheme { + if (!closeDialog) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Column( + modifier = Modifier + .background(Color.White) + .padding(16.dp) + .widthIn(min = 200.dp, max = 700.dp) + .wrapContentHeight() + ) { + Text( + "Create Graph", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + modifier = Modifier.padding(bottom = 10.dp) + ) + Row(modifier = Modifier.wrapContentHeight().fillMaxWidth()) { + RadioColumn( + "Select stored data:", + selectedStoredDataIndex, + listOf("Integer", "UInteger", "String"), + Modifier.weight(1f).padding(horizontal = 8.dp) + ) + RadioColumn( + "Select the orientation:", + selectedOrientationIndex, + listOf("Undirected", "Directed"), + Modifier.weight(1f).padding(horizontal = 8.dp) + ) + RadioColumn( + "Select the weightiness:", + selectedWeightinessIndex, + listOf("Unweighted", "Weighted"), + Modifier.weight(1f).padding(horizontal = 8.dp) + ) + } + Row( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.wrapContentSize(), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), + onClick = { + closeDialog = true + createGraphClicked = true + } + ) { + Text("Apply", color = Color.White) + } + } + } + } + } + if (createGraphClicked) { + createGraphFromTypesIndices( + viewModel, + selectedStoredDataIndex.value, + selectedOrientationIndex.value, + selectedWeightinessIndex.value + ) + } + } +} diff --git a/app/src/main/kotlin/view/utils/EditDBWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/EditDBWindow.kt similarity index 90% rename from app/src/main/kotlin/view/utils/EditDBWindow.kt rename to app/src/main/kotlin/view/components/dialogWindows/EditDBWindow.kt index 2ddcdc95..d42694ec 100644 --- a/app/src/main/kotlin/view/utils/EditDBWindow.kt +++ b/app/src/main/kotlin/view/components/dialogWindows/EditDBWindow.kt @@ -1,6 +1,5 @@ -package view.utils +package view.components.dialogWindows -import SQLITE import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -19,10 +18,11 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import model.io.sql.SQLDatabaseModule +import view.tabScreen.DatabaseTypes @OptIn(ExperimentalMaterialApi::class) @Composable -fun EditDBWindow(DBType: String, onDismiss: () -> Unit) { +fun EditDBWindow(DBType: DatabaseTypes, onDismiss: () -> Unit) { var showDialog by remember { mutableStateOf(false) } val graphNamesSQL = remember { mutableStateOf(arrayListOf>()) } var expanded by remember { mutableStateOf(false) } @@ -31,10 +31,18 @@ fun EditDBWindow(DBType: String, onDismiss: () -> Unit) { var updateGraphNames by remember { mutableStateOf(false) } var graphNameToReplaceWith by remember { mutableStateOf("")} - if (DBType == SQLITE) { - SQLDatabaseModule.getGraphNames(graphNamesSQL) + if (DBType == DatabaseTypes.SQLite) { + val errorMessage = SQLDatabaseModule.getGraphNames(graphNamesSQL) + if (errorMessage != null) ErrorWindow(errorMessage) {} + if (graphNamesSQL.value.isNotEmpty()) showDialog = true else ErrorWindow("Database doesn't have any Graphs", {}) + } else if (DBType == DatabaseTypes.NEO4J) { + ErrorWindow("Sorry! This feature will be implemented in future release") {} + // TODO + } else { + ErrorWindow("Sorry! This feature will be implemented in future release") {} + // TODO } @@ -160,7 +168,8 @@ fun EditDBWindow(DBType: String, onDismiss: () -> Unit) { } } if (updateGraphNames) { - SQLDatabaseModule.getGraphNames(graphNamesSQL) + val sqlErrorMessage = SQLDatabaseModule.getGraphNames(graphNamesSQL) + if (sqlErrorMessage != null) ErrorWindow(sqlErrorMessage) {} updateGraphNames = false } } diff --git a/app/src/main/kotlin/view/utils/ErrorWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/ErrorWindow.kt similarity index 94% rename from app/src/main/kotlin/view/utils/ErrorWindow.kt rename to app/src/main/kotlin/view/components/dialogWindows/ErrorWindow.kt index d652de71..544af9de 100644 --- a/app/src/main/kotlin/view/utils/ErrorWindow.kt +++ b/app/src/main/kotlin/view/components/dialogWindows/ErrorWindow.kt @@ -1,4 +1,4 @@ -package view.utils +package view.components.dialogWindows import androidx.compose.foundation.layout.* import androidx.compose.material.Button @@ -6,8 +6,6 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight diff --git a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/ImportGraphDialogWindow.kt similarity index 95% rename from app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt rename to app/src/main/kotlin/view/components/dialogWindows/ImportGraphDialogWindow.kt index e2daa531..ea228742 100644 --- a/app/src/main/kotlin/view/utils/ImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/components/dialogWindows/ImportGraphDialogWindow.kt @@ -1,24 +1,20 @@ -package view.utils +package view.components.dialogWindows import JSON import MyAppTheme import NEO4J import SQLITE import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.hoverable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import model.io.neo4j.Neo4jRepositoryHandler @Composable fun ImportGraphDialogWindow() { diff --git a/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/Neo4jImportGraphDialogWindow.kt similarity index 98% rename from app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt rename to app/src/main/kotlin/view/components/dialogWindows/Neo4jImportGraphDialogWindow.kt index ece47472..e8d98bc1 100644 --- a/app/src/main/kotlin/view/utils/Neo4jImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/components/dialogWindows/Neo4jImportGraphDialogWindow.kt @@ -1,4 +1,4 @@ -package view.utils +package view.components.dialogWindows import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -13,7 +13,6 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import model.io.neo4j.Neo4jRepositoryHandler -import model.io.sql.SQLDatabaseModule @OptIn(ExperimentalMaterialApi::class) @Composable diff --git a/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt b/app/src/main/kotlin/view/components/dialogWindows/Neo4jLoginDialog.kt similarity index 96% rename from app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt rename to app/src/main/kotlin/view/components/dialogWindows/Neo4jLoginDialog.kt index 5b0cc8bf..03f4ba10 100644 --- a/app/src/main/kotlin/view/utils/Neo4jLoginDialog.kt +++ b/app/src/main/kotlin/view/components/dialogWindows/Neo4jLoginDialog.kt @@ -1,15 +1,12 @@ -package view.utils +package view.components.dialogWindows import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.HorizontalAlignmentLine import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog diff --git a/app/src/main/kotlin/view/utils/SQLiteImportGraphDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/SQLiteImportGraphDialogWindow.kt similarity index 69% rename from app/src/main/kotlin/view/utils/SQLiteImportGraphDialogWindow.kt rename to app/src/main/kotlin/view/components/dialogWindows/SQLiteImportGraphDialogWindow.kt index c3e6b841..0e94ba80 100644 --- a/app/src/main/kotlin/view/utils/SQLiteImportGraphDialogWindow.kt +++ b/app/src/main/kotlin/view/components/dialogWindows/SQLiteImportGraphDialogWindow.kt @@ -1,12 +1,10 @@ -package view.utils +package view.components.dialogWindows import model.io.sql.SQLDatabaseModule import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -15,20 +13,22 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import viewmodel.importGraphAndRender @OptIn(ExperimentalMaterialApi::class) @Composable fun SQLiteImportGraphDialogWindow() { - val selectedGraphID = remember { mutableStateOf(0) } - val closeDialog = remember { mutableStateOf(false) } - val expanded = remember { mutableStateOf(false) } - val importFromDBRequired = remember { mutableStateOf(false) } - val selectedGraphName = remember { mutableStateOf("") } + var selectedGraphID by remember { mutableStateOf(0) } + var closeDialog by remember { mutableStateOf(false) } + var expanded by remember { mutableStateOf(false) } + var importFromDBRequired by remember { mutableStateOf(false) } + var selectedGraphName by remember { mutableStateOf("") } val graphs = remember { mutableStateOf(arrayListOf>()) } - SQLDatabaseModule.getGraphNames(graphs) + val errorMessage = SQLDatabaseModule.getGraphNames(graphs) + if (errorMessage != null) ErrorWindow(errorMessage) {} - if (!closeDialog.value) { + if (!closeDialog) { Dialog( onDismissRequest = {}, properties = DialogProperties(usePlatformDefaultWidth = false) @@ -49,30 +49,30 @@ fun SQLiteImportGraphDialogWindow() { horizontalArrangement = Arrangement.spacedBy(30.dp) ) { ExposedDropdownMenuBox( - expanded = expanded.value, + expanded = expanded, onExpandedChange = { - expanded.value = !expanded.value + expanded = !expanded }, modifier = Modifier.fillMaxWidth().fillMaxHeight() ) { TextField( - value = selectedGraphName.value, + value = selectedGraphName, onValueChange = {}, readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded.value) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, modifier = Modifier.fillMaxWidth().fillMaxHeight(), ) ExposedDropdownMenu( - expanded = expanded.value, - onDismissRequest = { expanded.value = false } + expanded = expanded, + onDismissRequest = { expanded = false } ) { graphs.value.forEach { graphName -> // TODO: fix its layout DropdownMenuItem( onClick = { - selectedGraphName.value = graphName.second - selectedGraphID.value = graphName.first - expanded.value = false + selectedGraphName = graphName.second + selectedGraphID = graphName.first + expanded = false } ) { Text(text = graphName.second) @@ -91,9 +91,9 @@ fun SQLiteImportGraphDialogWindow() { modifier = Modifier.width(145.dp).height(50.dp), colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), onClick = { - importFromDBRequired.value = true - expanded.value = false - closeDialog.value = true + importFromDBRequired = true + expanded = false + closeDialog = true } ) { Text("Import", color = Color.White) @@ -102,7 +102,7 @@ fun SQLiteImportGraphDialogWindow() { } } } - if (importFromDBRequired.value) { - return SQLDatabaseModule.importGraph(selectedGraphID.value) + if (importFromDBRequired) { + return importGraphAndRender(selectedGraphID) } } diff --git a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt b/app/src/main/kotlin/view/components/dialogWindows/SelectInitDialogWindow.kt similarity index 89% rename from app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt rename to app/src/main/kotlin/view/components/dialogWindows/SelectInitDialogWindow.kt index 170d3340..3b58df99 100644 --- a/app/src/main/kotlin/view/utils/SelectInitDialogWindow.kt +++ b/app/src/main/kotlin/view/components/dialogWindows/SelectInitDialogWindow.kt @@ -1,4 +1,4 @@ -package view.utils +package view.components.dialogWindows import MyAppTheme import androidx.compose.foundation.background @@ -15,15 +15,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import viewmodel.graph.SetupGraphViewModel +import viewmodel.graph.GraphViewModelFactory -class SelectInitDialogWindow( - private val showDialog: Boolean, -) { - var showGraphDialog by mutableStateOf(false) - var showCreateGraphDialog by mutableStateOf(false) - var showImportTab by mutableStateOf(false) +class SelectInitDialogWindow { + private var showGraphDialog by mutableStateOf(false) + private var showCreateGraphDialog by mutableStateOf(false) + private var showImportTab by mutableStateOf(false) @Composable fun GraphInitDialogWindow( @@ -81,7 +79,7 @@ class SelectInitDialogWindow( } if (showCreateGraphDialog) { - CreateGraphDialogWindow(SetupGraphViewModel()) + CreateGraphDialogWindow(GraphViewModelFactory) } if (showImportTab) { diff --git a/app/src/main/kotlin/view/graph/EdgeView.kt b/app/src/main/kotlin/view/graph/EdgeView.kt index 74a09597..13d811af 100644 --- a/app/src/main/kotlin/view/graph/EdgeView.kt +++ b/app/src/main/kotlin/view/graph/EdgeView.kt @@ -8,28 +8,19 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.zIndex -import maxEdgeStrokeWidth -import minEdgeStrokeWidth -import viewmodel.WindowViewModel import viewmodel.graph.EdgeViewModel +const val MAX_EDGE_STROKE_WIDTH = 12f +const val MIN_EDGE_STROKE_WIDTH = 4f @Composable fun EdgeView(viewModel: EdgeViewModel, scale: Float) { - val windowVM = WindowViewModel() - windowVM.SetCurrentDimensions() - val firstVertexCenter = viewModel.calculateFirstVertexCenter(scale) - val secondVertexCenter = viewModel.calculateSecondVertexCenter(scale) - - val firstVertexCenterX = firstVertexCenter.first - val firstVertexCenterY = firstVertexCenter.second - val secondVertexCenterX = secondVertexCenter.first - val secondVertexCenterY = secondVertexCenter.second + val (firstVertexCenterX, firstVertexCenterY) = viewModel.calculateFirstVertexCenter(scale) + val (secondVertexCenterX, secondVertexCenterY) = viewModel.calculateSecondVertexCenter(scale) val arrowPoints = viewModel.calculateArrowPoints(scale) @@ -39,7 +30,7 @@ fun EdgeView(viewModel: EdgeViewModel, scale: Float) { Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( color = edgeColor, - strokeWidth = (5f * scale).coerceIn(minEdgeStrokeWidth, maxEdgeStrokeWidth), + strokeWidth = (5f * scale).coerceIn(MIN_EDGE_STROKE_WIDTH, MAX_EDGE_STROKE_WIDTH), start = Offset( firstVertexCenterX.toPx(), diff --git a/app/src/main/kotlin/view/graph/GraphView.kt b/app/src/main/kotlin/view/graph/GraphView.kt index 62557c57..856f6466 100644 --- a/app/src/main/kotlin/view/graph/GraphView.kt +++ b/app/src/main/kotlin/view/graph/GraphView.kt @@ -4,33 +4,28 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.gestures.panBy import androidx.compose.foundation.gestures.rememberTransformableState -import androidx.compose.foundation.gestures.zoomBy -import androidx.compose.foundation.layout.* -import androidx.compose.material.FloatingActionButton -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import viewmodel.graph.GraphViewModel @Composable fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState) { - val offset = remember { mutableStateOf(Offset.Zero) } + var offset by remember { mutableStateOf(Offset.Zero) } val coroutineScope = rememberCoroutineScope { Dispatchers.Default } val updateRequired by remember { derivedStateOf { viewModel.updateIsRequired } } val transformationState = rememberTransformableState { zoomChange, offsetChange, _ -> currentScaleState.value *= zoomChange - offset.value += offsetChange + offset += offsetChange } Box( @@ -48,8 +43,8 @@ fun GraphView(viewModel: GraphViewModel, currentScaleState: MutableState< .graphicsLayer( scaleX = currentScaleState.value, scaleY = currentScaleState.value, - translationX = offset.value.x, - translationY = offset.value.y + translationX = offset.x, + translationY = offset.y ) ) { if (updateRequired.value) { diff --git a/app/src/main/kotlin/view/graph/VertexView.kt b/app/src/main/kotlin/view/graph/VertexView.kt index 3222ea8f..98d4dee8 100644 --- a/app/src/main/kotlin/view/graph/VertexView.kt +++ b/app/src/main/kotlin/view/graph/VertexView.kt @@ -23,20 +23,18 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import maxVertexRadius -import minVertexRadius -import viewmodel.WindowViewModel import viewmodel.graph.VertexViewModel +val MAX_VERTEX_RADIUS = 35.dp +val MIN_VERTEX_RADIUS = 7.dp + @Composable fun VertexView(viewModel: VertexViewModel, scale: Float) { val coroutineScope = rememberCoroutineScope { Dispatchers.Default } - val windowVM = WindowViewModel() - windowVM.SetCurrentDimensions() val adjustedX = viewModel.x.value val adjustedY = viewModel.y.value - var adjustedRadius = (viewModel.radius * scale).coerceIn(minVertexRadius, maxVertexRadius) + val adjustedRadius = (viewModel.radius * scale).coerceIn(MIN_VERTEX_RADIUS, MAX_VERTEX_RADIUS) val highlightColor by remember { derivedStateOf { viewModel.highlightColor } } val borderColor = highlightColor.value @@ -61,7 +59,7 @@ fun VertexView(viewModel: VertexViewModel, scale: Float) { } } detectTapGestures( - onTap = { viewModel.isSelected.value = !viewModel.isSelected.value } + onTap = { viewModel.switchSelection() } ) }, contentAlignment = Alignment.Center diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index 8c883ca9..c6a8e15e 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -1,32 +1,29 @@ package view.tabScreen -import JSON -import NEO4J -import SQLITE import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import viewmodel.graph.GraphViewModel -import androidx.compose.runtime.* -import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Dialog import kotlinx.coroutines.delay import model.io.neo4j.Neo4jRepositoryHandler import model.io.sql.SQLDatabaseModule -import view.utils.* +import view.components.dialogWindows.* +import viewmodel.graph.GraphViewModel import java.awt.FileDialog import java.awt.Frame +enum class DatabaseTypes { + SQLite, NEO4J, JSON +} + @OptIn(ExperimentalMaterialApi::class) @Composable fun FileControlTab(graphVM: GraphViewModel) { @@ -38,8 +35,7 @@ fun FileControlTab(graphVM: GraphViewModel) { var showEditDialog by remember { mutableStateOf(false) } var showNeo4jDialog by remember { mutableStateOf(false) } - val databases = arrayOf(SQLITE, NEO4J, JSON) - var selectedDatabase by remember { mutableStateOf(databases[0]) } + var selectedDatabase by remember { mutableStateOf(DatabaseTypes.SQLite) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { Row(modifier = Modifier.height(0.dp)) {} @@ -98,7 +94,7 @@ fun FileControlTab(graphVM: GraphViewModel) { modifier = Modifier.width(fieldWidth).fillMaxHeight() ) { TextField( - value = selectedDatabase, + value = selectedDatabase.toString(), onValueChange = { graphName = it }, readOnly = true, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, @@ -114,7 +110,7 @@ fun FileControlTab(graphVM: GraphViewModel) { expanded = false } ) { - databases.forEach { db -> + DatabaseTypes.entries.forEach { db -> DropdownMenuItem( modifier = Modifier, onClick = { @@ -122,13 +118,13 @@ fun FileControlTab(graphVM: GraphViewModel) { expanded = false } ) { - Text(text = db) + Text(db.toString()) } } } } - if (selectedDatabase == JSON) { + if (selectedDatabase == DatabaseTypes.JSON) { val fileDialog = FileDialog(null as Frame?, "Select File to Open") fileDialog.mode = FileDialog.LOAD Button( @@ -189,17 +185,18 @@ fun FileControlTab(graphVM: GraphViewModel) { } if (showSaveDialog) { - if (selectedDatabase == SQLITE) { + if (selectedDatabase == DatabaseTypes.SQLite) { val existingGraphNamesSQL = remember { mutableStateOf(arrayListOf>()) } - SQLDatabaseModule.getGraphNames(existingGraphNamesSQL) + val sqlErrorMessage = SQLDatabaseModule.getGraphNames(existingGraphNamesSQL) + if (sqlErrorMessage != null) ErrorWindow(sqlErrorMessage) {} if (existingGraphNamesSQL.value.any { it.second == graphName }) { showErrorWindow = true - errorMessage = "Graph with name: ${graphName} already exists" + errorMessage = "Graph with name: $graphName already exists" graphName = "" showSaveDialog = false } else { - SQLDatabaseModule.insertGraph(graphVM, graphName, graphVM.graphType.value) + SQLDatabaseModule.insertGraph(graphVM, graphName, graphVM.graphType) Dialog( onDismissRequest = { showSaveDialog = false @@ -223,7 +220,7 @@ fun FileControlTab(graphVM: GraphViewModel) { showSaveDialog = false } } - } else if (selectedDatabase == NEO4J) { + } else if (selectedDatabase == DatabaseTypes.NEO4J) { if (!Neo4jRepositoryHandler.isRepoInit) { showSaveDialog = false showNeo4jDialog = true @@ -233,7 +230,7 @@ fun FileControlTab(graphVM: GraphViewModel) { errorMessage = "$graphName is an invalid name." graphName = "" } else { - Neo4jRepositoryHandler.saveOrReplace(graphVM.graph, graphName, graphVM.isDirected.value, graphVM.isWeighted.value) + Neo4jRepositoryHandler.saveOrReplace(graphVM.graph, graphName, graphVM.isDirected, graphVM.isWeighted) Dialog( onDismissRequest = { @@ -262,8 +259,9 @@ fun FileControlTab(graphVM: GraphViewModel) { if (showLoadDialog) { when (selectedDatabase) { - SQLITE -> SQLiteImportGraphDialogWindow() - NEO4J -> Neo4jImportGraphDialogWindow { showLoadDialog = false } + DatabaseTypes.SQLite -> SQLiteImportGraphDialogWindow() + DatabaseTypes.NEO4J -> Neo4jImportGraphDialogWindow { showLoadDialog = false } + DatabaseTypes.JSON -> TODO() } } @@ -285,6 +283,7 @@ fun FileControlTab(graphVM: GraphViewModel) { } } +// TODO private fun isValidNeo4jName(name: String): Boolean { if (name.isEmpty()) return false for (i in name.indices) { diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index a87f8384..ca25a02c 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -11,12 +11,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import view.utils.ErrorWindow -import viewmodel.WindowViewModel +import view.components.dialogWindows.ErrorWindow import viewmodel.graph.GraphViewModel @Composable @@ -28,8 +26,7 @@ fun GeneralTab(graphVM: GraphViewModel) { var firstVertexId by remember { mutableStateOf("") } var secondVertexId by remember { mutableStateOf("") } var secondVertexData by remember { mutableStateOf("") } - var changesWereMade by remember { mutableStateOf(false) } - val showErrorWindow = remember { mutableStateOf(false) } + var showErrorWindow by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { Row(modifier = Modifier.height(0.dp)) {} @@ -126,7 +123,7 @@ fun GeneralTab(graphVM: GraphViewModel) { secondVertexId = "" firstVertexId = "" } else { - showErrorWindow.value = true + showErrorWindow = true } }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) @@ -167,7 +164,7 @@ fun GeneralTab(graphVM: GraphViewModel) { ) { Column( modifier = - Modifier.background(Color.White).padding(16.dp).width(350.dp).height(200.dp) + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) ) { if (graphVM.verticesVM.isEmpty()) { Text("Input data of second vertex to create and connect with") @@ -261,7 +258,7 @@ fun GeneralTab(graphVM: GraphViewModel) { } } } - if (showErrorWindow.value) { - ErrorWindow("No such Vertex", { showErrorWindow.value = false }) + if (showErrorWindow) { + ErrorWindow("No such Vertex") { showErrorWindow = false } } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt index 1d0fc7ad..fc863f6e 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/AnalyzeTab.kt @@ -7,12 +7,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import model.graphs.DirectedGraph -import model.graphs.UndirectedGraph -import model.graphs.WeightedDirectedGraph -import model.graphs.WeightedUndirectedGraph import view.tabScreen.analyzeTab.algorithmsUI.* +import viewmodel.graph.Algorithm import viewmodel.graph.GraphViewModel +import viewmodel.graph.getAlgorithmDisplayName val rowHeight = 75.dp val borderPadding = 10.dp @@ -21,31 +19,8 @@ val horizontalGap = 20.dp @OptIn(ExperimentalMaterialApi::class) @Composable fun AnalyzeTab(graphVM: GraphViewModel) { - val algorithms = mutableListOf( - "Layout", - "Find communities", - "Find key vertices" - ) - - if (graphVM.graph is DirectedGraph) { - algorithms += "Find SCCs" - algorithms += "Find cycles" - } - - if (graphVM.graph is UndirectedGraph) { - algorithms += "Find bridges" - } - - if (graphVM.graph is WeightedUndirectedGraph) { - algorithms += "Min spanning tree" - algorithms += "Find shortest path" - } - - if (graphVM.graph is WeightedDirectedGraph) { - algorithms += "Find shortest path" - } - - var selectedAlgorithm by remember { mutableStateOf(algorithms[0]) } + val algorithms = graphVM.getAvailableAlgorithms() + var selectedAlgorithm by remember { mutableStateOf(algorithms.first()) } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(15.dp)) { Row(modifier = Modifier.height(0.dp)) {} @@ -75,7 +50,7 @@ fun AnalyzeTab(graphVM: GraphViewModel) { modifier = Modifier.width(225.dp).fillMaxHeight() ) { TextField( - value = selectedAlgorithm, + value = getAlgorithmDisplayName(selectedAlgorithm), onValueChange = {}, readOnly = true, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, @@ -99,7 +74,7 @@ fun AnalyzeTab(graphVM: GraphViewModel) { expanded = false } ) { - Text(text = algorithm) + Text(text = getAlgorithmDisplayName(algorithm)) } } } @@ -108,14 +83,15 @@ fun AnalyzeTab(graphVM: GraphViewModel) { } when (selectedAlgorithm) { - "Layout" -> { LayoutUI(graphVM) } - "Find communities" -> { CommunitiesUI(graphVM) } - "Find key vertices" -> { KeyVerticesUI(graphVM) } - "Find shortest path" -> { ShortestPathUI(graphVM) } - "Find cycles" -> { CyclesUI(graphVM) } - "Find bridges" -> { BridgesUI(graphVM) } - "Find SCCs" -> { SCCUI(graphVM) } - "Min spanning tree" -> { MinSpanningTreeUI(graphVM) } + Algorithm.LAYOUT -> { LayoutUI(graphVM) } + Algorithm.FIND_COMMUNITIES -> { CommunitiesUI(graphVM) } + Algorithm.FIND_KEY_VERTICES -> { KeyVerticesUI(graphVM) } + Algorithm.FIND_SHORTEST_PATH -> { ShortestPathUI(graphVM) } + Algorithm.FIND_CYCLES -> { CyclesUI(graphVM) } + Algorithm.FIND_BRIDGES -> { BridgesUI(graphVM) } + Algorithm.FIND_SCCS -> { SCCUI(graphVM) } + Algorithm.MIN_SPANNING_TREE -> { MinSpanningTreeUI(graphVM) } } } } + diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt index 25372106..66f7b904 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/BridgesUI.kt @@ -1,44 +1,10 @@ package view.tabScreen.analyzeTab.algorithmsUI -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import view.tabScreen.analyzeTab.borderPadding -import view.tabScreen.analyzeTab.horizontalGap -import view.tabScreen.analyzeTab.rowHeight -import view.utils.ErrorWindow +import view.components.RunAlgoButton import viewmodel.graph.GraphViewModel @Composable fun BridgesUI(graphVM: GraphViewModel) { - val showErrorWindow = remember { mutableStateOf(false) } - - Row( - modifier = Modifier.height(rowHeight).padding(borderPadding), - horizontalArrangement = Arrangement.spacedBy(horizontalGap) - ) { - Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { - Button( - modifier = Modifier.fillMaxSize(), - onClick = { - if (!graphVM.findBridges()) { - showErrorWindow.value = true - } - }, - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) - ) { - Text("Run algorithm") - } - } - } - - if (showErrorWindow.value) { - ErrorWindow("No bridges were found", { showErrorWindow.value = false }) - } + RunAlgoButton("No bridges were found") { graphVM.findBridges() } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt index d4de43d2..ed3a5e28 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CommunitiesUI.kt @@ -1,44 +1,10 @@ package view.tabScreen.analyzeTab.algorithmsUI -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import view.tabScreen.analyzeTab.borderPadding -import view.tabScreen.analyzeTab.horizontalGap -import view.tabScreen.analyzeTab.rowHeight -import view.utils.ErrorWindow +import view.components.RunAlgoButton import viewmodel.graph.GraphViewModel @Composable fun CommunitiesUI(graphVM: GraphViewModel) { - val showErrorWindow = remember { mutableStateOf(false) } - - Row( - modifier = Modifier.height(rowHeight).padding(borderPadding), - horizontalArrangement = Arrangement.spacedBy(horizontalGap) - ) { - Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { - Button( - modifier = Modifier.fillMaxSize(), - onClick = { - if (!graphVM.findCommunities()) { - showErrorWindow.value = true - } - }, - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) - ) { - Text("Run algorithm") - } - } - } - - if (showErrorWindow.value) { - ErrorWindow("No communities were found", { showErrorWindow.value = false }) - } + RunAlgoButton("No communities were found") { graphVM.findCommunities() } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt index 7cf08e08..69b20479 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/CyclesUI.kt @@ -13,14 +13,14 @@ import androidx.compose.ui.unit.sp import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight -import view.utils.ErrorWindow +import view.components.dialogWindows.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun CyclesUI(graphVM: GraphViewModel) { var vertexId by remember { mutableStateOf("") } - val showErrorWindow = remember { mutableStateOf(false) } - val errorMessage = remember { mutableStateOf("") } + var showErrorWindow by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } Row( modifier = Modifier.height(rowHeight).padding(borderPadding), @@ -57,20 +57,20 @@ fun CyclesUI(graphVM: GraphViewModel) { modifier = Modifier.fillMaxSize(), onClick = { if (vertexId.isEmpty()) { - errorMessage.value = "Enter vertex's ID" - showErrorWindow.value = true + errorMessage = "Enter vertex's ID" + showErrorWindow = true } else if (!vertexId.all { char -> char.isDigit() }) { - errorMessage.value = "ID should be a number" - showErrorWindow.value = true + errorMessage = "ID should be a number" + showErrorWindow = true } else if (vertexId.toInt() > graphVM.graph.getVertices().size - 1) { - errorMessage.value = "No vertex with ID $vertexId" - showErrorWindow.value = true + errorMessage = "No vertex with ID $vertexId" + showErrorWindow = true } else if (!graphVM.findCycles(vertexId.toInt())) { - errorMessage.value = "No cycles were found" - showErrorWindow.value = true + errorMessage = "No cycles were found" + showErrorWindow = true } else { graphVM.highlighNextCycle() @@ -91,8 +91,8 @@ fun CyclesUI(graphVM: GraphViewModel) { modifier = Modifier.fillMaxSize(), onClick = { if (!graphVM.highlighNextCycle()) { - errorMessage.value = "Please run algorithm first" - showErrorWindow.value = true + errorMessage = "Please run algorithm first" + showErrorWindow = true } }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) @@ -102,7 +102,7 @@ fun CyclesUI(graphVM: GraphViewModel) { } } - if (showErrorWindow.value) { - ErrorWindow(errorMessage.value, { showErrorWindow.value = false }) + if (showErrorWindow) { + ErrorWindow(errorMessage) { showErrorWindow = false } } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt index 05239aba..149a4f66 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/KeyVerticesUI.kt @@ -1,44 +1,10 @@ package view.tabScreen.analyzeTab.algorithmsUI -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import view.tabScreen.analyzeTab.borderPadding -import view.tabScreen.analyzeTab.horizontalGap -import view.tabScreen.analyzeTab.rowHeight -import view.utils.ErrorWindow +import view.components.RunAlgoButton import viewmodel.graph.GraphViewModel @Composable fun KeyVerticesUI(graphVM: GraphViewModel) { - val showErrorWindow = remember { mutableStateOf(false) } - - Row( - modifier = Modifier.height(rowHeight).padding(borderPadding), - horizontalArrangement = Arrangement.spacedBy(horizontalGap) - ) { - Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { - Button( - modifier = Modifier.fillMaxSize(), - onClick = { - if (!graphVM.findKeyVertices()) { - showErrorWindow.value = true - } - }, - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) - ) { - Text("Run algorithm") - } - } - } - - if (showErrorWindow.value) { - ErrorWindow("No key vertices were found", { showErrorWindow.value = false }) - } + RunAlgoButton("No key vertices were found") { graphVM.findKeyVertices() } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt index 847e7576..acad8c9c 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/MinSpanningTreeUI.kt @@ -1,44 +1,10 @@ package view.tabScreen.analyzeTab.algorithmsUI -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import view.tabScreen.analyzeTab.borderPadding -import view.tabScreen.analyzeTab.horizontalGap -import view.tabScreen.analyzeTab.rowHeight -import view.utils.ErrorWindow +import view.components.RunAlgoButton import viewmodel.graph.GraphViewModel @Composable fun MinSpanningTreeUI(graphVM: GraphViewModel) { - val showErrorWindow = remember { mutableStateOf(false) } - - Row( - modifier = Modifier.height(rowHeight).padding(borderPadding), - horizontalArrangement = Arrangement.spacedBy(horizontalGap) - ) { - Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { - Button( - modifier = Modifier.fillMaxSize(), - onClick = { - if (!graphVM.findMinSpanningTree()) { - showErrorWindow.value = true - } - }, - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) - ) { - Text("Run algorithm") - } - } - } - - if (showErrorWindow.value) { - ErrorWindow("No min spanning tree was found", { showErrorWindow.value = false }) - } + RunAlgoButton("No min spanning tree was found") { graphVM.findMinSpanningTree() } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt index 0dfba1ff..785a1079 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/SCCUI.kt @@ -1,44 +1,10 @@ package view.tabScreen.analyzeTab.algorithmsUI -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import view.tabScreen.analyzeTab.borderPadding -import view.tabScreen.analyzeTab.horizontalGap -import view.tabScreen.analyzeTab.rowHeight -import view.utils.ErrorWindow +import view.components.RunAlgoButton import viewmodel.graph.GraphViewModel @Composable fun SCCUI(graphVM: GraphViewModel) { - val showErrorWindow = remember { mutableStateOf(false) } - - Row( - modifier = Modifier.height(rowHeight).padding(borderPadding), - horizontalArrangement = Arrangement.spacedBy(horizontalGap) - ) { - Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Center) { - Button( - modifier = Modifier.fillMaxSize(), - onClick = { - if (!graphVM.findSCCs()) { - showErrorWindow.value = true - } - }, - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) - ) { - Text("Run algorithm") - } - } - } - - if (showErrorWindow.value) { - ErrorWindow("No SCCs were found", { showErrorWindow.value = false }) - } + RunAlgoButton("No SCCs were found") { graphVM.findSCCs() } } diff --git a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt index f6de6c9d..c130ae2c 100644 --- a/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt +++ b/app/src/main/kotlin/view/tabScreen/analyzeTab/algorithmsUI/ShortestPathUI.kt @@ -13,15 +13,15 @@ import androidx.compose.ui.unit.sp import view.tabScreen.analyzeTab.borderPadding import view.tabScreen.analyzeTab.horizontalGap import view.tabScreen.analyzeTab.rowHeight -import view.utils.ErrorWindow +import view.components.dialogWindows.ErrorWindow import viewmodel.graph.GraphViewModel @Composable fun ShortestPathUI(graphVM: GraphViewModel) { var sourceVertexId by remember { mutableStateOf("") } var destVertexId by remember { mutableStateOf("") } - val showErrorWindow = remember { mutableStateOf(false) } - val errorMessage = remember { mutableStateOf("") } + var showErrorWindow by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } Row( modifier = Modifier.height(rowHeight).padding(borderPadding), @@ -79,30 +79,30 @@ fun ShortestPathUI(graphVM: GraphViewModel) { modifier = Modifier.fillMaxSize(), onClick = { if (sourceVertexId.isEmpty() || destVertexId.isEmpty()) { - errorMessage.value = "Enter vertices' IDs" - showErrorWindow.value = true + errorMessage = "Enter vertices' IDs" + showErrorWindow = true } else if ( !sourceVertexId.all { char -> char.isDigit() } || !destVertexId.all { char -> char.isDigit() } ) { - errorMessage.value = "ID should be a number" - showErrorWindow.value = true + errorMessage = "ID should be a number" + showErrorWindow = true } else if (sourceVertexId == destVertexId) { - errorMessage.value = "Vertices' IDs should be different" - showErrorWindow.value = true + errorMessage = "Vertices' IDs should be different" + showErrorWindow = true } else if ( sourceVertexId.toInt() > graphVM.graph.getVertices().size - 1 || destVertexId.toInt() > graphVM.graph.getVertices().size - 1 ) { - errorMessage.value = "No vertex with such ID" - showErrorWindow.value = true + errorMessage = "No vertex with such ID" + showErrorWindow = true } else if (!graphVM.findShortestPath(sourceVertexId.toInt(), destVertexId.toInt())) { - errorMessage.value = "Shortest path doesn't exist" - showErrorWindow.value = true + errorMessage = "Shortest path doesn't exist" + showErrorWindow = true } }, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary) @@ -112,7 +112,7 @@ fun ShortestPathUI(graphVM: GraphViewModel) { } } - if (showErrorWindow.value) { - ErrorWindow(errorMessage.value, { showErrorWindow.value = false }) + if (showErrorWindow) { + ErrorWindow(errorMessage) { showErrorWindow = false } } } diff --git a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt b/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt deleted file mode 100644 index a09964b0..00000000 --- a/app/src/main/kotlin/view/utils/CreateGraphDialogWindow.kt +++ /dev/null @@ -1,238 +0,0 @@ -package view.utils - -import MyAppTheme -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import viewmodel.graph.SetupGraphViewModel - -@Composable -fun CreateGraphDialogWindow(viewModel: SetupGraphViewModel) { - val closeDialog = remember { mutableStateOf(false) } - val selectedStoredDataIndex = remember { mutableStateOf(0) } - val selectedOrientationIndex = remember { mutableStateOf(0) } - val selectedWeightinessIndex = remember { mutableStateOf(0) } - val createGraphClicked = remember { mutableStateOf(false) } - - MyAppTheme { - if (!closeDialog.value) { - Dialog( - onDismissRequest = {}, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - Column( - modifier = - Modifier.background(Color.White).padding(16.dp).width(700.dp).height(290.dp) - ) { - Text( - "Create Graph", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - modifier = Modifier.padding(bottom = 10.dp) - ) - Row(modifier = Modifier.fillMaxWidth().height(200.dp)) { - Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { - Text("Select stored data:") - - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - - val radioOptions = listOf("Integer", "UInteger", "String") - - Column(modifier = Modifier.width(220.dp)) { - radioOptions.forEachIndexed { index, option -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedStoredDataIndex.value = index - } - ) { - RadioButton( - selected = selectedStoredDataIndex.value == index, - onClick = { selectedStoredDataIndex.value = index }, - colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary - ) - ) - Spacer(Modifier.width(1.dp)) - Text( - text = option, - style = TextStyle(fontSize = 16.sp), - color = - if (selectedStoredDataIndex.value == index) Color.Black - else Color.Gray - ) - } - } - } - } - - Column(modifier = Modifier.width(250.dp).fillMaxHeight()) { - Text("Select the orientation:") - - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - - val radioOptions = listOf("Undirected", "Directed") - Column(modifier = Modifier.width(220.dp)) { - radioOptions.forEachIndexed { index, option -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedOrientationIndex.value = index - } - ) { - RadioButton( - selected = selectedOrientationIndex.value == index, - onClick = { selectedOrientationIndex.value = index }, - colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary - ) - ) - Spacer(Modifier.width(1.dp)) - Text( - text = option, - style = TextStyle(fontSize = 16.sp), - color = - if (selectedOrientationIndex.value == index) Color.Black - else Color.Gray - ) - } - } - } - } - - Column(modifier = Modifier.width(250.dp).height(200.dp)) { - Text("Select the weightiness:") - - Row(modifier = Modifier.height(20.dp).fillMaxWidth()) {} - - Column(modifier = Modifier.width(220.dp)) { - val radioOptions = listOf("Unweighted", "Weighted") - - radioOptions.forEachIndexed { index, option -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.padding(vertical = 4.dp).fillMaxWidth().clickable { - selectedWeightinessIndex.value = index - } - ) { - RadioButton( - selected = selectedWeightinessIndex.value == index, - onClick = { selectedWeightinessIndex.value = index }, - colors = - RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colors.secondary - ) - ) - Spacer(Modifier.width(1.dp)) - Text( - text = option, - style = TextStyle(fontSize = 16.sp), - color = - if (selectedWeightinessIndex.value == index) Color.Black - else Color.Gray - ) - } - } - } - } - } - - Row( - modifier = Modifier.padding(10.dp).fillMaxWidth().height(50.dp), - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.End - ) { - Button( - modifier = Modifier.width(145.dp).height(50.dp), - colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary), - onClick = { - closeDialog.value = true - createGraphClicked.value = true - } - - ) { - Text("Apply", color = Color.White) - } - } - } - } - } - if (createGraphClicked.value) { - createGraphFromTypesIndices( - viewModel, - selectedStoredDataIndex.value, - selectedOrientationIndex.value, - selectedWeightinessIndex.value - ) - } - } -} - - -@Composable -fun createGraphFromTypesIndices( - viewModel: SetupGraphViewModel, - storedDataIndex: Int, - orientationIndex: Int, - weightnessIndex: Int -) { - val storedData = when (storedDataIndex) { - 0 -> SetupGraphViewModel.GraphType.Integer - 1 -> SetupGraphViewModel.GraphType.UInteger - 2 -> SetupGraphViewModel.GraphType.String - else -> SetupGraphViewModel.GraphType.Integer // default to integer - } - - val graphStructure = when (orientationIndex) { - 0 -> SetupGraphViewModel.GraphStructure.Undirected - 1 -> SetupGraphViewModel.GraphStructure.Directed - else -> SetupGraphViewModel.GraphStructure.Undirected // default to undirected - } - - val weight = when (weightnessIndex) { - 0 -> SetupGraphViewModel.Weight.Unweighted - 1 -> SetupGraphViewModel.Weight.Weighted - else -> SetupGraphViewModel.Weight.Unweighted // default to unweighted - } - - return viewModel.createGraphAndApplyScreen(storedData, graphStructure, weight) -} - -fun getGraphVMParameter(storedDataType: Int, structureType: Int, weightType: Int): Triple { - val storedData = when (storedDataType) { - 0 -> SetupGraphViewModel.GraphType.Integer - 1 -> SetupGraphViewModel.GraphType.UInteger - 2 -> SetupGraphViewModel.GraphType.String - else -> SetupGraphViewModel.GraphType.Integer // default to integer - } - - val graphStructure = when (structureType) { - 0 -> SetupGraphViewModel.GraphStructure.Undirected - 1 -> SetupGraphViewModel.GraphStructure.Directed - else -> SetupGraphViewModel.GraphStructure.Undirected // default to directed - } - - val weight = when (weightType) { - 0 -> SetupGraphViewModel.Weight.Unweighted - 1 -> SetupGraphViewModel.Weight.Weighted - else -> SetupGraphViewModel.Weight.Unweighted // default to weighted - } - - return Triple(storedData, graphStructure, weight) -} diff --git a/app/src/main/kotlin/model/FastFourierTransform.kt b/app/src/main/kotlin/viewmodel/FastFourierTransform.kt similarity index 99% rename from app/src/main/kotlin/model/FastFourierTransform.kt rename to app/src/main/kotlin/viewmodel/FastFourierTransform.kt index 30a20866..84d39077 100644 --- a/app/src/main/kotlin/model/FastFourierTransform.kt +++ b/app/src/main/kotlin/viewmodel/FastFourierTransform.kt @@ -1,4 +1,4 @@ -package model +package viewmodel /** * Copyright (C) 2021 José Alexandre Nalon diff --git a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 5e134f45..9ff3acec 100644 --- a/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/app/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -1,29 +1,28 @@ package viewmodel -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import model.graphs.abstractGraph.Graph -import model.io.neo4j.Neo4jRepository -import org.neo4j.driver.AuthTokens -import org.neo4j.driver.GraphDatabase import viewmodel.graph.GraphViewModel class MainScreenViewModel( graph: Graph, - currentGraphType: String, - existingGraphViewModel: GraphViewModel? = null + dataType: String, + existingGraphViewModel: GraphViewModel? = null, ) { - val showVerticesData = mutableStateOf(false) - val showVerticesIds = mutableStateOf(false) - val graphType = mutableStateOf(currentGraphType) + private val showVerticesData = mutableStateOf(false) + private val showVerticesIds = mutableStateOf(false) + val graphType = simplifyGraphString(graph.toString()) + "\nData type: " + dataType - fun setDirectionState(currentGraphType: String): MutableState { - return mutableStateOf(currentGraphType.contains("Directed")) + private fun simplifyGraphString(graphString: String): String { + return graphString.substringAfterLast('.').substringBefore('@') } - fun setWeightinessState(currentGraphType: String): MutableState { - return mutableStateOf(currentGraphType.contains("Weighted")) + private fun setDirectionState(currentGraphType: String): Boolean { + return currentGraphType.contains("Directed") + } + + private fun setWeightinessState(currentGraphType: String): Boolean { + return currentGraphType.contains("Weighted") } var graphViewModel: GraphViewModel = existingGraphViewModel @@ -32,7 +31,7 @@ class MainScreenViewModel( showVerticesIds, showVerticesData, graphType, - setDirectionState(currentGraphType), - setWeightinessState(currentGraphType) + setDirectionState(graph.toString()), + setWeightinessState(graph.toString()) ) } diff --git a/app/src/main/kotlin/viewmodel/SQLiteViewModel.kt b/app/src/main/kotlin/viewmodel/SQLiteViewModel.kt new file mode 100644 index 00000000..5126420c --- /dev/null +++ b/app/src/main/kotlin/viewmodel/SQLiteViewModel.kt @@ -0,0 +1,66 @@ +package viewmodel + +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.* +import model.graphs.abstractGraph.Graph +import model.io.sql.SQLDatabaseModule +import view.MainScreen +import view.components.dialogWindows.ErrorWindow +import viewmodel.graph.GraphViewModel +import viewmodel.graph.GraphViewModelFactory +import java.sql.SQLException +import kotlin.system.exitProcess + +@Suppress("UNCHECKED_CAST") +@Composable +fun importGraphAndRender(graphId: Int) { + val graphVMState = remember { mutableStateOf?>(null) } + var showErrorMessage by remember { mutableStateOf(false) } + var updateIsRequired by remember { mutableStateOf(false) } + var currentGraphSetup: Pair, String>? = null + + try { + currentGraphSetup = SQLDatabaseModule.importGraph(graphId, currentGraphSetup) + if (currentGraphSetup == null) showErrorMessage = true + + // Execute side-effect to create graph object + GraphViewModelFactory.createGraphObject( + currentGraphSetup?.first?.first as GraphViewModelFactory.GraphType, + currentGraphSetup?.first?.second as GraphViewModelFactory.GraphStructure, + currentGraphSetup?.first?.third as GraphViewModelFactory.Weight, + graphId, + graphVMState as MutableState>?> + ) + updateIsRequired = true + + } catch (e: SQLException) { + e.printStackTrace() + showErrorMessage = true + } + + if (updateIsRequired) return importGraphUI(showErrorMessage, graphVMState, graphId) +} + +@Composable +fun importGraphUI( + showErrorMessage: Boolean, + graphVMState: MutableState?>, + graphId: Int +) { + if (showErrorMessage) { + ErrorWindow("Graph with ID $graphId not found.") {} + } + if (graphVMState.value != null) { + graphVMState.value?.updateIsRequired?.value = true + + MainScreen( + MainScreenViewModel( + graphVMState.value?.graph as Graph, + graphVMState.value?.graphType as String, + graphVMState.value + ) + ) + + } else CircularProgressIndicator() +} \ No newline at end of file diff --git a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt b/app/src/main/kotlin/viewmodel/TFDPLayout.kt similarity index 91% rename from app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt rename to app/src/main/kotlin/viewmodel/TFDPLayout.kt index c2393d0c..6e756c6f 100644 --- a/app/src/main/kotlin/viewmodel/graph/TFDPLayout.kt +++ b/app/src/main/kotlin/viewmodel/TFDPLayout.kt @@ -1,21 +1,14 @@ -package viewmodel.graph +package viewmodel import androidx.compose.ui.unit.dp -import model.Complex -import model.recursiveFFT +import viewmodel.graph.VertexViewModel import kotlin.math.pow import kotlin.math.sqrt import kotlin.random.Random -class TFDPLayout() { - /** - * longRangeAttractionConstant - strength of attractive force (long-range) - B - * nearAttractionConstant - strength of attractive t-force (near) - A - * repulsiveConstant - extent and magnitude of the repulsive t-force that - controls the longest distance of neighbors in the layout - Y - **/ +object TFDPLayout { - fun fft2D(input: Array>): Array> { + private fun fft2D(input: Array>): Array> { val rows = input.size val cols = input[0].size val output = Array(rows) { Array(cols) { Complex(0.0, 0.0) } } @@ -45,7 +38,7 @@ class TFDPLayout() { return output } - fun ifft2D(input: Array>): Array> { + private fun ifft2D(input: Array>): Array> { val rows = input.size val cols = input[0].size val output = Array(rows) { Array(cols) { Complex() } } @@ -83,7 +76,22 @@ class TFDPLayout() { return output } - fun place(width: Double, height: Double, vertices: Collection>, gridSize: Int = 128, longRangeAttractionConstant: Double, nearAttractionConstant: Double, repulsiveConstant: Double) { + /** + * longRangeAttractionConstant - strength of attractive force (long-range) - B + * nearAttractionConstant - strength of attractive t-force (near) - A + * repulsiveConstant - extent and magnitude of the repulsive t-force that + controls the longest distance of neighbors in the layout - Y + **/ + + fun place( + width: Double, + height: Double, + vertices: Collection>, + gridSize: Int = 128, + longRangeAttractionConstant: Double, + nearAttractionConstant: Double, + repulsiveConstant: Double + ) { val forces = Array(vertices.size) { Pair(0.0, 0.0) } val grid = Array(gridSize) { Array(gridSize) { Complex(0.0, 0.0) } } val deltaX = width / gridSize diff --git a/app/src/main/kotlin/viewmodel/WindowViewModel.kt b/app/src/main/kotlin/viewmodel/WindowViewModel.kt deleted file mode 100644 index 2c2672cc..00000000 --- a/app/src/main/kotlin/viewmodel/WindowViewModel.kt +++ /dev/null @@ -1,29 +0,0 @@ -package viewmodel - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -class WindowViewModel( - private var width: MutableState = mutableStateOf(0.dp), - private var height: MutableState = mutableStateOf(0.dp), -) { - - @OptIn(ExperimentalComposeUiApi::class) - @Composable - fun SetCurrentDimensions() { - val configuration = LocalWindowInfo.current.containerSize - height.value = configuration.height.dp - width.value = configuration.width.dp - } - - val getHeight - get() = height.value - - val getWidth - get() = width.value -} diff --git a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index e1154689..7ce68446 100644 --- a/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -1,42 +1,43 @@ package viewmodel.graph -import androidx.compose.runtime.State -import androidx.compose.ui.unit.Dp -import ARROW_DEPTH -import ARROW_SIZE -import SQRT_3 import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import kotlin.math.sqrt +const val ARROW_SIZE = 20f +const val ARROW_DEPTH = 2.5f +const val SQRT_3 = 1.732f + class EdgeViewModel( - val firstVertex: VertexViewModel, - val secondVertex: VertexViewModel, - private val isDirected: State + private val firstVertex: VertexViewModel, + private val secondVertex: VertexViewModel, + private val isDirected: Boolean ) { - fun isDirected() = isDirected.value + fun isDirected() = isDirected - private val radius = firstVertex.radius + private val vertexRadius: Dp + get() = firstVertex.radius var highlightColor = mutableStateOf(Color.LightGray) internal fun calculateFirstVertexCenter(scale: Float): Pair { - val x = firstVertex.x.value + radius * scale - val y = firstVertex.y.value + radius * scale + val x = firstVertex.x.value + vertexRadius * scale + val y = firstVertex.y.value + vertexRadius * scale return Pair(x, y) } internal fun calculateSecondVertexCenter(scale: Float): Pair { - val x = secondVertex.x.value + radius * scale - val y = secondVertex.y.value + radius * scale + val x = secondVertex.x.value + vertexRadius * scale + val y = secondVertex.y.value + vertexRadius * scale return Pair(x, y) } internal fun calculateArrowPoints(scale: Float): List> { - if (!isDirected.value) return listOf() + if (!isDirected) return listOf() val firstVertexCenterX = calculateFirstVertexCenter(scale).first val firstVertexCenterY = calculateFirstVertexCenter(scale).second @@ -59,8 +60,8 @@ class EdgeViewModel( val bX = normedVectorX * SQRT_3 / 2 + normedVectorY * 1 / 2 val bY = -normedVectorX * 1 / 2 + normedVectorY * SQRT_3 / 2 - val arrowEndPointX = secondVertexCenterX - normedVectorX * (radius.value - ARROW_DEPTH) * scale - val arrowEndPointY = secondVertexCenterY - normedVectorY * (radius.value - ARROW_DEPTH) * scale + val arrowEndPointX = secondVertexCenterX - normedVectorX * (vertexRadius.value - ARROW_DEPTH) * scale + val arrowEndPointY = secondVertexCenterY - normedVectorY * (vertexRadius.value - ARROW_DEPTH) * scale val arrowLeftPointX = arrowEndPointX - aX * ARROW_SIZE * scale val arrowLeftPointY = arrowEndPointY - aY * ARROW_SIZE * scale diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index abda5039..11ff8ee2 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -3,29 +3,31 @@ package viewmodel.graph import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import model.graphs.abstractGraph.Edge -import model.graphs.abstractGraph.Graph -import model.graphs.abstractGraph.Vertex -import model.io.neo4j.Neo4jRepository -import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color -import model.graphs.* -import model.graphs.abstractGraph.* import model.algorithms.* import model.algorithms.clustering.CommunitiesFinder +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Edge +import model.graphs.abstractGraph.Graph +import model.graphs.abstractGraph.Vertex +import viewmodel.TFDPLayout + class GraphViewModel( - val currentGraph: Graph, + private val currentGraph: Graph, private val showVerticesData: State, var showVerticesID: MutableState, - val graphType: MutableState, - val isDirected: MutableState, - val isWeighted: MutableState + val graphType: String, + val isDirected: Boolean, + val isWeighted: Boolean ) { val updateIsRequired = mutableStateOf(false) - var _verticesViewModels = mutableMapOf, VertexViewModel>() + private var _verticesViewModels = mutableMapOf, VertexViewModel>() var _edgeViewModels = mutableMapOf, EdgeViewModel>() val verticesVM: List> get() = _verticesViewModels.values.toList() @@ -33,7 +35,7 @@ class GraphViewModel( val graph: Graph get() = currentGraph - fun updateEdgeViewModels(edge: Edge) { + private fun updateEdgeViewModels(edge: Edge) { val firstVertex: VertexViewModel = _verticesViewModels[edge.vertex1] ?: throw NoSuchElementException("No such View Model, with mentioned edges") @@ -45,7 +47,7 @@ class GraphViewModel( _edgeViewModels[edge] = EdgeViewModel(firstVertex, secondVertex, isDirected) } - fun updateVertexViewModels(vertex: Vertex) { + private fun updateVertexViewModels(vertex: Vertex) { _verticesViewModels[vertex] = VertexViewModel( dataVisible = showVerticesData, idVisible = showVerticesID, @@ -57,6 +59,7 @@ class GraphViewModel( return _verticesViewModels.keys.any { it.id == id } } + @Suppress("UNCHECKED_CAST") fun addVertex(data: String): Int { val newVertex = graph.addVertex(data as D) @@ -79,14 +82,24 @@ class GraphViewModel( updateEdgeViewModels(newEdge) } - fun applyForceDirectedLayout(width: Double, height: Double, a: Double, b: Double, c: Double) { - val layout = TFDPLayout() - layout.place(width, height, verticesVM, 128, a, b, c) + fun applyForceDirectedLayout( + width: Double, height: Double, longRangeAttractionConstant: Double, + nearAttractionConstant: Double, + repulsiveConstant: Double + ) { + TFDPLayout.place( + width, + height, + verticesVM, + 128, + longRangeAttractionConstant, + nearAttractionConstant, + repulsiveConstant + ) } fun randomize(width: Double, height: Double) { - val layout = TFDPLayout() - layout.randomize(width, height, verticesVM) + TFDPLayout.randomize(width, height, verticesVM) } fun findCommunities(): Boolean { @@ -100,7 +113,7 @@ class GraphViewModel( fun findKeyVertices(): Boolean { val keyVerticesFinder = KeyVerticesFinder() val keyVertices = keyVerticesFinder.findKeyVertices(graph) - if (keyVertices?.isEmpty() ?: false) return false + if (keyVertices?.isEmpty() == true) return false return highlightVertices(keyVertices) } @@ -117,8 +130,8 @@ class GraphViewModel( return false } - var cycles: List, Vertex>>>? = null - var currentCycleIndex = 0 + private var cycles: List, Vertex>>>? = null + private var currentCycleIndex = 0 fun findCycles(srcVertexId: Int): Boolean { val cyclesFinder = CyclesFinder() @@ -148,9 +161,9 @@ class GraphViewModel( } fun findSCCs(): Boolean { - val SCCFinder = SCCFinder() + val sccFinder = SCCFinder() if (graph is DirectedGraph) { - val SCCs = SCCFinder.findSCC(graph as DirectedGraph) + val SCCs = sccFinder.findSCC(graph as DirectedGraph) if (SCCs.isEmpty()) return false return highlightVerticesSets(SCCs) @@ -167,13 +180,12 @@ class GraphViewModel( if (graph is WeightedDirectedGraph) { val shortestPath = shortestPathFinder.findShortestPath(graph as WeightedDirectedGraph, src, dest) - if (shortestPath?.isEmpty() ?: false) return false + if (shortestPath?.isEmpty() == true) return false return highlightPath(shortestPath) - } - else if (graph is WeightedUndirectedGraph) { + } else if (graph is WeightedUndirectedGraph) { val shortestPath = shortestPathFinder.findShortestPath(graph as WeightedUndirectedGraph, src, dest) - if (shortestPath?.isEmpty() ?: false) return false + if (shortestPath?.isEmpty() == true) return false return highlightPath(shortestPath) } @@ -234,7 +246,7 @@ class GraphViewModel( Color(0x8334eb), Color(0xd834eb), Color(0xeb34a1), - ) + ) var i = 0 for (verticesSet in verticesSets) { @@ -270,8 +282,7 @@ class GraphViewModel( } fun highlighNextCycle(): Boolean { - var returnValue = false - returnValue = highlightPath(cycles?.get(currentCycleIndex)) + val returnValue: Boolean = highlightPath(cycles?.get(currentCycleIndex)) val size = cycles?.size ?: return false @@ -289,4 +300,56 @@ class GraphViewModel( _edgeViewModels[edge]?.highlightColor?.value = Color.LightGray } } + + fun getAvailableAlgorithms(): List { + val algorithms = mutableListOf( + Algorithm.LAYOUT, + Algorithm.FIND_COMMUNITIES, + Algorithm.FIND_KEY_VERTICES + ) + + if (graph is DirectedGraph) { + algorithms += Algorithm.FIND_SCCS + algorithms += Algorithm.FIND_CYCLES + } + + if (graph is UndirectedGraph) { + algorithms += Algorithm.FIND_BRIDGES + } + + if (graph is WeightedUndirectedGraph) { + algorithms += Algorithm.MIN_SPANNING_TREE + algorithms += Algorithm.FIND_SHORTEST_PATH + } + + if (graph is WeightedDirectedGraph) { + algorithms += Algorithm.FIND_SHORTEST_PATH + } + + return algorithms + } +} + +enum class Algorithm { + LAYOUT, + FIND_COMMUNITIES, + FIND_KEY_VERTICES, + FIND_SCCS, + FIND_CYCLES, + FIND_BRIDGES, + MIN_SPANNING_TREE, + FIND_SHORTEST_PATH +} + +fun getAlgorithmDisplayName(algorithm: Algorithm): String { + return when (algorithm) { + Algorithm.LAYOUT -> "Layout" + Algorithm.FIND_COMMUNITIES -> "Find communities" + Algorithm.FIND_KEY_VERTICES -> "Find key vertices" + Algorithm.FIND_SCCS -> "Find SCCs" + Algorithm.FIND_CYCLES -> "Find cycles" + Algorithm.FIND_BRIDGES -> "Find bridges" + Algorithm.MIN_SPANNING_TREE -> "Min spanning tree" + Algorithm.FIND_SHORTEST_PATH -> "Find shortest path" + } } diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModelFactory.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModelFactory.kt new file mode 100644 index 00000000..99a19719 --- /dev/null +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModelFactory.kt @@ -0,0 +1,127 @@ +package viewmodel.graph + +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.* +import model.io.sql.SQLDatabaseModule +import model.graphs.DirectedGraph +import model.graphs.UndirectedGraph +import model.graphs.WeightedDirectedGraph +import model.graphs.WeightedUndirectedGraph +import model.graphs.abstractGraph.Graph +import model.io.sql.SQLDatabaseModule.importGraph +import view.MainScreen +import view.components.dialogWindows.ErrorWindow +import viewmodel.MainScreenViewModel +import java.sql.SQLException +import kotlin.system.exitProcess + +object GraphViewModelFactory { + sealed class GraphType { + data object Integer : GraphType() + data object UInteger : GraphType() + data object String : GraphType() + } + + sealed class GraphStructure { + data object Directed : GraphStructure() + data object Undirected : GraphStructure() + } + + sealed class Weight { + data object Weighted : Weight() + data object Unweighted : Weight() + } + + // Simplified function to create a GraphViewModel based on the graph type + fun createGraphViewModel( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight + ): Graph> { + return when { + weight is Weight.Weighted && graphStructure is GraphStructure.Directed && storedData is GraphType.Integer -> WeightedDirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Directed && storedData is GraphType.UInteger -> WeightedDirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Directed && storedData is GraphType.String -> WeightedDirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.Integer -> WeightedUndirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.UInteger -> WeightedUndirectedGraph() + weight is Weight.Weighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.String -> WeightedUndirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Directed && storedData is GraphType.Integer -> DirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Directed && storedData is GraphType.UInteger -> DirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Directed && storedData is GraphType.String -> DirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.Integer -> UndirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.UInteger -> UndirectedGraph() + weight is Weight.Unweighted && graphStructure is GraphStructure.Undirected && storedData is GraphType.String -> UndirectedGraph() + else -> throw IllegalArgumentException("Invalid combination of parameters") + } + } + + @Composable + fun createGraphAndApplyScreen( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight + ) { + val graph = createGraphViewModel(storedData, graphStructure, weight) + MainScreen(MainScreenViewModel(graph, storedData.toString())) + } + + @Suppress("UNCHECKED_CAST") + fun createGraphObject( + storedData: GraphType, + graphStructure: GraphStructure, + weight: Weight, + graphId: Int, + graphVMState: MutableState>?> + ) { + val graph = createGraphViewModel(storedData, graphStructure, weight) as Graph> + graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( + graph, + graphId, + graphVMState as MutableState>?> + ) + } +} + +// Utility function to get the graph parameters +fun getGraphVMParameter( + storedDataType: Int, + structureType: Int, + weightType: Int +): Triple { + val storedData = when (storedDataType) { + 0 -> GraphViewModelFactory.GraphType.Integer + 1 -> GraphViewModelFactory.GraphType.UInteger + 2 -> GraphViewModelFactory.GraphType.String + else -> GraphViewModelFactory.GraphType.Integer // default to integer + } + + val graphStructure = when (structureType) { + 0 -> GraphViewModelFactory.GraphStructure.Undirected + 1 -> GraphViewModelFactory.GraphStructure.Directed + else -> GraphViewModelFactory.GraphStructure.Undirected // default to directed + } + + val weight = when (weightType) { + 0 -> GraphViewModelFactory.Weight.Unweighted + 1 -> GraphViewModelFactory.Weight.Weighted + else -> GraphViewModelFactory.Weight.Unweighted // default to weighted + } + + return Triple(storedData, graphStructure, weight) +} + +@Composable +fun createGraphFromTypesIndices( + viewModel: GraphViewModelFactory, + storedDataIndex: Int, + orientationIndex: Int, + weightnessIndex: Int +) { + val (storedData, graphStructure, weight) = getGraphVMParameter(storedDataIndex, orientationIndex, weightnessIndex) + viewModel.createGraphAndApplyScreen(storedData, graphStructure, weight) +} + + + + + diff --git a/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt deleted file mode 100644 index 570e2715..00000000 --- a/app/src/main/kotlin/viewmodel/graph/SetupGraphViewModel.kt +++ /dev/null @@ -1,264 +0,0 @@ -package viewmodel.graph - -import model.io.sql.SQLDatabaseModule -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import model.graphs.DirectedGraph -import model.graphs.UndirectedGraph -import model.graphs.WeightedDirectedGraph -import model.graphs.WeightedUndirectedGraph -import view.MainScreen -import viewmodel.MainScreenViewModel - -class SetupGraphViewModel { - sealed class GraphType { - data object Integer : GraphType() - data object UInteger : GraphType() - data object String : GraphType() - } - - sealed class GraphStructure { - data object Directed : GraphStructure() - data object Undirected : GraphStructure() - } - - sealed class Weight { - data object Weighted : Weight() - data object Unweighted : Weight() - } - - @Composable - fun createGraphAndApplyScreen( - storedData: GraphType, - graphStructure: GraphStructure, - weight: Weight - ) { // TODO looks too shitty (((((((((( + mb any could be changed - return when (weight) { - is Weight.Weighted -> { - when (graphStructure) { - is GraphStructure.Directed -> { - when (storedData) { - is GraphType.Integer -> MainScreen( - MainScreenViewModel( - WeightedDirectedGraph(), - "WeightedDirectedGraph Int" - ) - ) - - is GraphType.UInteger -> MainScreen( - MainScreenViewModel( - WeightedDirectedGraph(), - "WeightedDirectedGraph UInt" - ) - ) - - is GraphType.String -> MainScreen( - MainScreenViewModel( - WeightedDirectedGraph(), - "WeightedDirectedGraph String" - ) - ) - } - } - - is GraphStructure.Undirected -> { - when (storedData) { - is GraphType.Integer -> MainScreen( - MainScreenViewModel( - WeightedUndirectedGraph(), - "WeightedUndirectedGraph Int" - ) - ) - - is GraphType.UInteger -> MainScreen( - MainScreenViewModel( - WeightedUndirectedGraph(), - "WeightedUndirectedGraph UInt" - ) - ) - - is GraphType.String -> MainScreen( - MainScreenViewModel( - WeightedUndirectedGraph(), - "WeightedUndirectedGraph String" - ) - ) - } - } - } - } - - is Weight.Unweighted -> { - when (graphStructure) { - is GraphStructure.Directed -> { - when (storedData) { - is GraphType.Integer -> MainScreen( - MainScreenViewModel( - DirectedGraph(), - "DirectedGraph Int" - ) - ) - - is GraphType.UInteger -> MainScreen( - MainScreenViewModel( - DirectedGraph(), - "DirectedGraph UInt" - ) - ) - - is GraphType.String -> MainScreen( - MainScreenViewModel( - DirectedGraph(), - "DirectedGraph String" - ) - ) - } - } - - is GraphStructure.Undirected -> { - when (storedData) { - is GraphType.Integer -> MainScreen( - MainScreenViewModel( - UndirectedGraph(), - "UndirectedGraph Int" - ) - ) - - is GraphType.UInteger -> MainScreen( - MainScreenViewModel( - UndirectedGraph(), - "UndirectedGraph UInt" - ) - ) - - is GraphType.String -> - MainScreen( - MainScreenViewModel( - UndirectedGraph(), - "UndirectedGraph String" - ) - ) - } - } - } - } - } - } - - @Suppress("UNCHECKED_CAST") - fun createGraphObject( - storedData: GraphType, - graphStructure: GraphStructure, - weight: Weight, - graphId: Int, - graphVMState: MutableState?> - ) = when (weight) { - is Weight.Weighted -> { - when (graphStructure) { - is GraphStructure.Directed -> { - when (storedData) { - is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - WeightedDirectedGraph(), - graphId, graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - WeightedDirectedGraph(), - graphId, graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - WeightedDirectedGraph(), - graphId, graphVMState as MutableState?> - ) as GraphViewModel? - } - } - } - - is GraphStructure.Undirected -> { - when (storedData) { - is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - WeightedUndirectedGraph(), - graphId, graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - WeightedUndirectedGraph(), - graphId, graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - WeightedUndirectedGraph(), - graphId, graphVMState as MutableState?> - ) as GraphViewModel? - } - } - } - } - } - - is Weight.Unweighted -> { - when (graphStructure) { - is GraphStructure.Directed -> { - when (storedData) { - is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - DirectedGraph(), graphId, - graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - DirectedGraph(), graphId, - graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - DirectedGraph(), graphId, - graphVMState as MutableState?> - ) as GraphViewModel? - } - } - } - - is GraphStructure.Undirected -> { - when (storedData) { - is GraphType.Integer -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - UndirectedGraph(), graphId, - graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.UInteger -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - UndirectedGraph(), graphId, - graphVMState as MutableState?> - ) as GraphViewModel? - } - - is GraphType.String -> { - graphVMState.value = SQLDatabaseModule.updateImportedGraphVM( - UndirectedGraph(), graphId, - graphVMState as MutableState?> - ) as GraphViewModel? - } - } - } - } - } - } -} diff --git a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index c8f4bfeb..2072b698 100644 --- a/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -29,6 +29,10 @@ class VertexViewModel( y.value += dragAmount.y } + fun switchSelection() { + isSelected.value = !isSelected.value + } + val getVertexID get() = vertex.id } diff --git a/app/src/test/kotlin/integration/IntegrationTest.kt b/app/src/test/kotlin/integration/IntegrationTest.kt index ffbd1a0f..d7a3224d 100644 --- a/app/src/test/kotlin/integration/IntegrationTest.kt +++ b/app/src/test/kotlin/integration/IntegrationTest.kt @@ -2,15 +2,13 @@ package integration import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.material.* -import androidx.compose.runtime.* import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createComposeRule import kotlinx.coroutines.runBlocking import model.graphs.UndirectedGraph import org.junit.Rule import org.junit.Test -import view.utils.FAQBox +import view.components.FAQBox import viewmodel.MainScreenViewModel @@ -29,7 +27,7 @@ class IntegrationTest { val viewmodel = MainScreenViewModel(UndirectedGraph(),"UndirectedGraph") composeTestRule.setContent { - FAQBox(interactionSource, viewmodel) + FAQBox(interactionSource, viewmodel.graphType) } // UI TEST diff --git a/build.gradle.kts b/build.gradle.kts index 8ed64556..8b3c8522 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,4 @@ plugins { - kotlin("jvm") version "1.9.22" - id("org.jetbrains.compose") version "1.6.2" + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.compose) apply false } diff --git a/gradle.properties b/gradle.properties index 82aa302e..7c777834 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,4 @@ kotlinVersion=1.9.22 composeVersion=1.6.2 junitVersion=5.8.1 neo4jDriverVersion=5.6.0 -koinVersion=3.5.3 kotlinxCoroutinesVersion=1.7.3 diff --git a/settings.gradle.kts b/settings.gradle.kts index 32b11109..a8b80820 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,7 +2,6 @@ val neo4jDriverVersion: String by settings val composeVersion: String by settings val kotlinVersion: String by settings val junitVersion: String by settings -val koinVersion: String by settings pluginManagement { repositories { @@ -21,7 +20,6 @@ dependencyResolutionManagement { create("libs") { plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").version(kotlinVersion) plugin("compose", "org.jetbrains.compose").version(composeVersion) - library("koin-core", "io.insert-koin:koin-core:$koinVersion") library("junit-jupiter", "org.junit.jupiter:junit-jupiter:$junitVersion") } } From 00557820308786254f9905a4eaadbc8d3d3bbc5d Mon Sep 17 00:00:00 2001 From: Mike Gavrilenko <64466788+qrutyy@users.noreply.github.com> Date: Sat, 3 Aug 2024 10:27:16 +0300 Subject: [PATCH 77/77] fix: adjust dialog win. size + edit SQL query path #62 * fix: correct queries path * fix: adjust dialog window size --- app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt | 2 +- app/src/main/kotlin/view/tabScreen/FileControlTab.kt | 4 ++-- app/src/main/kotlin/view/tabScreen/GeneralTab.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt index 1f34d828..730c604f 100644 --- a/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt +++ b/app/src/main/kotlin/model/io/sql/SQLDatabaseModule.kt @@ -17,7 +17,7 @@ object SQLDatabaseModule { private const val DB_DIRECTORY = "database" private const val DB_NAME = "my_graph_database.db" private const val QUERY_NAME = "queries.txt" - private const val QUERY_DIRECTORY = "src/main/kotlin/model/io/sql/" + private const val QUERY_DIRECTORY = "app/src/main/kotlin/model/io/sql/" private val DB_URL = "jdbc:sqlite:${File(DB_DIRECTORY, DB_NAME).absolutePath}" private fun readQueriesFromFile(): String { diff --git a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt index c6a8e15e..7d430115 100644 --- a/app/src/main/kotlin/view/tabScreen/FileControlTab.kt +++ b/app/src/main/kotlin/view/tabScreen/FileControlTab.kt @@ -207,8 +207,8 @@ fun FileControlTab(graphVM: GraphViewModel) { modifier = Modifier .background(Color.White) .padding(16.dp) - .width(300.dp) - .height(100.dp) + .width(250.dp) + .height(30.dp) ) { Text("Graph '$graphName' saved successfully!") } diff --git a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt index ca25a02c..1e36e13a 100644 --- a/app/src/main/kotlin/view/tabScreen/GeneralTab.kt +++ b/app/src/main/kotlin/view/tabScreen/GeneralTab.kt @@ -164,7 +164,7 @@ fun GeneralTab(graphVM: GraphViewModel) { ) { Column( modifier = - Modifier.background(Color.White).padding(16.dp).width(350.dp).height(150.dp) + Modifier.background(Color.White).padding(16.dp).width(350.dp).height(180.dp) ) { if (graphVM.verticesVM.isEmpty()) { Text("Input data of second vertex to create and connect with") @@ -185,7 +185,7 @@ fun GeneralTab(graphVM: GraphViewModel) { } Button( - modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(vertical = 16.dp), onClick = { secondVertexData = secondVertexData.replace("\n", "")