From 37766a1af741b9519c27866f5b0db27ffbf007ae Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 13:26:41 +0000 Subject: [PATCH 001/139] Setting up GitHub Classroom Feedback From b331ddc8b44248d970c3ecc94ef5d588e9bcedae Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sat, 18 May 2024 16:33:52 +0300 Subject: [PATCH 002/139] structure: initial structure --- .gitignore | 62 ++++++ 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 | 15 ++ src/main/kotlin/Main.kt | 31 +++ 9 files changed, 475 insertions(+) create mode 100644 .gitignore 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 create mode 100644 src/main/kotlin/Main.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c2b01b --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +.DS_Store +.idea/shelf +.idea +/confluence/target +/dependencies/repo +/android.tests.dependencies +/dependencies/android.tests.dependencies +/dist +/local +/gh-pages +/ideaSDK +/clionSDK +/android-studio/sdk +out/ +/tmp +/intellij +workspace.xml +*.versionsBackup +/idea/testData/debugger/tinyApp/classes* +/jps-plugin/testData/kannotator +/js/js.translator/testData/out/ +/js/js.translator/testData/out-min/ +/js/js.translator/testData/out-pir/ +.gradle/ +build/ +!**/src/**/build +!**/test/**/build +*.iml +!**/testData/**/*.iml +.idea/artifacts +.idea/remote-targets.xml +.idea/libraries/Gradle*.xml +.idea/libraries/Maven*.xml +.idea/modules +.idea/runConfigurations/JPS_*.xml +.idea/runConfigurations/_JPS_*.xml +.idea/runConfigurations/PILL_*.xml +.idea/runConfigurations/_FP_*.xml +.idea/runConfigurations/_MT_*.xml +.idea/libraries +.idea/modules.xml +.idea/gradle.xml +.idea/compiler.xml +.idea/inspectionProfiles/profiles_settings.xml +.idea/.name +.idea/jarRepositories.xml +.idea/csv-plugin.xml +.idea/libraries-with-intellij-classes.xml +.idea/misc.xml +.idea/protoeditor.xml +node_modules/ +.rpt2_cache/ +libraries/tools/kotlin-test-js-runner/lib/ +local.properties +buildSrcTmp/ +distTmp/ +outTmp/ +/test.output +/kotlin-native/dist +kotlin-ide/ +.kotlin/ +.teamcity/ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..8da1bed --- /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 = "com.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +dependencies { + // Note, if you develop a library, you should use compose.desktop.common. + // compose.desktop.currentOs should be used in launcher-sourceSet + // (in a separate module for demo project and in testMain). + // With compose.desktop.common you will also lose @Preview functionality + implementation(compose.desktop.currentOs) +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "demo1" + packageVersion = "1.0.0" + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..98aed13 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +kotlin.code.style=official +kotlin.version=1.9.22 +compose.version=1.6.0 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 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3abffe1 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + gradlePluginPortal() + mavenCentral() + } + + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) + } +} + +rootProject.name = "demo1" diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..fed3f24 --- /dev/null +++ b/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() + } +} From d66e184f25a9379781b39a5f4ddcdc4aca3c6572 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 20 May 2024 01:08:21 +0300 Subject: [PATCH 003/139] feat: add interfaces for graph --- src/main/kotlin/model/graph/Edge.kt | 7 +++++++ src/main/kotlin/model/graph/Graph.kt | 12 ++++++++++++ src/main/kotlin/model/graph/Vertex.kt | 5 +++++ src/test/kotlin/model/graph/UndirectedGraphTest.kt | 11 +++++++++++ 4 files changed, 35 insertions(+) create mode 100644 src/main/kotlin/model/graph/Edge.kt create mode 100644 src/main/kotlin/model/graph/Graph.kt create mode 100644 src/main/kotlin/model/graph/Vertex.kt create mode 100644 src/test/kotlin/model/graph/UndirectedGraphTest.kt diff --git a/src/main/kotlin/model/graph/Edge.kt b/src/main/kotlin/model/graph/Edge.kt new file mode 100644 index 0000000..4597324 --- /dev/null +++ b/src/main/kotlin/model/graph/Edge.kt @@ -0,0 +1,7 @@ +package model.graph + +interface Edge { + val first: Vertex + val second: Vertex + var weight: Long +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt new file mode 100644 index 0000000..9fd4d9c --- /dev/null +++ b/src/main/kotlin/model/graph/Graph.kt @@ -0,0 +1,12 @@ +package model.graph + +interface Graph { + val vertices: Collection> + val adjacencyList: HashMap>> + + fun addVertex(key: V): Vertex? + fun removeVertex(key: V): Vertex? + fun updateVertex(key: V, newKey: V): Vertex? + fun addEdge(first: V, second: V): Edge? + fun removeEdge(first: V, second: V): Edge? +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/Vertex.kt b/src/main/kotlin/model/graph/Vertex.kt new file mode 100644 index 0000000..e65e094 --- /dev/null +++ b/src/main/kotlin/model/graph/Vertex.kt @@ -0,0 +1,5 @@ +package model.graph + +interface Vertex { + var key: V +} \ No newline at end of file diff --git a/src/test/kotlin/model/graph/UndirectedGraphTest.kt b/src/test/kotlin/model/graph/UndirectedGraphTest.kt new file mode 100644 index 0000000..a89dcd4 --- /dev/null +++ b/src/test/kotlin/model/graph/UndirectedGraphTest.kt @@ -0,0 +1,11 @@ +package model.graph + +import org.junit.jupiter.api.Test + +class GraphUndirectedTest { + + @Test + fun addVertex() { + assert(true) + } +} \ No newline at end of file From 89ea33a579f66dc0e7e088b7ad9c1cd931d98671 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 20 May 2024 01:15:32 +0300 Subject: [PATCH 004/139] feat: add UndirectedGraph and several tests for it removeEdge and updateVertex without tests --- build.gradle.kts | 7 +- .../kotlin/model/graph/UndirectedGraph.kt | 74 +++++++++ .../kotlin/model/graph/UndirectedGraphTest.kt | 144 +++++++++++++++++- 3 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/model/graph/UndirectedGraph.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8da1bed..7015b1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { // (in a separate module for demo project and in testMain). // With compose.desktop.common you will also lose @Preview functionality implementation(compose.desktop.currentOs) + testImplementation(kotlin("test")) } compose.desktop { @@ -28,8 +29,12 @@ compose.desktop { nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "demo1" + packageName = "graphs-3" packageVersion = "1.0.0" } } } + +tasks.test { + useJUnitPlatform() +} diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt new file mode 100644 index 0000000..e1a03e2 --- /dev/null +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -0,0 +1,74 @@ +package model.graph + +class UndirectedGraph: Graph { + private val _vertices = hashMapOf>() + private val _adjacencyList = hashMapOf>>() + + override val vertices: Collection> + get() = _vertices.values + + override val adjacencyList: HashMap>> + get() = _adjacencyList + + override fun addVertex(key: V): Vertex? { + if (_vertices[key] != null) return null + + val vertex = UndirectedVertex(key) + + _vertices[key] = vertex + _adjacencyList[key] = arrayListOf() + + return vertex + } + + override fun removeVertex(key: V): Vertex? { + val vertex = _vertices[key] ?: return null + + _vertices.remove(key) + _adjacencyList.remove(key) + + return vertex + } + + override fun addEdge(first: V, second: V): Edge? { + val vertex1 = _vertices[first] ?: return null + val vertex2 = _vertices[second] ?: return null + + // edge already exists + if (_adjacencyList[first]?.find { it.second.key == second } != null) return null + + _adjacencyList[first]?.add(UndirectedEdge(vertex1, vertex2)) + _adjacencyList[second]?.add(UndirectedEdge(vertex2, vertex1)) + + return _adjacencyList[first]?.last() + } + + override fun removeEdge(first: V, second: V): Edge? { + val edge1 = _adjacencyList[first]?.find { it.second.key == second } + val edge2 = _adjacencyList[second]?.find { it.second.key == first } + + // edge doesn't exist + if (edge1 == null || edge2 == null) return null + + _adjacencyList[first]?.remove(edge1) + _adjacencyList[second]?.remove(edge1) + + return edge1 + } + + override fun updateVertex(key: V, newKey: V): Vertex? { + val vertex = _vertices[key] ?: return null + if (_vertices[newKey] != null) return null + + vertex.key = newKey + return vertex + } + + private data class UndirectedVertex(override var key: V): Vertex + + private data class UndirectedEdge(override val first: Vertex, override val second: Vertex): Edge { + override var weight: Long + get() = 1 + set(value) {} + } +} \ No newline at end of file diff --git a/src/test/kotlin/model/graph/UndirectedGraphTest.kt b/src/test/kotlin/model/graph/UndirectedGraphTest.kt index a89dcd4..e3239a4 100644 --- a/src/test/kotlin/model/graph/UndirectedGraphTest.kt +++ b/src/test/kotlin/model/graph/UndirectedGraphTest.kt @@ -1,11 +1,147 @@ package model.graph +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull -class GraphUndirectedTest { +@Suppress("ClassName") +class UndirectedGraphTest { + lateinit var graph: UndirectedGraph - @Test - fun addVertex() { - assert(true) + fun Graph.getSize() = this.vertices.size + fun Graph.findVertex(key: Int) = this.vertices.find { it.key == key } + fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[key1]?.find { it.second.key == key2 } + + fun Graph.checkSize(size: Int) = assertEquals(this.getSize(), size) + fun Graph.checkContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), vertex) + fun Graph.checkNotContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), null) + fun Graph.checkNotNullEdgeArray(vertex: Vertex) = assertNotNull(this.adjacencyList[vertex.key]) + fun Graph.checkNullEdgeArray(vertex: Vertex) = assertNull(this.adjacencyList[vertex.key]) + + fun Graph.checkExistingUndirectedEdge(key1: Int, key2: Int) { + val edge1 = this.findEdge(key1, key2) + val edge2 = this.findEdge(key2, key1) + + assertNotNull(edge1) + assertNotNull(edge2) + assertEquals(edge1.weight, 1) + assertEquals(edge2.weight, 1) + } + + @BeforeEach + fun setup() { + graph = UndirectedGraph() + } + + @Nested + inner class addVertex { + @Test + fun `Empty graph`() { + val vertex = graph.addVertex(1) + + graph.checkSize(1) + assertEquals(graph.getSize(), 1) + assertNotNull(vertex) + assertEquals(vertex.key, 1) + graph.checkContainVertex(vertex) + graph.checkNotNullEdgeArray(vertex) + } + + @Test + fun `Non-empty graph`() { + graph.addVertex(1) + graph.addVertex(2) + val vertex = graph.addVertex(3) + + graph.checkSize(3) + assertNotNull(vertex) + assertEquals(vertex.key, 3) + graph.checkContainVertex(vertex) + graph.checkNotNullEdgeArray(vertex) + } + + @Test + fun `Existing vertex`() { + graph.addVertex(1) + val vertex = graph.addVertex(1) + + assertNull(vertex) + graph.checkSize(1) + } + } + + @Nested + inner class removeVertex { + @Test + fun `Existing vertex`() { + graph.addVertex(1) + val vertex = graph.removeVertex(1) + + graph.checkSize(0) + assertNotNull(vertex) + assertEquals(vertex.key, 1) + graph.checkNotContainVertex(vertex) + graph.checkNullEdgeArray(vertex) + assertNull(graph.adjacencyList[1]) + } + + @Test + fun `Non existing vertex`() { + graph.addVertex(2) + val vertex = graph.removeVertex(1) + + graph.checkSize(1) + assertNull(vertex) + } + } + + @Nested + inner class addEdge { + @Test + fun `Not linked vertices`() { + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + assertNotNull(vertex1) + assertNotNull(vertex2) + + val edge = graph.addEdge(vertex1.key, vertex2.key) + + assertNotNull(edge) + graph.checkExistingUndirectedEdge(1, 2) + } + + @Test + fun `Already linked vertices`() { + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + assertNotNull(vertex1) + assertNotNull(vertex2) + + val edge = graph.addEdge(vertex1.key, vertex2.key) + val edgeLinkedVertices = graph.addEdge(vertex1.key, vertex2.key) + + assertNull(edgeLinkedVertices) + graph.checkExistingUndirectedEdge(1, 2) + } + + @Test + fun `Edge with non existing vertex`() { + graph.addVertex(1) + + val edgeFirstNotExist = graph.addEdge(2, 1) + val edgeSecondNotExist = graph.addEdge(1, 2) + val edgeAllNotExist = graph.addEdge(3, 4) + + assertNull(edgeFirstNotExist) + assertNull(edgeSecondNotExist) + assertNull(edgeAllNotExist) + + assertEquals(graph.adjacencyList[1]?.size, 0) + } } } \ No newline at end of file From 1e2579969986e52bbe2d56ecb66949a4f238e770 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 01:08:50 +0300 Subject: [PATCH 005/139] refactor: change adjacencyList type --- src/main/kotlin/model/graph/Graph.kt | 2 +- .../kotlin/model/graph/UndirectedGraph.kt | 31 ++++++++++++------- .../kotlin/model/graph/UndirectedGraphTest.kt | 14 ++++----- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 9fd4d9c..0925897 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -2,7 +2,7 @@ package model.graph interface Graph { val vertices: Collection> - val adjacencyList: HashMap>> + val adjacencyList: HashMap, ArrayList>> fun addVertex(key: V): Vertex? fun removeVertex(key: V): Vertex? diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index e1a03e2..c51487c 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -2,12 +2,12 @@ package model.graph class UndirectedGraph: Graph { private val _vertices = hashMapOf>() - private val _adjacencyList = hashMapOf>>() + private val _adjacencyList = hashMapOf, ArrayList>>() override val vertices: Collection> get() = _vertices.values - override val adjacencyList: HashMap>> + override val adjacencyList: HashMap, ArrayList>> get() = _adjacencyList override fun addVertex(key: V): Vertex? { @@ -16,7 +16,7 @@ class UndirectedGraph: Graph { val vertex = UndirectedVertex(key) _vertices[key] = vertex - _adjacencyList[key] = arrayListOf() + _adjacencyList[vertex] = arrayListOf() return vertex } @@ -25,7 +25,7 @@ class UndirectedGraph: Graph { val vertex = _vertices[key] ?: return null _vertices.remove(key) - _adjacencyList.remove(key) + _adjacencyList.remove(vertex) return vertex } @@ -35,23 +35,26 @@ class UndirectedGraph: Graph { val vertex2 = _vertices[second] ?: return null // edge already exists - if (_adjacencyList[first]?.find { it.second.key == second } != null) return null + if (_adjacencyList[vertex1]?.find { it.second.key == second } != null) return null - _adjacencyList[first]?.add(UndirectedEdge(vertex1, vertex2)) - _adjacencyList[second]?.add(UndirectedEdge(vertex2, vertex1)) + _adjacencyList[vertex1]?.add(UndirectedEdge(vertex1, vertex2)) + _adjacencyList[vertex2]?.add(UndirectedEdge(vertex2, vertex1)) - return _adjacencyList[first]?.last() + return _adjacencyList[vertex1]?.last() } override fun removeEdge(first: V, second: V): Edge? { - val edge1 = _adjacencyList[first]?.find { it.second.key == second } - val edge2 = _adjacencyList[second]?.find { it.second.key == first } + val vertex1 = _vertices[first] + val vertex2 = _vertices[second] + + val edge1 = _adjacencyList[vertex1]?.find { it.second.key == second } + val edge2 = _adjacencyList[vertex2]?.find { it.second.key == first } // edge doesn't exist if (edge1 == null || edge2 == null) return null - _adjacencyList[first]?.remove(edge1) - _adjacencyList[second]?.remove(edge1) + _adjacencyList[vertex1]?.remove(edge1) + _adjacencyList[vertex2]?.remove(edge1) return edge1 } @@ -64,6 +67,10 @@ class UndirectedGraph: Graph { return vertex } + fun findVertex(key: V) = _vertices[key] + + fun getEdges(vertex: Vertex) = _adjacencyList[vertex] + private data class UndirectedVertex(override var key: V): Vertex private data class UndirectedEdge(override val first: Vertex, override val second: Vertex): Edge { diff --git a/src/test/kotlin/model/graph/UndirectedGraphTest.kt b/src/test/kotlin/model/graph/UndirectedGraphTest.kt index e3239a4..6bc875d 100644 --- a/src/test/kotlin/model/graph/UndirectedGraphTest.kt +++ b/src/test/kotlin/model/graph/UndirectedGraphTest.kt @@ -13,13 +13,13 @@ class UndirectedGraphTest { fun Graph.getSize() = this.vertices.size fun Graph.findVertex(key: Int) = this.vertices.find { it.key == key } - fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[key1]?.find { it.second.key == key2 } + fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[findVertex(key1)]?.find { it.second.key == key2 } fun Graph.checkSize(size: Int) = assertEquals(this.getSize(), size) fun Graph.checkContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), vertex) fun Graph.checkNotContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), null) - fun Graph.checkNotNullEdgeArray(vertex: Vertex) = assertNotNull(this.adjacencyList[vertex.key]) - fun Graph.checkNullEdgeArray(vertex: Vertex) = assertNull(this.adjacencyList[vertex.key]) + fun Graph.checkNotNullEdgeArray(vertex: Vertex) = assertNotNull(this.adjacencyList[vertex]) + fun Graph.checkNullEdgeArray(vertex: Vertex) = assertNull(this.adjacencyList[vertex]) fun Graph.checkExistingUndirectedEdge(key1: Int, key2: Int) { val edge1 = this.findEdge(key1, key2) @@ -85,7 +85,7 @@ class UndirectedGraphTest { assertEquals(vertex.key, 1) graph.checkNotContainVertex(vertex) graph.checkNullEdgeArray(vertex) - assertNull(graph.adjacencyList[1]) + assertNull(graph.adjacencyList[vertex]) } @Test @@ -122,7 +122,7 @@ class UndirectedGraphTest { assertNotNull(vertex1) assertNotNull(vertex2) - val edge = graph.addEdge(vertex1.key, vertex2.key) + graph.addEdge(vertex1.key, vertex2.key) val edgeLinkedVertices = graph.addEdge(vertex1.key, vertex2.key) assertNull(edgeLinkedVertices) @@ -131,7 +131,7 @@ class UndirectedGraphTest { @Test fun `Edge with non existing vertex`() { - graph.addVertex(1) + val vertex = graph.addVertex(1) val edgeFirstNotExist = graph.addEdge(2, 1) val edgeSecondNotExist = graph.addEdge(1, 2) @@ -141,7 +141,7 @@ class UndirectedGraphTest { assertNull(edgeSecondNotExist) assertNull(edgeAllNotExist) - assertEquals(graph.adjacencyList[1]?.size, 0) + assertEquals(graph.adjacencyList[vertex]?.size, 0) } } } \ No newline at end of file From 61d0f1a5ac38da684305f3e117238c5f389af0a0 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 01:09:06 +0300 Subject: [PATCH 006/139] fix: change root project name --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3abffe1..92d5e40 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,4 +12,4 @@ pluginManagement { } } -rootProject.name = "demo1" +rootProject.name = "graphs-graphs-3" From d1c8115094ae764e15aa35bfdd5742e355764dff Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 01:10:41 +0300 Subject: [PATCH 007/139] feat: add viewModels for Undirected Graph, Vertex and Edge --- src/main/kotlin/viewModel/EdgeViewModel.kt | 20 ++++++++ .../kotlin/viewModel/UndirectedViewModel.kt | 46 +++++++++++++++++ src/main/kotlin/viewModel/VertexViewModel.kt | 50 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/main/kotlin/viewModel/EdgeViewModel.kt create mode 100644 src/main/kotlin/viewModel/UndirectedViewModel.kt create mode 100644 src/main/kotlin/viewModel/VertexViewModel.kt diff --git a/src/main/kotlin/viewModel/EdgeViewModel.kt b/src/main/kotlin/viewModel/EdgeViewModel.kt new file mode 100644 index 0000000..a1f5b3d --- /dev/null +++ b/src/main/kotlin/viewModel/EdgeViewModel.kt @@ -0,0 +1,20 @@ +package viewModel + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import model.graph.Edge + +class EdgeViewModel ( + val first: VertexViewModel, + val second: VertexViewModel, + private val edge: Edge, + private val _weightVisibility: State +) { + private var _weight = mutableStateOf(edge.weight) + var weight + get() = _weight.value + set(value) { + _weight.value = value + edge.weight = value + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/UndirectedViewModel.kt b/src/main/kotlin/viewModel/UndirectedViewModel.kt new file mode 100644 index 0000000..843ffa5 --- /dev/null +++ b/src/main/kotlin/viewModel/UndirectedViewModel.kt @@ -0,0 +1,46 @@ +package viewModel + +import androidx.compose.runtime.mutableStateOf +import model.graph.UndirectedGraph +import model.graph.Vertex + +class UndirectedViewModel( + private val graph: UndirectedGraph, + showVerticesLabels: Boolean, +) { + private val _vertices = hashMapOf, VertexViewModel>() + private val _adjacencyList = hashMapOf, ArrayList>>() + + val vertices + get() = _vertices.values + + val adjacencyList + get() = _adjacencyList + + init { + graph.vertices.forEach { vertex -> + val vertexViewModel = + VertexViewModel( + showVerticesLabels, + vertex, + x = (-1000..1000).random().toFloat(), + y = (-1000..1000).random().toFloat() + ) + + _vertices[vertex] = vertexViewModel + } + + graph.vertices.forEach { vertex -> + val arrayList = arrayListOf>() + val vertexVM1 = _vertices[vertex] ?: throw IllegalStateException() + + graph.adjacencyList[vertex]?.forEach { edge -> + val vertexVM2 = _vertices[edge.second] ?: throw IllegalStateException() + + arrayList.add(EdgeViewModel(vertexVM1, vertexVM2, edge, mutableStateOf(false))) + } + + _adjacencyList[vertexVM1] = arrayList + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/VertexViewModel.kt b/src/main/kotlin/viewModel/VertexViewModel.kt new file mode 100644 index 0000000..f298779 --- /dev/null +++ b/src/main/kotlin/viewModel/VertexViewModel.kt @@ -0,0 +1,50 @@ +package viewModel + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import model.graph.Vertex + +class VertexViewModel( + private val _labelVisible: Boolean, + private val vertex: Vertex, + x: Float = 0f, + y: Float = 0f, + color: Color = Color.Black, + radius: Dp = 8f.dp, +) { + private val _x = mutableStateOf(x) + private val _y = mutableStateOf(y) + private val _color = mutableStateOf(color) + private val _radius = mutableStateOf(radius) + + var x: Float + get() = _x.value + set(value) { + _x.value = value + } + + var y: Float + get() = _y.value + set(value) { + _y.value = value + } + + var color: Color + get() = _color.value + set(value) { + _color.value = value + } + var radius: Dp + get() = _radius.value + set(value) { + _radius.value = value + } + + val label + get() = vertex.key.toString() + + val labelVisibility + get() = _labelVisible +} \ No newline at end of file From 99b3228d229cf1dd1d67f4930133816df5f0f96f Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 01:11:27 +0300 Subject: [PATCH 008/139] feat: add viewModels for Canvas, VertexCanvas and EdgeCanvas --- src/main/kotlin/viewModel/CanvasViewModel.kt | 29 +++++++++++++++++++ .../kotlin/viewModel/EdgeCanvasViewModel.kt | 6 ++++ .../kotlin/viewModel/VertexCanvasViewModel.kt | 18 ++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/main/kotlin/viewModel/CanvasViewModel.kt create mode 100644 src/main/kotlin/viewModel/EdgeCanvasViewModel.kt create mode 100644 src/main/kotlin/viewModel/VertexCanvasViewModel.kt diff --git a/src/main/kotlin/viewModel/CanvasViewModel.kt b/src/main/kotlin/viewModel/CanvasViewModel.kt new file mode 100644 index 0000000..cd18884 --- /dev/null +++ b/src/main/kotlin/viewModel/CanvasViewModel.kt @@ -0,0 +1,29 @@ +package viewModel + +import androidx.compose.ui.geometry.Offset + +class CanvasViewModel( + graphViewModel: UndirectedViewModel, + var zoom: Float, + var center: Offset, + var canvasSize: Offset +) { + private val _vertices = graphViewModel.vertices.associateWith { v -> + VertexCanvasViewModel(v, zoom, center, canvasSize) + } + + private val _edges = graphViewModel.adjacencyList.map { it.value }.flatten().map { + val vertex1 = + _vertices[it.first] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.first}") + val vertex2 = + _vertices[it.second] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.second}") + + EdgeCanvasViewModel(vertex1, vertex2) + } + + val vertices + get() = _vertices.values + + val edges + get() = _edges +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/EdgeCanvasViewModel.kt new file mode 100644 index 0000000..8917d71 --- /dev/null +++ b/src/main/kotlin/viewModel/EdgeCanvasViewModel.kt @@ -0,0 +1,6 @@ +package viewModel + +class EdgeCanvasViewModel( + val first: VertexCanvasViewModel, + val second: VertexCanvasViewModel +) \ No newline at end of file diff --git a/src/main/kotlin/viewModel/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/VertexCanvasViewModel.kt new file mode 100644 index 0000000..d9afbc9 --- /dev/null +++ b/src/main/kotlin/viewModel/VertexCanvasViewModel.kt @@ -0,0 +1,18 @@ +package viewModel + +import androidx.compose.ui.geometry.Offset + +class VertexCanvasViewModel( + val viewModel: VertexViewModel, + zoom: Float, + center: Offset, + canvasSize: Offset +) { + val offset = Offset( + (canvasSize.x / 2) + ((viewModel.x - center.x) * zoom), + (canvasSize.y / 2) + ((viewModel.y - center.y) * zoom) + ) + + val radius = viewModel.radius * zoom + val color = viewModel.color +} \ No newline at end of file From d6a62eca8867b13140b5e8afbe85078d9d668e4d Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 01:11:58 +0300 Subject: [PATCH 009/139] feat: add views for Undirected Graph, Vertex and Edge --- src/main/kotlin/view/EdgeView.kt | 30 ++++++++++++++++++ src/main/kotlin/view/UndirectedGraphView.kt | 24 ++++++++++++++ src/main/kotlin/view/VertexView.kt | 35 +++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/main/kotlin/view/EdgeView.kt create mode 100644 src/main/kotlin/view/UndirectedGraphView.kt create mode 100644 src/main/kotlin/view/VertexView.kt diff --git a/src/main/kotlin/view/EdgeView.kt b/src/main/kotlin/view/EdgeView.kt new file mode 100644 index 0000000..f480969 --- /dev/null +++ b/src/main/kotlin/view/EdgeView.kt @@ -0,0 +1,30 @@ +package view + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import viewModel.EdgeViewModel + +@Composable +fun EdgeView( + viewModel: EdgeViewModel +) { + Canvas(Modifier.fillMaxSize()) { + drawLine( + start = Offset( + viewModel.first.x.dp.toPx() + viewModel.first.radius.toPx(), + viewModel.first.y.dp.toPx() + viewModel.first.radius.toPx(), + ), + end = Offset( + viewModel.second.x.dp.toPx() + viewModel.second.radius.toPx(), + viewModel.second.y.dp.toPx() + viewModel.second.radius.toPx(), + ), + color = Color.Black, + strokeWidth = 1f.dp.toPx() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/UndirectedGraphView.kt b/src/main/kotlin/view/UndirectedGraphView.kt new file mode 100644 index 0000000..533c419 --- /dev/null +++ b/src/main/kotlin/view/UndirectedGraphView.kt @@ -0,0 +1,24 @@ +package view + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import viewModel.UndirectedViewModel + +@Composable +fun UndirectedGraphView( + viewModel: UndirectedViewModel +) { + Box(Modifier.fillMaxSize()) { + viewModel.vertices.forEach { v -> + VertexView(v) + } + + viewModel.vertices.forEach { v -> + viewModel.adjacencyList[v]?.forEach { e -> + EdgeView(e) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/VertexView.kt b/src/main/kotlin/view/VertexView.kt new file mode 100644 index 0000000..77781f7 --- /dev/null +++ b/src/main/kotlin/view/VertexView.kt @@ -0,0 +1,35 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import viewModel.VertexViewModel + +@Composable +fun VertexView( + viewModel: VertexViewModel, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(viewModel.radius * 2) + .offset(viewModel.x.dp, viewModel.y.dp) + .background(color = viewModel.color, shape = CircleShape) + ) { + if (viewModel.labelVisibility) { + Text( + modifier = Modifier + .align(Alignment.Center) + .offset(0f.dp, -viewModel.radius - 10f.dp), + text = viewModel.label + ) + } + } +} \ No newline at end of file From 14ef52c07b2a367fb7d42d14b2fbe568c7ce9f47 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 01:12:15 +0300 Subject: [PATCH 010/139] feat: add views for Canvas, VertexCanvas, EdgeCanvas --- src/main/kotlin/view/CanvasView.kt | 24 ++++++++++++++++++ src/main/kotlin/view/EdgeCanvasView.kt | 31 ++++++++++++++++++++++++ src/main/kotlin/view/VertexCanvasView.kt | 24 ++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 src/main/kotlin/view/CanvasView.kt create mode 100644 src/main/kotlin/view/EdgeCanvasView.kt create mode 100644 src/main/kotlin/view/VertexCanvasView.kt diff --git a/src/main/kotlin/view/CanvasView.kt b/src/main/kotlin/view/CanvasView.kt new file mode 100644 index 0000000..54f0a67 --- /dev/null +++ b/src/main/kotlin/view/CanvasView.kt @@ -0,0 +1,24 @@ +package view + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import viewModel.CanvasViewModel + +@Composable +fun CanvasView( + viewModel: CanvasViewModel, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + ) { + viewModel.vertices.forEach { + VertexCanvasView(it) + } + + viewModel.edges.forEach { + EdgeCanvasView(it) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/EdgeCanvasView.kt b/src/main/kotlin/view/EdgeCanvasView.kt new file mode 100644 index 0000000..e8f1d9f --- /dev/null +++ b/src/main/kotlin/view/EdgeCanvasView.kt @@ -0,0 +1,31 @@ +package view + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import viewModel.EdgeCanvasViewModel + +@Composable +fun EdgeCanvasView( + viewModel: EdgeCanvasViewModel, + modifier: Modifier = Modifier +) { + Canvas(Modifier.fillMaxSize()) { + drawLine( + start = Offset( + viewModel.first.offset.x.dp.toPx() + viewModel.first.radius.toPx(), + viewModel.first.offset.y.dp.toPx() + viewModel.first.radius.toPx(), + ), + end = Offset( + viewModel.second.offset.x.dp.toPx() + viewModel.second.radius.toPx(), + viewModel.second.offset.y.dp.toPx() + viewModel.second.radius.toPx(), + ), + color = Color.Black, + strokeWidth = 1f.dp.toPx() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/VertexCanvasView.kt b/src/main/kotlin/view/VertexCanvasView.kt new file mode 100644 index 0000000..0887f7a --- /dev/null +++ b/src/main/kotlin/view/VertexCanvasView.kt @@ -0,0 +1,24 @@ +package view + +import androidx.compose.foundation.background +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.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import viewModel.VertexCanvasViewModel + +@Composable +fun VertexCanvasView( + viewModel: VertexCanvasViewModel, + modifier: Modifier = Modifier +) { + Box( + modifier + .size(viewModel.radius * 2) + .offset(viewModel.offset.x.dp, viewModel.offset.y.dp) + .background(color = viewModel.color, shape = CircleShape) + ) +} \ No newline at end of file From 96775b6adc04e5e6d82ebd5da9f80c5d11fb697d Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 01:12:45 +0300 Subject: [PATCH 011/139] feat: add Main with working app and MainView --- src/main/kotlin/Main.kt | 42 ++++++++-------- src/main/kotlin/view/MainView.kt | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/view/MainView.kt diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index fed3f24..ecc1a10 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,31 +1,33 @@ -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.unit.Dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import model.graph.UndirectedGraph +import view.MainView +import viewModel.UndirectedViewModel -@Composable -@Preview -fun App() { - var text by remember { mutableStateOf("Hello, World!") } +val AMOUNT_NODES = 100 - MaterialTheme { - Button(onClick = { - text = "Hello, Desktop!" - }) { - Text(text) +val graph = UndirectedGraph().apply { + for (i in (0 until AMOUNT_NODES)) { + addVertex(i) + } + + for (i in (0 until AMOUNT_NODES)) { + for (j in (0 until AMOUNT_NODES)) { + if (Math.random() < 0.005) { + addEdge(i, j) + } } } } +val undirectedViewModel = UndirectedViewModel(graph, false) + fun main() = application { - Window(onCloseRequest = ::exitApplication) { - App() + Window( + onCloseRequest = ::exitApplication + ) { + MainView(undirectedViewModel) } } diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt new file mode 100644 index 0000000..0c0377a --- /dev/null +++ b/src/main/kotlin/view/MainView.kt @@ -0,0 +1,86 @@ +package view + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.PointerMatcher +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.* +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import viewModel.CanvasViewModel +import viewModel.UndirectedViewModel + +val HEADER_HEIGHT = 50f +val MENU_WIDTH = 100f + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Composable +fun MainView(undirectedViewModel: UndirectedViewModel) { + var zoom by remember { mutableFloatStateOf(1f) } + var center by remember { mutableStateOf(Offset(0f, 0f)) } + var canvasSize by remember { mutableStateOf(Offset(400f, 400f)) } + + val canvasViewModel = CanvasViewModel(undirectedViewModel, zoom, center, canvasSize) + + Row( + Modifier.fillMaxWidth().height(50f.dp).background(color = Color.Gray), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "Some Header", color = Color.White, fontSize = 20f.sp) + } + + Row(Modifier.offset(0f.dp, 50f.dp)) { + Column( + Modifier.fillMaxHeight().width(100f.dp).background(color = Color.Black), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = "Some menu", color = Color.White, fontSize = 20f.sp) + } + + CanvasView( + canvasViewModel, + Modifier + .fillMaxSize() + .border(1f.dp, Color.Red) + .onPointerEvent(PointerEventType.Scroll) { + if (it.changes.first().scrollDelta.y > 0) { + zoom -= zoom / 8 + } else { + zoom += zoom / 8 + + val awtEvent = it.awtEventOrNull + if (awtEvent != null) { + val xPosition = awtEvent.x.toFloat() - MENU_WIDTH + val yPosition = awtEvent.y.toFloat() - HEADER_HEIGHT + val pointerVector = (Offset(xPosition, yPosition) - (canvasSize / 2f)) * (1 / zoom) + center += pointerVector * 0.15f + } + } + }.pointerInput(Unit) { + detectDragGestures( + matcher = PointerMatcher.Primary + ) { + center -= it * (1 / zoom) + } + }.pointerHoverIcon(PointerIcon.Hand) + .onSizeChanged { + canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) + } + .clipToBounds() + ) + } +} \ No newline at end of file From 9116d6edc1ff5d8114e94b077c42dcbc1d7580f7 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 21 May 2024 16:59:26 +0300 Subject: [PATCH 012/139] feat: add animation to zoom and position transition --- src/main/kotlin/view/MainView.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 0c0377a..d5cc0b0 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -1,5 +1,9 @@ package view +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateOffsetAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.background @@ -29,10 +33,12 @@ val MENU_WIDTH = 100f @Composable fun MainView(undirectedViewModel: UndirectedViewModel) { var zoom by remember { mutableFloatStateOf(1f) } + val zoomAnimate by animateFloatAsState(zoom, tween(200, 0, LinearOutSlowInEasing)) var center by remember { mutableStateOf(Offset(0f, 0f)) } + val centerAnimate by animateOffsetAsState(center, tween(200, 0, LinearOutSlowInEasing)) var canvasSize by remember { mutableStateOf(Offset(400f, 400f)) } - val canvasViewModel = CanvasViewModel(undirectedViewModel, zoom, center, canvasSize) + val canvasViewModel = CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize) Row( Modifier.fillMaxWidth().height(50f.dp).background(color = Color.Gray), From 97473d6d028c60df7d164dba3a00a1e8a209456e Mon Sep 17 00:00:00 2001 From: Ilhom Kombaev Date: Tue, 21 May 2024 23:55:10 +0300 Subject: [PATCH 013/139] refactor: change graph interface --- src/main/kotlin/model/graph/Graph.kt | 2 +- src/main/kotlin/model/graph/UndirectedGraph.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 0925897..89b31f5 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -7,6 +7,6 @@ interface Graph { fun addVertex(key: V): Vertex? fun removeVertex(key: V): Vertex? fun updateVertex(key: V, newKey: V): Vertex? - fun addEdge(first: V, second: V): Edge? + fun addEdge(first: V, second: V, weight: Long = 1): Edge? fun removeEdge(first: V, second: V): Edge? } \ No newline at end of file diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index c51487c..4a95d2a 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -30,7 +30,7 @@ class UndirectedGraph: Graph { return vertex } - override fun addEdge(first: V, second: V): Edge? { + override fun addEdge(first: V, second: V, weight: Long): Edge? { val vertex1 = _vertices[first] ?: return null val vertex2 = _vertices[second] ?: return null From 7ac991a57de9e19d557566551d10983ea7250a31 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:37:49 +0300 Subject: [PATCH 014/139] feat: add Louvain library --- libs/louvain-1.0-SNAPSHOT-sources.jar | Bin 0 -> 5802 bytes libs/louvain-1.0-SNAPSHOT.jar | Bin 0 -> 23523 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 libs/louvain-1.0-SNAPSHOT-sources.jar create mode 100644 libs/louvain-1.0-SNAPSHOT.jar diff --git a/libs/louvain-1.0-SNAPSHOT-sources.jar b/libs/louvain-1.0-SNAPSHOT-sources.jar new file mode 100644 index 0000000000000000000000000000000000000000..d4c77a91ff8c2c3f444fb5acfcca21f128a49a26 GIT binary patch literal 5802 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 cf{J#L}d_ga9 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 literal 0 HcmV?d00001 From 5a2a06b084a8427692c0aa237ed475c955e901ed Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:43:55 +0300 Subject: [PATCH 015/139] feat: add Clustering class that can group vertices --- src/main/kotlin/model/algorithm/Clustering.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/main/kotlin/model/algorithm/Clustering.kt diff --git a/src/main/kotlin/model/algorithm/Clustering.kt b/src/main/kotlin/model/algorithm/Clustering.kt new file mode 100644 index 0000000..ab6f7bb --- /dev/null +++ b/src/main/kotlin/model/algorithm/Clustering.kt @@ -0,0 +1,48 @@ +package model.algorithm + +import model.graph.Graph +import model.graph.Vertex +import org.jetbrains.research.ictl.louvain.Link +import org.jetbrains.research.ictl.louvain.getPartition + +class Clustering(graph: Graph) { + val ids = hashMapOf, Int>() + val vIds = hashMapOf>() + val links = mutableListOf() + + init { + graph.vertices.forEachIndexed { i, v -> + ids[v] = i + vIds[i] = v + } + + graph.adjacencyList.values.flatten().forEach { e -> + val first = ids[e.first] ?: throw IllegalStateException("Vertex ${e.first} doesn't have id") + val second = ids[e.second] ?: throw IllegalStateException("Vertex ${e.second} doesn't have id") + + links.add(MyLink(first, second, e.weight.toDouble())) + } + + } + + fun calculate(): HashMap, Int> { + val map = getPartition(links, 0) + val result = map.mapKeys { vIds[it.key]!! } + return HashMap(result) + } + + inner class MyLink(private val source: Int, private val target: Int, private val weight: Double) : Link { + override fun source(): Int { + return source + } + + override fun target(): Int { + return target + } + + override fun weight(): Double { + return weight + } + } +} + From 0ee0986d33c2c893c3d7af1c3ba51c9b8997d97e Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:44:23 +0300 Subject: [PATCH 016/139] fix: notify gradle about dependency --- build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 7015b1d..23a2719 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,9 @@ repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") google() + flatDir { + dirs("libs") + } } dependencies { @@ -20,6 +23,7 @@ dependencies { // (in a separate module for demo project and in testMain). // With compose.desktop.common you will also lose @Preview functionality implementation(compose.desktop.currentOs) + implementation(":louvain-1.0-SNAPSHOT") testImplementation(kotlin("test")) } From 5591009a91626c4d7feb43c59c11ce90e964b2ed Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:45:18 +0300 Subject: [PATCH 017/139] fix: UndirectedGraph now can't add edge from node to same node --- src/main/kotlin/model/graph/UndirectedGraph.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index c51487c..7a3cf5b 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -class UndirectedGraph: Graph { +class UndirectedGraph : Graph { private val _vertices = hashMapOf>() private val _adjacencyList = hashMapOf, ArrayList>>() @@ -31,6 +31,8 @@ class UndirectedGraph: Graph { } override fun addEdge(first: V, second: V): Edge? { + if (first == second) return null + val vertex1 = _vertices[first] ?: return null val vertex2 = _vertices[second] ?: return null @@ -71,9 +73,9 @@ class UndirectedGraph: Graph { fun getEdges(vertex: Vertex) = _adjacencyList[vertex] - private data class UndirectedVertex(override var key: V): Vertex + private data class UndirectedVertex(override var key: V) : Vertex - private data class UndirectedEdge(override val first: Vertex, override val second: Vertex): Edge { + private data class UndirectedEdge(override val first: Vertex, override val second: Vertex) : Edge { override var weight: Long get() = 1 set(value) {} From fa9a981f2357197517b304de52b841a5420e9ec7 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:46:23 +0300 Subject: [PATCH 018/139] feat: add group property to UndirectedViewModel and replace placement method --- .../kotlin/viewModel/UndirectedViewModel.kt | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/viewModel/UndirectedViewModel.kt b/src/main/kotlin/viewModel/UndirectedViewModel.kt index 843ffa5..8b6446a 100644 --- a/src/main/kotlin/viewModel/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/UndirectedViewModel.kt @@ -1,15 +1,19 @@ package viewModel import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import model.graph.UndirectedGraph import model.graph.Vertex class UndirectedViewModel( private val graph: UndirectedGraph, showVerticesLabels: Boolean, + val groups: HashMap, Int> = hashMapOf() ) { private val _vertices = hashMapOf, VertexViewModel>() private val _adjacencyList = hashMapOf, ArrayList>>() + private val groupColors = hashMapOf(0 to Color.Black) val vertices get() = _vertices.values @@ -17,17 +21,53 @@ class UndirectedViewModel( val adjacencyList get() = _adjacencyList + private fun getColor(group: Int): Color { + val color = groupColors[group] + + if (color == null) { + val newColor = Color((0..255).random(), (0..255).random(), (0..255).random()) + groupColors[group] = newColor + return newColor + } + + return color + } + init { - graph.vertices.forEach { vertex -> - val vertexViewModel = - VertexViewModel( - showVerticesLabels, - vertex, - x = (-1000..1000).random().toFloat(), - y = (-1000..1000).random().toFloat() - ) + graph.vertices.forEachIndexed { i, vertex -> + val group = groups.getOrDefault(vertex, 0) + if (_vertices[vertex] != null) return@forEachIndexed + + val vertexViewModel = VertexViewModel( + showVerticesLabels, + vertex, + (-1000..1000).random().toFloat(), + (-1000..1000).random().toFloat(), + getColor(group), + ) _vertices[vertex] = vertexViewModel + + fun setOffsetEdges(vertex: Vertex, from: Offset) { + val edges = graph.adjacencyList[vertex] ?: return + edges.forEach { edge -> + val second = edge.second + if (_vertices[second] != null) return@forEach + + val secondVertexViewModel = VertexViewModel( + showVerticesLabels, + vertex, + (listOf(1f, -1f).random() * (40..90).random().toFloat()) + from.x, + (listOf(1f, -1f).random() * (40..90).random().toFloat()) + from.y, + getColor(groups.getOrDefault(second, 0)), + ) + + _vertices[second] = secondVertexViewModel + setOffsetEdges(second, Offset(secondVertexViewModel.x, secondVertexViewModel.y)) + } + } + + setOffsetEdges(vertex, Offset(vertexViewModel.x, vertexViewModel.y)) } graph.vertices.forEach { vertex -> From 5102f6144125e3456a3bbb9f57d50947611ff023 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:46:53 +0300 Subject: [PATCH 019/139] fix: edges place under vertices now --- src/main/kotlin/view/CanvasView.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/view/CanvasView.kt b/src/main/kotlin/view/CanvasView.kt index 54f0a67..aab9059 100644 --- a/src/main/kotlin/view/CanvasView.kt +++ b/src/main/kotlin/view/CanvasView.kt @@ -13,12 +13,12 @@ fun CanvasView( Box( modifier = modifier ) { - viewModel.vertices.forEach { - VertexCanvasView(it) - } - viewModel.edges.forEach { EdgeCanvasView(it) } + + viewModel.vertices.forEach { + VertexCanvasView(it) + } } } \ No newline at end of file From c055d20724081d759ce9f07d3e53b96c69c69058 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:49:08 +0300 Subject: [PATCH 020/139] feat: add example of clustering to main --- src/main/kotlin/Main.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index ecc1a10..7b53b24 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,12 +1,14 @@ -import androidx.compose.ui.unit.Dp import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState +import model.algorithm.Clustering import model.graph.UndirectedGraph import view.MainView import viewModel.UndirectedViewModel -val AMOUNT_NODES = 100 +val AMOUNT_NODES = 500 +val EDGE_CHANGE = 0.05f val graph = UndirectedGraph().apply { for (i in (0 until AMOUNT_NODES)) { @@ -15,18 +17,20 @@ val graph = UndirectedGraph().apply { for (i in (0 until AMOUNT_NODES)) { for (j in (0 until AMOUNT_NODES)) { - if (Math.random() < 0.005) { + if (Math.random() < EDGE_CHANGE / 100) { addEdge(i, j) } } } } -val undirectedViewModel = UndirectedViewModel(graph, false) +val groups = Clustering(graph).calculate() +val undirectedViewModel = UndirectedViewModel(graph, false, groups) fun main() = application { Window( - onCloseRequest = ::exitApplication + onCloseRequest = ::exitApplication, + state = rememberWindowState(placement = WindowPlacement.Maximized) ) { MainView(undirectedViewModel) } From 414cb0ba14e8708b37479bca20760dace3bed0ca Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 24 May 2024 12:57:44 +0300 Subject: [PATCH 021/139] fix: UndirectedGraph addEdge method now accept weight --- src/main/kotlin/model/graph/UndirectedGraph.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index 7a3cf5b..57738b9 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -30,7 +30,7 @@ class UndirectedGraph : Graph { return vertex } - override fun addEdge(first: V, second: V): Edge? { + override fun addEdge(first: V, second: V, weight: Long): Edge? { if (first == second) return null val vertex1 = _vertices[first] ?: return null From 4029a40285091733c4e036e32907354178e709fe Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Sun, 26 May 2024 01:25:38 +0300 Subject: [PATCH 022/139] Created a PageRank algorithm class --- src/main/kotlin/model/algoritm/PageRank.kt | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/kotlin/model/algoritm/PageRank.kt diff --git a/src/main/kotlin/model/algoritm/PageRank.kt b/src/main/kotlin/model/algoritm/PageRank.kt new file mode 100644 index 0000000..200002e --- /dev/null +++ b/src/main/kotlin/model/algoritm/PageRank.kt @@ -0,0 +1,47 @@ +package model.algoritm + +import model.graph.Graph +import model.graph.Vertex + +class PageRank( + private val graph: Graph, + private val dampingFactor: Double = 0.85, + private val iterations: Int = 100 +) { + fun computePageRank(topN: Int): List, Double>> { + val ranks = mutableMapOf, Double>() + val vertices = graph.vertices + + // Initialize ranks + vertices.forEach { vertex -> + ranks[vertex] = 1.0 / vertices.size + } + + repeat(iterations) { + val newRanks = mutableMapOf, Double>() + + vertices.forEach { vertex -> + var rankSum = 0.0 + vertices.forEach { neighbor -> + val edges = graph.adjacencyList[neighbor] + if (neighbor != vertex && edges != null) { + if (edges.any { it.second == vertex }) { + rankSum += ranks[neighbor]?.div(edges.size) ?: 0.0 + } + } + } + newRanks[vertex] = (1 - dampingFactor) / vertices.size + dampingFactor * rankSum + } + + // Update ranks + newRanks.forEach { (vertex, rank) -> + ranks[vertex] = rank + } + } + + return ranks.entries + .sortedByDescending { it.value } + .take(topN) + .map { it.toPair() } + } +} \ No newline at end of file From 7edf13bd3e40362dfe5375a34a869bdc7fca056a Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 27 May 2024 23:47:14 +0300 Subject: [PATCH 023/139] feat: created a directed graph class along with modifications to the access modifiers in the undirected graph --- src/main/kotlin/model/graph/DirectedGraph.kt | 25 +++++++++++++++++++ .../kotlin/model/graph/UndirectedGraph.kt | 6 ++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/model/graph/DirectedGraph.kt diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt new file mode 100644 index 0000000..015a9bd --- /dev/null +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -0,0 +1,25 @@ +package model.graph + +class DirectedGraph: UndirectedGraph() { + + override fun addEdge(first: V, second: V, weight: Long): Edge? { + if (first == second) return null + + val vertex1 = _vertices[first] ?: return null + val vertex2 = _vertices[second] ?: return null + + // edge already exists + if (_adjacencyList[vertex1]?.find { it.second.key == second } != null) return null + + _adjacencyList[vertex1]?.add(DirectedEdge(vertex1, vertex2)) + + + return _adjacencyList[vertex1]?.last() + } + + private data class DirectedEdge(override val first: Vertex, override val second: Vertex) : Edge { + override var weight: Long + get() = 1 + set(value) {} + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index 57738b9..15a67bb 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,8 +1,8 @@ package model.graph -class UndirectedGraph : Graph { - private val _vertices = hashMapOf>() - private val _adjacencyList = hashMapOf, ArrayList>>() +open class UndirectedGraph : Graph { + protected val _vertices = hashMapOf>() + protected val _adjacencyList = hashMapOf, ArrayList>>() override val vertices: Collection> get() = _vertices.values From 34396fb1ac1f292accdaac4458b567220f42f658 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 28 May 2024 03:21:38 +0300 Subject: [PATCH 024/139] feat: created tests for the directional graph --- .../kotlin/model/graph/DirectedGraphTest.kt | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/test/kotlin/model/graph/DirectedGraphTest.kt diff --git a/src/test/kotlin/model/graph/DirectedGraphTest.kt b/src/test/kotlin/model/graph/DirectedGraphTest.kt new file mode 100644 index 0000000..33a0201 --- /dev/null +++ b/src/test/kotlin/model/graph/DirectedGraphTest.kt @@ -0,0 +1,98 @@ +package model.graph + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class DirectedGraphTest { + val graph = DirectedGraph() + + fun Graph.findVertex(key: Int) = this.vertices.find { it.key == key } + fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[findVertex(key1)]?.find { it.second.key == key2 } + + fun Graph.checkExistingDirectedEdge(key1: Int, key2: Int) { + val edge1 = this.findEdge(key1, key2) + + assertNotNull(edge1) + assertEquals(edge1.weight, 1) + } + + @Nested + inner class addEdge { + @Test + fun `Not linked vertices`() { + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + assertNotNull(vertex1) + assertNotNull(vertex2) + + val edge = graph.addEdge(vertex1.key, vertex2.key) + + assertNotNull(edge) + graph.checkExistingDirectedEdge(1, 2) + } + + @Test + fun `Already linked vertices`() { + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + assertNotNull(vertex1) + assertNotNull(vertex2) + + graph.addEdge(vertex1.key, vertex2.key) + val edgeLinkedVertices = graph.addEdge(vertex1.key, vertex2.key) + + assertNull(edgeLinkedVertices) + graph.checkExistingDirectedEdge(1, 2) + } + + @Test + fun `Edge with non existing vertex`() { + val vertex = graph.addVertex(1) + + val edgeFirstNotExist = graph.addEdge(2, 1) + val edgeSecondNotExist = graph.addEdge(1, 2) + val edgeAllNotExist = graph.addEdge(3, 4) + + assertNull(edgeFirstNotExist) + assertNull(edgeSecondNotExist) + assertNull(edgeAllNotExist) + + assertEquals(graph.adjacencyList[vertex]?.size, 0) + } + + @Test + fun `Edge with identical vertices`() { + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + graph.addEdge(2, 1) + val edgeNotExist = graph.addEdge(1, 1) + + assertNull(edgeNotExist) + + assertEquals(graph.adjacencyList[vertex2]?.size, 1) + assertEquals(graph.adjacencyList[vertex1]?.size, 0) + + } + + @Test + fun `Identical edge`() { + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + graph.addEdge(2, 1) + val edgeNotExist = graph.addEdge(2, 1) + + assertNull(edgeNotExist) + + assertEquals(graph.adjacencyList[vertex2]?.size, 1) + assertEquals(graph.adjacencyList[vertex1]?.size, 0) + + } + } +} \ No newline at end of file From a102d71d2f5b4384a889af272ca98d2125e5c1ad Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 12:39:17 +0300 Subject: [PATCH 025/139] feat: add node settings window --- src/main/kotlin/Main.kt | 2 +- src/main/kotlin/view/HeaderView.kt | 26 ++++++ src/main/kotlin/view/MainView.kt | 24 +++--- src/main/kotlin/view/MenuView.kt | 26 ++++++ src/main/kotlin/view/SettingsView.kt | 76 ++++++++++++++++++ .../kotlin/viewModel/UndirectedViewModel.kt | 40 +++++++-- src/main/resources/Inter-Regular.ttf | Bin 0 -> 310252 bytes 7 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 src/main/kotlin/view/HeaderView.kt create mode 100644 src/main/kotlin/view/MenuView.kt create mode 100644 src/main/kotlin/view/SettingsView.kt create mode 100644 src/main/resources/Inter-Regular.ttf diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 7b53b24..a7d6d6a 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -25,7 +25,7 @@ val graph = UndirectedGraph().apply { } val groups = Clustering(graph).calculate() -val undirectedViewModel = UndirectedViewModel(graph, false, groups) +val undirectedViewModel = UndirectedViewModel(graph, false) fun main() = application { Window( diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt new file mode 100644 index 0000000..061086d --- /dev/null +++ b/src/main/kotlin/view/HeaderView.kt @@ -0,0 +1,26 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun HeaderView() { + println("HeaderView") + Row( + Modifier.fillMaxWidth().height(50f.dp).background(color = Color.Gray), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "Some Header", color = Color.White, fontSize = 20f.sp) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index d5cc0b0..ea7a69c 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -38,24 +38,13 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { val centerAnimate by animateOffsetAsState(center, tween(200, 0, LinearOutSlowInEasing)) var canvasSize by remember { mutableStateOf(Offset(400f, 400f)) } - val canvasViewModel = CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize) + val canvasViewModel = + CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize) - Row( - Modifier.fillMaxWidth().height(50f.dp).background(color = Color.Gray), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = "Some Header", color = Color.White, fontSize = 20f.sp) - } + HeaderView() Row(Modifier.offset(0f.dp, 50f.dp)) { - Column( - Modifier.fillMaxHeight().width(100f.dp).background(color = Color.Black), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text(text = "Some menu", color = Color.White, fontSize = 20f.sp) - } + MenuView() CanvasView( canvasViewModel, @@ -89,4 +78,9 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { .clipToBounds() ) } + + SettingsView( + undirectedViewModel::onColorChange, + undirectedViewModel::onSizeChange + ) } \ No newline at end of file diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt new file mode 100644 index 0000000..02758fd --- /dev/null +++ b/src/main/kotlin/view/MenuView.kt @@ -0,0 +1,26 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.width +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.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun MenuView() { + println("MenuView") + Column( + Modifier.fillMaxHeight().width(100f.dp).background(color = Color.Black), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = "Some menu", color = Color.White, fontSize = 20f.sp) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt new file mode 100644 index 0000000..46803a9 --- /dev/null +++ b/src/main/kotlin/view/SettingsView.kt @@ -0,0 +1,76 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +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.FontFamily +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.platform.Font +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex + +@Composable +fun MyText(text: String, fontSize: Float = 20f) { + val fontFamily = FontFamily(Font(resource = "Inter-Regular.ttf")) + Text(text = text, color = Color.White, fontFamily = fontFamily, fontSize = fontSize.sp) +} + +@Composable +fun MySlider(text: String, state: MutableState, range: ClosedFloatingPointRange = (0f..1f)) { + Row( + Modifier.padding(start = 5f.dp, end = 5f.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyText(text = text) + Slider( + modifier = Modifier.padding(0f.dp), + value = state.value, onValueChange = { change -> state.value = change }, + colors = SliderDefaults.colors( + thumbColor = Color.White, + activeTrackColor = Color.White, + inactiveTrackColor = Color.White, + ), + valueRange = range + ) + } +} + + +@Composable +fun SettingsView(onColorChange: (Color) -> Unit, onSizeChange: (Float) -> Unit) { + val redSlider = remember { mutableStateOf(0f) } + val greenSlider = remember { mutableStateOf(0f) } + val blueSlider = remember { mutableStateOf(0f) } + val sizeSlider = remember { mutableStateOf(10f) } + + onColorChange(Color(red = redSlider.value, green = greenSlider.value, blue = blueSlider.value)) + onSizeChange(sizeSlider.value) + + Box(Modifier.fillMaxSize().padding(top = 80f.dp, end = 20f.dp).zIndex(10f), contentAlignment = Alignment.TopEnd) { + Box( + Modifier.size(270f.dp, 270f.dp).background(Color(0xFF3D3D3D), RoundedCornerShape(10)) + ) { + Column { + Row(Modifier.fillMaxWidth().padding(top = 10f.dp), horizontalArrangement = Arrangement.Center) { + MyText("Node") + } + Row(Modifier.fillMaxWidth().padding(top = 10f.dp, start = 20f.dp)) { + Column { + MyText("Color:") + MySlider("R: ", redSlider) + MySlider("G: ", greenSlider) + MySlider("B: ", blueSlider) + MySlider("Size: ", sizeSlider, (5f..40f)) + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/UndirectedViewModel.kt b/src/main/kotlin/viewModel/UndirectedViewModel.kt index 8b6446a..8758ac6 100644 --- a/src/main/kotlin/viewModel/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/UndirectedViewModel.kt @@ -3,17 +3,26 @@ package viewModel import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import model.graph.UndirectedGraph import model.graph.Vertex class UndirectedViewModel( private val graph: UndirectedGraph, showVerticesLabels: Boolean, - val groups: HashMap, Int> = hashMapOf() + val groups: HashMap, Int> = hashMapOf(), ) { private val _vertices = hashMapOf, VertexViewModel>() private val _adjacencyList = hashMapOf, ArrayList>>() private val groupColors = hashMapOf(0 to Color.Black) + private val _color = mutableStateOf(Color.Black) + private val _size = mutableStateOf(10f) + + private var size + get() = _size.value + set(value) { + _size.value = value + } val vertices get() = _vertices.values @@ -22,17 +31,33 @@ class UndirectedViewModel( get() = _adjacencyList private fun getColor(group: Int): Color { - val color = groupColors[group] +// val color = groupColors[group] +// +// if (color == null) { +// val newColor = Color((0..255).random(), (0..255).random(), (0..255).random()) +// groupColors[group] = newColor +// return newColor +// } +// +// return color + return _color.value + } - if (color == null) { - val newColor = Color((0..255).random(), (0..255).random(), (0..255).random()) - groupColors[group] = newColor - return newColor + fun onColorChange(color: Color) { + _color.value = color + _vertices.forEach { + it.value.color = _color.value } + } - return color + fun onSizeChange(newSize: Float) { + size = newSize + _vertices.forEach { + it.value.radius = size.dp + } } + init { graph.vertices.forEachIndexed { i, vertex -> val group = groups.getOrDefault(vertex, 0) @@ -45,6 +70,7 @@ class UndirectedViewModel( (-1000..1000).random().toFloat(), (-1000..1000).random().toFloat(), getColor(group), + radius = size.dp ) _vertices[vertex] = vertexViewModel diff --git a/src/main/resources/Inter-Regular.ttf b/src/main/resources/Inter-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5e4851f0ab7e0268da6ce903306e2f871ee19821 GIT binary patch literal 310252 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 z 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%0D1lG~DE2ff7BeoTnFxKM!4172W=IjHdV9zf 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 Date: Tue, 28 May 2024 12:46:08 +0300 Subject: [PATCH 026/139] refactor: add folders for canvas and graph classes --- src/main/kotlin/Main.kt | 2 +- src/main/kotlin/view/MainView.kt | 9 +++------ src/main/kotlin/view/{ => canvas}/CanvasView.kt | 4 ++-- src/main/kotlin/view/{ => canvas}/EdgeCanvasView.kt | 4 ++-- src/main/kotlin/view/{ => canvas}/VertexCanvasView.kt | 4 ++-- src/main/kotlin/view/{ => graph}/EdgeView.kt | 4 ++-- src/main/kotlin/view/{ => graph}/UndirectedGraphView.kt | 4 ++-- src/main/kotlin/view/{ => graph}/VertexView.kt | 4 ++-- .../kotlin/viewModel/{ => canvas}/CanvasViewModel.kt | 3 ++- .../kotlin/viewModel/{ => canvas}/EdgeCanvasViewModel.kt | 2 +- .../viewModel/{ => canvas}/VertexCanvasViewModel.kt | 3 ++- src/main/kotlin/viewModel/{ => graph}/EdgeViewModel.kt | 4 ++-- .../kotlin/viewModel/{ => graph}/UndirectedViewModel.kt | 2 +- src/main/kotlin/viewModel/{ => graph}/VertexViewModel.kt | 2 +- 14 files changed, 25 insertions(+), 26 deletions(-) rename src/main/kotlin/view/{ => canvas}/CanvasView.kt (88%) rename src/main/kotlin/view/{ => canvas}/EdgeCanvasView.kt (93%) rename src/main/kotlin/view/{ => canvas}/VertexCanvasView.kt (91%) rename src/main/kotlin/view/{ => graph}/EdgeView.kt (94%) rename src/main/kotlin/view/{ => graph}/UndirectedGraphView.kt (89%) rename src/main/kotlin/view/{ => graph}/VertexView.kt (94%) rename src/main/kotlin/viewModel/{ => canvas}/CanvasViewModel.kt (92%) rename src/main/kotlin/viewModel/{ => canvas}/EdgeCanvasViewModel.kt (82%) rename src/main/kotlin/viewModel/{ => canvas}/VertexCanvasViewModel.kt (86%) rename src/main/kotlin/viewModel/{ => graph}/EdgeViewModel.kt (90%) rename src/main/kotlin/viewModel/{ => graph}/UndirectedViewModel.kt (99%) rename src/main/kotlin/viewModel/{ => graph}/VertexViewModel.kt (97%) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index a7d6d6a..f34324b 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.window.rememberWindowState import model.algorithm.Clustering import model.graph.UndirectedGraph import view.MainView -import viewModel.UndirectedViewModel +import viewModel.graph.UndirectedViewModel val AMOUNT_NODES = 500 val EDGE_CHANGE = 0.05f diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index ea7a69c..c6a467b 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -6,13 +6,10 @@ import androidx.compose.animation.core.animateOffsetAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* -import androidx.compose.material.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.awt.awtEventOrNull @@ -22,9 +19,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.* import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import viewModel.CanvasViewModel -import viewModel.UndirectedViewModel +import view.canvas.CanvasView +import viewModel.canvas.CanvasViewModel +import viewModel.graph.UndirectedViewModel val HEADER_HEIGHT = 50f val MENU_WIDTH = 100f diff --git a/src/main/kotlin/view/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt similarity index 88% rename from src/main/kotlin/view/CanvasView.kt rename to src/main/kotlin/view/canvas/CanvasView.kt index aab9059..db219fe 100644 --- a/src/main/kotlin/view/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -1,9 +1,9 @@ -package view +package view.canvas import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import viewModel.CanvasViewModel +import viewModel.canvas.CanvasViewModel @Composable fun CanvasView( diff --git a/src/main/kotlin/view/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt similarity index 93% rename from src/main/kotlin/view/EdgeCanvasView.kt rename to src/main/kotlin/view/canvas/EdgeCanvasView.kt index e8f1d9f..9e1544d 100644 --- a/src/main/kotlin/view/EdgeCanvasView.kt +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -1,4 +1,4 @@ -package view +package view.canvas import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize @@ -7,7 +7,7 @@ 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.EdgeCanvasViewModel +import viewModel.canvas.EdgeCanvasViewModel @Composable fun EdgeCanvasView( diff --git a/src/main/kotlin/view/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt similarity index 91% rename from src/main/kotlin/view/VertexCanvasView.kt rename to src/main/kotlin/view/canvas/VertexCanvasView.kt index 0887f7a..978578f 100644 --- a/src/main/kotlin/view/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -1,4 +1,4 @@ -package view +package view.canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -8,7 +8,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import viewModel.VertexCanvasViewModel +import viewModel.canvas.VertexCanvasViewModel @Composable fun VertexCanvasView( diff --git a/src/main/kotlin/view/EdgeView.kt b/src/main/kotlin/view/graph/EdgeView.kt similarity index 94% rename from src/main/kotlin/view/EdgeView.kt rename to src/main/kotlin/view/graph/EdgeView.kt index f480969..151157a 100644 --- a/src/main/kotlin/view/EdgeView.kt +++ b/src/main/kotlin/view/graph/EdgeView.kt @@ -1,4 +1,4 @@ -package view +package view.graph import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize @@ -7,7 +7,7 @@ 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.EdgeViewModel +import viewModel.graph.EdgeViewModel @Composable fun EdgeView( diff --git a/src/main/kotlin/view/UndirectedGraphView.kt b/src/main/kotlin/view/graph/UndirectedGraphView.kt similarity index 89% rename from src/main/kotlin/view/UndirectedGraphView.kt rename to src/main/kotlin/view/graph/UndirectedGraphView.kt index 533c419..da0966c 100644 --- a/src/main/kotlin/view/UndirectedGraphView.kt +++ b/src/main/kotlin/view/graph/UndirectedGraphView.kt @@ -1,10 +1,10 @@ -package view +package view.graph import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import viewModel.UndirectedViewModel +import viewModel.graph.UndirectedViewModel @Composable fun UndirectedGraphView( diff --git a/src/main/kotlin/view/VertexView.kt b/src/main/kotlin/view/graph/VertexView.kt similarity index 94% rename from src/main/kotlin/view/VertexView.kt rename to src/main/kotlin/view/graph/VertexView.kt index 77781f7..ccdffde 100644 --- a/src/main/kotlin/view/VertexView.kt +++ b/src/main/kotlin/view/graph/VertexView.kt @@ -1,4 +1,4 @@ -package view +package view.graph import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import viewModel.VertexViewModel +import viewModel.graph.VertexViewModel @Composable fun VertexView( diff --git a/src/main/kotlin/viewModel/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt similarity index 92% rename from src/main/kotlin/viewModel/CanvasViewModel.kt rename to src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index cd18884..5077209 100644 --- a/src/main/kotlin/viewModel/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -1,6 +1,7 @@ -package viewModel +package viewModel.canvas import androidx.compose.ui.geometry.Offset +import viewModel.graph.UndirectedViewModel class CanvasViewModel( graphViewModel: UndirectedViewModel, diff --git a/src/main/kotlin/viewModel/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt similarity index 82% rename from src/main/kotlin/viewModel/EdgeCanvasViewModel.kt rename to src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt index 8917d71..02b49aa 100644 --- a/src/main/kotlin/viewModel/EdgeCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -1,4 +1,4 @@ -package viewModel +package viewModel.canvas class EdgeCanvasViewModel( val first: VertexCanvasViewModel, diff --git a/src/main/kotlin/viewModel/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt similarity index 86% rename from src/main/kotlin/viewModel/VertexCanvasViewModel.kt rename to src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index d9afbc9..d5330fa 100644 --- a/src/main/kotlin/viewModel/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -1,6 +1,7 @@ -package viewModel +package viewModel.canvas import androidx.compose.ui.geometry.Offset +import viewModel.graph.VertexViewModel class VertexCanvasViewModel( val viewModel: VertexViewModel, diff --git a/src/main/kotlin/viewModel/EdgeViewModel.kt b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt similarity index 90% rename from src/main/kotlin/viewModel/EdgeViewModel.kt rename to src/main/kotlin/viewModel/graph/EdgeViewModel.kt index a1f5b3d..51b6823 100644 --- a/src/main/kotlin/viewModel/EdgeViewModel.kt +++ b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt @@ -1,10 +1,10 @@ -package viewModel +package viewModel.graph import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import model.graph.Edge -class EdgeViewModel ( +class EdgeViewModel( val first: VertexViewModel, val second: VertexViewModel, private val edge: Edge, diff --git a/src/main/kotlin/viewModel/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt similarity index 99% rename from src/main/kotlin/viewModel/UndirectedViewModel.kt rename to src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 8758ac6..16847c6 100644 --- a/src/main/kotlin/viewModel/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -1,4 +1,4 @@ -package viewModel +package viewModel.graph import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset diff --git a/src/main/kotlin/viewModel/VertexViewModel.kt b/src/main/kotlin/viewModel/graph/VertexViewModel.kt similarity index 97% rename from src/main/kotlin/viewModel/VertexViewModel.kt rename to src/main/kotlin/viewModel/graph/VertexViewModel.kt index f298779..738990a 100644 --- a/src/main/kotlin/viewModel/VertexViewModel.kt +++ b/src/main/kotlin/viewModel/graph/VertexViewModel.kt @@ -1,4 +1,4 @@ -package viewModel +package viewModel.graph import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color From 4b1cb23b89491f01161df214b6d2e471a6d9d985 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 12:52:48 +0300 Subject: [PATCH 027/139] feat: disable window header --- src/main/kotlin/Main.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index f34324b..8fe8a3b 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -30,7 +30,8 @@ val undirectedViewModel = UndirectedViewModel(graph, false) fun main() = application { Window( onCloseRequest = ::exitApplication, - state = rememberWindowState(placement = WindowPlacement.Maximized) + state = rememberWindowState(placement = WindowPlacement.Maximized), + undecorated = true, ) { MainView(undirectedViewModel) } From c22a9ba5cbb63166c9e7a3a40a4a0b33bfa8f623 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 13:18:36 +0300 Subject: [PATCH 028/139] feat: add config file --- src/main/kotlin/Config.kt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/kotlin/Config.kt diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt new file mode 100644 index 0000000..1696429 --- /dev/null +++ b/src/main/kotlin/Config.kt @@ -0,0 +1,5 @@ +import androidx.compose.ui.unit.dp + +object Config { + val headerHeight = 40f.dp +} \ No newline at end of file From 3d0543bdb10ce5a2a8fa3159f479c2a16520ff2f Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 20:12:53 +0300 Subject: [PATCH 029/139] feat: optimize render nodes that user don't see --- src/main/kotlin/view/canvas/CanvasView.kt | 2 +- src/main/kotlin/viewModel/canvas/CanvasViewModel.kt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index db219fe..db279dc 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -17,7 +17,7 @@ fun CanvasView( EdgeCanvasView(it) } - viewModel.vertices.forEach { + viewModel.getViews().forEach { VertexCanvasView(it) } } diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 5077209..f3af964 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -2,6 +2,7 @@ package viewModel.canvas import androidx.compose.ui.geometry.Offset import viewModel.graph.UndirectedViewModel +import kotlin.math.abs class CanvasViewModel( graphViewModel: UndirectedViewModel, @@ -27,4 +28,8 @@ class CanvasViewModel( val edges get() = _edges + + fun getViews(): Collection> { + return _vertices.filter { abs(it.value.offset.x) < canvasSize.x && abs(it.value.offset.y) < canvasSize.y }.values + } } \ No newline at end of file From 23723f00eec5733ef27d72d9f4134f4edab9d42c Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 20:14:17 +0300 Subject: [PATCH 030/139] feat: nodes' color now can be from clastering or settings --- .../viewModel/graph/UndirectedViewModel.kt | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 16847c6..0db92c2 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -31,19 +31,26 @@ class UndirectedViewModel( get() = _adjacencyList private fun getColor(group: Int): Color { -// val color = groupColors[group] -// -// if (color == null) { -// val newColor = Color((0..255).random(), (0..255).random(), (0..255).random()) -// groupColors[group] = newColor -// return newColor -// } -// -// return color + if (group > 0) { + val color = groupColors[group] + + if (color == null) { + val newColor = Color((0..255).random(), (0..255).random(), (0..255).random()) + groupColors[group] = newColor + return newColor + } + + return color + } + return _color.value } fun onColorChange(color: Color) { + if (groups.size > 0) { + return + } + _color.value = color _vertices.forEach { it.value.color = _color.value From 686b8869415805879743a0b614f333a21b27acb4 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 20:15:49 +0300 Subject: [PATCH 031/139] feat: node can be moved now --- src/main/kotlin/view/canvas/VertexCanvasView.kt | 4 ++++ src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt | 6 +++++- src/main/kotlin/viewModel/graph/VertexViewModel.kt | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index 978578f..7da82ce 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -1,6 +1,8 @@ package view.canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.onDrag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size @@ -10,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import viewModel.canvas.VertexCanvasViewModel +@OptIn(ExperimentalFoundationApi::class) @Composable fun VertexCanvasView( viewModel: VertexCanvasViewModel, @@ -20,5 +23,6 @@ fun VertexCanvasView( .size(viewModel.radius * 2) .offset(viewModel.offset.x.dp, viewModel.offset.y.dp) .background(color = viewModel.color, shape = CircleShape) + .onDrag(onDrag = viewModel::onDrag) ) } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index d5330fa..134426f 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -5,7 +5,7 @@ import viewModel.graph.VertexViewModel class VertexCanvasViewModel( val viewModel: VertexViewModel, - zoom: Float, + private val zoom: Float, center: Offset, canvasSize: Offset ) { @@ -16,4 +16,8 @@ class VertexCanvasViewModel( val radius = viewModel.radius * zoom val color = viewModel.color + + fun onDrag(it: Offset): Unit { + viewModel.onDrag(it * (1f / zoom)) + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/VertexViewModel.kt b/src/main/kotlin/viewModel/graph/VertexViewModel.kt index 738990a..5d16db3 100644 --- a/src/main/kotlin/viewModel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewModel/graph/VertexViewModel.kt @@ -1,6 +1,7 @@ package viewModel.graph import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -47,4 +48,9 @@ class VertexViewModel( val labelVisibility get() = _labelVisible + + fun onDrag(it: Offset): Unit { + x += it.x + y += it.y + } } \ No newline at end of file From 87a213016f172e1cd55e209e96bae3eff4a3892a Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 20:22:26 +0300 Subject: [PATCH 032/139] refactor: update config variables' type --- src/main/kotlin/Config.kt | 3 ++- src/main/kotlin/view/MainView.kt | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt index 1696429..5484716 100644 --- a/src/main/kotlin/Config.kt +++ b/src/main/kotlin/Config.kt @@ -1,5 +1,6 @@ import androidx.compose.ui.unit.dp object Config { - val headerHeight = 40f.dp + val headerHeight = 40f + val menuWidth = 80f } \ No newline at end of file diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index c6a467b..2bd352f 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -1,5 +1,6 @@ package view +import Config import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateOffsetAsState @@ -23,8 +24,8 @@ import view.canvas.CanvasView import viewModel.canvas.CanvasViewModel import viewModel.graph.UndirectedViewModel -val HEADER_HEIGHT = 50f -val MENU_WIDTH = 100f +val HEADER_HEIGHT = Config.headerHeight +val MENU_WIDTH = Config.menuWidth @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable @@ -38,9 +39,7 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { val canvasViewModel = CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize) - HeaderView() - - Row(Modifier.offset(0f.dp, 50f.dp)) { + Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { MenuView() CanvasView( From 5e7ed8b97fc7e8a3fa6793b3ef91f3a8b47f7af2 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 20:25:35 +0300 Subject: [PATCH 033/139] refactor: MyText now seperate view --- src/main/kotlin/view/MyText.kt | 14 ++++++++++++++ src/main/kotlin/view/SettingsView.kt | 5 ----- 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/view/MyText.kt diff --git a/src/main/kotlin/view/MyText.kt b/src/main/kotlin/view/MyText.kt new file mode 100644 index 0000000..b3229a6 --- /dev/null +++ b/src/main/kotlin/view/MyText.kt @@ -0,0 +1,14 @@ +package view + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.platform.Font +import androidx.compose.ui.unit.sp + +@Composable +fun MyText(text: String, fontSize: Float = 20f) { + val fontFamily = FontFamily(Font(resource = "Inter-Regular.ttf")) + Text(text = text, color = Color.White, fontFamily = fontFamily, fontSize = fontSize.sp) +} \ No newline at end of file diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt index 46803a9..de09384 100644 --- a/src/main/kotlin/view/SettingsView.kt +++ b/src/main/kotlin/view/SettingsView.kt @@ -16,11 +16,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -@Composable -fun MyText(text: String, fontSize: Float = 20f) { - val fontFamily = FontFamily(Font(resource = "Inter-Regular.ttf")) - Text(text = text, color = Color.White, fontFamily = fontFamily, fontSize = fontSize.sp) -} @Composable fun MySlider(text: String, state: MutableState, range: ClosedFloatingPointRange = (0f..1f)) { From 1c7f54a59dfb22257dccbe9e9033f9b46b31c8fb Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 23:16:46 +0300 Subject: [PATCH 034/139] feat: add header --- src/main/kotlin/Main.kt | 46 +++++++++++++++++++------ src/main/kotlin/view/HeaderView.kt | 55 +++++++++++++++++++++++++----- src/main/kotlin/view/MenuView.kt | 2 +- 3 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 8fe8a3b..979f4a5 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,9 +1,14 @@ -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowPlacement -import androidx.compose.ui.window.application -import androidx.compose.ui.window.rememberWindowState +import androidx.compose.foundation.window.WindowDraggableArea +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.* import model.algorithm.Clustering import model.graph.UndirectedGraph +import view.HeaderView import view.MainView import viewModel.graph.UndirectedViewModel @@ -28,11 +33,32 @@ val groups = Clustering(graph).calculate() val undirectedViewModel = UndirectedViewModel(graph, false) fun main() = application { - Window( - onCloseRequest = ::exitApplication, - state = rememberWindowState(placement = WindowPlacement.Maximized), - undecorated = true, - ) { - MainView(undirectedViewModel) + var isOpen by remember { mutableStateOf(true) } + var isMaximized by remember { mutableStateOf(true) } + var isMinimize by remember { mutableStateOf(false) } + var position: WindowPosition by remember { mutableStateOf(WindowPosition.PlatformDefault) } + var headerName by remember { mutableStateOf("Dimabase.db") } + + val windowState = WindowState( + placement = if (isMaximized) WindowPlacement.Maximized else WindowPlacement.Floating, + ) + + if (isOpen) { + Window( + onCloseRequest = ::exitApplication, + state = windowState, + undecorated = true, + ) { + WindowDraggableArea { + HeaderView(headerName, + { isOpen = false }, { + isMaximized = !isMaximized + }, + isMaximized, { windowState.isMinimized = !windowState.isMinimized }) + } + MainView( + undirectedViewModel, + ) + } } } diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index 061086d..4dbe6e9 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -1,26 +1,65 @@ package view +import Config +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.onDrag +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.onClick +import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Composable 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.unit.dp +import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp +@OptIn(ExperimentalFoundationApi::class) @Composable -fun HeaderView() { +fun HeaderView(name: String, close: () -> Unit, maximize: () -> Unit, isMaximize: Boolean, minimize: () -> Unit) { println("HeaderView") Row( - Modifier.fillMaxWidth().height(50f.dp).background(color = Color.Gray), - horizontalArrangement = Arrangement.Center, + Modifier.fillMaxWidth().height(Config.headerHeight.dp) + .background(color = Color(0xFF3D3D3D)) + .onDrag { + if (isMaximize) { + maximize() + } + }, + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(text = "Some Header", color = Color.White, fontSize = 20f.sp) + Row(Modifier.padding(start = Config.menuWidth.dp)) { + MyText("File") + } + Row { + Text(text = "$name", color = Color.White, fontSize = 20f.sp) + } + Row { + Box(Modifier.fillMaxHeight().width(Config.headerHeight.dp).background(Color(0xFF5A5959)).onClick { + minimize() + }, contentAlignment = Alignment.Center) { + Box(Modifier.size(10f.dp, 2f.dp).border(2f.dp, Color.White)) + } + Box(Modifier.fillMaxHeight().width(Config.headerHeight.dp).background(Color(0xFF5A5959)).onClick { + maximize() + }, contentAlignment = Alignment.Center) { + Box(Modifier.size(10f.dp, 8f.dp).border(2f.dp, Color.White)) + } + Box(Modifier.fillMaxHeight().width(Config.headerHeight.dp).background(Color(0xFFC80000)).onClick { + close() + }, contentAlignment = Alignment.Center) { + + Icon(imageVector = Icons.Filled.Close, "Done", tint = Color.White) + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 02758fd..2bc31ce 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.unit.sp fun MenuView() { println("MenuView") Column( - Modifier.fillMaxHeight().width(100f.dp).background(color = Color.Black), + Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { From 50e220d2018421ff5fec70eef66b3a6d6b7ae833 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Wed, 29 May 2024 01:41:52 +0300 Subject: [PATCH 035/139] feat: created a jacoco subreddit to track test coverage --- build.gradle.kts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 23a2719..1b146b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { kotlin("jvm") id("org.jetbrains.compose") + id("jacoco") } group = "com.example" @@ -25,6 +26,8 @@ dependencies { implementation(compose.desktop.currentOs) implementation(":louvain-1.0-SNAPSHOT") testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1") } compose.desktop { @@ -41,4 +44,17 @@ compose.desktop { tasks.test { useJUnitPlatform() + finalizedBy("jacocoTestReport") } + +tasks.jacocoTestReport{ + dependsOn(tasks.test) + reports{ + xml.required.set(true) + html.required.set(true) + } +} + +jacoco { + toolVersion = "0.8.12" +} \ No newline at end of file From a34a698847762f00acde76d22ea05ec57d5d3227 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 28 May 2024 23:37:47 +0300 Subject: [PATCH 036/139] feat: add shadow to button File --- src/main/kotlin/view/HeaderView.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index 4dbe6e9..ebe5f92 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.gestures.onDrag import androidx.compose.foundation.layout.* import androidx.compose.foundation.onClick +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -16,6 +17,7 @@ import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -38,7 +40,17 @@ fun HeaderView(name: String, close: () -> Unit, maximize: () -> Unit, isMaximize verticalAlignment = Alignment.CenterVertically ) { Row(Modifier.padding(start = Config.menuWidth.dp)) { - MyText("File") + Box( + Modifier + .size(Config.headerHeight.dp) + .shadow( + elevation = 5f.dp, + spotColor = Color.Black + ).background(Color(0xFF3D3D3D)), + contentAlignment = Alignment.Center + ) { + MyText("File", 16f) + } } Row { Text(text = "$name", color = Color.White, fontSize = 20f.sp) From ddd9fb63daae5a7039001468743ff65f12c3d31f Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 00:09:39 +0300 Subject: [PATCH 037/139] feat: add logo --- src/main/kotlin/view/HeaderView.kt | 38 ++++++++++++++++++------------ src/main/resources/Dima.svg | 33 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 src/main/resources/Dima.svg diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index ebe5f92..3c2695c 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -1,12 +1,9 @@ package view import Config -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border +import androidx.compose.foundation.* import androidx.compose.foundation.gestures.onDrag import androidx.compose.foundation.layout.* -import androidx.compose.foundation.onClick import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text @@ -20,6 +17,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.key.Key.Companion.R +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp @@ -39,17 +40,24 @@ fun HeaderView(name: String, close: () -> Unit, maximize: () -> Unit, isMaximize horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Row(Modifier.padding(start = Config.menuWidth.dp)) { - Box( - Modifier - .size(Config.headerHeight.dp) - .shadow( - elevation = 5f.dp, - spotColor = Color.Black - ).background(Color(0xFF3D3D3D)), - contentAlignment = Alignment.Center - ) { - MyText("File", 16f) + Row(Modifier) { + Row(Modifier.padding(start = 7f.dp, top = 7f.dp)) { + Image( + modifier = Modifier.padding(end = (Config.menuWidth - 30f - 7f).dp), + painter = painterResource("Dima.svg"), + contentDescription = "Icon" + ) + Box( + Modifier + .size(Config.headerHeight.dp) + .shadow( + elevation = 5f.dp, + spotColor = Color.Black + ).background(Color(0xFF3D3D3D)), + contentAlignment = Alignment.Center + ) { + MyText("File", 16f) + } } } Row { diff --git a/src/main/resources/Dima.svg b/src/main/resources/Dima.svg new file mode 100644 index 0000000..eb4c11c --- /dev/null +++ b/src/main/resources/Dima.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b0aa24849b07faef41d5681afec8a4d4867e85f9 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 00:22:02 +0300 Subject: [PATCH 038/139] feat: add icons for menu --- src/main/kotlin/view/MenuView.kt | 24 ++++++--- src/main/resources/Clustering.svg | 90 +++++++++++++++++++++++++++++++ src/main/resources/Nodes.svg | 5 ++ src/main/resources/PageRank.svg | 51 ++++++++++++++++++ src/main/resources/Ribs.svg | 6 +++ 5 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/Clustering.svg create mode 100644 src/main/resources/Nodes.svg create mode 100644 src/main/resources/PageRank.svg create mode 100644 src/main/resources/Ribs.svg diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 2bc31ce..fd6cd38 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -1,17 +1,23 @@ package view +import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text +import androidx.compose.foundation.layout.* 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.res.painterResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp + +@Composable +fun MenuIcon(name: String, description: String, modifier: Modifier = Modifier) { + Image( + painter = painterResource(name), + contentDescription = description, + modifier = modifier.padding(bottom = 10f.dp) + ) +} @Composable fun MenuView() { @@ -19,8 +25,10 @@ fun MenuView() { Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center ) { - Text(text = "Some menu", color = Color.White, fontSize = 20f.sp) + MenuIcon("Nodes.svg", "Add Node", Modifier.padding(top = 25f.dp)) + MenuIcon("Ribs.svg", "Add Edge") + MenuIcon("Clustering.svg", "Clustering") + MenuIcon("PageRank.svg", "Analysis graph") } } \ No newline at end of file diff --git a/src/main/resources/Clustering.svg b/src/main/resources/Clustering.svg new file mode 100644 index 0000000..0eff72e --- /dev/null +++ b/src/main/resources/Clustering.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/Nodes.svg b/src/main/resources/Nodes.svg new file mode 100644 index 0000000..418fcf7 --- /dev/null +++ b/src/main/resources/Nodes.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/PageRank.svg b/src/main/resources/PageRank.svg new file mode 100644 index 0000000..b641c2a --- /dev/null +++ b/src/main/resources/PageRank.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/Ribs.svg b/src/main/resources/Ribs.svg new file mode 100644 index 0000000..42c3024 --- /dev/null +++ b/src/main/resources/Ribs.svg @@ -0,0 +1,6 @@ + + + + + + From 73527384ee8973b2a669b204ee402b98b0faa7dc Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 00:26:58 +0300 Subject: [PATCH 039/139] fix: remove red border around canvas --- src/main/kotlin/view/MainView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 2bd352f..9e2a8b0 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -46,7 +46,6 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { canvasViewModel, Modifier .fillMaxSize() - .border(1f.dp, Color.Red) .onPointerEvent(PointerEventType.Scroll) { if (it.changes.first().scrollDelta.y > 0) { zoom -= zoom / 8 From 4c43261fa353a26cabafdf8d448ffc74b277e073 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 00:47:58 +0300 Subject: [PATCH 040/139] feat: now clustering button works --- src/main/kotlin/Main.kt | 5 ++++- src/main/kotlin/view/MainView.kt | 6 +++-- src/main/kotlin/view/MenuView.kt | 15 +++++++++---- .../viewModel/graph/UndirectedViewModel.kt | 22 +++++++++++++------ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 979f4a5..6d7b992 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -10,6 +10,7 @@ import model.algorithm.Clustering import model.graph.UndirectedGraph import view.HeaderView import view.MainView +import view.MenuView import viewModel.graph.UndirectedViewModel val AMOUNT_NODES = 500 @@ -30,7 +31,7 @@ val graph = UndirectedGraph().apply { } val groups = Clustering(graph).calculate() -val undirectedViewModel = UndirectedViewModel(graph, false) +val undirectedViewModel = UndirectedViewModel(graph, false, groups) fun main() = application { var isOpen by remember { mutableStateOf(true) } @@ -39,6 +40,8 @@ fun main() = application { var position: WindowPosition by remember { mutableStateOf(WindowPosition.PlatformDefault) } var headerName by remember { mutableStateOf("Dimabase.db") } + var isClustering by remember { mutableStateOf(false) } + val windowState = WindowState( placement = if (isMaximized) WindowPlacement.Maximized else WindowPlacement.Floating, ) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 9e2a8b0..1e0b7fe 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -36,12 +36,14 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { val centerAnimate by animateOffsetAsState(center, tween(200, 0, LinearOutSlowInEasing)) var canvasSize by remember { mutableStateOf(Offset(400f, 400f)) } + var isClustering by remember { mutableStateOf(false) } + undirectedViewModel.clustering = isClustering + val canvasViewModel = CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize) Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { - MenuView() - + MenuView { isClustering = !isClustering } CanvasView( canvasViewModel, Modifier diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index fd6cd38..cf3eeb2 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -1,8 +1,10 @@ package view +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.onClick import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -10,17 +12,20 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +@OptIn(ExperimentalFoundationApi::class) @Composable -fun MenuIcon(name: String, description: String, modifier: Modifier = Modifier) { +fun MenuIcon(name: String, description: String, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { Image( painter = painterResource(name), contentDescription = description, - modifier = modifier.padding(bottom = 10f.dp) + modifier = modifier + .padding(bottom = 10f.dp) + .onClick(onClick = onClick) ) } @Composable -fun MenuView() { +fun MenuView(onClusteringChange: () -> Unit) { println("MenuView") Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), @@ -28,7 +33,9 @@ fun MenuView() { ) { MenuIcon("Nodes.svg", "Add Node", Modifier.padding(top = 25f.dp)) MenuIcon("Ribs.svg", "Add Edge") - MenuIcon("Clustering.svg", "Clustering") + MenuIcon("Clustering.svg", "Clustering") { + onClusteringChange() + } MenuIcon("PageRank.svg", "Analysis graph") } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 0db92c2..c20bd26 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -17,6 +17,7 @@ class UndirectedViewModel( private val groupColors = hashMapOf(0 to Color.Black) private val _color = mutableStateOf(Color.Black) private val _size = mutableStateOf(10f) + private val _clustering = mutableStateOf(false) private var size get() = _size.value @@ -30,8 +31,15 @@ class UndirectedViewModel( val adjacencyList get() = _adjacencyList + var clustering + get() = _clustering.value + set(value) { + _clustering.value = value + updateColor() + } + private fun getColor(group: Int): Color { - if (group > 0) { + if (clustering) { val color = groupColors[group] if (color == null) { @@ -46,15 +54,15 @@ class UndirectedViewModel( return _color.value } - fun onColorChange(color: Color) { - if (groups.size > 0) { - return + fun updateColor() { + _vertices.forEach { + it.value.color = getColor(groups.getOrDefault(it.key, 0)) } + } + fun onColorChange(color: Color) { _color.value = color - _vertices.forEach { - it.value.color = _color.value - } + updateColor() } fun onSizeChange(newSize: Float) { From e7089acc0e40798fd8c965f500c2677a5ecc1a76 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 19:26:11 +0300 Subject: [PATCH 041/139] fix: move PageRank algorithm --- src/main/kotlin/model/{algoritm => algorithm}/PageRank.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/kotlin/model/{algoritm => algorithm}/PageRank.kt (98%) diff --git a/src/main/kotlin/model/algoritm/PageRank.kt b/src/main/kotlin/model/algorithm/PageRank.kt similarity index 98% rename from src/main/kotlin/model/algoritm/PageRank.kt rename to src/main/kotlin/model/algorithm/PageRank.kt index 200002e..d851699 100644 --- a/src/main/kotlin/model/algoritm/PageRank.kt +++ b/src/main/kotlin/model/algorithm/PageRank.kt @@ -1,4 +1,4 @@ -package model.algoritm +package model.algorithm import model.graph.Graph import model.graph.Vertex From 81da3c6c31cd9740552830b9a6861407b2f74ecd Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Wed, 29 May 2024 19:54:02 +0300 Subject: [PATCH 042/139] Created tests for the PageRank algorithm --- src/main/kotlin/model/algorithm/PageRank.kt | 47 +++++++++++++++++++ .../kotlin/model/algorithm/PageRankTest.kt | 38 +++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/main/kotlin/model/algorithm/PageRank.kt create mode 100644 src/test/kotlin/model/algorithm/PageRankTest.kt diff --git a/src/main/kotlin/model/algorithm/PageRank.kt b/src/main/kotlin/model/algorithm/PageRank.kt new file mode 100644 index 0000000..d851699 --- /dev/null +++ b/src/main/kotlin/model/algorithm/PageRank.kt @@ -0,0 +1,47 @@ +package model.algorithm + +import model.graph.Graph +import model.graph.Vertex + +class PageRank( + private val graph: Graph, + private val dampingFactor: Double = 0.85, + private val iterations: Int = 100 +) { + fun computePageRank(topN: Int): List, Double>> { + val ranks = mutableMapOf, Double>() + val vertices = graph.vertices + + // Initialize ranks + vertices.forEach { vertex -> + ranks[vertex] = 1.0 / vertices.size + } + + repeat(iterations) { + val newRanks = mutableMapOf, Double>() + + vertices.forEach { vertex -> + var rankSum = 0.0 + vertices.forEach { neighbor -> + val edges = graph.adjacencyList[neighbor] + if (neighbor != vertex && edges != null) { + if (edges.any { it.second == vertex }) { + rankSum += ranks[neighbor]?.div(edges.size) ?: 0.0 + } + } + } + newRanks[vertex] = (1 - dampingFactor) / vertices.size + dampingFactor * rankSum + } + + // Update ranks + newRanks.forEach { (vertex, rank) -> + ranks[vertex] = rank + } + } + + return ranks.entries + .sortedByDescending { it.value } + .take(topN) + .map { it.toPair() } + } +} \ No newline at end of file diff --git a/src/test/kotlin/model/algorithm/PageRankTest.kt b/src/test/kotlin/model/algorithm/PageRankTest.kt new file mode 100644 index 0000000..f04641e --- /dev/null +++ b/src/test/kotlin/model/algorithm/PageRankTest.kt @@ -0,0 +1,38 @@ +package model.algorithm + +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import org.junit.jupiter.api.Assertions.assertNotNull +import kotlin.test.Test + +class BetweennesCentralityTest { + + @Test + fun basic() { + val graph = UndirectedGraph() + for (i in 0..9) { + graph.addVertex(i) + } + graph.addEdge(1, 2) + graph.addEdge(1, 3) + graph.addEdge(1, 4) + graph.addEdge(2, 3) + graph.addEdge(2, 4) + graph.addEdge(3, 4) + graph.addEdge(2, 5) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(5, 7) + graph.addEdge(6, 7) + graph.addEdge(6, 8) + graph.addEdge(6, 9) + graph.addEdge(7, 8) + graph.addEdge(7, 9) + graph.addEdge(8, 9) + val centrality = PageRank(graph).computePageRank(2) + for ((vertex, value) in centrality) { + println("Vertex: $vertex, Betweenness Centrality: $value") + } + assertNotNull(centrality) + } +} \ No newline at end of file From 55695f3bd9d2873330953cb3d46539d5bb49d185 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Wed, 29 May 2024 19:56:40 +0300 Subject: [PATCH 043/139] feat: created new tests for the PageRank algorithm --- .../kotlin/model/algorithm/PageRankTest.kt | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/test/kotlin/model/algorithm/PageRankTest.kt b/src/test/kotlin/model/algorithm/PageRankTest.kt index f04641e..6bc7b87 100644 --- a/src/test/kotlin/model/algorithm/PageRankTest.kt +++ b/src/test/kotlin/model/algorithm/PageRankTest.kt @@ -4,35 +4,42 @@ import model.graph.DirectedGraph import model.graph.UndirectedGraph import org.junit.jupiter.api.Assertions.assertNotNull import kotlin.test.Test +import kotlin.test.assertEquals class BetweennesCentralityTest { @Test - fun basic() { - val graph = UndirectedGraph() - for (i in 0..9) { + fun basicDirectedGraph() { + val graph = DirectedGraph() + for (i in 0..3) { graph.addVertex(i) } + graph.addEdge(0, 1) + graph.addEdge(0, 2) graph.addEdge(1, 2) - graph.addEdge(1, 3) - graph.addEdge(1, 4) - graph.addEdge(2, 3) - graph.addEdge(2, 4) - graph.addEdge(3, 4) - graph.addEdge(2, 5) - graph.addEdge(4, 5) - graph.addEdge(5, 6) - graph.addEdge(5, 7) - graph.addEdge(6, 7) - graph.addEdge(6, 8) - graph.addEdge(6, 9) - graph.addEdge(7, 8) - graph.addEdge(7, 9) - graph.addEdge(8, 9) - val centrality = PageRank(graph).computePageRank(2) - for ((vertex, value) in centrality) { - println("Vertex: $vertex, Betweenness Centrality: $value") + graph.addEdge(3, 2) + graph.addEdge(2 ,0) + + val centrality = PageRank(graph).computePageRank(1) + + assertNotNull(centrality) + assertEquals(centrality[0].first.key, 2) + } + + @Test + fun basicUndirectedGraph() { + val graph = UndirectedGraph() + for (i in 0..3) { + graph.addVertex(i) } + graph.addEdge(0, 1) + graph.addEdge(0, 2) + graph.addEdge(1, 2) + graph.addEdge(3, 2) + + val centrality = PageRank(graph).computePageRank(1) + assertNotNull(centrality) + assertEquals(centrality[0].first.key, 2) } } \ No newline at end of file From 7af16ad91531e371017bc43e8d3beaed7da8d9b4 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 20:21:36 +0300 Subject: [PATCH 044/139] feat: add reader interface --- src/main/kotlin/model/reader/Reader.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/kotlin/model/reader/Reader.kt diff --git a/src/main/kotlin/model/reader/Reader.kt b/src/main/kotlin/model/reader/Reader.kt new file mode 100644 index 0000000..479f1d3 --- /dev/null +++ b/src/main/kotlin/model/reader/Reader.kt @@ -0,0 +1,8 @@ +package model.reader + +import model.graph.Graph + +interface Reader { + fun saveGraph(graph: Graph, filepath: String): Unit + fun loadGraph(filepath: String): Graph +} \ No newline at end of file From fed8fb84360bb91e78fc8a429a8fee0ae6f3bcfc Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 20:54:01 +0300 Subject: [PATCH 045/139] feat: add optimize parameter to config file --- src/main/kotlin/Config.kt | 1 + src/main/kotlin/viewModel/canvas/CanvasViewModel.kt | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt index 5484716..c145094 100644 --- a/src/main/kotlin/Config.kt +++ b/src/main/kotlin/Config.kt @@ -3,4 +3,5 @@ import androidx.compose.ui.unit.dp object Config { val headerHeight = 40f val menuWidth = 80f + val optimizeCanvas = false } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index f3af964..75965d0 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -30,6 +30,10 @@ class CanvasViewModel( get() = _edges fun getViews(): Collection> { - return _vertices.filter { abs(it.value.offset.x) < canvasSize.x && abs(it.value.offset.y) < canvasSize.y }.values + if (Config.optimizeCanvas) { + return _vertices.filter { abs(it.value.offset.x) < canvasSize.x && abs(it.value.offset.y) < canvasSize.y }.values + } + + return _vertices.values } } \ No newline at end of file From 5189e68b2085b904b17070959274984aa014db3c Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 20:55:11 +0300 Subject: [PATCH 046/139] refactor: canvas and nodes are now a different color --- src/main/kotlin/view/SettingsView.kt | 6 +++--- src/main/kotlin/view/canvas/CanvasView.kt | 4 +++- src/main/kotlin/view/canvas/VertexCanvasView.kt | 14 +++++++++++--- .../viewModel/canvas/VertexCanvasViewModel.kt | 3 +++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt index de09384..c8c9ccf 100644 --- a/src/main/kotlin/view/SettingsView.kt +++ b/src/main/kotlin/view/SettingsView.kt @@ -40,10 +40,10 @@ fun MySlider(text: String, state: MutableState, range: ClosedFloatingPoin @Composable fun SettingsView(onColorChange: (Color) -> Unit, onSizeChange: (Float) -> Unit) { - val redSlider = remember { mutableStateOf(0f) } + val redSlider = remember { mutableStateOf(1f / (0xFF / 0x8F)) } val greenSlider = remember { mutableStateOf(0f) } - val blueSlider = remember { mutableStateOf(0f) } - val sizeSlider = remember { mutableStateOf(10f) } + val blueSlider = remember { mutableStateOf(1f) } + val sizeSlider = remember { mutableStateOf(35f) } onColorChange(Color(red = redSlider.value, green = greenSlider.value, blue = blueSlider.value)) onSizeChange(sizeSlider.value) diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index db279dc..06cacfb 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -1,8 +1,10 @@ package view.canvas +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import viewModel.canvas.CanvasViewModel @Composable @@ -11,7 +13,7 @@ fun CanvasView( modifier: Modifier = Modifier ) { Box( - modifier = modifier + modifier = modifier.background(Color(0xFF242424)) ) { viewModel.edges.forEach { EdgeCanvasView(it) diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index 7da82ce..e7d7b7f 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -2,14 +2,18 @@ package view.canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.gestures.onDrag 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.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import view.MyText import viewModel.canvas.VertexCanvasViewModel @OptIn(ExperimentalFoundationApi::class) @@ -22,7 +26,11 @@ fun VertexCanvasView( modifier .size(viewModel.radius * 2) .offset(viewModel.offset.x.dp, viewModel.offset.y.dp) - .background(color = viewModel.color, shape = CircleShape) - .onDrag(onDrag = viewModel::onDrag) - ) + .border(color = viewModel.color, width = viewModel.strokeWidth.dp, shape = CircleShape) + .background(color = Color(0xFF242424), shape = CircleShape) + .onDrag(onDrag = viewModel::onDrag), + contentAlignment = Alignment.Center + ) { + MyText(viewModel.viewModel.label, viewModel.textSize.value) + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index 134426f..cb30c27 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -1,6 +1,7 @@ package viewModel.canvas import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.dp import viewModel.graph.VertexViewModel class VertexCanvasViewModel( @@ -16,6 +17,8 @@ class VertexCanvasViewModel( val radius = viewModel.radius * zoom val color = viewModel.color + val strokeWidth = (3f * zoom) + val textSize = (viewModel.radius * 0.8f * zoom) fun onDrag(it: Offset): Unit { viewModel.onDrag(it * (1f / zoom)) From 663d6f8f15886c50f4dab2bfbbd0db29212d7576 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 21:20:31 +0300 Subject: [PATCH 047/139] fix: fix bug in edge first and second node are same --- src/main/kotlin/viewModel/graph/UndirectedViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index c20bd26..0a12217 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -97,7 +97,7 @@ class UndirectedViewModel( val secondVertexViewModel = VertexViewModel( showVerticesLabels, - vertex, + second, (listOf(1f, -1f).random() * (40..90).random().toFloat()) + from.x, (listOf(1f, -1f).random() * (40..90).random().toFloat()) + from.y, getColor(groups.getOrDefault(second, 0)), From 276cbfca17db44496df3ee2d449f0cc8ff52d972 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 22:15:01 +0300 Subject: [PATCH 048/139] feat: edge now prettier --- src/main/kotlin/Config.kt | 6 ++ src/main/kotlin/view/canvas/EdgeCanvasView.kt | 55 +++++++++++++++---- .../viewModel/canvas/CanvasViewModel.kt | 2 +- .../viewModel/canvas/EdgeCanvasViewModel.kt | 12 +++- .../kotlin/viewModel/graph/EdgeViewModel.kt | 22 +++++++- 5 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt index c145094..870b698 100644 --- a/src/main/kotlin/Config.kt +++ b/src/main/kotlin/Config.kt @@ -1,7 +1,13 @@ +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp object Config { val headerHeight = 40f val menuWidth = 80f val optimizeCanvas = false + + object Edge { + val color = Color(0xFF00E0FF) + val strokeWidth = 8f + } } \ No newline at end of file diff --git a/src/main/kotlin/view/canvas/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt index 9e1544d..f5e81b3 100644 --- a/src/main/kotlin/view/canvas/EdgeCanvasView.kt +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -15,17 +15,48 @@ fun EdgeCanvasView( modifier: Modifier = Modifier ) { Canvas(Modifier.fillMaxSize()) { - drawLine( - start = Offset( - viewModel.first.offset.x.dp.toPx() + viewModel.first.radius.toPx(), - viewModel.first.offset.y.dp.toPx() + viewModel.first.radius.toPx(), - ), - end = Offset( - viewModel.second.offset.x.dp.toPx() + viewModel.second.radius.toPx(), - viewModel.second.offset.y.dp.toPx() + viewModel.second.radius.toPx(), - ), - color = Color.Black, - strokeWidth = 1f.dp.toPx() - ) + // something hard thing for drawing edge from border of node, not from center + val firstCenter = viewModel.first.offset + Offset(viewModel.first.radius.value, viewModel.first.radius.value) + val secondCenter = + viewModel.second.offset + Offset(viewModel.second.radius.value, viewModel.second.radius.value) + + val vector = (secondCenter - firstCenter) + val vectorNorm = vector / vector.getDistance() + val radiusVector = vectorNorm * viewModel.first.radius.value + + val start = firstCenter + radiusVector + val end = secondCenter - radiusVector + + if ((secondCenter - firstCenter).getDistance() > viewModel.first.radius.value + viewModel.second.radius.value) { + drawLine( + start = start, + end = end, + color = viewModel.color, + strokeWidth = viewModel.strokeWidth.dp.toPx() + ) + } + + if (viewModel.showOrientation) { + drawLine( + start = end, + end = end - rotateVector(radiusVector * 0.8f, 30.0), + color = viewModel.color, + strokeWidth = viewModel.strokeWidth * 0.8f + ) + + drawLine( + start = end, + end = end - rotateVector(radiusVector * 0.8f, -30.0), + color = viewModel.color, + strokeWidth = viewModel.strokeWidth * 0.8f + ) + } } +} + +fun rotateVector(vec: Offset, angle: Double): Offset { + val radians = Math.toRadians(angle) + val cos = Math.cos(radians).toFloat() + val sin = Math.sin(radians).toFloat() + return Offset(vec.x * cos - sin * vec.y, sin * vec.x + cos * vec.y) } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 75965d0..6b061e4 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -20,7 +20,7 @@ class CanvasViewModel( val vertex2 = _vertices[it.second] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.second}") - EdgeCanvasViewModel(vertex1, vertex2) + EdgeCanvasViewModel(vertex1, vertex2, it.color, it.strokeWidth, zoom) } val vertices diff --git a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt index 02b49aa..d08b435 100644 --- a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -1,6 +1,14 @@ package viewModel.canvas +import androidx.compose.ui.graphics.Color + class EdgeCanvasViewModel( val first: VertexCanvasViewModel, - val second: VertexCanvasViewModel -) \ No newline at end of file + val second: VertexCanvasViewModel, + val color: Color, + strokeWidth: Float, + zoom: Float, + val showOrientation: Boolean = true +) { + val strokeWidth = strokeWidth * zoom +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt index 51b6823..9e97991 100644 --- a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt +++ b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt @@ -1,14 +1,18 @@ package viewModel.graph +import Config import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color import model.graph.Edge class EdgeViewModel( val first: VertexViewModel, val second: VertexViewModel, private val edge: Edge, - private val _weightVisibility: State + private val _weightVisibility: State, + color: Color = Config.Edge.color, + strokeWidth: Float = Config.Edge.strokeWidth ) { private var _weight = mutableStateOf(edge.weight) var weight @@ -17,4 +21,18 @@ class EdgeViewModel( _weight.value = value edge.weight = value } -} \ No newline at end of file + + private var _color = mutableStateOf(color) + var color + get() = _color.value + set(value) { + _color.value = value + } + + private var _strokeWidth = mutableStateOf(strokeWidth) + var strokeWidth + get() = _strokeWidth.value + set(value) { + _strokeWidth.value = value + } +} From 7d8e6c34ea4af71b1d239d8655a098dae3fad1d3 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 22:22:47 +0300 Subject: [PATCH 049/139] feat: orientated options in settings --- src/main/kotlin/view/MainView.kt | 6 ++++-- src/main/kotlin/view/SettingsView.kt | 16 ++++++++++++++-- .../kotlin/viewModel/canvas/CanvasViewModel.kt | 5 +++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 1e0b7fe..0c12e43 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -36,11 +36,12 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { val centerAnimate by animateOffsetAsState(center, tween(200, 0, LinearOutSlowInEasing)) var canvasSize by remember { mutableStateOf(Offset(400f, 400f)) } + var isOrientated by remember { mutableStateOf(false) } var isClustering by remember { mutableStateOf(false) } undirectedViewModel.clustering = isClustering val canvasViewModel = - CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize) + CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize, isOrientated) Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { MenuView { isClustering = !isClustering } @@ -78,6 +79,7 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { SettingsView( undirectedViewModel::onColorChange, - undirectedViewModel::onSizeChange + undirectedViewModel::onSizeChange, + { isOrientated = !isOrientated } ) } \ No newline at end of file diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt index c8c9ccf..aaaf2eb 100644 --- a/src/main/kotlin/view/SettingsView.kt +++ b/src/main/kotlin/view/SettingsView.kt @@ -39,18 +39,19 @@ fun MySlider(text: String, state: MutableState, range: ClosedFloatingPoin @Composable -fun SettingsView(onColorChange: (Color) -> Unit, onSizeChange: (Float) -> Unit) { +fun SettingsView(onColorChange: (Color) -> Unit, onSizeChange: (Float) -> Unit, onOrientatedChange: () -> Unit) { val redSlider = remember { mutableStateOf(1f / (0xFF / 0x8F)) } val greenSlider = remember { mutableStateOf(0f) } val blueSlider = remember { mutableStateOf(1f) } val sizeSlider = remember { mutableStateOf(35f) } + val orientatedCheckBox = remember { mutableStateOf(false) } onColorChange(Color(red = redSlider.value, green = greenSlider.value, blue = blueSlider.value)) onSizeChange(sizeSlider.value) Box(Modifier.fillMaxSize().padding(top = 80f.dp, end = 20f.dp).zIndex(10f), contentAlignment = Alignment.TopEnd) { Box( - Modifier.size(270f.dp, 270f.dp).background(Color(0xFF3D3D3D), RoundedCornerShape(10)) + Modifier.size(270f.dp, 320f.dp).background(Color(0xFF3D3D3D), RoundedCornerShape(10)) ) { Column { Row(Modifier.fillMaxWidth().padding(top = 10f.dp), horizontalArrangement = Arrangement.Center) { @@ -65,6 +66,17 @@ fun SettingsView(onColorChange: (Color) -> Unit, onSizeChange: (Float) -> Unit) MySlider("Size: ", sizeSlider, (5f..40f)) } } + + Row( + Modifier.fillMaxWidth().padding(start = 20f.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyText("Orientated") + Checkbox(orientatedCheckBox.value, onCheckedChange = { + onOrientatedChange() + orientatedCheckBox.value = !orientatedCheckBox.value + }) + } } } } diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 6b061e4..64663aa 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -8,7 +8,8 @@ class CanvasViewModel( graphViewModel: UndirectedViewModel, var zoom: Float, var center: Offset, - var canvasSize: Offset + var canvasSize: Offset, + var isOrientated: Boolean ) { private val _vertices = graphViewModel.vertices.associateWith { v -> VertexCanvasViewModel(v, zoom, center, canvasSize) @@ -20,7 +21,7 @@ class CanvasViewModel( val vertex2 = _vertices[it.second] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.second}") - EdgeCanvasViewModel(vertex1, vertex2, it.color, it.strokeWidth, zoom) + EdgeCanvasViewModel(vertex1, vertex2, it.color, it.strokeWidth, zoom, showOrientation = isOrientated) } val vertices From 8dee8964f2bc463a3cd2a35e0c8417d9af5868ca Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 22:32:03 +0300 Subject: [PATCH 050/139] refactor: settings now have sizeSlider from 5 to 80 --- src/main/kotlin/view/SettingsView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt index aaaf2eb..72d90ed 100644 --- a/src/main/kotlin/view/SettingsView.kt +++ b/src/main/kotlin/view/SettingsView.kt @@ -63,7 +63,7 @@ fun SettingsView(onColorChange: (Color) -> Unit, onSizeChange: (Float) -> Unit, MySlider("R: ", redSlider) MySlider("G: ", greenSlider) MySlider("B: ", blueSlider) - MySlider("Size: ", sizeSlider, (5f..40f)) + MySlider("Size: ", sizeSlider, (5f..80f)) } } From d2a7e7e4859288c5958430147ffc7953f3d8aa34 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 22:32:26 +0300 Subject: [PATCH 051/139] refactor: node's strokeWidth now larger and text smaller --- src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index cb30c27..0168eea 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -17,8 +17,8 @@ class VertexCanvasViewModel( val radius = viewModel.radius * zoom val color = viewModel.color - val strokeWidth = (3f * zoom) - val textSize = (viewModel.radius * 0.8f * zoom) + val strokeWidth = (8f * zoom) + val textSize = (viewModel.radius * 0.6f * zoom) fun onDrag(it: Offset): Unit { viewModel.onDrag(it * (1f / zoom)) From c74514b1441f21c2210b236dfc10b7d3a2e785b5 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 23:20:36 +0300 Subject: [PATCH 052/139] feat: add node button now works --- src/main/kotlin/view/MainView.kt | 9 ++++++++- src/main/kotlin/view/MenuView.kt | 31 +++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 0c12e43..08d5ae6 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* +import androidx.compose.foundation.onClick import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -38,13 +39,19 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { var isOrientated by remember { mutableStateOf(false) } var isClustering by remember { mutableStateOf(false) } + var isNodeCreatingMode by remember { mutableStateOf(false) } + undirectedViewModel.clustering = isClustering val canvasViewModel = CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize, isOrientated) Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { - MenuView { isClustering = !isClustering } + MenuView( + isNodeCreatingMode, + { isNodeCreatingMode = !isNodeCreatingMode }, + isClustering, + { isClustering = !isClustering }) CanvasView( canvasViewModel, Modifier diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index cf3eeb2..603ddfc 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -1,11 +1,10 @@ package view -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.onClick +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -19,23 +18,39 @@ fun MenuIcon(name: String, description: String, modifier: Modifier = Modifier, o painter = painterResource(name), contentDescription = description, modifier = modifier - .padding(bottom = 10f.dp) .onClick(onClick = onClick) ) + Spacer(Modifier.height(10f.dp)) } @Composable -fun MenuView(onClusteringChange: () -> Unit) { +fun MenuView( + isNodeCreating: Boolean, + onNodeCreatingChange: () -> Unit, + isClustering: Boolean, + onClusteringChange: () -> Unit +) { println("MenuView") Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), horizontalAlignment = Alignment.CenterHorizontally, ) { - MenuIcon("Nodes.svg", "Add Node", Modifier.padding(top = 25f.dp)) + Spacer(Modifier.height(25f.dp)) + MenuIcon( + "Nodes.svg", "Add Node", Modifier.glow(isNodeCreating) + ) { + onNodeCreatingChange() + } MenuIcon("Ribs.svg", "Add Edge") - MenuIcon("Clustering.svg", "Clustering") { + MenuIcon("Clustering.svg", "Clustering", Modifier.glow(isClustering)) { onClusteringChange() } MenuIcon("PageRank.svg", "Analysis graph") } +} + +fun Modifier.glow(flag: Boolean): Modifier { + if (!flag) return Modifier + + return Modifier.border(4f.dp, color = Color(0xFFFF00FF), shape = CircleShape) } \ No newline at end of file From 726d6d11e47f1587ed045738065eb46e09641377 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 23:25:46 +0300 Subject: [PATCH 053/139] refactor: graph now can be only Int type :( --- src/main/kotlin/Main.kt | 8 ++---- src/main/kotlin/model/algorithm/Clustering.kt | 8 +++--- src/main/kotlin/model/algorithm/PageRank.kt | 8 +++--- src/main/kotlin/model/graph/DirectedGraph.kt | 20 ++++++------- src/main/kotlin/model/graph/Edge.kt | 6 ++-- src/main/kotlin/model/graph/Graph.kt | 16 +++++------ .../kotlin/model/graph/UndirectedGraph.kt | 28 +++++++++---------- src/main/kotlin/model/graph/Vertex.kt | 4 +-- src/main/kotlin/model/reader/Reader.kt | 4 +-- src/main/kotlin/view/MainView.kt | 2 +- src/main/kotlin/view/canvas/CanvasView.kt | 4 +-- src/main/kotlin/view/canvas/EdgeCanvasView.kt | 4 +-- .../kotlin/view/canvas/VertexCanvasView.kt | 4 +-- src/main/kotlin/view/graph/EdgeView.kt | 4 +-- .../kotlin/view/graph/UndirectedGraphView.kt | 4 +-- src/main/kotlin/view/graph/VertexView.kt | 4 +-- .../viewModel/canvas/CanvasViewModel.kt | 6 ++-- .../viewModel/canvas/EdgeCanvasViewModel.kt | 6 ++-- .../viewModel/canvas/VertexCanvasViewModel.kt | 4 +-- .../kotlin/viewModel/graph/EdgeViewModel.kt | 8 +++--- .../viewModel/graph/UndirectedViewModel.kt | 14 +++++----- .../kotlin/viewModel/graph/VertexViewModel.kt | 4 +-- 22 files changed, 84 insertions(+), 86 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 6d7b992..cbd8eea 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -13,10 +13,10 @@ import view.MainView import view.MenuView import viewModel.graph.UndirectedViewModel -val AMOUNT_NODES = 500 -val EDGE_CHANGE = 0.05f +val AMOUNT_NODES = 4 +val EDGE_CHANGE = 100f -val graph = UndirectedGraph().apply { +val graph = UndirectedGraph().apply { for (i in (0 until AMOUNT_NODES)) { addVertex(i) } @@ -40,8 +40,6 @@ fun main() = application { var position: WindowPosition by remember { mutableStateOf(WindowPosition.PlatformDefault) } var headerName by remember { mutableStateOf("Dimabase.db") } - var isClustering by remember { mutableStateOf(false) } - val windowState = WindowState( placement = if (isMaximized) WindowPlacement.Maximized else WindowPlacement.Floating, ) diff --git a/src/main/kotlin/model/algorithm/Clustering.kt b/src/main/kotlin/model/algorithm/Clustering.kt index ab6f7bb..c2c009f 100644 --- a/src/main/kotlin/model/algorithm/Clustering.kt +++ b/src/main/kotlin/model/algorithm/Clustering.kt @@ -5,9 +5,9 @@ import model.graph.Vertex import org.jetbrains.research.ictl.louvain.Link import org.jetbrains.research.ictl.louvain.getPartition -class Clustering(graph: Graph) { - val ids = hashMapOf, Int>() - val vIds = hashMapOf>() +class Clustering(graph: Graph) { + val ids = hashMapOf() + val vIds = hashMapOf() val links = mutableListOf() init { @@ -25,7 +25,7 @@ class Clustering(graph: Graph) { } - fun calculate(): HashMap, Int> { + fun calculate(): HashMap { val map = getPartition(links, 0) val result = map.mapKeys { vIds[it.key]!! } return HashMap(result) diff --git a/src/main/kotlin/model/algorithm/PageRank.kt b/src/main/kotlin/model/algorithm/PageRank.kt index d851699..34659f0 100644 --- a/src/main/kotlin/model/algorithm/PageRank.kt +++ b/src/main/kotlin/model/algorithm/PageRank.kt @@ -4,12 +4,12 @@ import model.graph.Graph import model.graph.Vertex class PageRank( - private val graph: Graph, + private val graph: Graph, private val dampingFactor: Double = 0.85, private val iterations: Int = 100 ) { - fun computePageRank(topN: Int): List, Double>> { - val ranks = mutableMapOf, Double>() + fun computePageRank(topN: Int): List> { + val ranks = mutableMapOf() val vertices = graph.vertices // Initialize ranks @@ -18,7 +18,7 @@ class PageRank( } repeat(iterations) { - val newRanks = mutableMapOf, Double>() + val newRanks = mutableMapOf() vertices.forEach { vertex -> var rankSum = 0.0 diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index 015a9bd..4d4e043 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,23 +1,23 @@ package model.graph -class DirectedGraph: UndirectedGraph() { +class DirectedGraph : UndirectedGraph() { - override fun addEdge(first: V, second: V, weight: Long): Edge? { - if (first == second) return null + override fun addEdge(first: Int, second: Int, weight: Long): Edge? { + if (first == second) return null - val vertex1 = _vertices[first] ?: return null - val vertex2 = _vertices[second] ?: return null + val vertex1 = _vertices[first] ?: return null + val vertex2 = _vertices[second] ?: return null - // edge already exists - if (_adjacencyList[vertex1]?.find { it.second.key == second } != null) return null + // edge already exists + if (_adjacencyList[vertex1]?.find { it.second.key == second } != null) return null - _adjacencyList[vertex1]?.add(DirectedEdge(vertex1, vertex2)) + _adjacencyList[vertex1]?.add(DirectedEdge(vertex1, vertex2)) - return _adjacencyList[vertex1]?.last() + return _adjacencyList[vertex1]?.last() } - private data class DirectedEdge(override val first: Vertex, override val second: Vertex) : Edge { + private data class DirectedEdge(override val first: Vertex, override val second: Vertex) : Edge { override var weight: Long get() = 1 set(value) {} diff --git a/src/main/kotlin/model/graph/Edge.kt b/src/main/kotlin/model/graph/Edge.kt index 4597324..4b39b1f 100644 --- a/src/main/kotlin/model/graph/Edge.kt +++ b/src/main/kotlin/model/graph/Edge.kt @@ -1,7 +1,7 @@ package model.graph -interface Edge { - val first: Vertex - val second: Vertex +interface Edge { + val first: Vertex + val second: Vertex var weight: Long } \ No newline at end of file diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 89b31f5..e2e2888 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -1,12 +1,12 @@ package model.graph -interface Graph { - val vertices: Collection> - val adjacencyList: HashMap, ArrayList>> +interface Graph { + val vertices: Collection + val adjacencyList: HashMap> - fun addVertex(key: V): Vertex? - fun removeVertex(key: V): Vertex? - fun updateVertex(key: V, newKey: V): Vertex? - fun addEdge(first: V, second: V, weight: Long = 1): Edge? - fun removeEdge(first: V, second: V): Edge? + fun addVertex(key: Int): Vertex? + fun removeVertex(key: Int): Vertex? + fun updateVertex(key: Int, newKey: Int): Vertex? + fun addEdge(first: Int, second: Int, weight: Long = 1): Edge? + fun removeEdge(first: Int, second: Int): Edge? } \ No newline at end of file diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index 15a67bb..9f80745 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,16 +1,16 @@ package model.graph -open class UndirectedGraph : Graph { - protected val _vertices = hashMapOf>() - protected val _adjacencyList = hashMapOf, ArrayList>>() +open class UndirectedGraph : Graph { + protected val _vertices = hashMapOf() + protected val _adjacencyList = hashMapOf>() - override val vertices: Collection> + override val vertices: Collection get() = _vertices.values - override val adjacencyList: HashMap, ArrayList>> + override val adjacencyList: HashMap> get() = _adjacencyList - override fun addVertex(key: V): Vertex? { + override fun addVertex(key: Int): Vertex? { if (_vertices[key] != null) return null val vertex = UndirectedVertex(key) @@ -21,7 +21,7 @@ open class UndirectedGraph : Graph { return vertex } - override fun removeVertex(key: V): Vertex? { + override fun removeVertex(key: Int): Vertex? { val vertex = _vertices[key] ?: return null _vertices.remove(key) @@ -30,7 +30,7 @@ open class UndirectedGraph : Graph { return vertex } - override fun addEdge(first: V, second: V, weight: Long): Edge? { + override fun addEdge(first: Int, second: Int, weight: Long): Edge? { if (first == second) return null val vertex1 = _vertices[first] ?: return null @@ -45,7 +45,7 @@ open class UndirectedGraph : Graph { return _adjacencyList[vertex1]?.last() } - override fun removeEdge(first: V, second: V): Edge? { + override fun removeEdge(first: Int, second: Int): Edge? { val vertex1 = _vertices[first] val vertex2 = _vertices[second] @@ -61,7 +61,7 @@ open class UndirectedGraph : Graph { return edge1 } - override fun updateVertex(key: V, newKey: V): Vertex? { + override fun updateVertex(key: Int, newKey: Int): Vertex? { val vertex = _vertices[key] ?: return null if (_vertices[newKey] != null) return null @@ -69,13 +69,13 @@ open class UndirectedGraph : Graph { return vertex } - fun findVertex(key: V) = _vertices[key] + fun findVertex(key: Int) = _vertices[key] - fun getEdges(vertex: Vertex) = _adjacencyList[vertex] + fun getEdges(vertex: Vertex) = _adjacencyList[vertex] - private data class UndirectedVertex(override var key: V) : Vertex + private data class UndirectedVertex(override var key: Int) : Vertex - private data class UndirectedEdge(override val first: Vertex, override val second: Vertex) : Edge { + private data class UndirectedEdge(override val first: Vertex, override val second: Vertex) : Edge { override var weight: Long get() = 1 set(value) {} diff --git a/src/main/kotlin/model/graph/Vertex.kt b/src/main/kotlin/model/graph/Vertex.kt index e65e094..27a3d38 100644 --- a/src/main/kotlin/model/graph/Vertex.kt +++ b/src/main/kotlin/model/graph/Vertex.kt @@ -1,5 +1,5 @@ package model.graph -interface Vertex { - var key: V +interface Vertex { + var key: Int } \ No newline at end of file diff --git a/src/main/kotlin/model/reader/Reader.kt b/src/main/kotlin/model/reader/Reader.kt index 479f1d3..2078dad 100644 --- a/src/main/kotlin/model/reader/Reader.kt +++ b/src/main/kotlin/model/reader/Reader.kt @@ -3,6 +3,6 @@ package model.reader import model.graph.Graph interface Reader { - fun saveGraph(graph: Graph, filepath: String): Unit - fun loadGraph(filepath: String): Graph + fun saveGraph(graph: Graph, filepath: String): Unit + fun loadGraph(filepath: String): Graph } \ No newline at end of file diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 08d5ae6..ccaf5cc 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -30,7 +30,7 @@ val MENU_WIDTH = Config.menuWidth @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable -fun MainView(undirectedViewModel: UndirectedViewModel) { +fun MainView(undirectedViewModel: UndirectedViewModel) { var zoom by remember { mutableFloatStateOf(1f) } val zoomAnimate by animateFloatAsState(zoom, tween(200, 0, LinearOutSlowInEasing)) var center by remember { mutableStateOf(Offset(0f, 0f)) } diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index 06cacfb..6b2db38 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -8,8 +8,8 @@ import androidx.compose.ui.graphics.Color import viewModel.canvas.CanvasViewModel @Composable -fun CanvasView( - viewModel: CanvasViewModel, +fun CanvasView( + viewModel: CanvasViewModel, modifier: Modifier = Modifier ) { Box( diff --git a/src/main/kotlin/view/canvas/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt index f5e81b3..8be99e4 100644 --- a/src/main/kotlin/view/canvas/EdgeCanvasView.kt +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -10,8 +10,8 @@ import androidx.compose.ui.unit.dp import viewModel.canvas.EdgeCanvasViewModel @Composable -fun EdgeCanvasView( - viewModel: EdgeCanvasViewModel, +fun EdgeCanvasView( + viewModel: EdgeCanvasViewModel, modifier: Modifier = Modifier ) { Canvas(Modifier.fillMaxSize()) { diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index e7d7b7f..ea33d78 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -18,8 +18,8 @@ import viewModel.canvas.VertexCanvasViewModel @OptIn(ExperimentalFoundationApi::class) @Composable -fun VertexCanvasView( - viewModel: VertexCanvasViewModel, +fun VertexCanvasView( + viewModel: VertexCanvasViewModel, modifier: Modifier = Modifier ) { Box( diff --git a/src/main/kotlin/view/graph/EdgeView.kt b/src/main/kotlin/view/graph/EdgeView.kt index 151157a..c59e73c 100644 --- a/src/main/kotlin/view/graph/EdgeView.kt +++ b/src/main/kotlin/view/graph/EdgeView.kt @@ -10,8 +10,8 @@ import androidx.compose.ui.unit.dp import viewModel.graph.EdgeViewModel @Composable -fun EdgeView( - viewModel: EdgeViewModel +fun EdgeView( + viewModel: EdgeViewModel ) { Canvas(Modifier.fillMaxSize()) { drawLine( diff --git a/src/main/kotlin/view/graph/UndirectedGraphView.kt b/src/main/kotlin/view/graph/UndirectedGraphView.kt index da0966c..727c007 100644 --- a/src/main/kotlin/view/graph/UndirectedGraphView.kt +++ b/src/main/kotlin/view/graph/UndirectedGraphView.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.Modifier import viewModel.graph.UndirectedViewModel @Composable -fun UndirectedGraphView( - viewModel: UndirectedViewModel +fun UndirectedGraphView( + viewModel: UndirectedViewModel ) { Box(Modifier.fillMaxSize()) { viewModel.vertices.forEach { v -> diff --git a/src/main/kotlin/view/graph/VertexView.kt b/src/main/kotlin/view/graph/VertexView.kt index ccdffde..357fa27 100644 --- a/src/main/kotlin/view/graph/VertexView.kt +++ b/src/main/kotlin/view/graph/VertexView.kt @@ -13,8 +13,8 @@ import androidx.compose.ui.unit.dp import viewModel.graph.VertexViewModel @Composable -fun VertexView( - viewModel: VertexViewModel, +fun VertexView( + viewModel: VertexViewModel, modifier: Modifier = Modifier ) { Box( diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 64663aa..883968e 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -4,8 +4,8 @@ import androidx.compose.ui.geometry.Offset import viewModel.graph.UndirectedViewModel import kotlin.math.abs -class CanvasViewModel( - graphViewModel: UndirectedViewModel, +class CanvasViewModel( + graphViewModel: UndirectedViewModel, var zoom: Float, var center: Offset, var canvasSize: Offset, @@ -30,7 +30,7 @@ class CanvasViewModel( val edges get() = _edges - fun getViews(): Collection> { + fun getViews(): Collection { if (Config.optimizeCanvas) { return _vertices.filter { abs(it.value.offset.x) < canvasSize.x && abs(it.value.offset.y) < canvasSize.y }.values } diff --git a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt index d08b435..61028c6 100644 --- a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -2,9 +2,9 @@ package viewModel.canvas import androidx.compose.ui.graphics.Color -class EdgeCanvasViewModel( - val first: VertexCanvasViewModel, - val second: VertexCanvasViewModel, +class EdgeCanvasViewModel( + val first: VertexCanvasViewModel, + val second: VertexCanvasViewModel, val color: Color, strokeWidth: Float, zoom: Float, diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index 0168eea..ba727e4 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -4,8 +4,8 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.dp import viewModel.graph.VertexViewModel -class VertexCanvasViewModel( - val viewModel: VertexViewModel, +class VertexCanvasViewModel( + val viewModel: VertexViewModel, private val zoom: Float, center: Offset, canvasSize: Offset diff --git a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt index 9e97991..cca76b4 100644 --- a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt +++ b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import model.graph.Edge -class EdgeViewModel( - val first: VertexViewModel, - val second: VertexViewModel, - private val edge: Edge, +class EdgeViewModel( + val first: VertexViewModel, + val second: VertexViewModel, + private val edge: Edge, private val _weightVisibility: State, color: Color = Config.Edge.color, strokeWidth: Float = Config.Edge.strokeWidth diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 0a12217..611d47f 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -7,13 +7,13 @@ import androidx.compose.ui.unit.dp import model.graph.UndirectedGraph import model.graph.Vertex -class UndirectedViewModel( - private val graph: UndirectedGraph, +class UndirectedViewModel( + private val graph: UndirectedGraph, showVerticesLabels: Boolean, - val groups: HashMap, Int> = hashMapOf(), + val groups: HashMap = hashMapOf(), ) { - private val _vertices = hashMapOf, VertexViewModel>() - private val _adjacencyList = hashMapOf, ArrayList>>() + private val _vertices = hashMapOf() + private val _adjacencyList = hashMapOf>() private val groupColors = hashMapOf(0 to Color.Black) private val _color = mutableStateOf(Color.Black) private val _size = mutableStateOf(10f) @@ -89,7 +89,7 @@ class UndirectedViewModel( ) _vertices[vertex] = vertexViewModel - fun setOffsetEdges(vertex: Vertex, from: Offset) { + fun setOffsetEdges(vertex: Vertex, from: Offset) { val edges = graph.adjacencyList[vertex] ?: return edges.forEach { edge -> val second = edge.second @@ -112,7 +112,7 @@ class UndirectedViewModel( } graph.vertices.forEach { vertex -> - val arrayList = arrayListOf>() + val arrayList = arrayListOf() val vertexVM1 = _vertices[vertex] ?: throw IllegalStateException() graph.adjacencyList[vertex]?.forEach { edge -> diff --git a/src/main/kotlin/viewModel/graph/VertexViewModel.kt b/src/main/kotlin/viewModel/graph/VertexViewModel.kt index 5d16db3..2806f7f 100644 --- a/src/main/kotlin/viewModel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewModel/graph/VertexViewModel.kt @@ -7,9 +7,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import model.graph.Vertex -class VertexViewModel( +class VertexViewModel( private val _labelVisible: Boolean, - private val vertex: Vertex, + private val vertex: Vertex, x: Float = 0f, y: Float = 0f, color: Color = Color.Black, From 1dce8587b0dff354af70656921de54a62582d4b9 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Wed, 29 May 2024 23:55:27 +0300 Subject: [PATCH 054/139] feat: now you can add nodes --- src/main/kotlin/view/MainView.kt | 10 +++++++++ src/main/kotlin/view/canvas/CanvasView.kt | 2 +- .../viewModel/canvas/CanvasViewModel.kt | 12 +++++++++-- .../viewModel/graph/UndirectedViewModel.kt | 21 ++++++++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index ccaf5cc..e17b385 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.onClick import androidx.compose.runtime.* @@ -41,6 +42,8 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { var isClustering by remember { mutableStateOf(false) } var isNodeCreatingMode by remember { mutableStateOf(false) } + var update = mutableStateOf(false) + undirectedViewModel.clustering = isClustering val canvasViewModel = @@ -76,6 +79,13 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { ) { center -= it * (1 / zoom) } + }.pointerInput(Unit) { + detectTapGestures { + if (isNodeCreatingMode) { + canvasViewModel.createVertex(it - (canvasSize / 2f)) + zoom += 0.000001f // костыль для рекомпозиции + } + } }.pointerHoverIcon(PointerIcon.Hand) .onSizeChanged { canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index 6b2db38..018df83 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -19,7 +19,7 @@ fun CanvasView( EdgeCanvasView(it) } - viewModel.getViews().forEach { + viewModel.vertices.forEach { VertexCanvasView(it) } } diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 883968e..e0e7e8d 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -2,10 +2,11 @@ package viewModel.canvas import androidx.compose.ui.geometry.Offset import viewModel.graph.UndirectedViewModel +import viewModel.graph.VertexViewModel import kotlin.math.abs class CanvasViewModel( - graphViewModel: UndirectedViewModel, + val graphViewModel: UndirectedViewModel, var zoom: Float, var center: Offset, var canvasSize: Offset, @@ -13,7 +14,7 @@ class CanvasViewModel( ) { private val _vertices = graphViewModel.vertices.associateWith { v -> VertexCanvasViewModel(v, zoom, center, canvasSize) - } + }.toMutableMap() private val _edges = graphViewModel.adjacencyList.map { it.value }.flatten().map { val vertex1 = @@ -37,4 +38,11 @@ class CanvasViewModel( return _vertices.values } + + fun createVertex(offset: Offset) { + val coordinates = offset * zoom + center + val viewModel = graphViewModel.createVertex(coordinates) ?: return + + _vertices[viewModel] = VertexCanvasViewModel(viewModel, zoom, center, canvasSize) + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 611d47f..456cdff 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -3,13 +3,14 @@ package viewModel.graph import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.dp import model.graph.UndirectedGraph import model.graph.Vertex class UndirectedViewModel( private val graph: UndirectedGraph, - showVerticesLabels: Boolean, + val showVerticesLabels: Boolean, val groups: HashMap = hashMapOf(), ) { private val _vertices = hashMapOf() @@ -72,6 +73,24 @@ class UndirectedViewModel( } } + fun createVertex(coordinates: Offset): VertexViewModel? { + val vertex = graph.addVertex(graph.vertices.last().key + 1) + + if (vertex == null) return null + + val viewModel = VertexViewModel( + showVerticesLabels, + vertex, + coordinates.x - size, + coordinates.y - size, + getColor(groups.getOrDefault(vertex, 0)), + radius = size.dp + ) + + _vertices[vertex] = viewModel + + return viewModel + } init { graph.vertices.forEachIndexed { i, vertex -> From 82fe793f59a9a0177976067cae5f1844f9e941f6 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Wed, 29 May 2024 23:57:17 +0300 Subject: [PATCH 055/139] feat: created algorithm for find bridges in graph --- .../kotlin/model/algorithm/FindBridges.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/kotlin/model/algorithm/FindBridges.kt diff --git a/src/main/kotlin/model/algorithm/FindBridges.kt b/src/main/kotlin/model/algorithm/FindBridges.kt new file mode 100644 index 0000000..1af252c --- /dev/null +++ b/src/main/kotlin/model/algorithm/FindBridges.kt @@ -0,0 +1,58 @@ +package model.algorithm + + +import model.graph.* + + +class FindBridges( + private val graph: Graph +) { + fun findBridges(): List> { + val visited = mutableMapOf, Boolean>() + val disc = mutableMapOf, Int>() + val low = mutableMapOf, Int>() + val parent = mutableMapOf, Vertex?>() + val bridges = mutableListOf>() + var time = 0 + + graph.vertices.forEach { vertex -> + if (visited[vertex] != true) { + findBridgesUtil(vertex, visited, disc, low, parent, bridges, time) + } + } + + return bridges + } + + private fun findBridgesUtil( + u: Vertex, + visited: MutableMap, Boolean>, + disc: MutableMap, Int>, + low: MutableMap, Int>, + parent: MutableMap, Vertex?>, + bridges: MutableList>, + time: Int + ) { + var currentTime = time + visited[u] = true + disc[u] = currentTime + low[u] = currentTime + currentTime++ + + graph.adjacencyList[u]?.forEach { edge -> + val v = if (edge.first == u) edge.second else edge.first + if (visited[v] != true) { + parent[v] = u + findBridgesUtil(v, visited, disc, low, parent, bridges, currentTime) + + low[u] = minOf(low[u]!!, low[v]!!) + + if (low[v]!! > disc[u]!!) { + bridges.add(edge) + } + } else if (v != parent[u]) { + low[u] = minOf(low[u]!!, disc[v]!!) + } + } + } +} From fd5183aae4f5eb6818be5e9b63c900c65a5f6652 Mon Sep 17 00:00:00 2001 From: Ilhom Kombaev Date: Thu, 30 May 2024 00:02:58 +0300 Subject: [PATCH 056/139] Create README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e40299 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +### Graph3 + +With our program you can: +- Add nodes, change their color, size +- Move nodes over canvas +- Drag and zoom canvas (Mouse scroll and drag LMB) +- Cluster nodes + +![Screenshot_20240530_000236](https://github.com/spbu-coding-2023/graphs-graphs-3/assets/39369841/00b2e6a4-aec1-46d0-845f-ce099df9dbf3) From 74d325d563193be862b24aec510c283ff2a504e0 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 30 May 2024 00:14:51 +0300 Subject: [PATCH 057/139] fix: fix add node button --- src/main/kotlin/view/MainView.kt | 2 +- src/main/kotlin/viewModel/canvas/CanvasViewModel.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index e17b385..b3656ea 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -82,7 +82,7 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { }.pointerInput(Unit) { detectTapGestures { if (isNodeCreatingMode) { - canvasViewModel.createVertex(it - (canvasSize / 2f)) + canvasViewModel.createVertex(it - (canvasSize / 2f), center, zoom) zoom += 0.000001f // костыль для рекомпозиции } } diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index e0e7e8d..dd78267 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -39,8 +39,9 @@ class CanvasViewModel( return _vertices.values } - fun createVertex(offset: Offset) { - val coordinates = offset * zoom + center + fun createVertex(offset: Offset, center: Offset, zoom: Float) { + println(center) + val coordinates = offset * (1 / zoom) + center val viewModel = graphViewModel.createVertex(coordinates) ?: return _vertices[viewModel] = VertexCanvasViewModel(viewModel, zoom, center, canvasSize) From 07541adc285d1d63d8cb072830814ae816afed0f Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 30 May 2024 00:15:51 +0300 Subject: [PATCH 058/139] refactor: remove println functions --- src/main/kotlin/view/HeaderView.kt | 1 - src/main/kotlin/view/MenuView.kt | 1 - src/main/kotlin/viewModel/canvas/CanvasViewModel.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index 3c2695c..68e5486 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.unit.sp @OptIn(ExperimentalFoundationApi::class) @Composable fun HeaderView(name: String, close: () -> Unit, maximize: () -> Unit, isMaximize: Boolean, minimize: () -> Unit) { - println("HeaderView") Row( Modifier.fillMaxWidth().height(Config.headerHeight.dp) .background(color = Color(0xFF3D3D3D)) diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 603ddfc..c4dcb1a 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -30,7 +30,6 @@ fun MenuView( isClustering: Boolean, onClusteringChange: () -> Unit ) { - println("MenuView") Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index dd78267..0e7696d 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -40,7 +40,6 @@ class CanvasViewModel( } fun createVertex(offset: Offset, center: Offset, zoom: Float) { - println(center) val coordinates = offset * (1 / zoom) + center val viewModel = graphViewModel.createVertex(coordinates) ?: return From 087e618a9d1a4b76a4da4f1a7dc7cc62d95b5e5d Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 30 May 2024 00:19:58 +0300 Subject: [PATCH 059/139] refactor: change nodes and layout algorithm --- src/main/kotlin/Main.kt | 6 ++++-- src/main/kotlin/viewModel/graph/UndirectedViewModel.kt | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index cbd8eea..eba65fb 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -7,14 +7,15 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import model.algorithm.Clustering +import model.algorithm.PageRank import model.graph.UndirectedGraph import view.HeaderView import view.MainView import view.MenuView import viewModel.graph.UndirectedViewModel -val AMOUNT_NODES = 4 -val EDGE_CHANGE = 100f +val AMOUNT_NODES = 16 +val EDGE_CHANGE = 5f val graph = UndirectedGraph().apply { for (i in (0 until AMOUNT_NODES)) { @@ -31,6 +32,7 @@ val graph = UndirectedGraph().apply { } val groups = Clustering(graph).calculate() +val ranks = PageRank(graph).computePageRank(3) val undirectedViewModel = UndirectedViewModel(graph, false, groups) fun main() = application { diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 456cdff..41ab8af 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -117,8 +117,8 @@ class UndirectedViewModel( val secondVertexViewModel = VertexViewModel( showVerticesLabels, second, - (listOf(1f, -1f).random() * (40..90).random().toFloat()) + from.x, - (listOf(1f, -1f).random() * (40..90).random().toFloat()) + from.y, + (listOf(1f, -1f).random() * (100..200).random().toFloat()) + from.x, + (listOf(1f, -1f).random() * (100..200).random().toFloat()) + from.y, getColor(groups.getOrDefault(second, 0)), ) From 1eaea30edf043bdbdad9af5bb296bbb1c125078b Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Thu, 30 May 2024 00:24:31 +0300 Subject: [PATCH 060/139] feat: created tests for the algorithm for finding bridges in a graph --- .../kotlin/model/algorithm/FindBridgesTest.kt | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/test/kotlin/model/algorithm/FindBridgesTest.kt diff --git a/src/test/kotlin/model/algorithm/FindBridgesTest.kt b/src/test/kotlin/model/algorithm/FindBridgesTest.kt new file mode 100644 index 0000000..672e1fc --- /dev/null +++ b/src/test/kotlin/model/algorithm/FindBridgesTest.kt @@ -0,0 +1,121 @@ +package model.algorithm + +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class FindBridgesTest { + @Nested + inner class `find bridges in directed graph` { + @Test + fun `class base test`(){ + val graph = DirectedGraph() + + graph.addVertex(1) + graph.addVertex(2) + graph.addVertex(3) + graph.addVertex(4) + graph.addVertex(5) + graph.addVertex(6) + + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 1) + graph.addEdge(1, 4) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(6, 4) + + val bridges = FindBridges(graph).findBridges() + + assertEquals(Pair(bridges[0].first.key, bridges[0].second.key), Pair(1, 4)) + } + @Test + fun `crossing multiple bridges`() { + val graph = DirectedGraph() + + graph.addVertex(1) + graph.addVertex(2) + graph.addVertex(3) + graph.addVertex(4) + graph.addVertex(5) + graph.addVertex(6) + graph.addVertex(7) + + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 1) + graph.addEdge(1, 4) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(6, 7) + graph.addEdge(7, 5) + + val bridges = FindBridges(graph).findBridges() + + assertEquals( + listOf(Pair(bridges[0].first.key, bridges[0].second.key), + Pair(bridges[1].first.key, bridges[1].second.key)), + listOf(Pair(4, 5), Pair(1, 4)) + ) + } + } + + @Nested + inner class `find bridges in undirected graph` { + @Test + fun `class base test`(){ + val graph = UndirectedGraph() + + graph.addVertex(1) + graph.addVertex(2) + graph.addVertex(3) + graph.addVertex(4) + graph.addVertex(5) + graph.addVertex(6) + + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 1) + graph.addEdge(1, 4) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(6, 4) + + val bridges = FindBridges(graph).findBridges() + + assertEquals(Pair(bridges[0].first.key, bridges[0].second.key), Pair(1, 4)) + } + @Test + fun `crossing multiple bridges`() { + val graph = UndirectedGraph() + + graph.addVertex(1) + graph.addVertex(2) + graph.addVertex(3) + graph.addVertex(4) + graph.addVertex(5) + graph.addVertex(6) + graph.addVertex(7) + + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 1) + graph.addEdge(1, 4) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(6, 7) + graph.addEdge(7, 5) + + val bridges = FindBridges(graph).findBridges() + + assertEquals( + listOf(Pair(bridges[0].first.key, bridges[0].second.key), + Pair(bridges[1].first.key, bridges[1].second.key)), + listOf(Pair(4, 5), Pair(1, 4)) + ) + } + } +} \ No newline at end of file From 95ca1e45f371e21a111bc90c71ac1f96119ecd1a Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 30 May 2024 00:53:18 +0300 Subject: [PATCH 061/139] feat: now you can analysis nodes by rank button --- src/main/kotlin/Main.kt | 2 +- src/main/kotlin/model/algorithm/PageRank.kt | 2 +- src/main/kotlin/view/MainView.kt | 8 ++++--- src/main/kotlin/view/MenuView.kt | 8 +++++-- src/main/kotlin/view/canvas/EdgeCanvasView.kt | 11 +++++---- .../viewModel/graph/UndirectedViewModel.kt | 23 +++++++++++++++++++ 6 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index eba65fb..c56ee92 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -33,7 +33,7 @@ val graph = UndirectedGraph().apply { val groups = Clustering(graph).calculate() val ranks = PageRank(graph).computePageRank(3) -val undirectedViewModel = UndirectedViewModel(graph, false, groups) +val undirectedViewModel = UndirectedViewModel(graph, false, groups, ranks) fun main() = application { var isOpen by remember { mutableStateOf(true) } diff --git a/src/main/kotlin/model/algorithm/PageRank.kt b/src/main/kotlin/model/algorithm/PageRank.kt index 34659f0..78d79df 100644 --- a/src/main/kotlin/model/algorithm/PageRank.kt +++ b/src/main/kotlin/model/algorithm/PageRank.kt @@ -3,7 +3,7 @@ package model.algorithm import model.graph.Graph import model.graph.Vertex -class PageRank( +class PageRank( private val graph: Graph, private val dampingFactor: Double = 0.85, private val iterations: Int = 100 diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index b3656ea..7f33048 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -40,11 +40,11 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { var isOrientated by remember { mutableStateOf(false) } var isClustering by remember { mutableStateOf(false) } + var isRanked by remember { mutableStateOf(false) } var isNodeCreatingMode by remember { mutableStateOf(false) } - var update = mutableStateOf(false) - undirectedViewModel.clustering = isClustering + undirectedViewModel.ranked = isRanked val canvasViewModel = CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize, isOrientated) @@ -54,7 +54,9 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { isNodeCreatingMode, { isNodeCreatingMode = !isNodeCreatingMode }, isClustering, - { isClustering = !isClustering }) + { isClustering = !isClustering }, + isRanked, + { isRanked = !isRanked }) CanvasView( canvasViewModel, Modifier diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index c4dcb1a..80a867f 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -28,7 +28,9 @@ fun MenuView( isNodeCreating: Boolean, onNodeCreatingChange: () -> Unit, isClustering: Boolean, - onClusteringChange: () -> Unit + onClusteringChange: () -> Unit, + isRanked: Boolean, + onRankedChange: () -> Unit ) { Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), @@ -44,7 +46,9 @@ fun MenuView( MenuIcon("Clustering.svg", "Clustering", Modifier.glow(isClustering)) { onClusteringChange() } - MenuIcon("PageRank.svg", "Analysis graph") + MenuIcon("PageRank.svg", "Analysis graph", Modifier.glow(isRanked)) { + onRankedChange() + } } } diff --git a/src/main/kotlin/view/canvas/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt index 8be99e4..181227c 100644 --- a/src/main/kotlin/view/canvas/EdgeCanvasView.kt +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -22,10 +22,11 @@ fun EdgeCanvasView( val vector = (secondCenter - firstCenter) val vectorNorm = vector / vector.getDistance() - val radiusVector = vectorNorm * viewModel.first.radius.value + val radiusVectorFirst = vectorNorm * viewModel.first.radius.value + val radiusVectorSecond = vectorNorm * viewModel.second.radius.value - val start = firstCenter + radiusVector - val end = secondCenter - radiusVector + val start = firstCenter + radiusVectorFirst + val end = secondCenter - radiusVectorSecond if ((secondCenter - firstCenter).getDistance() > viewModel.first.radius.value + viewModel.second.radius.value) { drawLine( @@ -39,14 +40,14 @@ fun EdgeCanvasView( if (viewModel.showOrientation) { drawLine( start = end, - end = end - rotateVector(radiusVector * 0.8f, 30.0), + end = end - rotateVector(radiusVectorSecond * 0.8f, 30.0), color = viewModel.color, strokeWidth = viewModel.strokeWidth * 0.8f ) drawLine( start = end, - end = end - rotateVector(radiusVector * 0.8f, -30.0), + end = end - rotateVector(radiusVectorSecond * 0.8f, -30.0), color = viewModel.color, strokeWidth = viewModel.strokeWidth * 0.8f ) diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 41ab8af..476cb8d 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -12,6 +12,7 @@ class UndirectedViewModel( private val graph: UndirectedGraph, val showVerticesLabels: Boolean, val groups: HashMap = hashMapOf(), + val ranks: List> ) { private val _vertices = hashMapOf() private val _adjacencyList = hashMapOf>() @@ -19,6 +20,7 @@ class UndirectedViewModel( private val _color = mutableStateOf(Color.Black) private val _size = mutableStateOf(10f) private val _clustering = mutableStateOf(false) + private val _ranked = mutableStateOf(false) private var size get() = _size.value @@ -39,6 +41,13 @@ class UndirectedViewModel( updateColor() } + var ranked + get() = _ranked.value + set(value) { + _ranked.value = value + updateSizes() + } + private fun getColor(group: Int): Color { if (clustering) { val color = groupColors[group] @@ -61,6 +70,20 @@ class UndirectedViewModel( } } + fun updateSizes() { + if (ranked) { + ranks.forEach { + val vertex = _vertices[it.first] ?: return + + vertex.radius = (vertex.radius.value * 2f).dp + } + + return + } + + _vertices.forEach { it.value.radius = size.dp } + } + fun onColorChange(color: Color) { _color.value = color updateColor() From 26e1c46107820f30963a90129976da08b62e560e Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 30 May 2024 00:58:36 +0300 Subject: [PATCH 062/139] fix: delete generic type from algorithm --- .../kotlin/model/algorithm/FindBridges.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/model/algorithm/FindBridges.kt b/src/main/kotlin/model/algorithm/FindBridges.kt index 1af252c..686b00e 100644 --- a/src/main/kotlin/model/algorithm/FindBridges.kt +++ b/src/main/kotlin/model/algorithm/FindBridges.kt @@ -4,15 +4,15 @@ package model.algorithm import model.graph.* -class FindBridges( - private val graph: Graph +class FindBridges( + private val graph: Graph ) { - fun findBridges(): List> { - val visited = mutableMapOf, Boolean>() - val disc = mutableMapOf, Int>() - val low = mutableMapOf, Int>() - val parent = mutableMapOf, Vertex?>() - val bridges = mutableListOf>() + fun findBridges(): List { + val visited = mutableMapOf() + val disc = mutableMapOf() + val low = mutableMapOf() + val parent = mutableMapOf() + val bridges = mutableListOf() var time = 0 graph.vertices.forEach { vertex -> @@ -25,12 +25,12 @@ class FindBridges( } private fun findBridgesUtil( - u: Vertex, - visited: MutableMap, Boolean>, - disc: MutableMap, Int>, - low: MutableMap, Int>, - parent: MutableMap, Vertex?>, - bridges: MutableList>, + u: Vertex, + visited: MutableMap, + disc: MutableMap, + low: MutableMap, + parent: MutableMap, + bridges: MutableList, time: Int ) { var currentTime = time From 3b74b2de16705fe3a34352b4e269f97ae59e9de3 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 30 May 2024 01:00:38 +0300 Subject: [PATCH 063/139] fix: delete generic type from tests --- .../kotlin/model/algorithm/FindBridgesTest.kt | 26 ++++++++++++------- .../kotlin/model/algorithm/PageRankTest.kt | 6 ++--- .../kotlin/model/graph/DirectedGraphTest.kt | 8 +++--- .../kotlin/model/graph/UndirectedGraphTest.kt | 20 +++++++------- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/test/kotlin/model/algorithm/FindBridgesTest.kt b/src/test/kotlin/model/algorithm/FindBridgesTest.kt index 672e1fc..d03c617 100644 --- a/src/test/kotlin/model/algorithm/FindBridgesTest.kt +++ b/src/test/kotlin/model/algorithm/FindBridgesTest.kt @@ -10,8 +10,8 @@ class FindBridgesTest { @Nested inner class `find bridges in directed graph` { @Test - fun `class base test`(){ - val graph = DirectedGraph() + fun `class base test`() { + val graph = DirectedGraph() graph.addVertex(1) graph.addVertex(2) @@ -32,9 +32,10 @@ class FindBridgesTest { assertEquals(Pair(bridges[0].first.key, bridges[0].second.key), Pair(1, 4)) } + @Test fun `crossing multiple bridges`() { - val graph = DirectedGraph() + val graph = DirectedGraph() graph.addVertex(1) graph.addVertex(2) @@ -56,8 +57,10 @@ class FindBridgesTest { val bridges = FindBridges(graph).findBridges() assertEquals( - listOf(Pair(bridges[0].first.key, bridges[0].second.key), - Pair(bridges[1].first.key, bridges[1].second.key)), + listOf( + Pair(bridges[0].first.key, bridges[0].second.key), + Pair(bridges[1].first.key, bridges[1].second.key) + ), listOf(Pair(4, 5), Pair(1, 4)) ) } @@ -66,8 +69,8 @@ class FindBridgesTest { @Nested inner class `find bridges in undirected graph` { @Test - fun `class base test`(){ - val graph = UndirectedGraph() + fun `class base test`() { + val graph = UndirectedGraph() graph.addVertex(1) graph.addVertex(2) @@ -88,9 +91,10 @@ class FindBridgesTest { assertEquals(Pair(bridges[0].first.key, bridges[0].second.key), Pair(1, 4)) } + @Test fun `crossing multiple bridges`() { - val graph = UndirectedGraph() + val graph = UndirectedGraph() graph.addVertex(1) graph.addVertex(2) @@ -112,8 +116,10 @@ class FindBridgesTest { val bridges = FindBridges(graph).findBridges() assertEquals( - listOf(Pair(bridges[0].first.key, bridges[0].second.key), - Pair(bridges[1].first.key, bridges[1].second.key)), + listOf( + Pair(bridges[0].first.key, bridges[0].second.key), + Pair(bridges[1].first.key, bridges[1].second.key) + ), listOf(Pair(4, 5), Pair(1, 4)) ) } diff --git a/src/test/kotlin/model/algorithm/PageRankTest.kt b/src/test/kotlin/model/algorithm/PageRankTest.kt index 6bc7b87..1baa9e3 100644 --- a/src/test/kotlin/model/algorithm/PageRankTest.kt +++ b/src/test/kotlin/model/algorithm/PageRankTest.kt @@ -10,7 +10,7 @@ class BetweennesCentralityTest { @Test fun basicDirectedGraph() { - val graph = DirectedGraph() + val graph = DirectedGraph() for (i in 0..3) { graph.addVertex(i) } @@ -18,7 +18,7 @@ class BetweennesCentralityTest { graph.addEdge(0, 2) graph.addEdge(1, 2) graph.addEdge(3, 2) - graph.addEdge(2 ,0) + graph.addEdge(2, 0) val centrality = PageRank(graph).computePageRank(1) @@ -28,7 +28,7 @@ class BetweennesCentralityTest { @Test fun basicUndirectedGraph() { - val graph = UndirectedGraph() + val graph = UndirectedGraph() for (i in 0..3) { graph.addVertex(i) } diff --git a/src/test/kotlin/model/graph/DirectedGraphTest.kt b/src/test/kotlin/model/graph/DirectedGraphTest.kt index 33a0201..7e5c948 100644 --- a/src/test/kotlin/model/graph/DirectedGraphTest.kt +++ b/src/test/kotlin/model/graph/DirectedGraphTest.kt @@ -7,12 +7,12 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class DirectedGraphTest { - val graph = DirectedGraph() + val graph = DirectedGraph() - fun Graph.findVertex(key: Int) = this.vertices.find { it.key == key } - fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[findVertex(key1)]?.find { it.second.key == key2 } + fun Graph.findVertex(key: Int) = this.vertices.find { it.key == key } + fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[findVertex(key1)]?.find { it.second.key == key2 } - fun Graph.checkExistingDirectedEdge(key1: Int, key2: Int) { + fun Graph.checkExistingDirectedEdge(key1: Int, key2: Int) { val edge1 = this.findEdge(key1, key2) assertNotNull(edge1) diff --git a/src/test/kotlin/model/graph/UndirectedGraphTest.kt b/src/test/kotlin/model/graph/UndirectedGraphTest.kt index 6bc875d..2577a79 100644 --- a/src/test/kotlin/model/graph/UndirectedGraphTest.kt +++ b/src/test/kotlin/model/graph/UndirectedGraphTest.kt @@ -9,19 +9,19 @@ import kotlin.test.assertNull @Suppress("ClassName") class UndirectedGraphTest { - lateinit var graph: UndirectedGraph + lateinit var graph: UndirectedGraph - fun Graph.getSize() = this.vertices.size - fun Graph.findVertex(key: Int) = this.vertices.find { it.key == key } - fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[findVertex(key1)]?.find { it.second.key == key2 } + fun Graph.getSize() = this.vertices.size + fun Graph.findVertex(key: Int) = this.vertices.find { it.key == key } + fun Graph.findEdge(key1: Int, key2: Int) = this.adjacencyList[findVertex(key1)]?.find { it.second.key == key2 } - fun Graph.checkSize(size: Int) = assertEquals(this.getSize(), size) - fun Graph.checkContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), vertex) - fun Graph.checkNotContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), null) - fun Graph.checkNotNullEdgeArray(vertex: Vertex) = assertNotNull(this.adjacencyList[vertex]) - fun Graph.checkNullEdgeArray(vertex: Vertex) = assertNull(this.adjacencyList[vertex]) + fun Graph.checkSize(size: Int) = assertEquals(this.getSize(), size) + fun Graph.checkContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), vertex) + fun Graph.checkNotContainVertex(vertex: Vertex) = assertEquals(this.findVertex(vertex.key), null) + fun Graph.checkNotNullEdgeArray(vertex: Vertex) = assertNotNull(this.adjacencyList[vertex]) + fun Graph.checkNullEdgeArray(vertex: Vertex) = assertNull(this.adjacencyList[vertex]) - fun Graph.checkExistingUndirectedEdge(key1: Int, key2: Int) { + fun Graph.checkExistingUndirectedEdge(key1: Int, key2: Int) { val edge1 = this.findEdge(key1, key2) val edge2 = this.findEdge(key2, key1) From 9c14f2dd683d63ce497f55d46a4ad217bf38eff1 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 26 May 2024 23:49:45 +0300 Subject: [PATCH 064/139] feat: add class WeightedGraph --- src/main/kotlin/model/graph/WeightedGraph.kt | 41 ++++++++++ .../kotlin/model/graph/WeightedGraphTest.kt | 75 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/main/kotlin/model/graph/WeightedGraph.kt create mode 100644 src/test/kotlin/model/graph/WeightedGraphTest.kt diff --git a/src/main/kotlin/model/graph/WeightedGraph.kt b/src/main/kotlin/model/graph/WeightedGraph.kt new file mode 100644 index 0000000..3f66c28 --- /dev/null +++ b/src/main/kotlin/model/graph/WeightedGraph.kt @@ -0,0 +1,41 @@ +package model.graph + +class WeightedGraph: UndirectedGraph() { + + override fun addEdge(first: V, second: V, weight: Long): Edge? { + + val vertex1 = _vertices[first] ?: return null + val vertex2 = _vertices[second] ?: return null + + val edgesVertex1 = _adjacencyList[vertex1] + val edgesVertex2 = _adjacencyList[vertex2] + + if (edgesVertex1 == null || edgesVertex2 == null) + return addNewEdge(vertex1, vertex2, weight) + + val edge1 = edgesVertex1.find { it.second.key == second } + val edge2 = edgesVertex2.find { it.second.key == first } + + if (edge1 == null || edge2 == null) return addNewEdge(vertex1, vertex2, weight) + + if (edge1.weight == weight) return null + + edge1.weight = weight + edge2.weight = weight + + return edge1 + } + + private fun addNewEdge(vertex1: Vertex, vertex2: Vertex, weight: Long): Edge?{ + + _adjacencyList[vertex1]?.add(WeightedEdge(vertex1, vertex2, weight)) + _adjacencyList[vertex2]?.add(WeightedEdge(vertex2, vertex1, weight)) + + return _adjacencyList[vertex1]?.last() + } + + private data class WeightedEdge(override val first: Vertex, override val second: Vertex, + override var weight: Long) : Edge + +} + diff --git a/src/test/kotlin/model/graph/WeightedGraphTest.kt b/src/test/kotlin/model/graph/WeightedGraphTest.kt new file mode 100644 index 0000000..0e320ae --- /dev/null +++ b/src/test/kotlin/model/graph/WeightedGraphTest.kt @@ -0,0 +1,75 @@ +package model.graph + +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 kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class WeightedGraphTest: UndirectedGraphTest() { + + @Nested + inner class addEdge { + + @BeforeEach + fun setup() { + graph = WeightedGraph() + } + + @Test + fun `Graph without edges`() { + + val vertex1 = graph.addVertex(1) + graph.addVertex(2) + + val edge = graph.addEdge(1, 2, 10) + + assertNotNull(edge) + assertEquals(edge.weight, 10) + + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10), it.map{it.weight}) } + } + + @Test + fun `Connect vertices without edge`() { + + val vertex1 = graph.addVertex(1) + graph.addVertex(2) + val vertex2 = graph.addVertex(3) + graph.addVertex(4) + + graph.addEdge(1, 2, 10) + graph.addEdge(3, 4, 11) + graph.addEdge(3, 1, 12) + + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10, 12), it.map{it.weight}) } + graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(11, 12), it.map{it.weight}) } + } + + @Test + fun `Add edge with the same weight`() { + + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + graph.addEdge(1, 2, 10) + assertEquals(null, graph.addEdge(1, 2, 10)) + + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10), it.map{it.weight}) } + graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(10), it.map{it.weight}) } + } + + @Test + fun `Add the same edge with new weight`() { + val vertex1 = graph.addVertex(1) + val vertex2 = graph.addVertex(2) + + graph.addEdge(1, 2, 10) + graph.addEdge(1, 2, 11) + + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(11), it.map{it.weight}) } + graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(11), it.map{it.weight}) } + } + } +} \ No newline at end of file From 9c8cf0333b84cbffb9acfcb835852d0a97134c45 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 29 May 2024 20:31:04 +0300 Subject: [PATCH 065/139] fix: now test only for method addEdge --- src/test/kotlin/model/graph/WeightedGraphTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/model/graph/WeightedGraphTest.kt b/src/test/kotlin/model/graph/WeightedGraphTest.kt index 0e320ae..c1fbf5f 100644 --- a/src/test/kotlin/model/graph/WeightedGraphTest.kt +++ b/src/test/kotlin/model/graph/WeightedGraphTest.kt @@ -7,7 +7,8 @@ import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull -class WeightedGraphTest: UndirectedGraphTest() { +class WeightedGraphTest { + lateinit var graph: WeightedGraph @Nested inner class addEdge { From 2bad60c2f1426d1a1cbf76a1917dcf91db9b971d Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 30 May 2024 01:07:40 +0300 Subject: [PATCH 066/139] fix: delete generic types for weightedGraph --- src/main/kotlin/model/graph/WeightedGraph.kt | 12 +++++++----- src/test/kotlin/model/graph/WeightedGraphTest.kt | 16 ++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/model/graph/WeightedGraph.kt b/src/main/kotlin/model/graph/WeightedGraph.kt index 3f66c28..67711f9 100644 --- a/src/main/kotlin/model/graph/WeightedGraph.kt +++ b/src/main/kotlin/model/graph/WeightedGraph.kt @@ -1,8 +1,8 @@ package model.graph -class WeightedGraph: UndirectedGraph() { +class WeightedGraph : UndirectedGraph() { - override fun addEdge(first: V, second: V, weight: Long): Edge? { + override fun addEdge(first: Int, second: Int, weight: Long): Edge? { val vertex1 = _vertices[first] ?: return null val vertex2 = _vertices[second] ?: return null @@ -26,7 +26,7 @@ class WeightedGraph: UndirectedGraph() { return edge1 } - private fun addNewEdge(vertex1: Vertex, vertex2: Vertex, weight: Long): Edge?{ + private fun addNewEdge(vertex1: Vertex, vertex2: Vertex, weight: Long): Edge? { _adjacencyList[vertex1]?.add(WeightedEdge(vertex1, vertex2, weight)) _adjacencyList[vertex2]?.add(WeightedEdge(vertex2, vertex1, weight)) @@ -34,8 +34,10 @@ class WeightedGraph: UndirectedGraph() { return _adjacencyList[vertex1]?.last() } - private data class WeightedEdge(override val first: Vertex, override val second: Vertex, - override var weight: Long) : Edge + private data class WeightedEdge( + override val first: Vertex, override val second: Vertex, + override var weight: Long + ) : Edge } diff --git a/src/test/kotlin/model/graph/WeightedGraphTest.kt b/src/test/kotlin/model/graph/WeightedGraphTest.kt index c1fbf5f..1f17120 100644 --- a/src/test/kotlin/model/graph/WeightedGraphTest.kt +++ b/src/test/kotlin/model/graph/WeightedGraphTest.kt @@ -8,7 +8,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull class WeightedGraphTest { - lateinit var graph: WeightedGraph + lateinit var graph: WeightedGraph @Nested inner class addEdge { @@ -29,7 +29,7 @@ class WeightedGraphTest { assertNotNull(edge) assertEquals(edge.weight, 10) - graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10), it.map{it.weight}) } + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10), it.map { it.weight }) } } @Test @@ -44,8 +44,8 @@ class WeightedGraphTest { graph.addEdge(3, 4, 11) graph.addEdge(3, 1, 12) - graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10, 12), it.map{it.weight}) } - graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(11, 12), it.map{it.weight}) } + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10, 12), it.map { it.weight }) } + graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(11, 12), it.map { it.weight }) } } @Test @@ -57,8 +57,8 @@ class WeightedGraphTest { graph.addEdge(1, 2, 10) assertEquals(null, graph.addEdge(1, 2, 10)) - graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10), it.map{it.weight}) } - graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(10), it.map{it.weight}) } + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(10), it.map { it.weight }) } + graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(10), it.map { it.weight }) } } @Test @@ -69,8 +69,8 @@ class WeightedGraphTest { graph.addEdge(1, 2, 10) graph.addEdge(1, 2, 11) - graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(11), it.map{it.weight}) } - graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(11), it.map{it.weight}) } + graph.adjacencyList[vertex1]?.let { assertEquals(arrayListOf(11), it.map { it.weight }) } + graph.adjacencyList[vertex2]?.let { assertEquals(arrayListOf(11), it.map { it.weight }) } } } } \ No newline at end of file From 27fa599e941cb64272e5221c07e7ff69a277cdee Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 12:54:20 +0300 Subject: [PATCH 067/139] feat: add cycle finding algorithm --- src/main/kotlin/model/algorithm/FindCycle.kt | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/main/kotlin/model/algorithm/FindCycle.kt diff --git a/src/main/kotlin/model/algorithm/FindCycle.kt b/src/main/kotlin/model/algorithm/FindCycle.kt new file mode 100644 index 0000000..22d9fd4 --- /dev/null +++ b/src/main/kotlin/model/algorithm/FindCycle.kt @@ -0,0 +1,59 @@ +package model.algorithm + +import model.graph.Edge +import model.graph.Graph +import model.graph.Vertex + +class FindCycle(private val graph: Graph) { + val color: MutableMap = graph.vertices.associateWith { Color.WHITE }.toMutableMap() + val path: MutableList = mutableListOf() + var isFound: Boolean = false + + fun calculate(vertex: Vertex): List { + dfs(vertex) + + return path + } + + private fun dfs(vertex: Vertex) { + if (isFound) return + + color[vertex] = Color.GRAY + + for (childEdge in getChildren(vertex)) { + if (isFound) break + if (isSameEdgeWithLast(childEdge)) continue + + when (color[childEdge.second]) { + Color.WHITE -> { + path.add(childEdge) + dfs(childEdge.second) + } + + Color.GRAY -> { + path.add(childEdge) + isFound = true + } + + else -> path.dropLast(1) + } + } + + color[vertex] = Color.BLACK + } + + private fun getChildren(vertex: Vertex): List { + val children = + graph.adjacencyList[vertex] ?: throw IllegalStateException("Vertex must have at least empty adjacency list") + + return children + } + + private fun isSameEdgeWithLast(edge: Edge): Boolean { + val lastEdge = path.lastOrNull() ?: return false + + return (lastEdge.first == edge.second && lastEdge.second == edge.first) + } + + enum class Color { GRAY, WHITE, BLACK } +} \ No newline at end of file From 5b11ec6f0cbf70ed99e12ba5c550d35fa4c9edc5 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 13:05:16 +0300 Subject: [PATCH 068/139] fix: fix bugs in FindCycle algorithm --- src/main/kotlin/model/algorithm/FindCycle.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/model/algorithm/FindCycle.kt b/src/main/kotlin/model/algorithm/FindCycle.kt index 22d9fd4..3a320bc 100644 --- a/src/main/kotlin/model/algorithm/FindCycle.kt +++ b/src/main/kotlin/model/algorithm/FindCycle.kt @@ -12,7 +12,11 @@ class FindCycle(private val graph: Graph) { fun calculate(vertex: Vertex): List { dfs(vertex) - return path + if (isFound) { + return path + } + + return emptyList() } private fun dfs(vertex: Vertex) { @@ -34,12 +38,15 @@ class FindCycle(private val graph: Graph) { path.add(childEdge) isFound = true } - - else -> path.dropLast(1) + + else -> continue } } color[vertex] = Color.BLACK + if (!isFound) { + path.removeLastOrNull() + } } private fun getChildren(vertex: Vertex): List { From cd542dbb44ac4a291e551e8b1c6aa52c76ef989c Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 13:05:37 +0300 Subject: [PATCH 069/139] feat: add tests for FindCycle algorithm for undirected graph --- .../kotlin/model/algorithm/FindCycleTest.kt | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/test/kotlin/model/algorithm/FindCycleTest.kt diff --git a/src/test/kotlin/model/algorithm/FindCycleTest.kt b/src/test/kotlin/model/algorithm/FindCycleTest.kt new file mode 100644 index 0000000..1cb59a5 --- /dev/null +++ b/src/test/kotlin/model/algorithm/FindCycleTest.kt @@ -0,0 +1,107 @@ +package model.algorithm + +import model.graph.Edge +import model.graph.Graph +import model.graph.UndirectedGraph +import model.graph.Vertex +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class FindCycleTest { + private fun Graph.addVertices(vararg indexes: Int): Vertex { + for (index in indexes.drop(1)) { + addVertex(index) + } + + return addVertex(indexes.first()) ?: throw IllegalStateException() + } + + private fun Graph.addEdges(vararg edges: Pair) { + for (edge in edges) { + addEdge(edge.first, edge.second) + } + } + + private fun checkPath(expected: List>, path: List) { + assertEquals(expected, path.map { it.first.key to it.second.key }) + } + + @Nested + inner class `Undirected graph` { + @Test + fun `Connected graph with 3 nodes`() { + var vertex: Vertex; + + val graph = UndirectedGraph().apply { + vertex = addVertices(1, 2, 3) + addEdges(1 to 2, 1 to 3, 2 to 3) + } + + val result = FindCycle(graph).calculate(vertex) + checkPath(listOf(1 to 2, 2 to 3, 3 to 1), result) + } + + @Test + fun `Connected graph with 5 nodes`() { + var vertex: Vertex; + + val graph = UndirectedGraph().apply { + vertex = addVertices(1, 2, 3, 4, 5) + addEdges(1 to 2, 1 to 3, 1 to 4, 1 to 5, 2 to 3, 2 to 4, 2 to 5, 3 to 4, 3 to 5, 4 to 5) + } + + val result = FindCycle(graph).calculate(vertex) + checkPath(listOf(1 to 2, 2 to 3, 3 to 1), result) + } + + @Test + fun `Graph with 5 nodes with one cycle`() { + var vertex: Vertex; + + val graph = UndirectedGraph().apply { + vertex = addVertices(1, 2, 3, 4, 5) + addEdges(1 to 2, 2 to 3, 3 to 4, 4 to 5, 5 to 1) + } + + val result = FindCycle(graph).calculate(vertex) + checkPath(listOf(1 to 2, 2 to 3, 3 to 4, 4 to 5, 5 to 1), result) + } + + // 2 - 3 + // / + // 1 + // \ + // 4 - 5 + @Test + fun `Graph with 5 nodes without cycle`() { + var vertex: Vertex; + + val graph = UndirectedGraph().apply { + vertex = addVertices(1, 2, 3, 4, 5) + addEdges(1 to 2, 2 to 3, 4 to 5, 5 to 1) + } + + val result = FindCycle(graph).calculate(vertex) + checkPath(listOf(), result) + } + + // 4 + // | + // 1 - 2 - 3 - 5 - 7 - 1 + // | + // 6 + @Test + fun `Graph with one cycle and with additional paths`() { + var vertex: Vertex; + + val graph = UndirectedGraph().apply { + vertex = addVertices(1, 2, 3, 4, 5, 6, 7) + addEdges(1 to 2, 2 to 3, 3 to 4, 3 to 5, 5 to 6, 5 to 7, 7 to 1) + } + + val result = FindCycle(graph).calculate(vertex) + checkPath(listOf(1 to 2, 2 to 3, 3 to 5, 5 to 7, 7 to 1), result) + } + } +} \ No newline at end of file From ffeccb3f03061be7526d0cae1e0cf177998db5ce Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 13:15:54 +0300 Subject: [PATCH 070/139] feat: add CI for tests and build --- .github/workflows/tests.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..d8e7034 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Kotlin CI With Gradle + +on: + push: + branches: [ "main", "dev" ] + pull_request: + branches: [ "main", "dev" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 20 + uses: actions/setup-java@v4 + with: + java-version: '20' + distribution: 'temurin' + + - name: Build with Gradle Wrapper + run: ./gradlew build + + - name: Launch tests + run: ./gradlew test \ No newline at end of file From 6e83c86b3eb098f424a98964afbd95050f013f14 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 15:20:03 +0300 Subject: [PATCH 071/139] feat: add BellmanFord algorithm --- .../kotlin/model/algorithm/BellmanFord.kt | 83 +++++++++++++++++++ .../kotlin/model/algorithm/BellmanFordTest.kt | 57 +++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/main/kotlin/model/algorithm/BellmanFord.kt create mode 100644 src/test/kotlin/model/algorithm/BellmanFordTest.kt diff --git a/src/main/kotlin/model/algorithm/BellmanFord.kt b/src/main/kotlin/model/algorithm/BellmanFord.kt new file mode 100644 index 0000000..c460571 --- /dev/null +++ b/src/main/kotlin/model/algorithm/BellmanFord.kt @@ -0,0 +1,83 @@ +package model.algorithm + +import model.graph.Edge +import model.graph.Vertex +import model.graph.WeightedGraph + +class BellmanFord(private val graph: WeightedGraph) { + val parentMap = HashMap() + val dist = graph.vertices.associateWith { Long.MAX_VALUE }.toMutableMap() + + private fun getDistance(v: Vertex): Long { + return dist[v] ?: throw IllegalStateException("Distance don't initialized") + } + + fun calculate(start: Vertex, dest: Vertex): List { + dist[start] = 0 + + for (i in 0 until graph.vertices.size - 1) { + updateDist { v, u, uDistance, weight -> + dist[v] = uDistance + weight + parentMap[v] = u + } + + if (updateDist()) { + // Negative cycle found + return listOf() + } + } + + + val result = mutableListOf() + var vertex = dest + var parent = parentMap[vertex] + while (parent != start) { + if (parent == null) return listOf() + + result.add(findEdge(parent, vertex)) + + vertex = parent + parent = parentMap[vertex] + } + + result.add(findEdge(parent, vertex)) + return result.reversed() + } + + private fun updateDist(action: (Vertex, Vertex, Long, Long) -> Unit = { _, _, _, _ -> }): Boolean { + var flag = false + + for (v in graph.vertices) { + for (u in graph.vertices) { + val uDistance = getDistance(u) + val vDistance = getDistance(v) + if (!isEdgeExists(u, v)) continue + + val weight = getWeight(u, v) + if (uDistance != Long.MAX_VALUE && uDistance + weight < vDistance) { + action(v, u, uDistance, weight) + } + } + } + + return flag + } + + private fun getWeight(first: Vertex, second: Vertex): Long { + return findEdge(first, second).weight + } + + private fun findEdge(first: Vertex, second: Vertex): Edge { + val adj = graph.adjacencyList[first] ?: throw IllegalStateException("vertex must have adjacency list") + val edge = adj.find { it.second == second } ?: throw IllegalStateException("There is no edge between vertices") + + return edge + } + + private fun isEdgeExists(first: Vertex, second: Vertex): Boolean { + val adj = graph.adjacencyList[first] ?: throw IllegalStateException("vertex must have adjacency list") + val edge = adj.find { it.second == second } + + return edge !== null + } +} \ No newline at end of file diff --git a/src/test/kotlin/model/algorithm/BellmanFordTest.kt b/src/test/kotlin/model/algorithm/BellmanFordTest.kt new file mode 100644 index 0000000..3fd7d2b --- /dev/null +++ b/src/test/kotlin/model/algorithm/BellmanFordTest.kt @@ -0,0 +1,57 @@ +package model.algorithm + +import model.graph.Graph +import model.graph.Vertex +import model.graph.WeightedGraph +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class BellmanFordTest { + private fun Graph.addVertices(vararg indexes: Int): Pair { + for (index in indexes.drop(1).dropLast(1)) { + addVertex(index) + } + + val first = addVertex(indexes.first()) ?: throw IllegalStateException() + val second = addVertex(indexes.last()) ?: throw IllegalStateException() + + return first to second + } + + private fun Graph.addEdges(vararg edges: Triple) { + for (edge in edges) { + addEdge(edge.first, edge.second, edge.third) + } + } + + private infix fun Pair.weight(weight: Long): Triple { + return Triple(this.first, this.second, weight) + } + + @Nested + inner class `Homka` { + @Test + fun `test 1`() { + var first: Vertex + var second: Vertex + + val graph = WeightedGraph().apply { + val pair = addVertices(1, 2, 3, 4) + first = pair.first + second = pair.second + + addEdges( + 1 to 2 weight 2, + 2 to 4 weight -2, + 1 to 3 weight -2, + 3 to 4 weight 4 + ) + } + + val result = BellmanFord(graph).calculate(first, second).map { it.first.key to it.second.key } + assertEquals(listOf(1 to 2, 2 to 4), result) + } + } + +} \ No newline at end of file From 62a62917175ac72b64bfe886123d37ac38d7a624 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 20:59:30 +0300 Subject: [PATCH 072/139] refactor: add edges button not active --- src/main/kotlin/view/MenuView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 80a867f..7488d7f 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -42,7 +43,7 @@ fun MenuView( ) { onNodeCreatingChange() } - MenuIcon("Ribs.svg", "Add Edge") + MenuIcon("Ribs.svg", "Add Edge", modifier = Modifier.alpha(0.2f)) MenuIcon("Clustering.svg", "Clustering", Modifier.glow(isClustering)) { onClusteringChange() } From 9c784d117c39035fceeb20125f3709e5531f319e Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 21:03:59 +0300 Subject: [PATCH 073/139] refactor: rename test forgot to change --- src/test/kotlin/model/algorithm/BellmanFordTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/model/algorithm/BellmanFordTest.kt b/src/test/kotlin/model/algorithm/BellmanFordTest.kt index 3fd7d2b..09fc5cd 100644 --- a/src/test/kotlin/model/algorithm/BellmanFordTest.kt +++ b/src/test/kotlin/model/algorithm/BellmanFordTest.kt @@ -30,9 +30,9 @@ class BellmanFordTest { } @Nested - inner class `Homka` { + inner class `Undirected graph` { @Test - fun `test 1`() { + fun `two straight paths to node`() { var first: Vertex var second: Vertex From a149560e4af2a77a05743cd33c64754aba5c9846 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 21:18:14 +0300 Subject: [PATCH 074/139] fix: fix BellmanFord algorithm with negative edges --- src/main/kotlin/model/algorithm/BellmanFord.kt | 4 +++- src/test/kotlin/model/algorithm/BellmanFordTest.kt | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/model/algorithm/BellmanFord.kt b/src/main/kotlin/model/algorithm/BellmanFord.kt index c460571..67d72e2 100644 --- a/src/main/kotlin/model/algorithm/BellmanFord.kt +++ b/src/main/kotlin/model/algorithm/BellmanFord.kt @@ -1,10 +1,11 @@ package model.algorithm import model.graph.Edge +import model.graph.Graph import model.graph.Vertex import model.graph.WeightedGraph -class BellmanFord(private val graph: WeightedGraph) { +class BellmanFord(private val graph: Graph) { val parentMap = HashMap() val dist = graph.vertices.associateWith { Long.MAX_VALUE }.toMutableMap() @@ -56,6 +57,7 @@ class BellmanFord(private val graph: WeightedGraph) { val weight = getWeight(u, v) if (uDistance != Long.MAX_VALUE && uDistance + weight < vDistance) { action(v, u, uDistance, weight) + flag = true } } } diff --git a/src/test/kotlin/model/algorithm/BellmanFordTest.kt b/src/test/kotlin/model/algorithm/BellmanFordTest.kt index 09fc5cd..df12a44 100644 --- a/src/test/kotlin/model/algorithm/BellmanFordTest.kt +++ b/src/test/kotlin/model/algorithm/BellmanFordTest.kt @@ -32,7 +32,7 @@ class BellmanFordTest { @Nested inner class `Undirected graph` { @Test - fun `two straight paths to node`() { + fun `graph with negative edge don't have shortest path (negative cycle)`() { var first: Vertex var second: Vertex @@ -44,13 +44,13 @@ class BellmanFordTest { addEdges( 1 to 2 weight 2, 2 to 4 weight -2, - 1 to 3 weight -2, + 1 to 3 weight 2, 3 to 4 weight 4 ) } val result = BellmanFord(graph).calculate(first, second).map { it.first.key to it.second.key } - assertEquals(listOf(1 to 2, 2 to 4), result) + assertEquals(listOf(), result) } } From 33cbfac2be2a3a07b5495601cd77dd5651bb5047 Mon Sep 17 00:00:00 2001 From: Ilhom Kombaev Date: Fri, 31 May 2024 21:25:55 +0300 Subject: [PATCH 075/139] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7d547a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ilhom Kombaev, Anastasiia Kuzmina, Dmitry Sheiko + +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 1304025614a3f4e6203e2d196024b727712ea050 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 21:39:59 +0300 Subject: [PATCH 076/139] feat: add tests for BellmanFord algorithm --- .../kotlin/model/algorithm/BellmanFordTest.kt | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/model/algorithm/BellmanFordTest.kt b/src/test/kotlin/model/algorithm/BellmanFordTest.kt index df12a44..e8bd388 100644 --- a/src/test/kotlin/model/algorithm/BellmanFordTest.kt +++ b/src/test/kotlin/model/algorithm/BellmanFordTest.kt @@ -3,11 +3,19 @@ package model.algorithm import model.graph.Graph import model.graph.Vertex import model.graph.WeightedGraph +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class BellmanFordTest { + lateinit var graph: Graph + + /* + * Add vertices to graph + * return first and last vertex + * */ private fun Graph.addVertices(vararg indexes: Int): Pair { for (index in indexes.drop(1).dropLast(1)) { addVertex(index) @@ -31,12 +39,81 @@ class BellmanFordTest { @Nested inner class `Undirected graph` { + @BeforeEach + fun setup() { + graph = WeightedGraph() + } + + // 2 + // / \ + // 1 4 + // \ / + // 3 + @Test + fun `graph with two straight paths`() { + val (first, last) = graph.addVertices(1, 2, 3, 4).toList() + graph.addEdges( + 1 to 2 weight 2, + 2 to 4 weight 2, + 1 to 3 weight 2, + 3 to 4 weight 4 + ) + + val result = BellmanFord(graph).calculate(first, last).map { it.first.key to it.second.key } + assertEquals(listOf(1 to 2, 2 to 4), result) + } + + // 2 + // / \ + // 1-3-5 + // \ / + // 4 + @Test + fun `graph with three straight paths`() { + val (first, last) = graph.addVertices(1, 2, 3, 4, 5).toList() + graph.addEdges( + 1 to 2 weight 2, + 2 to 5 weight 2, + 1 to 3 weight 2, + 3 to 5 weight 6, + 1 to 4 weight 1, + 4 to 5 weight 4, + ) + + val result = BellmanFord(graph).calculate(first, last).map { it.first.key to it.second.key } + assertEquals(listOf(1 to 2, 2 to 5), result) + } + + // 2 + // / \ + // 1 4 + // \ / + // 3 + @Test + fun `graph with same weight's sum`() { + val (first, last) = graph.addVertices(1, 2, 3, 4).toList() + graph.addEdges( + 1 to 2 weight 2, + 2 to 4 weight 2, + 1 to 3 weight 2, + 3 to 4 weight 2 + ) + + val result = BellmanFord(graph).calculate(first, last).map { it.first.key to it.second.key } + assertEquals(true, listOf(1 to 2, 2 to 4) == result || listOf(1 to 3, 3 to 4) == result) + } + + // 2 + // / \ + // 1 4 + // \ / + // 3 @Test fun `graph with negative edge don't have shortest path (negative cycle)`() { var first: Vertex var second: Vertex - val graph = WeightedGraph().apply { + graph.apply { val pair = addVertices(1, 2, 3, 4) first = pair.first second = pair.second @@ -52,6 +129,15 @@ class BellmanFordTest { val result = BellmanFord(graph).calculate(first, second).map { it.first.key to it.second.key } assertEquals(listOf(), result) } + + @Test + fun `dest and source are same`() { + val firstVertex = (graph.addVertices(1, 2, 3, 4)).first + + val result = BellmanFord(graph).calculate(firstVertex, firstVertex) + + assertEquals(listOf(), result) + } } } \ No newline at end of file From e4097abea1b68fe0cd38eeb87f90148792a68328 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 22:03:22 +0300 Subject: [PATCH 077/139] feat: add tests for cycles for FindCycle algorithm --- .../kotlin/model/algorithm/FindCycleTest.kt | 84 +++++++++++++++++-- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/src/test/kotlin/model/algorithm/FindCycleTest.kt b/src/test/kotlin/model/algorithm/FindCycleTest.kt index 1cb59a5..5cf6cdb 100644 --- a/src/test/kotlin/model/algorithm/FindCycleTest.kt +++ b/src/test/kotlin/model/algorithm/FindCycleTest.kt @@ -1,14 +1,14 @@ package model.algorithm -import model.graph.Edge -import model.graph.Graph -import model.graph.UndirectedGraph -import model.graph.Vertex +import model.graph.* +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import kotlin.test.assertEquals class FindCycleTest { + lateinit var graph: Graph + private fun Graph.addVertices(vararg indexes: Int): Vertex { for (index in indexes.drop(1)) { addVertex(index) @@ -27,13 +27,31 @@ class FindCycleTest { assertEquals(expected, path.map { it.first.key to it.second.key }) } + private infix fun Int.goto(value: Int): MutableList> { + return mutableListOf(Pair(this, value)) + } + + private infix fun MutableList>.goto(value: Int): MutableList> { + this.add(this.last().second to value) + return this + } + + private fun Graph.createPath(list: List>) { + list.forEach { addEdge(it.first, it.second) } + } + @Nested inner class `Undirected graph` { + @BeforeEach + fun setup() { + graph = UndirectedGraph() + } + @Test fun `Connected graph with 3 nodes`() { var vertex: Vertex; - val graph = UndirectedGraph().apply { + graph.apply { vertex = addVertices(1, 2, 3) addEdges(1 to 2, 1 to 3, 2 to 3) } @@ -46,7 +64,7 @@ class FindCycleTest { fun `Connected graph with 5 nodes`() { var vertex: Vertex; - val graph = UndirectedGraph().apply { + graph.apply { vertex = addVertices(1, 2, 3, 4, 5) addEdges(1 to 2, 1 to 3, 1 to 4, 1 to 5, 2 to 3, 2 to 4, 2 to 5, 3 to 4, 3 to 5, 4 to 5) } @@ -59,7 +77,7 @@ class FindCycleTest { fun `Graph with 5 nodes with one cycle`() { var vertex: Vertex; - val graph = UndirectedGraph().apply { + graph.apply { vertex = addVertices(1, 2, 3, 4, 5) addEdges(1 to 2, 2 to 3, 3 to 4, 4 to 5, 5 to 1) } @@ -77,7 +95,7 @@ class FindCycleTest { fun `Graph with 5 nodes without cycle`() { var vertex: Vertex; - val graph = UndirectedGraph().apply { + graph.apply { vertex = addVertices(1, 2, 3, 4, 5) addEdges(1 to 2, 2 to 3, 4 to 5, 5 to 1) } @@ -95,7 +113,7 @@ class FindCycleTest { fun `Graph with one cycle and with additional paths`() { var vertex: Vertex; - val graph = UndirectedGraph().apply { + graph.apply { vertex = addVertices(1, 2, 3, 4, 5, 6, 7) addEdges(1 to 2, 2 to 3, 3 to 4, 3 to 5, 5 to 6, 5 to 7, 7 to 1) } @@ -104,4 +122,52 @@ class FindCycleTest { checkPath(listOf(1 to 2, 2 to 3, 3 to 5, 5 to 7, 7 to 1), result) } } + + @Nested + inner class `Directed graph` { + @BeforeEach + fun setup() { + graph = DirectedGraph() + } + + @Test + fun `Straight graph`() { + val vertex = graph.addVertices(1, 2, 3, 4, 5) + graph.addEdges(1 to 2, 2 to 3, 3 to 4, 4 to 5) + + val result = FindCycle(graph).calculate(vertex) + assertEquals(listOf(), result) + } + + // 2-3-4 + // / \ + // 1 5 + // \ / + // 6-7-8 + @Test + fun `Circle without cycles`() { + val vertex = graph.addVertices(1, 2, 3, 4, 5, 6, 7, 8) + + graph.createPath(1 goto 2 goto 3 goto 4 goto 5) + graph.createPath(1 goto 6 goto 7 goto 8 goto 5) + + val result = FindCycle(graph).calculate(vertex) + assertEquals(listOf(), result) + } + + // 2-3-4 + // / \ + // 1 5 + // \ / + // 6-7-8 + @Test + fun `Circle with cycle`() { + val vertex = graph.addVertices(1, 2, 3, 4, 5, 6, 7, 8) + + graph.createPath(1 goto 2 goto 3 goto 4 goto 5 goto 8 goto 7 goto 6 goto 1) + + val result = FindCycle(graph).calculate(vertex).map { it.first.key to it.second.key } + assertEquals((1 goto 2 goto 3 goto 4 goto 5 goto 8 goto 7 goto 6 goto 1), result) + } + } } \ No newline at end of file From 2d28e8577f99fc7824d4b13fee74d8662da403e7 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 22:17:56 +0300 Subject: [PATCH 078/139] refactor: refactor HeaderView --- src/main/kotlin/view/HeaderView.kt | 91 +++++++++++++++++++----------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index 68e5486..2420b8c 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -41,44 +41,69 @@ fun HeaderView(name: String, close: () -> Unit, maximize: () -> Unit, isMaximize ) { Row(Modifier) { Row(Modifier.padding(start = 7f.dp, top = 7f.dp)) { - Image( - modifier = Modifier.padding(end = (Config.menuWidth - 30f - 7f).dp), - painter = painterResource("Dima.svg"), - contentDescription = "Icon" - ) - Box( - Modifier - .size(Config.headerHeight.dp) - .shadow( - elevation = 5f.dp, - spotColor = Color.Black - ).background(Color(0xFF3D3D3D)), - contentAlignment = Alignment.Center - ) { - MyText("File", 16f) - } + Logo() + FileButton() } } Row { - Text(text = "$name", color = Color.White, fontSize = 20f.sp) + MyText(name) } Row { - Box(Modifier.fillMaxHeight().width(Config.headerHeight.dp).background(Color(0xFF5A5959)).onClick { - minimize() - }, contentAlignment = Alignment.Center) { - Box(Modifier.size(10f.dp, 2f.dp).border(2f.dp, Color.White)) - } - Box(Modifier.fillMaxHeight().width(Config.headerHeight.dp).background(Color(0xFF5A5959)).onClick { - maximize() - }, contentAlignment = Alignment.Center) { - Box(Modifier.size(10f.dp, 8f.dp).border(2f.dp, Color.White)) - } - Box(Modifier.fillMaxHeight().width(Config.headerHeight.dp).background(Color(0xFFC80000)).onClick { - close() - }, contentAlignment = Alignment.Center) { - - Icon(imageVector = Icons.Filled.Close, "Done", tint = Color.White) - } + MinimizeButton(minimize) + MaximizeButton(maximize) + CloseButton(close) } } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun Modifier.menuButton(color: Color, onClick: () -> Unit): Modifier { + return Modifier.fillMaxHeight().width(Config.headerHeight.dp).background(color).onClick { + onClick() + } +} + +@Composable +private fun MinimizeButton(minimize: () -> Unit) { + Box(Modifier.menuButton(Color(0xFF5A5959), minimize), contentAlignment = Alignment.Center) { + Box(Modifier.size(10f.dp, 2f.dp).border(2f.dp, Color.White)) + } +} + +@Composable +private fun MaximizeButton(maximize: () -> Unit) { + Box(Modifier.menuButton(Color(0xFF5A5959), maximize), contentAlignment = Alignment.Center) { + Box(Modifier.size(10f.dp, 8f.dp).border(2f.dp, Color.White)) + } +} + +@Composable +private fun CloseButton(close: () -> Unit) { + Box(Modifier.menuButton(Color(0xFFC80000), close), contentAlignment = Alignment.Center) { + Icon(imageVector = Icons.Filled.Close, "Done", tint = Color.White) + } +} + +@Composable +private fun Logo() { + Image( + modifier = Modifier.padding(end = (Config.menuWidth - 30f - 7f).dp), + painter = painterResource("Dima.svg"), + contentDescription = "Icon" + ) +} + +@Composable +private fun FileButton() { + Box( + Modifier + .size(Config.headerHeight.dp) + .shadow( + elevation = 5f.dp, + spotColor = Color.Black + ).background(Color(0xFF3D3D3D)), + contentAlignment = Alignment.Center + ) { + MyText("File", 16f) + } } \ No newline at end of file From 4f95491d00e6247241b62012abde855ce01c5f1c Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 31 May 2024 22:31:22 +0300 Subject: [PATCH 079/139] fix: program null safety now --- src/main/kotlin/model/algorithm/Clustering.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/model/algorithm/Clustering.kt b/src/main/kotlin/model/algorithm/Clustering.kt index c2c009f..fe20caf 100644 --- a/src/main/kotlin/model/algorithm/Clustering.kt +++ b/src/main/kotlin/model/algorithm/Clustering.kt @@ -27,7 +27,7 @@ class Clustering(graph: Graph) { fun calculate(): HashMap { val map = getPartition(links, 0) - val result = map.mapKeys { vIds[it.key]!! } + val result = map.mapKeys { vIds[it.key] ?: throw IllegalStateException() } return HashMap(result) } From d9275eff6429f0b5cabbd956e398c3eabe2f28e8 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Sat, 17 Aug 2024 15:23:25 +0300 Subject: [PATCH 080/139] feat: add Dijkstra algorithm --- src/main/kotlin/model/algorithm/Dijkstra.kt | 88 +++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/main/kotlin/model/algorithm/Dijkstra.kt diff --git a/src/main/kotlin/model/algorithm/Dijkstra.kt b/src/main/kotlin/model/algorithm/Dijkstra.kt new file mode 100644 index 0000000..87b24f2 --- /dev/null +++ b/src/main/kotlin/model/algorithm/Dijkstra.kt @@ -0,0 +1,88 @@ +package model.algorithm + +import model.graph.* + +class Dijkstra(private val graph: WeightedGraph) { + + fun findShortestPath(startKey: Int, endKey: Int): List? { + val startVertex = graph.vertices.find { it.key == startKey } ?: return null + val endVertex = graph.vertices.find { it.key == endKey } ?: return null + + // Инициализация расстояний и предшественников + val distances = mutableMapOf().withDefault { Long.MAX_VALUE } + val previous = mutableMapOf() + val visited = mutableSetOf() + + distances[startVertex] = 0 + + val priorityQueue = java.util.PriorityQueue(compareBy { distances.getValue(it) }) + priorityQueue.add(startVertex) + + while (priorityQueue.isNotEmpty()) { + val currentVertex = priorityQueue.poll() + if (currentVertex == endVertex) break + + visited.add(currentVertex) + + val edges = graph.adjacencyList[currentVertex] ?: continue + for (edge in edges) { + val neighbor = edge.second + if (neighbor in visited) continue + + val newDist = distances.getValue(currentVertex) + edge.weight + if (newDist < distances.getValue(neighbor)) { + distances[neighbor] = newDist + previous[neighbor] = currentVertex + priorityQueue.add(neighbor) + } + } + } + + // Восстановление пути + val path = mutableListOf() + var currentVertex: Vertex? = endVertex + + while (currentVertex != null && currentVertex != startVertex) { + val prevVertex = previous[currentVertex] ?: break + val edge = graph.adjacencyList[prevVertex]?.find { it.second == currentVertex } + if (edge != null) { + path.add(edge) + } + currentVertex = prevVertex + } + + return if (path.isEmpty()) null else path.reversed() + } +} +fun main(){ + val graph = WeightedGraph() + graph.addVertex(1) + graph.addVertex(2) + graph.addVertex(3) + graph.addVertex(4) + graph.addVertex(5) + graph.addVertex(6) + graph.addVertex(7) + graph.addVertex(8) + graph.addVertex(9) + + graph.addEdge(1, 2, 4) + graph.addEdge(2, 5, 2) + graph.addEdge(1, 3, 3) + graph.addEdge(3, 5, 1) + graph.addEdge(1, 4, 2) + graph.addEdge(4, 5, 3) + graph.addEdge(5, 6, 5) + graph.addEdge(6, 7, 8) + graph.addEdge(6, 8, 9) + graph.addEdge(7, 9, 1) + graph.addEdge(8, 9, 2) + + val result = Dijkstra(graph).findShortestPath(1, 9) + if (result != null) { + for (i in result){ + println(listOf(i.first, i.second, i.weight)) + } + } + +} From a78b38dc20b50f67d89e72a7e726fd85b150d70f Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Sat, 17 Aug 2024 15:30:06 +0300 Subject: [PATCH 081/139] refactor: removed the unnecessary part in the file --- src/main/kotlin/model/algorithm/Dijkstra.kt | 34 --------------------- 1 file changed, 34 deletions(-) diff --git a/src/main/kotlin/model/algorithm/Dijkstra.kt b/src/main/kotlin/model/algorithm/Dijkstra.kt index 87b24f2..cb297cf 100644 --- a/src/main/kotlin/model/algorithm/Dijkstra.kt +++ b/src/main/kotlin/model/algorithm/Dijkstra.kt @@ -8,7 +8,6 @@ class Dijkstra(private val graph: WeightedGraph) { val startVertex = graph.vertices.find { it.key == startKey } ?: return null val endVertex = graph.vertices.find { it.key == endKey } ?: return null - // Инициализация расстояний и предшественников val distances = mutableMapOf().withDefault { Long.MAX_VALUE } val previous = mutableMapOf() val visited = mutableSetOf() @@ -38,7 +37,6 @@ class Dijkstra(private val graph: WeightedGraph) { } } - // Восстановление пути val path = mutableListOf() var currentVertex: Vertex? = endVertex @@ -54,35 +52,3 @@ class Dijkstra(private val graph: WeightedGraph) { return if (path.isEmpty()) null else path.reversed() } } -fun main(){ - val graph = WeightedGraph() - graph.addVertex(1) - graph.addVertex(2) - graph.addVertex(3) - graph.addVertex(4) - graph.addVertex(5) - graph.addVertex(6) - graph.addVertex(7) - graph.addVertex(8) - graph.addVertex(9) - - graph.addEdge(1, 2, 4) - graph.addEdge(2, 5, 2) - graph.addEdge(1, 3, 3) - graph.addEdge(3, 5, 1) - graph.addEdge(1, 4, 2) - graph.addEdge(4, 5, 3) - graph.addEdge(5, 6, 5) - graph.addEdge(6, 7, 8) - graph.addEdge(6, 8, 9) - graph.addEdge(7, 9, 1) - graph.addEdge(8, 9, 2) - - val result = Dijkstra(graph).findShortestPath(1, 9) - if (result != null) { - for (i in result){ - println(listOf(i.first, i.second, i.weight)) - } - } - -} From 0c98d1b52491584a466102adc4ee349f7f2eb1e8 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Sat, 17 Aug 2024 16:01:47 +0300 Subject: [PATCH 082/139] fix: fixed the algorithm class attribute --- src/main/kotlin/model/algorithm/Dijkstra.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/model/algorithm/Dijkstra.kt b/src/main/kotlin/model/algorithm/Dijkstra.kt index cb297cf..33e0ad1 100644 --- a/src/main/kotlin/model/algorithm/Dijkstra.kt +++ b/src/main/kotlin/model/algorithm/Dijkstra.kt @@ -2,7 +2,7 @@ package model.algorithm import model.graph.* -class Dijkstra(private val graph: WeightedGraph) { +class Dijkstra(private val graph: Graph) { fun findShortestPath(startKey: Int, endKey: Int): List? { val startVertex = graph.vertices.find { it.key == startKey } ?: return null From 2b9809a58f60d56c93ee8dbbf1df4bdfa5583538 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Sun, 18 Aug 2024 00:18:57 +0300 Subject: [PATCH 083/139] =?UTF-8?q?refactor:=20=D1=81hanged=20the=20output?= =?UTF-8?q?=20of=20the=20algorithm=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/model/algorithm/Dijkstra.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/model/algorithm/Dijkstra.kt b/src/main/kotlin/model/algorithm/Dijkstra.kt index 33e0ad1..a1f3c95 100644 --- a/src/main/kotlin/model/algorithm/Dijkstra.kt +++ b/src/main/kotlin/model/algorithm/Dijkstra.kt @@ -1,10 +1,11 @@ package model.algorithm import model.graph.* +import java.util.concurrent.TransferQueue class Dijkstra(private val graph: Graph) { - fun findShortestPath(startKey: Int, endKey: Int): List? { + fun findShortestPath(startKey: Int, endKey: Int): List>? { val startVertex = graph.vertices.find { it.key == startKey } ?: return null val endVertex = graph.vertices.find { it.key == endKey } ?: return null @@ -37,14 +38,14 @@ class Dijkstra(private val graph: Graph) { } } - val path = mutableListOf() + val path = mutableListOf>() var currentVertex: Vertex? = endVertex while (currentVertex != null && currentVertex != startVertex) { val prevVertex = previous[currentVertex] ?: break val edge = graph.adjacencyList[prevVertex]?.find { it.second == currentVertex } if (edge != null) { - path.add(edge) + path.add(Triple(edge.first.key, edge.second.key, edge.weight)) } currentVertex = prevVertex } From 265af6f2be26669d27177d9f39a4835d926f950f Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Sun, 18 Aug 2024 01:12:01 +0300 Subject: [PATCH 084/139] feat: add test for Dijkstra algorithm --- .../kotlin/model/algorithm/DijkstraTest.kt | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/test/kotlin/model/algorithm/DijkstraTest.kt diff --git a/src/test/kotlin/model/algorithm/DijkstraTest.kt b/src/test/kotlin/model/algorithm/DijkstraTest.kt new file mode 100644 index 0000000..3ae07d3 --- /dev/null +++ b/src/test/kotlin/model/algorithm/DijkstraTest.kt @@ -0,0 +1,125 @@ +package model.algorithm + +import model.graph.Graph +import model.graph.WeightedGraph +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class DijkstraTest { + lateinit var graph: Graph + + @Nested + inner class `Undirected graph`{ + @BeforeEach + fun setup(){ + graph = WeightedGraph() + } + + @Test + fun `base test`(){ + for (i in 1..5) { + graph.addVertex(i) + } + graph.addEdge(1, 2, 2) + graph.addEdge(2, 5, 4) + graph.addEdge(1, 4, 4) + graph.addEdge(4, 2, 1) + graph.addEdge(1, 3, 3) + graph.addEdge(4, 5, 1) + graph.addEdge(3, 5, 5) + + val path = Dijkstra(graph).findShortestPath(1, 5) + + assertNotNull(path) + assertEquals>>(path ?: emptyList(), + listOf(Triple(1, 2, 2), + Triple(2, 4, 1), + Triple(4, 5, 1))) + } + + @Test + fun `test with identical paths`(){ + for (i in 1..5) { + graph.addVertex(i) + } + graph.addEdge(1, 2, 1) + graph.addEdge(1, 3, 1) + graph.addEdge(1, 4, 2) + graph.addEdge(2, 5, 1) + graph.addEdge(3, 5, 1) + graph.addEdge(4, 5, 2) + + val path = Dijkstra(graph).findShortestPath(1, 5) + + assertNotNull(path) + assertEquals>>(path ?: emptyList(), + listOf(Triple(1, 2, 1), + Triple(2, 5, 1))) + } + + @Test + fun `test this bridges`(){ + for (i in 1..9) { + graph.addVertex(i) + } + graph.addEdge(1, 2, 4) + graph.addEdge(2, 5, 2) + graph.addEdge(1, 3, 3) + graph.addEdge(3, 5, 1) + graph.addEdge(1, 4, 2) + graph.addEdge(4, 5, 3) + graph.addEdge(5, 6, 5) + graph.addEdge(6, 7, 8) + graph.addEdge(6, 8, 9) + graph.addEdge(7, 9, 1) + graph.addEdge(8, 9, 2) + + val path = Dijkstra(graph).findShortestPath(1, 9) + + assertNotNull(path) + assertEquals>>(path ?: emptyList(), + listOf(Triple(1, 3, 3), + Triple(3, 5, 1), + Triple(5, 6, 5), + Triple(6, 7, 8), + Triple(7, 9, 1))) + } + + @Test + fun `test with a nonexistent path`(){ + for (i in 1..9) { + graph.addVertex(i) + } + graph.addEdge(1, 1, 4) + graph.addEdge(2, 5, 2) + graph.addEdge(1, 3, 3) + graph.addEdge(3, 5, 1) + graph.addEdge(1, 4, 2) + graph.addEdge(4, 5, 3) + graph.addEdge(5, 5, 5) + graph.addEdge(6, 7, 8) + graph.addEdge(6, 8, 9) + graph.addEdge(7, 9, 1) + graph.addEdge(8, 9, 2) + + val path = Dijkstra(graph).findShortestPath(1, 9) + + assertEquals(path,null) + } + + @Test + fun `test with one vertex`(){ + for (i in 1..1) { + graph.addVertex(i) + } + graph.addEdge(1, 1, 4) + + val path = Dijkstra(graph).findShortestPath(1, 1) + + assertEquals(path, null) + } + } +} \ No newline at end of file From f261912b7602886b64bc599385e1adab86d60bab Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sun, 18 Aug 2024 22:36:55 +0300 Subject: [PATCH 085/139] fix: delete unused graph view --- src/main/kotlin/view/graph/EdgeView.kt | 30 ---------------- .../kotlin/view/graph/UndirectedGraphView.kt | 24 ------------- src/main/kotlin/view/graph/VertexView.kt | 35 ------------------- 3 files changed, 89 deletions(-) delete mode 100644 src/main/kotlin/view/graph/EdgeView.kt delete mode 100644 src/main/kotlin/view/graph/UndirectedGraphView.kt delete mode 100644 src/main/kotlin/view/graph/VertexView.kt diff --git a/src/main/kotlin/view/graph/EdgeView.kt b/src/main/kotlin/view/graph/EdgeView.kt deleted file mode 100644 index c59e73c..0000000 --- a/src/main/kotlin/view/graph/EdgeView.kt +++ /dev/null @@ -1,30 +0,0 @@ -package view.graph - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import viewModel.graph.EdgeViewModel - -@Composable -fun EdgeView( - viewModel: EdgeViewModel -) { - Canvas(Modifier.fillMaxSize()) { - drawLine( - start = Offset( - viewModel.first.x.dp.toPx() + viewModel.first.radius.toPx(), - viewModel.first.y.dp.toPx() + viewModel.first.radius.toPx(), - ), - end = Offset( - viewModel.second.x.dp.toPx() + viewModel.second.radius.toPx(), - viewModel.second.y.dp.toPx() + viewModel.second.radius.toPx(), - ), - color = Color.Black, - strokeWidth = 1f.dp.toPx() - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/UndirectedGraphView.kt b/src/main/kotlin/view/graph/UndirectedGraphView.kt deleted file mode 100644 index 727c007..0000000 --- a/src/main/kotlin/view/graph/UndirectedGraphView.kt +++ /dev/null @@ -1,24 +0,0 @@ -package view.graph - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import viewModel.graph.UndirectedViewModel - -@Composable -fun UndirectedGraphView( - viewModel: UndirectedViewModel -) { - Box(Modifier.fillMaxSize()) { - viewModel.vertices.forEach { v -> - VertexView(v) - } - - viewModel.vertices.forEach { v -> - viewModel.adjacencyList[v]?.forEach { e -> - EdgeView(e) - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/VertexView.kt b/src/main/kotlin/view/graph/VertexView.kt deleted file mode 100644 index 357fa27..0000000 --- a/src/main/kotlin/view/graph/VertexView.kt +++ /dev/null @@ -1,35 +0,0 @@ -package view.graph - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import viewModel.graph.VertexViewModel - -@Composable -fun VertexView( - viewModel: VertexViewModel, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .size(viewModel.radius * 2) - .offset(viewModel.x.dp, viewModel.y.dp) - .background(color = viewModel.color, shape = CircleShape) - ) { - if (viewModel.labelVisibility) { - Text( - modifier = Modifier - .align(Alignment.Center) - .offset(0f.dp, -viewModel.radius - 10f.dp), - text = viewModel.label - ) - } - } -} \ No newline at end of file From 17188911cecc9acad5a6da0fac41a3622a754ac5 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 19 Aug 2024 18:12:39 +0300 Subject: [PATCH 086/139] feat: added a dependency for SQLite --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 1b146b7..72f8d30 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1") + implementation("org.xerial:sqlite-jdbc:3.41.2.2") + } compose.desktop { From 8fd8afef572bc57895de3a60a31795373b1cb8ca Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 19 Aug 2024 19:15:42 +0300 Subject: [PATCH 087/139] refactor: added the nameGraph parameter to the loadGraph function --- src/main/kotlin/model/reader/Reader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/model/reader/Reader.kt b/src/main/kotlin/model/reader/Reader.kt index 2078dad..c037565 100644 --- a/src/main/kotlin/model/reader/Reader.kt +++ b/src/main/kotlin/model/reader/Reader.kt @@ -4,5 +4,5 @@ import model.graph.Graph interface Reader { fun saveGraph(graph: Graph, filepath: String): Unit - fun loadGraph(filepath: String): Graph + fun loadGraph(filepath: String, nameGraph: String): Graph } \ No newline at end of file From 994c6210a00a63e8a4a889aff42c00953d122353 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 20 Aug 2024 13:09:48 +0300 Subject: [PATCH 088/139] refactor: added the nameGraph parameter to the saveGraph function --- src/main/kotlin/model/reader/Reader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/model/reader/Reader.kt b/src/main/kotlin/model/reader/Reader.kt index c037565..9d41a66 100644 --- a/src/main/kotlin/model/reader/Reader.kt +++ b/src/main/kotlin/model/reader/Reader.kt @@ -3,6 +3,6 @@ package model.reader import model.graph.Graph interface Reader { - fun saveGraph(graph: Graph, filepath: String): Unit + fun saveGraph(graph: Graph, filepath: String, nameGraph: String): Unit fun loadGraph(filepath: String, nameGraph: String): Graph } \ No newline at end of file From 7fe14896971452242dd4a0a97d64408e3bb72ee7 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 20 Aug 2024 16:53:02 +0300 Subject: [PATCH 089/139] refactor: seperate logic, add new viewModels --- .../kotlin/{view => components}/MyText.kt | 2 +- src/main/kotlin/view/SettingsView.kt | 83 ------------------- 2 files changed, 1 insertion(+), 84 deletions(-) rename src/main/kotlin/{view => components}/MyText.kt (96%) delete mode 100644 src/main/kotlin/view/SettingsView.kt diff --git a/src/main/kotlin/view/MyText.kt b/src/main/kotlin/components/MyText.kt similarity index 96% rename from src/main/kotlin/view/MyText.kt rename to src/main/kotlin/components/MyText.kt index b3229a6..a8f4d86 100644 --- a/src/main/kotlin/view/MyText.kt +++ b/src/main/kotlin/components/MyText.kt @@ -1,4 +1,4 @@ -package view +package components import androidx.compose.material.Text import androidx.compose.runtime.Composable diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt deleted file mode 100644 index 72d90ed..0000000 --- a/src/main/kotlin/view/SettingsView.kt +++ /dev/null @@ -1,83 +0,0 @@ -package view - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -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.FontFamily -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.platform.Font -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex - - -@Composable -fun MySlider(text: String, state: MutableState, range: ClosedFloatingPointRange = (0f..1f)) { - Row( - Modifier.padding(start = 5f.dp, end = 5f.dp), - verticalAlignment = Alignment.CenterVertically - ) { - MyText(text = text) - Slider( - modifier = Modifier.padding(0f.dp), - value = state.value, onValueChange = { change -> state.value = change }, - colors = SliderDefaults.colors( - thumbColor = Color.White, - activeTrackColor = Color.White, - inactiveTrackColor = Color.White, - ), - valueRange = range - ) - } -} - - -@Composable -fun SettingsView(onColorChange: (Color) -> Unit, onSizeChange: (Float) -> Unit, onOrientatedChange: () -> Unit) { - val redSlider = remember { mutableStateOf(1f / (0xFF / 0x8F)) } - val greenSlider = remember { mutableStateOf(0f) } - val blueSlider = remember { mutableStateOf(1f) } - val sizeSlider = remember { mutableStateOf(35f) } - val orientatedCheckBox = remember { mutableStateOf(false) } - - onColorChange(Color(red = redSlider.value, green = greenSlider.value, blue = blueSlider.value)) - onSizeChange(sizeSlider.value) - - Box(Modifier.fillMaxSize().padding(top = 80f.dp, end = 20f.dp).zIndex(10f), contentAlignment = Alignment.TopEnd) { - Box( - Modifier.size(270f.dp, 320f.dp).background(Color(0xFF3D3D3D), RoundedCornerShape(10)) - ) { - Column { - Row(Modifier.fillMaxWidth().padding(top = 10f.dp), horizontalArrangement = Arrangement.Center) { - MyText("Node") - } - Row(Modifier.fillMaxWidth().padding(top = 10f.dp, start = 20f.dp)) { - Column { - MyText("Color:") - MySlider("R: ", redSlider) - MySlider("G: ", greenSlider) - MySlider("B: ", blueSlider) - MySlider("Size: ", sizeSlider, (5f..80f)) - } - } - - Row( - Modifier.fillMaxWidth().padding(start = 20f.dp), - verticalAlignment = Alignment.CenterVertically - ) { - MyText("Orientated") - Checkbox(orientatedCheckBox.value, onCheckedChange = { - onOrientatedChange() - orientatedCheckBox.value = !orientatedCheckBox.value - }) - } - } - } - } -} \ No newline at end of file From 19a5a47fa8b2a32c618887a858efc654215aabff Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 20 Aug 2024 16:53:08 +0300 Subject: [PATCH 090/139] refactor: seperate logic, add new viewModels --- src/main/kotlin/Main.kt | 8 +- src/main/kotlin/components/MySlider.kt | 32 +++++ src/main/kotlin/view/HeaderView.kt | 11 +- src/main/kotlin/view/MainView.kt | 77 +---------- src/main/kotlin/view/SettingsView.kt | 60 +++++++++ src/main/kotlin/view/canvas/CanvasView.kt | 24 ++++ src/main/kotlin/view/canvas/EdgeCanvasView.kt | 18 ++- .../kotlin/view/canvas/VertexCanvasView.kt | 4 +- src/main/kotlin/viewModel/MainViewModel.kt | 15 +++ .../kotlin/viewModel/SettingsViewModel.kt | 10 ++ .../viewModel/canvas/CanvasViewModel.kt | 124 +++++++++++++++--- .../viewModel/canvas/EdgeCanvasViewModel.kt | 3 +- .../viewModel/canvas/VertexCanvasViewModel.kt | 67 ++++++++-- .../viewModel/graph/UndirectedViewModel.kt | 2 +- 14 files changed, 335 insertions(+), 120 deletions(-) create mode 100644 src/main/kotlin/components/MySlider.kt create mode 100644 src/main/kotlin/view/SettingsView.kt create mode 100644 src/main/kotlin/viewModel/MainViewModel.kt create mode 100644 src/main/kotlin/viewModel/SettingsViewModel.kt diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index c56ee92..b74ef14 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -12,10 +12,11 @@ import model.graph.UndirectedGraph import view.HeaderView import view.MainView import view.MenuView +import viewModel.MainViewModel import viewModel.graph.UndirectedViewModel -val AMOUNT_NODES = 16 -val EDGE_CHANGE = 5f +val AMOUNT_NODES = 2 +val EDGE_CHANGE = 100 val graph = UndirectedGraph().apply { for (i in (0 until AMOUNT_NODES)) { @@ -34,6 +35,7 @@ val graph = UndirectedGraph().apply { val groups = Clustering(graph).calculate() val ranks = PageRank(graph).computePageRank(3) val undirectedViewModel = UndirectedViewModel(graph, false, groups, ranks) +val mainViewModel = MainViewModel(graph) fun main() = application { var isOpen by remember { mutableStateOf(true) } @@ -60,7 +62,7 @@ fun main() = application { isMaximized, { windowState.isMinimized = !windowState.isMinimized }) } MainView( - undirectedViewModel, + mainViewModel, ) } } diff --git a/src/main/kotlin/components/MySlider.kt b/src/main/kotlin/components/MySlider.kt new file mode 100644 index 0000000..82c4dde --- /dev/null +++ b/src/main/kotlin/components/MySlider.kt @@ -0,0 +1,32 @@ +package components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Slider +import androidx.compose.material.SliderDefaults +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.unit.dp + +@Composable +fun MySlider(text: String, state: MutableState, range: ClosedFloatingPointRange = (0f..1f)) { + Row( + Modifier.padding(start = 5f.dp, end = 5f.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyText(text = text) + Slider( + modifier = Modifier.padding(0f.dp), + value = state.value, onValueChange = { change -> state.value = change }, + colors = SliderDefaults.colors( + thumbColor = Color.White, + activeTrackColor = Color.White, + inactiveTrackColor = Color.White, + ), + valueRange = range + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index 2420b8c..2b9500c 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -4,26 +4,17 @@ import Config import androidx.compose.foundation.* import androidx.compose.foundation.gestures.onDrag import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Done -import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.key.Key.Companion.R import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.max -import androidx.compose.ui.unit.sp +import components.MyText @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 7f33048..ce6e645 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -1,54 +1,25 @@ package view import Config -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.animateOffsetAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.PointerMatcher -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* -import androidx.compose.foundation.onClick import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.awtEventOrNull -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.* -import androidx.compose.ui.layout.* import androidx.compose.ui.unit.dp import view.canvas.CanvasView -import viewModel.canvas.CanvasViewModel -import viewModel.graph.UndirectedViewModel +import viewModel.MainViewModel val HEADER_HEIGHT = Config.headerHeight val MENU_WIDTH = Config.menuWidth @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable -fun MainView(undirectedViewModel: UndirectedViewModel) { - var zoom by remember { mutableFloatStateOf(1f) } - val zoomAnimate by animateFloatAsState(zoom, tween(200, 0, LinearOutSlowInEasing)) - var center by remember { mutableStateOf(Offset(0f, 0f)) } - val centerAnimate by animateOffsetAsState(center, tween(200, 0, LinearOutSlowInEasing)) - var canvasSize by remember { mutableStateOf(Offset(400f, 400f)) } - - var isOrientated by remember { mutableStateOf(false) } +fun MainView(mainViewModel: MainViewModel) { var isClustering by remember { mutableStateOf(false) } var isRanked by remember { mutableStateOf(false) } var isNodeCreatingMode by remember { mutableStateOf(false) } - undirectedViewModel.clustering = isClustering - undirectedViewModel.ranked = isRanked - - val canvasViewModel = - CanvasViewModel(undirectedViewModel, zoomAnimate, centerAnimate, canvasSize, isOrientated) - Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { MenuView( isNodeCreatingMode, @@ -57,48 +28,12 @@ fun MainView(undirectedViewModel: UndirectedViewModel) { { isClustering = !isClustering }, isRanked, { isRanked = !isRanked }) + CanvasView( - canvasViewModel, - Modifier - .fillMaxSize() - .onPointerEvent(PointerEventType.Scroll) { - if (it.changes.first().scrollDelta.y > 0) { - zoom -= zoom / 8 - } else { - zoom += zoom / 8 - - val awtEvent = it.awtEventOrNull - if (awtEvent != null) { - val xPosition = awtEvent.x.toFloat() - MENU_WIDTH - val yPosition = awtEvent.y.toFloat() - HEADER_HEIGHT - val pointerVector = (Offset(xPosition, yPosition) - (canvasSize / 2f)) * (1 / zoom) - center += pointerVector * 0.15f - } - } - }.pointerInput(Unit) { - detectDragGestures( - matcher = PointerMatcher.Primary - ) { - center -= it * (1 / zoom) - } - }.pointerInput(Unit) { - detectTapGestures { - if (isNodeCreatingMode) { - canvasViewModel.createVertex(it - (canvasSize / 2f), center, zoom) - zoom += 0.000001f // костыль для рекомпозиции - } - } - }.pointerHoverIcon(PointerIcon.Hand) - .onSizeChanged { - canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) - } - .clipToBounds() + mainViewModel.canvasViewModel, + Modifier.fillMaxSize() ) } - SettingsView( - undirectedViewModel::onColorChange, - undirectedViewModel::onSizeChange, - { isOrientated = !isOrientated } - ) + SettingsView(mainViewModel.settingsViewModel) } \ No newline at end of file diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt new file mode 100644 index 0000000..43b9126 --- /dev/null +++ b/src/main/kotlin/view/SettingsView.kt @@ -0,0 +1,60 @@ +package view + +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.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import components.MySlider +import components.MyText +import viewModel.SettingsViewModel + + +@Composable +fun SettingsView(viewModel: SettingsViewModel) { + val redSlider = remember { mutableStateOf(1f / (0xFF / 0x8F)) } + val greenSlider = remember { mutableStateOf(0f) } + val blueSlider = remember { mutableStateOf(1f) } + val sizeSlider = remember { mutableStateOf(35f) } + val orientatedCheckBox = remember { mutableStateOf(false) } + + viewModel.onColorChange(Color(red = redSlider.value, green = greenSlider.value, blue = blueSlider.value)) + viewModel.onSizeChange(sizeSlider.value) + + Box(Modifier.fillMaxSize().padding(top = 80f.dp, end = 20f.dp).zIndex(10f), contentAlignment = Alignment.TopEnd) { + Box( + Modifier.size(270f.dp, 320f.dp).background(Color(0xFF3D3D3D), RoundedCornerShape(10)) + ) { + Column { + Row(Modifier.fillMaxWidth().padding(top = 10f.dp), horizontalArrangement = Arrangement.Center) { + MyText("Node") + } + Row(Modifier.fillMaxWidth().padding(top = 10f.dp, start = 20f.dp)) { + Column { + MyText("Color:") + MySlider("R: ", redSlider) + MySlider("G: ", greenSlider) + MySlider("B: ", blueSlider) + MySlider("Size: ", sizeSlider, (5f..80f)) + } + } + + Row( + Modifier.fillMaxWidth().padding(start = 20f.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyText("Orientated") + Checkbox(orientatedCheckBox.value, onCheckedChange = { + viewModel.onOrientatedChange(it) + orientatedCheckBox.value = !orientatedCheckBox.value + }) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index 018df83..e57d6ab 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -1,19 +1,43 @@ package view.canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.onSizeChanged import viewModel.canvas.CanvasViewModel +import java.util.* +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun CanvasView( viewModel: CanvasViewModel, modifier: Modifier = Modifier ) { + println("[${Date()}] render canvas") Box( modifier = modifier.background(Color(0xFF242424)) + .onPointerEvent(PointerEventType.Scroll, onEvent = viewModel.onScroll) + .pointerInput(Unit, viewModel.onDrag) +// .pointerInput(Unit) { +// detectTapGestures { +// if (isNodeCreatingMode) { +// canvasViewModel.createVertex(it - (canvasSize / 2f), center, zoom) +// zoom += 0.000001f // костыль для рекомпозиции +// } +// } +// } + .pointerHoverIcon(PointerIcon.Hand) + .onSizeChanged { + viewModel.canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) + } + .clipToBounds() ) { viewModel.edges.forEach { EdgeCanvasView(it) diff --git a/src/main/kotlin/view/canvas/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt index 181227c..c561ab5 100644 --- a/src/main/kotlin/view/canvas/EdgeCanvasView.kt +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -8,17 +8,29 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import viewModel.canvas.EdgeCanvasViewModel +import java.util.* @Composable fun EdgeCanvasView( viewModel: EdgeCanvasViewModel, modifier: Modifier = Modifier ) { + println("[${Date()}] render edge") + Canvas(Modifier.fillMaxSize()) { + println("[${Date()}] render canvas inside edge") + // something hard thing for drawing edge from border of node, not from center - val firstCenter = viewModel.first.offset + Offset(viewModel.first.radius.value, viewModel.first.radius.value) + val firstCenter = + viewModel.first.offset + Offset( + viewModel.first.radius.value, + viewModel.first.radius.value + ) val secondCenter = - viewModel.second.offset + Offset(viewModel.second.radius.value, viewModel.second.radius.value) + viewModel.second.offset + Offset( + viewModel.second.radius.value, + viewModel.second.radius.value + ) val vector = (secondCenter - firstCenter) val vectorNorm = vector / vector.getDistance() @@ -37,7 +49,7 @@ fun EdgeCanvasView( ) } - if (viewModel.showOrientation) { + if (viewModel.showOrientation.value) { drawLine( start = end, end = end - rotateVector(radiusVectorSecond * 0.8f, 30.0), diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index ea33d78..4be14f9 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import view.MyText +import components.MyText import viewModel.canvas.VertexCanvasViewModel @OptIn(ExperimentalFoundationApi::class) @@ -31,6 +31,6 @@ fun VertexCanvasView( .onDrag(onDrag = viewModel::onDrag), contentAlignment = Alignment.Center ) { - MyText(viewModel.viewModel.label, viewModel.textSize.value) + MyText(viewModel.vertexViewModel.label, viewModel.textSize.value) } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/MainViewModel.kt b/src/main/kotlin/viewModel/MainViewModel.kt new file mode 100644 index 0000000..db1edaa --- /dev/null +++ b/src/main/kotlin/viewModel/MainViewModel.kt @@ -0,0 +1,15 @@ +package viewModel + +import model.graph.UndirectedGraph +import viewModel.canvas.CanvasViewModel + +class MainViewModel( + graph: UndirectedGraph +) { + val canvasViewModel = CanvasViewModel(graph) + val settingsViewModel = SettingsViewModel( + canvasViewModel::onColorChange, + canvasViewModel::onSizeChange, + canvasViewModel::onOrientatedChange + ) +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/SettingsViewModel.kt b/src/main/kotlin/viewModel/SettingsViewModel.kt new file mode 100644 index 0000000..a6b6eea --- /dev/null +++ b/src/main/kotlin/viewModel/SettingsViewModel.kt @@ -0,0 +1,10 @@ +package viewModel + +import androidx.compose.ui.graphics.Color + +class SettingsViewModel( + val onColorChange: (Color) -> Unit, + val onSizeChange: (Float) -> Unit, + val onOrientatedChange: (Boolean) -> Unit +) { +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 0e7696d..a9c2dfb 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -1,19 +1,62 @@ package viewModel.canvas +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.PointerMatcher +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerInputScope +import model.graph.UndirectedGraph +import view.HEADER_HEIGHT +import view.MENU_WIDTH import viewModel.graph.UndirectedViewModel -import viewModel.graph.VertexViewModel -import kotlin.math.abs class CanvasViewModel( - val graphViewModel: UndirectedViewModel, - var zoom: Float, - var center: Offset, - var canvasSize: Offset, - var isOrientated: Boolean + val graph: UndirectedGraph, ) { + private val graphViewModel = UndirectedViewModel(graph, true) + + private val _zoom = mutableStateOf(1f) + var zoom + get() = _zoom.value + set(value) { + _zoom.value = value + updateVertexes() + } + + private val _center = mutableStateOf(Offset(0f, 0f)) + var center + get() = _center.value + set(value) { + _center.value = value + updateVertexes() + } + + private val _canvasSize = mutableStateOf(Offset(400f, 400f)) + var canvasSize + get() = _canvasSize.value + set(value) { + _canvasSize.value = value + updateVertexes() + } + + private val _isOrientated = mutableStateOf(false) + var isOrientated + get() = _isOrientated.value + set(value) { + _isOrientated.value = value + } + private val _vertices = graphViewModel.vertices.associateWith { v -> - VertexCanvasViewModel(v, zoom, center, canvasSize) + VertexCanvasViewModel(v, _zoom, _center, _canvasSize) }.toMutableMap() private val _edges = graphViewModel.adjacencyList.map { it.value }.flatten().map { @@ -22,7 +65,7 @@ class CanvasViewModel( val vertex2 = _vertices[it.second] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.second}") - EdgeCanvasViewModel(vertex1, vertex2, it.color, it.strokeWidth, zoom, showOrientation = isOrientated) + EdgeCanvasViewModel(vertex1, vertex2, it.color, it.strokeWidth, zoom, _isOrientated) } val vertices @@ -31,18 +74,65 @@ class CanvasViewModel( val edges get() = _edges - fun getViews(): Collection { - if (Config.optimizeCanvas) { - return _vertices.filter { abs(it.value.offset.x) < canvasSize.x && abs(it.value.offset.y) < canvasSize.y }.values + private fun updateVertexes() { + vertices.forEach { it.updateVertex() } + } + + private fun updateEdges() { + + } + + val onScroll: AwaitPointerEventScope.(PointerEvent) -> Unit = { + if (it.changes.first().scrollDelta.y > 0) { + zoom -= zoom / 8 + } else { + zoom += zoom / 8 + + val awtEvent = it.awtEventOrNull + if (awtEvent != null) { + val xPosition = awtEvent.x.toFloat() - MENU_WIDTH + val yPosition = awtEvent.y.toFloat() - HEADER_HEIGHT + val pointerVector = + (Offset(xPosition, yPosition) - (canvasSize / 2f)) * (1 / zoom) + center += pointerVector * 0.15f + } } + } - return _vertices.values + @OptIn(ExperimentalFoundationApi::class) + val onDrag: suspend PointerInputScope.() -> Unit = { + detectDragGestures( + matcher = PointerMatcher.Primary + ) { + center -= it * (1 / zoom) + } } - fun createVertex(offset: Offset, center: Offset, zoom: Float) { - val coordinates = offset * (1 / zoom) + center - val viewModel = graphViewModel.createVertex(coordinates) ?: return + fun onColorChange(color: Color) { + graphViewModel.onColorChange(color) + } + + fun onSizeChange(newSize: Float) { + graphViewModel.onSizeChange(newSize) + updateVertexes() + } - _vertices[viewModel] = VertexCanvasViewModel(viewModel, zoom, center, canvasSize) + fun onOrientatedChange(isOrientated: Boolean) { + this.isOrientated = isOrientated } + +// fun getViews(): Collection { +// if (Config.optimizeCanvas) { +// return _vertices.filter { abs(it.value.offset.x) < canvasSize.x && abs(it.value.offset.y) < canvasSize.y }.values +// } +// +// return _vertices.values +// } + +// fun createVertex(offset: Offset, center: Offset, zoom: Float) { +// val coordinates = offset * (1 / zoom) + center +// val viewModel = graphViewModel.createVertex(coordinates) ?: return +// +// _vertices[viewModel] = VertexCanvasViewModel(viewModel, _zoom, center, canvasSize) +// } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt index 61028c6..ab0fdda 100644 --- a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -1,5 +1,6 @@ package viewModel.canvas +import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color class EdgeCanvasViewModel( @@ -8,7 +9,7 @@ class EdgeCanvasViewModel( val color: Color, strokeWidth: Float, zoom: Float, - val showOrientation: Boolean = true + val showOrientation: MutableState ) { val strokeWidth = strokeWidth * zoom } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index ba727e4..872cd05 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -1,26 +1,69 @@ package viewModel.canvas +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.dp import viewModel.graph.VertexViewModel class VertexCanvasViewModel( - val viewModel: VertexViewModel, - private val zoom: Float, - center: Offset, - canvasSize: Offset + val vertexViewModel: VertexViewModel, + private val zoom: MutableState, + private val center: MutableState, + private val canvasSize: MutableState ) { - val offset = Offset( - (canvasSize.x / 2) + ((viewModel.x - center.x) * zoom), - (canvasSize.y / 2) + ((viewModel.y - center.y) * zoom) + private val _offset = mutableStateOf( + calculateOffset() ) + var offset + get() = _offset.value + set(value) { + _offset.value = value + } - val radius = viewModel.radius * zoom - val color = viewModel.color - val strokeWidth = (8f * zoom) - val textSize = (viewModel.radius * 0.6f * zoom) + private val _radius = mutableStateOf(calculateRadius()) + var radius + get() = _radius.value + set(value) { + _radius.value = value + } + + var color + get() = vertexViewModel.color + set(value) { + vertexViewModel.color = value + } + + private val _strokeWidth = mutableStateOf(calculateStrokeWidth()) + var strokeWidth + get() = _strokeWidth.value + set(value) { + _strokeWidth.value = value + } + + private val _textSize = mutableStateOf(calculateTextSize()) + var textSize + get() = _textSize.value + set(value) { + _textSize.value = value + } fun onDrag(it: Offset): Unit { - viewModel.onDrag(it * (1f / zoom)) + vertexViewModel.onDrag(it * (1f / zoom.value)) + } + + fun updateVertex() { + offset = calculateOffset() + radius = calculateRadius() + textSize = calculateTextSize() } + + private fun calculateOffset() = Offset( + (canvasSize.value.x / 2) + ((vertexViewModel.x - center.value.x) * zoom.value), + (canvasSize.value.y / 2) + ((vertexViewModel.y - center.value.y) * zoom.value) + ) + + private fun calculateRadius() = vertexViewModel.radius * zoom.value + private fun calculateStrokeWidth() = 8f * zoom.value + private fun calculateTextSize() = vertexViewModel.radius * 0.6f * zoom.value } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 476cb8d..f198c97 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -12,7 +12,7 @@ class UndirectedViewModel( private val graph: UndirectedGraph, val showVerticesLabels: Boolean, val groups: HashMap = hashMapOf(), - val ranks: List> + val ranks: List> = listOf() ) { private val _vertices = hashMapOf() private val _adjacencyList = hashMapOf>() From 9ecc1dfa21d489e0cc4c7d87744dd931d18f5fc7 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 20 Aug 2024 23:08:48 +0300 Subject: [PATCH 091/139] feat: created a function to save graph (saveGraph) --- src/main/kotlin/model/reader/SQLiteReader.kt | 101 +++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/main/kotlin/model/reader/SQLiteReader.kt diff --git a/src/main/kotlin/model/reader/SQLiteReader.kt b/src/main/kotlin/model/reader/SQLiteReader.kt new file mode 100644 index 0000000..f3e922c --- /dev/null +++ b/src/main/kotlin/model/reader/SQLiteReader.kt @@ -0,0 +1,101 @@ +package model.reader + +import model.graph.Graph +import model.graph.Vertex + +import java.sql.Connection +import java.sql.DriverManager +import java.sql.PreparedStatement + +class SQLiteReader: Reader { + + private fun createTable(connection: Connection) { + val statement = connection.createStatement() + val createTableVertex = """ + CREATE TABLE IF NOT EXISTS vertex ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vertex_key INTEGER, + graph_id INTEGER, + FOREIGN KEY (graph_id) REFERENCES graph(graph_id) + ) + """ + val createTableEdge = """ + CREATE TABLE IF NOT EXISTS edge ( + start_vertex_id INTEGER, + end_vertex_id INTEGER, + weight INTEGER, + FOREIGN KEY (end_vertex_id) REFERENCES graph(graph_id), + FOREIGN KEY (start_vertex_id) REFERENCES graph(graph_id) + ) + """ + val createTableGraph = """ + CREATE TABLE IF NOT EXISTS graph ( + graph_id INTEGER PRIMARY KEY AUTOINCREMENT, + graph_name TEXT NOT NULL + ) + """ + statement.execute(createTableGraph) + statement.execute(createTableVertex) + statement.execute(createTableEdge) + statement.close() + } + + private fun insertGraph(connect: Connection, graph: Graph, nameGraph: String){ + val insertName = "INSERT INTO graph (graph_name) VALUES (?)" + val insertNameStmt: PreparedStatement = connect.prepareStatement(insertName) + insertNameStmt.setString(1, nameGraph) + insertNameStmt.executeUpdate() + + val graphId = insertNameStmt.generatedKeys.getInt(1) + insertNameStmt.close() + + val insertVertexSql = "INSERT INTO vertex (vertex_key, graph_id) VALUES (?, ?)" + val insertVertexStmt: PreparedStatement = connect.prepareStatement(insertVertexSql) + + val vertexIdMap = mutableMapOf() + + for (vertex in graph.vertices){ + insertVertexStmt.setInt(1, vertex.key) + insertVertexStmt.setInt(2, graphId) + + insertVertexStmt.executeUpdate() + + val vertexIdResult = insertVertexStmt.generatedKeys + if (vertexIdResult.next()) { + val vertexId = vertexIdResult.getInt(1) + vertexIdMap[vertex] = vertexId + } + } + insertVertexStmt.close() + + val insertEdgeSql = "INSERT INTO edge (start_vertex_id, end_vertex_id, weight) VALUES (?, ?, ?)" + val insertEdgeStmt: PreparedStatement = connect.prepareStatement(insertEdgeSql) + + for ((vertex, edges) in graph.adjacencyList) { + val startVertexId = vertexIdMap[vertex] ?: throw Exception("Vertex not found in vertexIdMap") + for (edge in edges) { + val endVertexId = vertexIdMap[edge.second] ?: throw Exception("End vertex not found in vertexIdMap") + insertEdgeStmt.setInt(1, startVertexId) + insertEdgeStmt.setInt(2, endVertexId) + insertEdgeStmt.setLong(3, edge.weight) + insertEdgeStmt.executeUpdate() + } + } + insertEdgeStmt.close() + } + + override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) { + //Сконектились с базой + val connection = DriverManager.getConnection("jdbc:sqlite:$filepath") + //Создали таблицы и связи между ними + createTable(connection) + //Сохранили граф по полочкам:) + insertGraph(connection, graph, nameGraph) + } + + override fun loadGraph(filepath: String, nameGraph: String): Graph { + TODO("Load graph") + } +} + + From 9b7bcd5666f30a7191bfa429ab737c215796da1c Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Wed, 21 Aug 2024 00:19:48 +0300 Subject: [PATCH 092/139] feat: created function loadGraph (loadGraph) --- src/main/kotlin/model/reader/SQLiteReader.kt | 87 +++++++++++++++++++- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/model/reader/SQLiteReader.kt b/src/main/kotlin/model/reader/SQLiteReader.kt index f3e922c..0dae36a 100644 --- a/src/main/kotlin/model/reader/SQLiteReader.kt +++ b/src/main/kotlin/model/reader/SQLiteReader.kt @@ -2,6 +2,7 @@ package model.reader import model.graph.Graph import model.graph.Vertex +import model.graph.WeightedGraph import java.sql.Connection import java.sql.DriverManager @@ -31,7 +32,7 @@ class SQLiteReader: Reader { val createTableGraph = """ CREATE TABLE IF NOT EXISTS graph ( graph_id INTEGER PRIMARY KEY AUTOINCREMENT, - graph_name TEXT NOT NULL + graph_name TEXT NOT NULL UNIQUE ) """ statement.execute(createTableGraph) @@ -84,17 +85,97 @@ class SQLiteReader: Reader { insertEdgeStmt.close() } + private fun connect(filepath: String): Connection = DriverManager.getConnection("jdbc:sqlite:$filepath") + + override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) { + //Сконектились с базой - val connection = DriverManager.getConnection("jdbc:sqlite:$filepath") + val connection = connect(filepath) + //Создали таблицы и связи между ними createTable(connection) + //Сохранили граф по полочкам:) insertGraph(connection, graph, nameGraph) } override fun loadGraph(filepath: String, nameGraph: String): Graph { - TODO("Load graph") + + //Сконектились с базой + val connection = connect(filepath) + + val graph: Graph = WeightedGraph() + + //Сделали запрос на получение id графа + val graphStmt = connection.prepareStatement( + "SELECT graph_id FROM graph WHERE graph_name = ?" + ) + + graphStmt.setString(1, nameGraph) + val graphResultSet = graphStmt.executeQuery() + + if (!graphResultSet.next()) { + throw IllegalArgumentException("Graph with name $nameGraph not found") + } + + val graphId = graphResultSet.getInt("id") + graphResultSet.close() + graphStmt.close() + + //Сделали запрос на получение id и ключа вершины + val vertexStmt = connection.prepareStatement( + "SELECT id, vertex_key FROM vertex WHERE graph_id = ?" + ) + + vertexStmt.setInt(1, graphId) + val vertexResultSet = vertexStmt.executeQuery() + + // Нужна для нахождение вершин ребра через их id + val vertexMap = mutableMapOf() + + while (vertexResultSet.next()){ + val vertexId = vertexResultSet.getInt("id") + val vertexKey = vertexResultSet.getInt("vertex_key") + val vertex = graph.addVertex(vertexKey) + + if (vertex != null){ + vertexMap[vertexId] = vertex + } + } + + vertexResultSet.close() + vertexStmt.close() + + /* + Сделали запрос на получение id начальной и конечной вершины, а также веса, ребра, + через id вершины полученной от graph_id + */ + val edgeStmt = connection.prepareStatement( + "SELECT start_vertex_id, end_vertex_id, weight FROM edge WHERE start_vertex_id" + + " IN (SELECT id FROM vertex WHERE graph_id = ?)" + ) + + edgeStmt.setInt(1, graphId) + val edgeResultSet = edgeStmt.executeQuery() + + while (edgeResultSet.next()) { + val startVertexId = edgeResultSet.getInt("start_vertex_id") + val endVertexId = edgeResultSet.getInt("end_vertex_id") + val weight = edgeResultSet.getLong("weight") + + val startVertex = vertexMap[startVertexId] + val endVertex = vertexMap[endVertexId] + + if (startVertex != null && endVertex != null) { + graph.addEdge(startVertex.key, endVertex.key, weight) + } + } + edgeResultSet.close() + edgeStmt.close() + + connection.close() + return graph } } From a7014f7ca64581f7afda78e4acb2a632e40adfe1 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Fri, 23 Aug 2024 15:13:58 +0300 Subject: [PATCH 093/139] refactor: delete prints --- src/main/kotlin/view/canvas/CanvasView.kt | 2 -- src/main/kotlin/view/canvas/EdgeCanvasView.kt | 5 ----- 2 files changed, 7 deletions(-) diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index e57d6ab..a37752f 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.onSizeChanged import viewModel.canvas.CanvasViewModel -import java.util.* @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable @@ -20,7 +19,6 @@ fun CanvasView( viewModel: CanvasViewModel, modifier: Modifier = Modifier ) { - println("[${Date()}] render canvas") Box( modifier = modifier.background(Color(0xFF242424)) .onPointerEvent(PointerEventType.Scroll, onEvent = viewModel.onScroll) diff --git a/src/main/kotlin/view/canvas/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt index c561ab5..ae3da58 100644 --- a/src/main/kotlin/view/canvas/EdgeCanvasView.kt +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -5,21 +5,16 @@ 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.canvas.EdgeCanvasViewModel -import java.util.* @Composable fun EdgeCanvasView( viewModel: EdgeCanvasViewModel, modifier: Modifier = Modifier ) { - println("[${Date()}] render edge") Canvas(Modifier.fillMaxSize()) { - println("[${Date()}] render canvas inside edge") - // something hard thing for drawing edge from border of node, not from center val firstCenter = viewModel.first.offset + Offset( From 26fdbf58a2bd64be91e42b19aeb3005fc393051e Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 23 Aug 2024 21:03:30 +0300 Subject: [PATCH 094/139] refactor: made the DirectedGraph class open --- src/main/kotlin/model/graph/DirectedGraph.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index 4d4e043..938f9eb 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,6 +1,6 @@ package model.graph -class DirectedGraph : UndirectedGraph() { +open class DirectedGraph : UndirectedGraph() { override fun addEdge(first: Int, second: Int, weight: Long): Edge? { if (first == second) return null From 344e7acd6f496a74476d84cad0c1480e2083058d Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 23 Aug 2024 21:05:50 +0300 Subject: [PATCH 095/139] feat: add WeightedDirectedGraph class --- .../model/graph/WeightedDirectedGraph.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/kotlin/model/graph/WeightedDirectedGraph.kt diff --git a/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/src/main/kotlin/model/graph/WeightedDirectedGraph.kt new file mode 100644 index 0000000..cc767de --- /dev/null +++ b/src/main/kotlin/model/graph/WeightedDirectedGraph.kt @@ -0,0 +1,24 @@ +package model.graph + +class WeightedDirectedGraph: DirectedGraph() { + override fun addEdge(first: Int, second: Int, weight: Long): Edge? { + if (first == second) return null + + val vertex1 = _vertices[first] ?: return null + val vertex2 = _vertices[second] ?: return null + + // edge already exists + if (_adjacencyList[vertex1]?.find { it.second.key == second } != null) return null + + _adjacencyList[vertex1]?.add(WeightedDirectedEdge(vertex1, vertex2, weight)) + + + return _adjacencyList[vertex1]?.last() + } + + private data class WeightedDirectedEdge( + override val first: Vertex, + override val second: Vertex, + override var weight: Long + ) : Edge +} \ No newline at end of file From 0cc068e4841909c1eed538268ab1d8081831c785 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 23 Aug 2024 21:07:07 +0300 Subject: [PATCH 096/139] feat: add WeightedDirectedGraphTest class --- .../model/graph/WeightedDirectedGraphTest.kt | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/test/kotlin/model/graph/WeightedDirectedGraphTest.kt diff --git a/src/test/kotlin/model/graph/WeightedDirectedGraphTest.kt b/src/test/kotlin/model/graph/WeightedDirectedGraphTest.kt new file mode 100644 index 0000000..95b9361 --- /dev/null +++ b/src/test/kotlin/model/graph/WeightedDirectedGraphTest.kt @@ -0,0 +1,89 @@ +package model.graph + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class WeightedDirectedGraphTest { + + @Nested + inner class addEdge { + private val graph = WeightedDirectedGraph() + + @Test + fun `Not linked vertices`() { + assertNotNull(graph.addVertex(2)) + assertNotNull(graph.addVertex(1)) + + val edge = graph.addEdge(1, 2) + + assertNotNull(edge) + + } + + @Test + fun `First vertex == second vertex`() { + assertNotNull(graph.addVertex(1)) + + val edge = graph.addEdge(1, 1) + + assertNull(edge) + } + + @Test + fun `First edge == second edge`() { + assertNotNull(graph.addVertex(1)) + assertNotNull(graph.addVertex(2)) + + val edge1 = graph.addEdge(1, 2) + val edge2 = graph.addEdge(1, 2) + + assertNotNull(edge1) + assertNull(edge2) + } + + @Test + fun `Already linked vertices`() { + assertNotNull(graph.addVertex(1)) + assertNotNull(graph.addVertex(2)) + + val edge = graph.addEdge(1, 2, 5) + + assertNotNull(edge) + + assertEquals(edge.weight, 5) + } + + @Test + fun `Base graph test`() { + assertNotNull(graph.addVertex(1)) + assertNotNull(graph.addVertex(2)) + assertNotNull(graph.addVertex(3)) + + val edge1 = graph.addEdge(1, 2, 5) + val edge2 = graph.addEdge(2, 3, 7) + val edge3 = graph.addEdge(3, 1, 9) + + assertNotNull(edge1) + assertNotNull(edge2) + assertNotNull(edge3) + + assertEquals(edge1.weight, 5) + assertEquals(edge2.weight, 7) + assertEquals(edge3.weight, 9) + } + + @Test + fun `Edge with non existing vertex`() { + val vertex = graph.addVertex(1) + + assertNull(graph.addEdge(2, 1)) + assertNull(graph.addEdge(1, 2)) + assertNull(graph.addEdge(3, 4)) + + assertEquals(graph.adjacencyList[vertex]?.size, 0) + } + } +} \ No newline at end of file From 7a0289e6a6c52541ad57d73ae5df238a7a04d385 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 23 Aug 2024 21:09:36 +0300 Subject: [PATCH 097/139] feat: added to the loadGraph and saveGraph functions to take into account the graph type --- src/main/kotlin/model/reader/SQLiteReader.kt | 40 ++++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/model/reader/SQLiteReader.kt b/src/main/kotlin/model/reader/SQLiteReader.kt index 0dae36a..46b0b82 100644 --- a/src/main/kotlin/model/reader/SQLiteReader.kt +++ b/src/main/kotlin/model/reader/SQLiteReader.kt @@ -1,8 +1,6 @@ package model.reader -import model.graph.Graph -import model.graph.Vertex -import model.graph.WeightedGraph +import model.graph.* import java.sql.Connection import java.sql.DriverManager @@ -32,7 +30,8 @@ class SQLiteReader: Reader { val createTableGraph = """ CREATE TABLE IF NOT EXISTS graph ( graph_id INTEGER PRIMARY KEY AUTOINCREMENT, - graph_name TEXT NOT NULL UNIQUE + graph_name TEXT NOT NULL UNIQUE, + graph_type_flag INTEGER ) """ statement.execute(createTableGraph) @@ -42,9 +41,24 @@ class SQLiteReader: Reader { } private fun insertGraph(connect: Connection, graph: Graph, nameGraph: String){ - val insertName = "INSERT INTO graph (graph_name) VALUES (?)" + + val insertName = "INSERT INTO graph (graph_name, graph_type_flag) VALUES (?, ?)" val insertNameStmt: PreparedStatement = connect.prepareStatement(insertName) insertNameStmt.setString(1, nameGraph) + + if (graph is WeightedGraph){ + insertNameStmt.setInt(2, 1) + } + if (graph is UndirectedGraph){ + insertNameStmt.setInt(2, 2) + } + if (graph is DirectedGraph){ + insertNameStmt.setInt(2, 3) + } + if (graph is WeightedDirectedGraph){ + insertNameStmt.setInt(2, 4) + } + insertNameStmt.executeUpdate() val graphId = insertNameStmt.generatedKeys.getInt(1) @@ -105,11 +119,11 @@ class SQLiteReader: Reader { //Сконектились с базой val connection = connect(filepath) - val graph: Graph = WeightedGraph() + val graph: Graph //Сделали запрос на получение id графа val graphStmt = connection.prepareStatement( - "SELECT graph_id FROM graph WHERE graph_name = ?" + "SELECT graph_id, graph_type_flag FROM graph WHERE graph_name = ?" ) graphStmt.setString(1, nameGraph) @@ -119,7 +133,17 @@ class SQLiteReader: Reader { throw IllegalArgumentException("Graph with name $nameGraph not found") } - val graphId = graphResultSet.getInt("id") + val graphId = graphResultSet.getInt("graph_id") + val graphType = graphResultSet.getInt("graph_type_flag") + + graph = when (graphType) { + 1 -> WeightedGraph() + 2 -> UndirectedGraph() + 3 -> DirectedGraph() + 4 -> WeightedDirectedGraph() + else -> throw IllegalArgumentException("Unknown graph type: $graphType") + } + graphResultSet.close() graphStmt.close() From 35a8b879f3776af22d4adb295a1ea46e384d4269 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 23 Aug 2024 21:10:49 +0300 Subject: [PATCH 098/139] feat: added a directory with the trial database --- src/main/kotlin/model/reader/database/graph.db | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/kotlin/model/reader/database/graph.db diff --git a/src/main/kotlin/model/reader/database/graph.db b/src/main/kotlin/model/reader/database/graph.db new file mode 100644 index 0000000..e69de29 From e49ac6125d7cbc67279d3e7d39f595bdea100ee2 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 2 Sep 2024 13:48:17 +0300 Subject: [PATCH 099/139] feat: add algorithms icon --- src/main/resources/Algorithm.svg | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/resources/Algorithm.svg diff --git a/src/main/resources/Algorithm.svg b/src/main/resources/Algorithm.svg new file mode 100644 index 0000000..6e1333e --- /dev/null +++ b/src/main/resources/Algorithm.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From ecb9694a69f08c743896b8a0ebf0dd5a4b7f46c2 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 2 Sep 2024 22:28:58 +0300 Subject: [PATCH 100/139] feat: implemented algorithms icon in GUI --- src/main/kotlin/view/MainView.kt | 6 +++++- src/main/kotlin/view/MenuView.kt | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index ce6e645..2571d3a 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -19,6 +19,7 @@ fun MainView(mainViewModel: MainViewModel) { var isClustering by remember { mutableStateOf(false) } var isRanked by remember { mutableStateOf(false) } var isNodeCreatingMode by remember { mutableStateOf(false) } + var isAlgorithmMenuOpen by remember { mutableStateOf(false)} Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { MenuView( @@ -27,7 +28,10 @@ fun MainView(mainViewModel: MainViewModel) { isClustering, { isClustering = !isClustering }, isRanked, - { isRanked = !isRanked }) + { isRanked = !isRanked }, + isAlgorithmMenuOpen, + { isAlgorithmMenuOpen = !isAlgorithmMenuOpen }) + CanvasView( mainViewModel.canvasViewModel, diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 7488d7f..ad005da 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -31,7 +31,9 @@ fun MenuView( isClustering: Boolean, onClusteringChange: () -> Unit, isRanked: Boolean, - onRankedChange: () -> Unit + onRankedChange: () -> Unit, + isAlgorithmMenuOpen: Boolean, + onAlgorithmMenuOpenChange: () -> Unit ) { Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), @@ -50,6 +52,9 @@ fun MenuView( MenuIcon("PageRank.svg", "Analysis graph", Modifier.glow(isRanked)) { onRankedChange() } + MenuIcon("Algorithm.svg", "Algorithms...", Modifier.glow(isAlgorithmMenuOpen)){ + onAlgorithmMenuOpenChange() + } } } From ac54f5db478505ef3dbe96060b83c20bb9f42844 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Thu, 5 Sep 2024 20:00:54 +0300 Subject: [PATCH 101/139] feat: add image with description for button --- src/main/resources/AddEdge.svg | 6 ++++++ src/main/resources/AddNode.svg | 6 ++++++ src/main/resources/Algorithms....svg | 5 +++++ src/main/resources/AnalysisGraph.svg | 6 ++++++ src/main/resources/ClusterD.svg | 6 ++++++ 5 files changed, 29 insertions(+) create mode 100644 src/main/resources/AddEdge.svg create mode 100644 src/main/resources/AddNode.svg create mode 100644 src/main/resources/Algorithms....svg create mode 100644 src/main/resources/AnalysisGraph.svg create mode 100644 src/main/resources/ClusterD.svg diff --git a/src/main/resources/AddEdge.svg b/src/main/resources/AddEdge.svg new file mode 100644 index 0000000..ac56564 --- /dev/null +++ b/src/main/resources/AddEdge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/AddNode.svg b/src/main/resources/AddNode.svg new file mode 100644 index 0000000..eeda9bc --- /dev/null +++ b/src/main/resources/AddNode.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/Algorithms....svg b/src/main/resources/Algorithms....svg new file mode 100644 index 0000000..2b76310 --- /dev/null +++ b/src/main/resources/Algorithms....svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/AnalysisGraph.svg b/src/main/resources/AnalysisGraph.svg new file mode 100644 index 0000000..461e240 --- /dev/null +++ b/src/main/resources/AnalysisGraph.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/ClusterD.svg b/src/main/resources/ClusterD.svg new file mode 100644 index 0000000..abac7e0 --- /dev/null +++ b/src/main/resources/ClusterD.svg @@ -0,0 +1,6 @@ + + + + + + From d9ba9bfc31c3d1f5a65157f3cf046c52331ec238 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Thu, 5 Sep 2024 20:05:00 +0300 Subject: [PATCH 102/139] feat: implement description display --- src/main/kotlin/view/MenuView.kt | 74 ++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index ad005da..219caec 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -3,25 +3,83 @@ package view import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State +import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import kotlin.math.roundToInt -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Composable fun MenuIcon(name: String, description: String, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { + var isHovered by remember { mutableStateOf(false) } + val popupOffset: Offset = Offset(80f, -180f) + var iconPosition by remember { mutableStateOf(IntOffset.Zero) } + var iconSize by remember { mutableStateOf(IntSize.Zero) } + Image( painter = painterResource(name), contentDescription = description, modifier = modifier .onClick(onClick = onClick) + .pointerMoveFilter( + onEnter = { + isHovered = true + true + }, + onExit = { + isHovered = false + false + } + ) + .onGloballyPositioned { layoutCoordinates -> + // Получаем положение и размер иконки + iconPosition = layoutCoordinates.positionInRoot().run { + IntOffset(x.roundToInt(), y.roundToInt()) + } + iconSize = layoutCoordinates.size + } ) Spacer(Modifier.height(10f.dp)) + if (isHovered){ + Popup( + offset = with(LocalDensity.current) { + IntOffset( + (iconPosition.x + popupOffset.x.toDp().roundToPx()), + (iconPosition.y + iconSize.height + popupOffset.y.toDp().roundToPx()) + ) + }, + alignment = Alignment.TopStart, + properties = PopupProperties(focusable = false) + ) { + DisplayDescription(description) + } + } +} + +@Composable +fun DisplayDescription(name : String) { + Image( + painter = painterResource(name), + contentDescription = "Descript", + modifier = Modifier.size(350f.dp), + contentScale = ContentScale.Fit + ) } @Composable @@ -41,18 +99,18 @@ fun MenuView( ) { Spacer(Modifier.height(25f.dp)) MenuIcon( - "Nodes.svg", "Add Node", Modifier.glow(isNodeCreating) + "Nodes.svg", "AddNode.svg", Modifier.glow(isNodeCreating) ) { onNodeCreatingChange() } - MenuIcon("Ribs.svg", "Add Edge", modifier = Modifier.alpha(0.2f)) - MenuIcon("Clustering.svg", "Clustering", Modifier.glow(isClustering)) { + MenuIcon("Ribs.svg", "AddEdge.svg", modifier = Modifier.alpha(0.2f)) + MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glow(isClustering)) { onClusteringChange() } - MenuIcon("PageRank.svg", "Analysis graph", Modifier.glow(isRanked)) { + MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glow(isRanked)) { onRankedChange() } - MenuIcon("Algorithm.svg", "Algorithms...", Modifier.glow(isAlgorithmMenuOpen)){ + MenuIcon("Algorithm.svg", "Algorithms....svg", Modifier.glow(isAlgorithmMenuOpen)){ onAlgorithmMenuOpenChange() } } From 15c7c1fe0b22f2b613e297716d23fea9f4f7b9eb Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 6 Sep 2024 23:22:21 +0300 Subject: [PATCH 103/139] refactor: delete comment --- src/main/kotlin/view/MenuView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 219caec..8540627 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -48,7 +48,6 @@ fun MenuIcon(name: String, description: String, modifier: Modifier = Modifier, o } ) .onGloballyPositioned { layoutCoordinates -> - // Получаем положение и размер иконки iconPosition = layoutCoordinates.positionInRoot().run { IntOffset(x.roundToInt(), y.roundToInt()) } From ce1d985f869b2480f94012ab36fb834804438f4a Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 6 Sep 2024 23:23:52 +0300 Subject: [PATCH 104/139] feat: added icons for the algorithm menu --- src/main/resources/Bellman-Ford.svg | 5 +++++ src/main/resources/Dijkstra.svg | 5 +++++ src/main/resources/DownMenuAlgorithm.svg | 4 ++++ src/main/resources/FindBridge.svg | 5 +++++ src/main/resources/FindCycle.svg | 5 +++++ src/main/resources/IslandTree.svg | 5 +++++ src/main/resources/StrongConnectivityComponent.svg | 5 +++++ 7 files changed, 34 insertions(+) create mode 100644 src/main/resources/Bellman-Ford.svg create mode 100644 src/main/resources/Dijkstra.svg create mode 100644 src/main/resources/DownMenuAlgorithm.svg create mode 100644 src/main/resources/FindBridge.svg create mode 100644 src/main/resources/FindCycle.svg create mode 100644 src/main/resources/IslandTree.svg create mode 100644 src/main/resources/StrongConnectivityComponent.svg diff --git a/src/main/resources/Bellman-Ford.svg b/src/main/resources/Bellman-Ford.svg new file mode 100644 index 0000000..701e743 --- /dev/null +++ b/src/main/resources/Bellman-Ford.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/Dijkstra.svg b/src/main/resources/Dijkstra.svg new file mode 100644 index 0000000..5013f28 --- /dev/null +++ b/src/main/resources/Dijkstra.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/DownMenuAlgorithm.svg b/src/main/resources/DownMenuAlgorithm.svg new file mode 100644 index 0000000..413215b --- /dev/null +++ b/src/main/resources/DownMenuAlgorithm.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/FindBridge.svg b/src/main/resources/FindBridge.svg new file mode 100644 index 0000000..c222ef8 --- /dev/null +++ b/src/main/resources/FindBridge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/FindCycle.svg b/src/main/resources/FindCycle.svg new file mode 100644 index 0000000..a1dfe0f --- /dev/null +++ b/src/main/resources/FindCycle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/IslandTree.svg b/src/main/resources/IslandTree.svg new file mode 100644 index 0000000..23dfdbc --- /dev/null +++ b/src/main/resources/IslandTree.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/StrongConnectivityComponent.svg b/src/main/resources/StrongConnectivityComponent.svg new file mode 100644 index 0000000..a214d9d --- /dev/null +++ b/src/main/resources/StrongConnectivityComponent.svg @@ -0,0 +1,5 @@ + + + + + From 5d325c6ce80df5d49e27e678fce4ba68d53c9ca8 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Fri, 6 Sep 2024 23:25:01 +0300 Subject: [PATCH 105/139] feat: made a menu of algorithms --- src/main/kotlin/view/MainView.kt | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 2571d3a..195bdc0 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -2,17 +2,90 @@ package view import Config import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties import view.canvas.CanvasView import viewModel.MainViewModel val HEADER_HEIGHT = Config.headerHeight val MENU_WIDTH = Config.menuWidth +@Composable +fun DisplayAlgorithmMenu(name : String) { + + val imageResources = listOf( + "FindBridge.svg", + "Dijkstra.svg", + "Bellman-Ford.svg", + "IslandTree.svg", + "StrongConnectivityComponent.svg", + "FindCycle.svg" + ) + Box( + modifier = Modifier.padding(top = 240.dp, start = 80.dp) + ) { + + // Изображение + Image( + painter = painterResource(name), + contentDescription = "Padded Image", + modifier = Modifier.size(452.dp), + contentScale = ContentScale.Fit + ) + + LazyColumn( + modifier = Modifier + .size(450.dp, 300.dp) + .background(Color.Transparent) + .padding(top = 150.dp, start = 30.dp) + ) { + items(imageResources) { image -> + ImageButton( + imageResourceId = image, + onClick = { + } + ) + } + } + } +} + +@Composable +fun ImageButton(imageResourceId: String, onClick: () -> Unit) { + Box( + modifier = Modifier + .size(440.dp, 60.dp) + .padding(1.dp) + .clickable { onClick() } + .background(Color(0x00)) + ) { + Image( + painter = painterResource(imageResourceId), + contentDescription = "Button Image", + modifier = Modifier.size(445.dp, 59.dp), + contentScale = ContentScale.Crop + ) + } +} + @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun MainView(mainViewModel: MainViewModel) { @@ -39,5 +112,9 @@ fun MainView(mainViewModel: MainViewModel) { ) } + if (isAlgorithmMenuOpen){ + DisplayAlgorithmMenu("DownMenuAlgorithm.svg") + } + SettingsView(mainViewModel.settingsViewModel) } \ No newline at end of file From a846928322eaa2ce3d47c5a5ba69d51f4f82229b Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 9 Sep 2024 23:23:39 +0300 Subject: [PATCH 106/139] feat: add icon for file menu --- src/main/resources/DataBase.svg | 4 ++++ src/main/resources/DataBaseLoad.svg | 4 ++++ src/main/resources/JSON.svg | 4 ++++ src/main/resources/JSONLoad.svg | 4 ++++ src/main/resources/Neo4j.svg | 4 ++++ src/main/resources/Neo4jLoad.svg | 4 ++++ 6 files changed, 24 insertions(+) create mode 100644 src/main/resources/DataBase.svg create mode 100644 src/main/resources/DataBaseLoad.svg create mode 100644 src/main/resources/JSON.svg create mode 100644 src/main/resources/JSONLoad.svg create mode 100644 src/main/resources/Neo4j.svg create mode 100644 src/main/resources/Neo4jLoad.svg diff --git a/src/main/resources/DataBase.svg b/src/main/resources/DataBase.svg new file mode 100644 index 0000000..8fc91f9 --- /dev/null +++ b/src/main/resources/DataBase.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/DataBaseLoad.svg b/src/main/resources/DataBaseLoad.svg new file mode 100644 index 0000000..7108c31 --- /dev/null +++ b/src/main/resources/DataBaseLoad.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/JSON.svg b/src/main/resources/JSON.svg new file mode 100644 index 0000000..5c13ffc --- /dev/null +++ b/src/main/resources/JSON.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/JSONLoad.svg b/src/main/resources/JSONLoad.svg new file mode 100644 index 0000000..e22b409 --- /dev/null +++ b/src/main/resources/JSONLoad.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/Neo4j.svg b/src/main/resources/Neo4j.svg new file mode 100644 index 0000000..3586484 --- /dev/null +++ b/src/main/resources/Neo4j.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/Neo4jLoad.svg b/src/main/resources/Neo4jLoad.svg new file mode 100644 index 0000000..43fc524 --- /dev/null +++ b/src/main/resources/Neo4jLoad.svg @@ -0,0 +1,4 @@ + + + + From 1a2bf8be83bfe3d41115c6bb861b35d15c3ef368 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 9 Sep 2024 23:25:03 +0300 Subject: [PATCH 107/139] feat: implemented file menu view --- src/main/kotlin/view/HeaderView.kt | 79 +++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index 2b9500c..2de5ff1 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -4,16 +4,24 @@ import Config import androidx.compose.foundation.* import androidx.compose.foundation.gestures.onDrag import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items + import androidx.compose.material.Icon +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.SemanticsActions.OnClick import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties import components.MyText @OptIn(ExperimentalFoundationApi::class) @@ -84,10 +92,68 @@ private fun Logo() { ) } +@Composable +fun ImageButtonFile(imageResourceId: String, onClick: () -> Unit) { + Box( + modifier = Modifier + .size(300.dp, 50.dp) + .padding(0.dp) + .clickable { onClick() } + .background(Color(0x00)) + ) { + Image( + painter = painterResource(imageResourceId), + contentDescription = "Button Image", + Modifier.size(400.dp, 50.dp), + contentScale = ContentScale.Crop + ) + } +} + +@Composable +private fun FileMenu(){ + val imageResources = listOf( + "DataBase.svg", + "JSON.svg", + "Neo4j.svg", + "DataBaseLoad.svg", + "JSONLoad.svg", + "Neo4jLoad.svg" + ) + + Box( + Modifier + .size(300.dp, 300.dp) + .shadow( + elevation = 5f.dp, + spotColor = Color.Black + ) + .background(Color(0xFF3D3D3D)), + + ) { + LazyColumn( + modifier = Modifier + .size(300.dp, 300.dp) + + ) { + items(imageResources) { image -> + ImageButtonFile( + imageResourceId = image, + onClick = { + } + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) @Composable private fun FileButton() { + var isImageVisible by remember { mutableStateOf(false) } Box( Modifier + .clickable{ isImageVisible = !isImageVisible } .size(Config.headerHeight.dp) .shadow( elevation = 5f.dp, @@ -96,5 +162,16 @@ private fun FileButton() { contentAlignment = Alignment.Center ) { MyText("File", 16f) + } + if (isImageVisible) { + Box( + Modifier.padding(top = 33.dp) + ) { + Popup ( + properties = PopupProperties(focusable = false) + ) { + FileMenu() + } + } } } \ No newline at end of file From 1a2e48ff2df01e843a7e4237eb68e18433a13a65 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Mon, 9 Sep 2024 23:28:49 +0300 Subject: [PATCH 108/139] refactor: deleted unnecessary imports --- src/main/kotlin/view/HeaderView.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt index 2de5ff1..8ca2327 100644 --- a/src/main/kotlin/view/HeaderView.kt +++ b/src/main/kotlin/view/HeaderView.kt @@ -6,9 +6,7 @@ import androidx.compose.foundation.gestures.onDrag import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items - import androidx.compose.material.Icon -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.* @@ -18,7 +16,6 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.SemanticsActions.OnClick import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties From b88cddf60909582e50112e78b52488e003f3ac89 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 10 Sep 2024 16:11:31 +0300 Subject: [PATCH 109/139] fix: stroke width now scales with zoom --- src/main/kotlin/viewModel/canvas/CanvasViewModel.kt | 3 ++- src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt | 6 +++++- src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index a9c2dfb..b526376 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -30,6 +30,7 @@ class CanvasViewModel( set(value) { _zoom.value = value updateVertexes() + updateEdges() } private val _center = mutableStateOf(Offset(0f, 0f)) @@ -79,7 +80,7 @@ class CanvasViewModel( } private fun updateEdges() { - + edges.forEach { it.updateEdge(zoom) } } val onScroll: AwaitPointerEventScope.(PointerEvent) -> Unit = { diff --git a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt index ab0fdda..3df074b 100644 --- a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -11,5 +11,9 @@ class EdgeCanvasViewModel( zoom: Float, val showOrientation: MutableState ) { - val strokeWidth = strokeWidth * zoom + var strokeWidth = strokeWidth * (zoom) + + fun updateEdge(zoom: Float) { + strokeWidth = 8f * zoom + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index 872cd05..45ae177 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -55,6 +55,7 @@ class VertexCanvasViewModel( fun updateVertex() { offset = calculateOffset() radius = calculateRadius() + strokeWidth = calculateStrokeWidth() textSize = calculateTextSize() } From 5e6ee3f27c7f0697b0903d00cd2a1fe2e7399aca Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 10 Sep 2024 16:41:35 +0300 Subject: [PATCH 110/139] fix: now clustering and pagerank buttons work --- src/main/kotlin/Main.kt | 4 +- src/main/kotlin/view/MainView.kt | 13 +----- src/main/kotlin/view/MenuView.kt | 21 ++++----- src/main/kotlin/view/canvas/CanvasView.kt | 14 +++--- src/main/kotlin/viewModel/MainViewModel.kt | 11 +++++ src/main/kotlin/viewModel/MenuViewModel.kt | 39 ++++++++++++++++ .../viewModel/canvas/CanvasViewModel.kt | 46 ++++++++++++++----- .../viewModel/graph/UndirectedViewModel.kt | 9 +++- 8 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/viewModel/MenuViewModel.kt diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index b74ef14..bb96c56 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -15,8 +15,8 @@ import view.MenuView import viewModel.MainViewModel import viewModel.graph.UndirectedViewModel -val AMOUNT_NODES = 2 -val EDGE_CHANGE = 100 +val AMOUNT_NODES = 16 +val EDGE_CHANGE = 5.0 val graph = UndirectedGraph().apply { for (i in (0 until AMOUNT_NODES)) { diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index ce6e645..abb1927 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -16,19 +16,8 @@ val MENU_WIDTH = Config.menuWidth @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun MainView(mainViewModel: MainViewModel) { - var isClustering by remember { mutableStateOf(false) } - var isRanked by remember { mutableStateOf(false) } - var isNodeCreatingMode by remember { mutableStateOf(false) } - Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { - MenuView( - isNodeCreatingMode, - { isNodeCreatingMode = !isNodeCreatingMode }, - isClustering, - { isClustering = !isClustering }, - isRanked, - { isRanked = !isRanked }) - + MenuView(mainViewModel.menuViewModel) CanvasView( mainViewModel.canvasViewModel, Modifier.fillMaxSize() diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 7488d7f..7c45107 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -4,13 +4,13 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import viewModel.MenuViewModel @OptIn(ExperimentalFoundationApi::class) @Composable @@ -26,12 +26,7 @@ fun MenuIcon(name: String, description: String, modifier: Modifier = Modifier, o @Composable fun MenuView( - isNodeCreating: Boolean, - onNodeCreatingChange: () -> Unit, - isClustering: Boolean, - onClusteringChange: () -> Unit, - isRanked: Boolean, - onRankedChange: () -> Unit + viewModel: MenuViewModel ) { Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), @@ -39,16 +34,16 @@ fun MenuView( ) { Spacer(Modifier.height(25f.dp)) MenuIcon( - "Nodes.svg", "Add Node", Modifier.glow(isNodeCreating) + "Nodes.svg", "Add Node", Modifier.glow(viewModel.isNodeCreating) ) { - onNodeCreatingChange() + viewModel.onNodeCreatingChange() } MenuIcon("Ribs.svg", "Add Edge", modifier = Modifier.alpha(0.2f)) - MenuIcon("Clustering.svg", "Clustering", Modifier.glow(isClustering)) { - onClusteringChange() + MenuIcon("Clustering.svg", "Clustering", Modifier.glow(viewModel.isClustering)) { + viewModel.onClusteringChange() } - MenuIcon("PageRank.svg", "Analysis graph", Modifier.glow(isRanked)) { - onRankedChange() + MenuIcon("PageRank.svg", "Analysis graph", Modifier.glow(viewModel.isRanked)) { + viewModel.onRankedChange() } } } diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index a37752f..a0f369b 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -2,6 +2,7 @@ package view.canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi @@ -23,14 +24,11 @@ fun CanvasView( modifier = modifier.background(Color(0xFF242424)) .onPointerEvent(PointerEventType.Scroll, onEvent = viewModel.onScroll) .pointerInput(Unit, viewModel.onDrag) -// .pointerInput(Unit) { -// detectTapGestures { -// if (isNodeCreatingMode) { -// canvasViewModel.createVertex(it - (canvasSize / 2f), center, zoom) -// zoom += 0.000001f // костыль для рекомпозиции -// } -// } -// } + .pointerInput(Unit) { + detectTapGestures { + viewModel.createNode(it) + } + } .pointerHoverIcon(PointerIcon.Hand) .onSizeChanged { viewModel.canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) diff --git a/src/main/kotlin/viewModel/MainViewModel.kt b/src/main/kotlin/viewModel/MainViewModel.kt index db1edaa..0c3676f 100644 --- a/src/main/kotlin/viewModel/MainViewModel.kt +++ b/src/main/kotlin/viewModel/MainViewModel.kt @@ -1,15 +1,26 @@ package viewModel +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import model.graph.UndirectedGraph import viewModel.canvas.CanvasViewModel class MainViewModel( graph: UndirectedGraph ) { + var isClustering = mutableStateOf(false) + var isRanked = mutableStateOf(false) + var isNodeCreatingMode = mutableStateOf(false) + val canvasViewModel = CanvasViewModel(graph) + val settingsViewModel = SettingsViewModel( canvasViewModel::onColorChange, canvasViewModel::onSizeChange, canvasViewModel::onOrientatedChange ) + + val menuViewModel = MenuViewModel(canvasViewModel) } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/MenuViewModel.kt b/src/main/kotlin/viewModel/MenuViewModel.kt new file mode 100644 index 0000000..16fa9ae --- /dev/null +++ b/src/main/kotlin/viewModel/MenuViewModel.kt @@ -0,0 +1,39 @@ +package viewModel + +import androidx.compose.runtime.mutableStateOf +import viewModel.canvas.CanvasViewModel + +class MenuViewModel( + val canvasViewModel: CanvasViewModel +) { + var isNodeCreating + get() = canvasViewModel.isNodeCreatingMode + set(value) { + canvasViewModel.isNodeCreatingMode = value + } + + fun onNodeCreatingChange() { + isNodeCreating = !isNodeCreating + } + + var isClustering + get() = canvasViewModel.isClustering + set(value) { + canvasViewModel.isClustering = value + } + + fun onClusteringChange() { + isClustering = !isClustering + } + + var isRanked + get() = canvasViewModel.isRanked + set(value) { + canvasViewModel.isRanked = value + } + + fun onRankedChange() { + isRanked = !isRanked + } + +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index b526376..103a0b4 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -2,18 +2,16 @@ package viewModel.canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher -import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.layout.LayoutCoordinates import model.graph.UndirectedGraph import view.HEADER_HEIGHT import view.MENU_WIDTH @@ -23,6 +21,39 @@ class CanvasViewModel( val graph: UndirectedGraph, ) { private val graphViewModel = UndirectedViewModel(graph, true) + val verticesSize = mutableStateOf(0) + + var isClustering + get() = graphViewModel.clustering + set(value) { + graphViewModel.clustering = value + } + + var isRanked + get() = graphViewModel.ranked + set(value) { + graphViewModel.ranked = value + } + + val _isNodeCreatingMode = mutableStateOf(false) + var isNodeCreatingMode + get() = _isNodeCreatingMode.value + set(value) { + _isNodeCreatingMode.value = value + } + + fun createNode(offset: Offset) { + if (isNodeCreatingMode) { + val coordinates = offset * (1 / zoom) + center + val viewModel = graphViewModel.createVertex(coordinates) ?: return + + zoom += 0.000001f // костыль для рекомпозиции + _vertices[viewModel] = VertexCanvasViewModel(viewModel, _zoom, _center, _canvasSize) + updateVertexes() + println(_vertices.size) + } + } + private val _zoom = mutableStateOf(1f) var zoom @@ -129,11 +160,4 @@ class CanvasViewModel( // // return _vertices.values // } - -// fun createVertex(offset: Offset, center: Offset, zoom: Float) { -// val coordinates = offset * (1 / zoom) + center -// val viewModel = graphViewModel.createVertex(coordinates) ?: return -// -// _vertices[viewModel] = VertexCanvasViewModel(viewModel, _zoom, center, canvasSize) -// } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index f198c97..f4783a7 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -5,14 +5,16 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.dp +import model.algorithm.Clustering +import model.algorithm.PageRank import model.graph.UndirectedGraph import model.graph.Vertex class UndirectedViewModel( private val graph: UndirectedGraph, val showVerticesLabels: Boolean, - val groups: HashMap = hashMapOf(), - val ranks: List> = listOf() + var groups: HashMap = hashMapOf(), + var ranks: List> = listOf() ) { private val _vertices = hashMapOf() private val _adjacencyList = hashMapOf>() @@ -38,6 +40,7 @@ class UndirectedViewModel( get() = _clustering.value set(value) { _clustering.value = value + groups = Clustering(graph).calculate() updateColor() } @@ -45,6 +48,7 @@ class UndirectedViewModel( get() = _ranked.value set(value) { _ranked.value = value + ranks = PageRank(graph).computePageRank(3) updateSizes() } @@ -94,6 +98,7 @@ class UndirectedViewModel( _vertices.forEach { it.value.radius = size.dp } + updateSizes() } fun createVertex(coordinates: Offset): VertexViewModel? { From 762fad9d6d8d1ffcf71aa0c4251eb2e93cb1a84e Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sat, 14 Sep 2024 20:28:43 +0300 Subject: [PATCH 111/139] fix: now adding nodes button works --- .../kotlin/viewModel/canvas/CanvasViewModel.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 103a0b4..f8d6211 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -11,17 +11,16 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerInputScope -import androidx.compose.ui.layout.LayoutCoordinates import model.graph.UndirectedGraph import view.HEADER_HEIGHT import view.MENU_WIDTH import viewModel.graph.UndirectedViewModel +import viewModel.graph.VertexViewModel class CanvasViewModel( val graph: UndirectedGraph, ) { private val graphViewModel = UndirectedViewModel(graph, true) - val verticesSize = mutableStateOf(0) var isClustering get() = graphViewModel.clustering @@ -44,13 +43,12 @@ class CanvasViewModel( fun createNode(offset: Offset) { if (isNodeCreatingMode) { - val coordinates = offset * (1 / zoom) + center + val coordinates = (offset - (canvasSize / 2.0F)) * (1 / zoom) + center + println(offset - (canvasSize / 2.0F)) val viewModel = graphViewModel.createVertex(coordinates) ?: return - zoom += 0.000001f // костыль для рекомпозиции _vertices[viewModel] = VertexCanvasViewModel(viewModel, _zoom, _center, _canvasSize) updateVertexes() - println(_vertices.size) } } @@ -87,9 +85,13 @@ class CanvasViewModel( _isOrientated.value = value } - private val _vertices = graphViewModel.vertices.associateWith { v -> - VertexCanvasViewModel(v, _zoom, _center, _canvasSize) - }.toMutableMap() + private val _vertices = mutableStateMapOf() + + init { + graphViewModel.vertices.forEach { v -> + _vertices[v] = VertexCanvasViewModel(v, _zoom, _center, _canvasSize) + } + } private val _edges = graphViewModel.adjacencyList.map { it.value }.flatten().map { val vertex1 = From a25c617f64a493f050187a29ebbe3f4e1bc205a8 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sat, 14 Sep 2024 20:35:22 +0300 Subject: [PATCH 112/139] refactor: delete unused variables in main file --- src/main/kotlin/Main.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index bb96c56..f54609b 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -32,17 +32,12 @@ val graph = UndirectedGraph().apply { } } -val groups = Clustering(graph).calculate() -val ranks = PageRank(graph).computePageRank(3) -val undirectedViewModel = UndirectedViewModel(graph, false, groups, ranks) val mainViewModel = MainViewModel(graph) fun main() = application { var isOpen by remember { mutableStateOf(true) } var isMaximized by remember { mutableStateOf(true) } - var isMinimize by remember { mutableStateOf(false) } - var position: WindowPosition by remember { mutableStateOf(WindowPosition.PlatformDefault) } - var headerName by remember { mutableStateOf("Dimabase.db") } + val headerName by remember { mutableStateOf("Dimabase.db") } val windowState = WindowState( placement = if (isMaximized) WindowPlacement.Maximized else WindowPlacement.Floating, From 0f3d03a373ee529aef6d30591a7c9cfd10a1ad8a Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sat, 14 Sep 2024 20:36:25 +0300 Subject: [PATCH 113/139] refactor: optimize imports --- src/main/kotlin/Config.kt | 1 - src/main/kotlin/Main.kt | 11 ++++------- src/main/kotlin/model/algorithm/BellmanFord.kt | 1 - src/main/kotlin/model/algorithm/Dijkstra.kt | 4 ++-- src/main/kotlin/model/algorithm/FindBridges.kt | 4 +++- src/main/kotlin/view/MainView.kt | 6 ++++-- src/main/kotlin/view/MenuView.kt | 1 + src/main/kotlin/view/SettingsView.kt | 6 ++++-- src/main/kotlin/viewModel/MainViewModel.kt | 3 --- src/main/kotlin/viewModel/MenuViewModel.kt | 1 - .../kotlin/viewModel/canvas/VertexCanvasViewModel.kt | 1 - .../kotlin/viewModel/graph/UndirectedViewModel.kt | 1 - 12 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt index 870b698..39b77dc 100644 --- a/src/main/kotlin/Config.kt +++ b/src/main/kotlin/Config.kt @@ -1,5 +1,4 @@ import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp object Config { val headerHeight = 40f diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index f54609b..b4355e3 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -3,17 +3,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.* -import model.algorithm.Clustering -import model.algorithm.PageRank +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application import model.graph.UndirectedGraph import view.HeaderView import view.MainView -import view.MenuView import viewModel.MainViewModel -import viewModel.graph.UndirectedViewModel val AMOUNT_NODES = 16 val EDGE_CHANGE = 5.0 diff --git a/src/main/kotlin/model/algorithm/BellmanFord.kt b/src/main/kotlin/model/algorithm/BellmanFord.kt index 67d72e2..ef5079d 100644 --- a/src/main/kotlin/model/algorithm/BellmanFord.kt +++ b/src/main/kotlin/model/algorithm/BellmanFord.kt @@ -3,7 +3,6 @@ package model.algorithm import model.graph.Edge import model.graph.Graph import model.graph.Vertex -import model.graph.WeightedGraph class BellmanFord(private val graph: Graph) { val parentMap = HashMap() diff --git a/src/main/kotlin/model/algorithm/Dijkstra.kt b/src/main/kotlin/model/algorithm/Dijkstra.kt index a1f3c95..2dbded8 100644 --- a/src/main/kotlin/model/algorithm/Dijkstra.kt +++ b/src/main/kotlin/model/algorithm/Dijkstra.kt @@ -1,7 +1,7 @@ package model.algorithm -import model.graph.* -import java.util.concurrent.TransferQueue +import model.graph.Graph +import model.graph.Vertex class Dijkstra(private val graph: Graph) { diff --git a/src/main/kotlin/model/algorithm/FindBridges.kt b/src/main/kotlin/model/algorithm/FindBridges.kt index 686b00e..afef70b 100644 --- a/src/main/kotlin/model/algorithm/FindBridges.kt +++ b/src/main/kotlin/model/algorithm/FindBridges.kt @@ -1,7 +1,9 @@ package model.algorithm -import model.graph.* +import model.graph.Edge +import model.graph.Graph +import model.graph.Vertex class FindBridges( diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index abb1927..77b251e 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -2,8 +2,10 @@ package view import Config import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 7c45107..1b9e8a7 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -1,5 +1,6 @@ package view +import Config import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt index 43b9126..5d454d1 100644 --- a/src/main/kotlin/view/SettingsView.kt +++ b/src/main/kotlin/view/SettingsView.kt @@ -3,8 +3,10 @@ package view 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.material.Checkbox +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 diff --git a/src/main/kotlin/viewModel/MainViewModel.kt b/src/main/kotlin/viewModel/MainViewModel.kt index 0c3676f..ae66cc7 100644 --- a/src/main/kotlin/viewModel/MainViewModel.kt +++ b/src/main/kotlin/viewModel/MainViewModel.kt @@ -1,9 +1,6 @@ package viewModel -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import model.graph.UndirectedGraph import viewModel.canvas.CanvasViewModel diff --git a/src/main/kotlin/viewModel/MenuViewModel.kt b/src/main/kotlin/viewModel/MenuViewModel.kt index 16fa9ae..638b5d9 100644 --- a/src/main/kotlin/viewModel/MenuViewModel.kt +++ b/src/main/kotlin/viewModel/MenuViewModel.kt @@ -1,6 +1,5 @@ package viewModel -import androidx.compose.runtime.mutableStateOf import viewModel.canvas.CanvasViewModel class MenuViewModel( diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index 45ae177..836a3c5 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -3,7 +3,6 @@ package viewModel.canvas import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.unit.dp import viewModel.graph.VertexViewModel class VertexCanvasViewModel( diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index f4783a7..ce010e1 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -3,7 +3,6 @@ package viewModel.graph import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.dp import model.algorithm.Clustering import model.algorithm.PageRank From d61ab86842754b9ee5d37a2a4662354366e71977 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sat, 14 Sep 2024 20:37:09 +0300 Subject: [PATCH 114/139] refactor: delete unused variables --- src/main/kotlin/Config.kt | 1 - src/main/kotlin/view/MainView.kt | 3 --- src/main/kotlin/viewModel/MainViewModel.kt | 5 ----- 3 files changed, 9 deletions(-) diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt index 39b77dc..f0b4354 100644 --- a/src/main/kotlin/Config.kt +++ b/src/main/kotlin/Config.kt @@ -3,7 +3,6 @@ import androidx.compose.ui.graphics.Color object Config { val headerHeight = 40f val menuWidth = 80f - val optimizeCanvas = false object Edge { val color = Color(0xFF00E0FF) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 77b251e..1ee53c1 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -1,12 +1,10 @@ package view import Config -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import view.canvas.CanvasView @@ -15,7 +13,6 @@ import viewModel.MainViewModel val HEADER_HEIGHT = Config.headerHeight val MENU_WIDTH = Config.menuWidth -@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun MainView(mainViewModel: MainViewModel) { Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { diff --git a/src/main/kotlin/viewModel/MainViewModel.kt b/src/main/kotlin/viewModel/MainViewModel.kt index ae66cc7..b6ea42f 100644 --- a/src/main/kotlin/viewModel/MainViewModel.kt +++ b/src/main/kotlin/viewModel/MainViewModel.kt @@ -1,16 +1,11 @@ package viewModel -import androidx.compose.runtime.mutableStateOf import model.graph.UndirectedGraph import viewModel.canvas.CanvasViewModel class MainViewModel( graph: UndirectedGraph ) { - var isClustering = mutableStateOf(false) - var isRanked = mutableStateOf(false) - var isNodeCreatingMode = mutableStateOf(false) - val canvasViewModel = CanvasViewModel(graph) val settingsViewModel = SettingsViewModel( From c492c56b4600116a9b11f4248291fdf8e6e80c56 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sat, 14 Sep 2024 20:46:14 +0300 Subject: [PATCH 115/139] refactor: seperate box style in SettingsView --- src/main/kotlin/view/SettingsView.kt | 55 ++++++++++++++++------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt index 5d454d1..755d200 100644 --- a/src/main/kotlin/view/SettingsView.kt +++ b/src/main/kotlin/view/SettingsView.kt @@ -16,7 +16,6 @@ import components.MySlider import components.MyText import viewModel.SettingsViewModel - @Composable fun SettingsView(viewModel: SettingsViewModel) { val redSlider = remember { mutableStateOf(1f / (0xFF / 0x8F)) } @@ -28,34 +27,42 @@ fun SettingsView(viewModel: SettingsViewModel) { viewModel.onColorChange(Color(red = redSlider.value, green = greenSlider.value, blue = blueSlider.value)) viewModel.onSizeChange(sizeSlider.value) + SettingsContainer { + Row(Modifier.fillMaxWidth().padding(top = 10f.dp), horizontalArrangement = Arrangement.Center) { + MyText("Node") + } + + Row(Modifier.fillMaxWidth().padding(top = 10f.dp, start = 20f.dp)) { + Column { + MyText("Color:") + MySlider("R: ", redSlider) + MySlider("G: ", greenSlider) + MySlider("B: ", blueSlider) + MySlider("Size: ", sizeSlider, (5f..80f)) + } + } + + Row( + Modifier.fillMaxWidth().padding(start = 20f.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MyText("Orientated") + Checkbox(orientatedCheckBox.value, onCheckedChange = { + viewModel.onOrientatedChange(it) + orientatedCheckBox.value = !orientatedCheckBox.value + }) + } + } +} + +@Composable +fun SettingsContainer(content: @Composable () -> Unit) { Box(Modifier.fillMaxSize().padding(top = 80f.dp, end = 20f.dp).zIndex(10f), contentAlignment = Alignment.TopEnd) { Box( Modifier.size(270f.dp, 320f.dp).background(Color(0xFF3D3D3D), RoundedCornerShape(10)) ) { Column { - Row(Modifier.fillMaxWidth().padding(top = 10f.dp), horizontalArrangement = Arrangement.Center) { - MyText("Node") - } - Row(Modifier.fillMaxWidth().padding(top = 10f.dp, start = 20f.dp)) { - Column { - MyText("Color:") - MySlider("R: ", redSlider) - MySlider("G: ", greenSlider) - MySlider("B: ", blueSlider) - MySlider("Size: ", sizeSlider, (5f..80f)) - } - } - - Row( - Modifier.fillMaxWidth().padding(start = 20f.dp), - verticalAlignment = Alignment.CenterVertically - ) { - MyText("Orientated") - Checkbox(orientatedCheckBox.value, onCheckedChange = { - viewModel.onOrientatedChange(it) - orientatedCheckBox.value = !orientatedCheckBox.value - }) - } + content() } } } From ab4358a317594fdac6ba9694583c6e06ceeefa12 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 19 Sep 2024 15:35:50 +0300 Subject: [PATCH 116/139] refactor: refactor menu, change variables initilization --- src/main/kotlin/view/MenuView.kt | 27 ++++++------- src/main/kotlin/viewModel/MenuViewModel.kt | 46 +++------------------- 2 files changed, 19 insertions(+), 54 deletions(-) diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 49914c9..3a5befc 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -86,26 +86,25 @@ fun DisplayDescription(name: String) { fun MenuView( viewModel: MenuViewModel ) { + var isNodeCreating by viewModel::isNodeCreating + var isClustering by viewModel::isClustering + var isRanked by viewModel::isRanked + var isAlgorithmMenuOpen by viewModel::isAlgorithmMenuOpen + Column( Modifier.fillMaxHeight().width(Config.menuWidth.dp).background(color = Color(0xFF3D3D3D)), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.height(25f.dp)) - MenuIcon( - "Nodes.svg", "AddNode.svg", Modifier.glow(viewModel.isNodeCreating) - ) { - viewModel.onNodeCreatingChange() - } + MenuIcon("Nodes.svg", "AddNode.svg", Modifier.glow(isNodeCreating)) { isNodeCreating = !isNodeCreating } MenuIcon("Ribs.svg", "AddEdge.svg", modifier = Modifier.alpha(0.2f)) - MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glow(viewModel.isClustering)) { - viewModel.onClusteringChange() - } - MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glow(viewModel.isRanked)) { - viewModel.onRankedChange() - } - MenuIcon("Algorithm.svg", "Algorithms....svg", Modifier.glow(viewModel.isAlgorithmMenuOpen)) { - viewModel.onAlgorithmMenuOpenChange() - } + MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glow(isClustering)) { isClustering = !isClustering } + MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glow(viewModel.isRanked)) { isRanked = !isRanked } + MenuIcon( + "Algorithm.svg", + "Algorithms....svg", + Modifier.glow(viewModel.isAlgorithmMenuOpen) + ) { isAlgorithmMenuOpen = !isAlgorithmMenuOpen } } } diff --git a/src/main/kotlin/viewModel/MenuViewModel.kt b/src/main/kotlin/viewModel/MenuViewModel.kt index 8279cbf..de14aca 100644 --- a/src/main/kotlin/viewModel/MenuViewModel.kt +++ b/src/main/kotlin/viewModel/MenuViewModel.kt @@ -1,49 +1,15 @@ package viewModel +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import viewModel.canvas.CanvasViewModel class MenuViewModel( val canvasViewModel: CanvasViewModel ) { - var isNodeCreating - get() = canvasViewModel.isNodeCreatingMode - set(value) { - canvasViewModel.isNodeCreatingMode = value - } - - fun onNodeCreatingChange() { - isNodeCreating = !isNodeCreating - } - - var isClustering - get() = canvasViewModel.isClustering - set(value) { - canvasViewModel.isClustering = value - } - - fun onClusteringChange() { - isClustering = !isClustering - } - - var isRanked - get() = canvasViewModel.isRanked - set(value) { - canvasViewModel.isRanked = value - } - - fun onRankedChange() { - isRanked = !isRanked - } - - val _isAlgorithmMenuOpen = mutableStateOf(false) - var isAlgorithmMenuOpen - get() = _isAlgorithmMenuOpen.value - set(value) { - _isAlgorithmMenuOpen.value = value - } - - fun onAlgorithmMenuOpenChange() { - isAlgorithmMenuOpen = !isAlgorithmMenuOpen - } + var isNodeCreating by canvasViewModel::isNodeCreatingMode + var isClustering by canvasViewModel::isClustering + var isRanked by canvasViewModel::isRanked + var isAlgorithmMenuOpen by mutableStateOf(false) } \ No newline at end of file From 4384fb96a77a52b5bc099d7250b4c1d9252faf8d Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 19 Sep 2024 16:02:04 +0300 Subject: [PATCH 117/139] refactor: refactor vertecies' state initilization --- .../viewModel/canvas/CanvasViewModel.kt | 9 ---- .../viewModel/canvas/VertexCanvasViewModel.kt | 54 ++++--------------- .../kotlin/viewModel/graph/VertexViewModel.kt | 33 +++--------- 3 files changed, 15 insertions(+), 81 deletions(-) diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index f8d6211..8d0c80a 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -48,7 +48,6 @@ class CanvasViewModel( val viewModel = graphViewModel.createVertex(coordinates) ?: return _vertices[viewModel] = VertexCanvasViewModel(viewModel, _zoom, _center, _canvasSize) - updateVertexes() } } @@ -58,7 +57,6 @@ class CanvasViewModel( get() = _zoom.value set(value) { _zoom.value = value - updateVertexes() updateEdges() } @@ -67,7 +65,6 @@ class CanvasViewModel( get() = _center.value set(value) { _center.value = value - updateVertexes() } private val _canvasSize = mutableStateOf(Offset(400f, 400f)) @@ -75,7 +72,6 @@ class CanvasViewModel( get() = _canvasSize.value set(value) { _canvasSize.value = value - updateVertexes() } private val _isOrientated = mutableStateOf(false) @@ -108,10 +104,6 @@ class CanvasViewModel( val edges get() = _edges - private fun updateVertexes() { - vertices.forEach { it.updateVertex() } - } - private fun updateEdges() { edges.forEach { it.updateEdge(zoom) } } @@ -148,7 +140,6 @@ class CanvasViewModel( fun onSizeChange(newSize: Float) { graphViewModel.onSizeChange(newSize) - updateVertexes() } fun onOrientatedChange(isOrientated: Boolean) { diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index 836a3c5..da6cfc2 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -11,59 +11,23 @@ class VertexCanvasViewModel( private val center: MutableState, private val canvasSize: MutableState ) { - private val _offset = mutableStateOf( - calculateOffset() - ) - var offset - get() = _offset.value - set(value) { - _offset.value = value - } - - private val _radius = mutableStateOf(calculateRadius()) - var radius - get() = _radius.value - set(value) { - _radius.value = value - } - - var color - get() = vertexViewModel.color - set(value) { - vertexViewModel.color = value - } - - private val _strokeWidth = mutableStateOf(calculateStrokeWidth()) - var strokeWidth - get() = _strokeWidth.value - set(value) { - _strokeWidth.value = value - } + var color by vertexViewModel::color - private val _textSize = mutableStateOf(calculateTextSize()) - var textSize - get() = _textSize.value - set(value) { - _textSize.value = value - } + val strokeWidth + get() = 8f * zoom.value + val radius + get() = vertexViewModel.radius * zoom.value + val offset + get() = calculateOffset() + val textSize + get() = vertexViewModel.radius * 0.6f * zoom.value fun onDrag(it: Offset): Unit { vertexViewModel.onDrag(it * (1f / zoom.value)) } - fun updateVertex() { - offset = calculateOffset() - radius = calculateRadius() - strokeWidth = calculateStrokeWidth() - textSize = calculateTextSize() - } - private fun calculateOffset() = Offset( (canvasSize.value.x / 2) + ((vertexViewModel.x - center.value.x) * zoom.value), (canvasSize.value.y / 2) + ((vertexViewModel.y - center.value.y) * zoom.value) ) - - private fun calculateRadius() = vertexViewModel.radius * zoom.value - private fun calculateStrokeWidth() = 8f * zoom.value - private fun calculateTextSize() = vertexViewModel.radius * 0.6f * zoom.value } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/VertexViewModel.kt b/src/main/kotlin/viewModel/graph/VertexViewModel.kt index 2806f7f..cdde6bb 100644 --- a/src/main/kotlin/viewModel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewModel/graph/VertexViewModel.kt @@ -1,6 +1,8 @@ package viewModel.graph +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp @@ -15,33 +17,10 @@ class VertexViewModel( color: Color = Color.Black, radius: Dp = 8f.dp, ) { - private val _x = mutableStateOf(x) - private val _y = mutableStateOf(y) - private val _color = mutableStateOf(color) - private val _radius = mutableStateOf(radius) - - var x: Float - get() = _x.value - set(value) { - _x.value = value - } - - var y: Float - get() = _y.value - set(value) { - _y.value = value - } - - var color: Color - get() = _color.value - set(value) { - _color.value = value - } - var radius: Dp - get() = _radius.value - set(value) { - _radius.value = value - } + var x by mutableStateOf(x) + var y by mutableStateOf(y) + var color by mutableStateOf(color) + var radius by mutableStateOf(radius) val label get() = vertex.key.toString() From 17ff642c37e1886a26d6638b96b5185319646232 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Thu, 19 Sep 2024 17:07:39 +0300 Subject: [PATCH 118/139] refactor: refactor canvas, edge, vertex state's initialization --- src/main/kotlin/view/canvas/EdgeCanvasView.kt | 2 +- .../viewModel/canvas/CanvasViewModel.kt | 78 ++++--------------- .../viewModel/canvas/EdgeCanvasViewModel.kt | 14 ++-- .../viewModel/canvas/VertexCanvasViewModel.kt | 22 +++--- .../kotlin/viewModel/graph/EdgeViewModel.kt | 17 +--- .../viewModel/graph/UndirectedViewModel.kt | 10 +-- 6 files changed, 40 insertions(+), 103 deletions(-) diff --git a/src/main/kotlin/view/canvas/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt index ae3da58..e4679a1 100644 --- a/src/main/kotlin/view/canvas/EdgeCanvasView.kt +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -44,7 +44,7 @@ fun EdgeCanvasView( ) } - if (viewModel.showOrientation.value) { + if (viewModel.showOrientation) { drawLine( start = end, end = end - rotateVector(radiusVectorSecond * 0.8f, 30.0), diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 8d0c80a..e75c0a4 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -3,8 +3,10 @@ package viewModel.canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -22,24 +24,16 @@ class CanvasViewModel( ) { private val graphViewModel = UndirectedViewModel(graph, true) - var isClustering - get() = graphViewModel.clustering - set(value) { - graphViewModel.clustering = value - } - - var isRanked - get() = graphViewModel.ranked - set(value) { - graphViewModel.ranked = value - } + var isClustering by graphViewModel::clustering + var isRanked by graphViewModel::ranked - val _isNodeCreatingMode = mutableStateOf(false) - var isNodeCreatingMode - get() = _isNodeCreatingMode.value - set(value) { - _isNodeCreatingMode.value = value - } + var isNodeCreatingMode by mutableStateOf(false) + var zoom by mutableStateOf(1f) + var center by mutableStateOf(Offset(0f, 0f)) + var canvasSize by mutableStateOf(Offset(400f, 400f)) + var isOrientated by mutableStateOf(false) + + private val _vertices = mutableStateMapOf() fun createNode(offset: Offset) { if (isNodeCreatingMode) { @@ -47,45 +41,13 @@ class CanvasViewModel( println(offset - (canvasSize / 2.0F)) val viewModel = graphViewModel.createVertex(coordinates) ?: return - _vertices[viewModel] = VertexCanvasViewModel(viewModel, _zoom, _center, _canvasSize) + _vertices[viewModel] = VertexCanvasViewModel(viewModel, this) } } - - private val _zoom = mutableStateOf(1f) - var zoom - get() = _zoom.value - set(value) { - _zoom.value = value - updateEdges() - } - - private val _center = mutableStateOf(Offset(0f, 0f)) - var center - get() = _center.value - set(value) { - _center.value = value - } - - private val _canvasSize = mutableStateOf(Offset(400f, 400f)) - var canvasSize - get() = _canvasSize.value - set(value) { - _canvasSize.value = value - } - - private val _isOrientated = mutableStateOf(false) - var isOrientated - get() = _isOrientated.value - set(value) { - _isOrientated.value = value - } - - private val _vertices = mutableStateMapOf() - init { graphViewModel.vertices.forEach { v -> - _vertices[v] = VertexCanvasViewModel(v, _zoom, _center, _canvasSize) + _vertices[v] = VertexCanvasViewModel(v, this) } } @@ -95,7 +57,7 @@ class CanvasViewModel( val vertex2 = _vertices[it.second] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.second}") - EdgeCanvasViewModel(vertex1, vertex2, it.color, it.strokeWidth, zoom, _isOrientated) + EdgeCanvasViewModel(vertex1, vertex2, it.color, it, this) } val vertices @@ -104,10 +66,6 @@ class CanvasViewModel( val edges get() = _edges - private fun updateEdges() { - edges.forEach { it.updateEdge(zoom) } - } - val onScroll: AwaitPointerEventScope.(PointerEvent) -> Unit = { if (it.changes.first().scrollDelta.y > 0) { zoom -= zoom / 8 @@ -145,12 +103,4 @@ class CanvasViewModel( fun onOrientatedChange(isOrientated: Boolean) { this.isOrientated = isOrientated } - -// fun getViews(): Collection { -// if (Config.optimizeCanvas) { -// return _vertices.filter { abs(it.value.offset.x) < canvasSize.x && abs(it.value.offset.y) < canvasSize.y }.values -// } -// -// return _vertices.values -// } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt index 3df074b..b94acdb 100644 --- a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -1,19 +1,17 @@ package viewModel.canvas -import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color +import viewModel.graph.EdgeViewModel class EdgeCanvasViewModel( val first: VertexCanvasViewModel, val second: VertexCanvasViewModel, val color: Color, - strokeWidth: Float, - zoom: Float, - val showOrientation: MutableState + val edgeViewModel: EdgeViewModel, + private val canvasViewModel: CanvasViewModel, ) { - var strokeWidth = strokeWidth * (zoom) + var showOrientation by canvasViewModel::isOrientated - fun updateEdge(zoom: Float) { - strokeWidth = 8f * zoom - } + val strokeWidth + get() = edgeViewModel.strokeWidth * canvasViewModel.zoom } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index da6cfc2..3fbbd96 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -1,33 +1,33 @@ package viewModel.canvas -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset import viewModel.graph.VertexViewModel class VertexCanvasViewModel( val vertexViewModel: VertexViewModel, - private val zoom: MutableState, - private val center: MutableState, - private val canvasSize: MutableState + private val canvasViewModel: CanvasViewModel, ) { var color by vertexViewModel::color + private var zoom by canvasViewModel::zoom + private var center by canvasViewModel::center + private var canvasSize by canvasViewModel::canvasSize val strokeWidth - get() = 8f * zoom.value + get() = 8f * zoom val radius - get() = vertexViewModel.radius * zoom.value + get() = vertexViewModel.radius * zoom val offset get() = calculateOffset() + val textSize - get() = vertexViewModel.radius * 0.6f * zoom.value + get() = vertexViewModel.radius * 0.6f * zoom fun onDrag(it: Offset): Unit { - vertexViewModel.onDrag(it * (1f / zoom.value)) + vertexViewModel.onDrag(it * (1f / zoom)) } private fun calculateOffset() = Offset( - (canvasSize.value.x / 2) + ((vertexViewModel.x - center.value.x) * zoom.value), - (canvasSize.value.y / 2) + ((vertexViewModel.y - center.value.y) * zoom.value) + (canvasSize.x / 2) + ((vertexViewModel.x - center.x) * zoom), + (canvasSize.y / 2) + ((vertexViewModel.y - center.y) * zoom) ) } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt index cca76b4..3b3027b 100644 --- a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt +++ b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt @@ -2,7 +2,9 @@ package viewModel.graph import Config import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import model.graph.Edge @@ -22,17 +24,6 @@ class EdgeViewModel( edge.weight = value } - private var _color = mutableStateOf(color) - var color - get() = _color.value - set(value) { - _color.value = value - } - - private var _strokeWidth = mutableStateOf(strokeWidth) - var strokeWidth - get() = _strokeWidth.value - set(value) { - _strokeWidth.value = value - } + var color by mutableStateOf(color) + var strokeWidth by mutableStateOf(strokeWidth) } diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index ce010e1..d24069e 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -1,6 +1,8 @@ package viewModel.graph +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -18,16 +20,12 @@ class UndirectedViewModel( private val _vertices = hashMapOf() private val _adjacencyList = hashMapOf>() private val groupColors = hashMapOf(0 to Color.Black) + private val _color = mutableStateOf(Color.Black) - private val _size = mutableStateOf(10f) private val _clustering = mutableStateOf(false) private val _ranked = mutableStateOf(false) - private var size - get() = _size.value - set(value) { - _size.value = value - } + private var size by mutableStateOf(10f) val vertices get() = _vertices.values From 52d519766fda38a03caa777826ff33a0fb6f11e1 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Sun, 22 Sep 2024 20:07:03 +0300 Subject: [PATCH 119/139] refactor: refactor modelView of canvas and vertex --- .../kotlin/view/canvas/VertexCanvasView.kt | 2 +- .../viewModel/canvas/CanvasViewModel.kt | 3 +-- .../viewModel/canvas/VertexCanvasViewModel.kt | 20 +++++++++---------- .../viewModel/graph/UndirectedViewModel.kt | 4 +--- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index 4be14f9..9c39f6c 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -31,6 +31,6 @@ fun VertexCanvasView( .onDrag(onDrag = viewModel::onDrag), contentAlignment = Alignment.Center ) { - MyText(viewModel.vertexViewModel.label, viewModel.textSize.value) + MyText(viewModel.label, viewModel.textSize.value) } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index e75c0a4..669751c 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -32,13 +32,12 @@ class CanvasViewModel( var center by mutableStateOf(Offset(0f, 0f)) var canvasSize by mutableStateOf(Offset(400f, 400f)) var isOrientated by mutableStateOf(false) - + private val _vertices = mutableStateMapOf() fun createNode(offset: Offset) { if (isNodeCreatingMode) { val coordinates = (offset - (canvasSize / 2.0F)) * (1 / zoom) + center - println(offset - (canvasSize / 2.0F)) val viewModel = graphViewModel.createVertex(coordinates) ?: return _vertices[viewModel] = VertexCanvasViewModel(viewModel, this) diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index 3fbbd96..b4f993a 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -4,30 +4,28 @@ import androidx.compose.ui.geometry.Offset import viewModel.graph.VertexViewModel class VertexCanvasViewModel( - val vertexViewModel: VertexViewModel, + private val vertexViewModel: VertexViewModel, private val canvasViewModel: CanvasViewModel, ) { - var color by vertexViewModel::color - private var zoom by canvasViewModel::zoom - private var center by canvasViewModel::center - private var canvasSize by canvasViewModel::canvasSize + val color by vertexViewModel::color + val label by vertexViewModel::label val strokeWidth - get() = 8f * zoom + get() = 8f * canvasViewModel.zoom val radius - get() = vertexViewModel.radius * zoom + get() = vertexViewModel.radius * canvasViewModel.zoom val offset get() = calculateOffset() val textSize - get() = vertexViewModel.radius * 0.6f * zoom + get() = vertexViewModel.radius * 0.6f * canvasViewModel.zoom fun onDrag(it: Offset): Unit { - vertexViewModel.onDrag(it * (1f / zoom)) + vertexViewModel.onDrag(it * (1f / canvasViewModel.zoom)) } private fun calculateOffset() = Offset( - (canvasSize.x / 2) + ((vertexViewModel.x - center.x) * zoom), - (canvasSize.y / 2) + ((vertexViewModel.y - center.y) * zoom) + (canvasViewModel.canvasSize.x / 2) + ((vertexViewModel.x - canvasViewModel.center.x) * canvasViewModel.zoom), + (canvasViewModel.canvasSize.y / 2) + ((vertexViewModel.y - canvasViewModel.center.y) * canvasViewModel.zoom) ) } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index d24069e..d7a26a3 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -99,9 +99,7 @@ class UndirectedViewModel( } fun createVertex(coordinates: Offset): VertexViewModel? { - val vertex = graph.addVertex(graph.vertices.last().key + 1) - - if (vertex == null) return null + val vertex = graph.addVertex(graph.vertices.last().key + 1) ?: return null val viewModel = VertexViewModel( showVerticesLabels, From 6e5978a6f781c839726ffc63ebc5475ad6538d12 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 23 Sep 2024 18:22:56 +0300 Subject: [PATCH 120/139] feat: add Neo4jReader --- build.gradle.kts | 6 +- src/main/kotlin/model/reader/Neo4jReader.kt | 106 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/model/reader/Neo4jReader.kt diff --git a/build.gradle.kts b/build.gradle.kts index 72f8d30..7032eb0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,7 +29,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1") implementation("org.xerial:sqlite-jdbc:3.41.2.2") - + implementation("org.neo4j.driver", "neo4j-java-driver", "5.6.0") } compose.desktop { @@ -49,9 +49,9 @@ tasks.test { finalizedBy("jacocoTestReport") } -tasks.jacocoTestReport{ +tasks.jacocoTestReport { dependsOn(tasks.test) - reports{ + reports { xml.required.set(true) html.required.set(true) } diff --git a/src/main/kotlin/model/reader/Neo4jReader.kt b/src/main/kotlin/model/reader/Neo4jReader.kt new file mode 100644 index 0000000..0baf0c7 --- /dev/null +++ b/src/main/kotlin/model/reader/Neo4jReader.kt @@ -0,0 +1,106 @@ +package model.reader + +import model.graph.Edge +import model.graph.Graph +import model.graph.UndirectedGraph +import model.graph.Vertex +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase +import org.neo4j.driver.Transaction + +class Neo4jReader(uri: String, user: String, password: String) : Reader { + + private val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + private val session = driver.session() + + private fun createNode(node: Vertex, graphName: String, txInput: Transaction?) { + val tx = txInput ?: session.beginTransaction() + + tx.run( + "MERGE (n:Node {graphName: \$graphName, key: \$key})", + mapOf("key" to node.key, "graphName" to graphName) + ) + + if (txInput == null) { + tx.commit() + tx.close() + } + } + + private fun createEdge(edge: Edge, nameGraph: String, txInput: Transaction?) { + val tx = txInput ?: session.beginTransaction() + + tx.run( + "MERGE (v1:Node {graphName: \$graphName, key: \$key1})" + + "MERGE (v2:Node {graphName: \$graphName, key: \$key2})" + + "MERGE (v1)-[:DIRECTED_TO {weight: \$weight}]->(v2)", + mapOf( + "key1" to edge.first.key, + "key2" to edge.second.key, + "weight" to edge.weight, + "graphName" to nameGraph + ) + ) + + if (txInput == null) { + tx.commit() + tx.close() + } + } + + private fun deleteGraph(graphName: String, txInput: Transaction?) { + val tx = txInput ?: session.beginTransaction() + + tx.run( + "MATCH (n:Node {graphName: \$graphName}) DETACH DELETE n", + mapOf( + "graphName" to graphName + ) + ) + + if (txInput == null) { + tx.commit() + tx.close() + } + } + + override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) { + val transaction = session.beginTransaction() + + deleteGraph(nameGraph, transaction) + + graph.vertices.forEach { v -> + createNode(v, nameGraph, transaction) + + graph.adjacencyList[v]?.forEach { e -> + createEdge(e, nameGraph, transaction) + } + } + + transaction.commit() + } + + override fun loadGraph(filepath: String, nameGraph: String): Graph { + val graph = UndirectedGraph() + + session.executeRead { tx -> + tx.run("MATCH (n:Node {graphName: \$graphName}) return n", mapOf("graphName" to nameGraph)) + .forEach { v -> graph.addVertex((v.get("n").get("key").asInt())) } + + tx.run( + "MATCH p=(v1: Node {graphName: \$graphName})-[r]-(v2: Node {graphName: \$graphName}) return v1, v2, r", + mapOf("graphName" to nameGraph) + ).forEach { v -> + val values = v.values() + println(v) + graph.addEdge( + values[0].get("key").asInt(), + values[1].get("key").asInt(), + values[2].get("weight").asLong() + ) + } + } + + return graph + } +} \ No newline at end of file From 7aaa4db554417e7cf729a1bfb45b89114a49f615 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 23 Sep 2024 18:42:14 +0300 Subject: [PATCH 121/139] refactor: delete println in the method --- src/main/kotlin/model/reader/Neo4jReader.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/model/reader/Neo4jReader.kt b/src/main/kotlin/model/reader/Neo4jReader.kt index 0baf0c7..42e4ca4 100644 --- a/src/main/kotlin/model/reader/Neo4jReader.kt +++ b/src/main/kotlin/model/reader/Neo4jReader.kt @@ -92,7 +92,6 @@ class Neo4jReader(uri: String, user: String, password: String) : Reader { mapOf("graphName" to nameGraph) ).forEach { v -> val values = v.values() - println(v) graph.addEdge( values[0].get("key").asInt(), values[1].get("key").asInt(), From 7a01dc500c781c8398c77cb94812b9b194ea4336 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 23 Sep 2024 19:05:53 +0300 Subject: [PATCH 122/139] fix: close transaction after exiting from method --- src/main/kotlin/model/reader/Neo4jReader.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/model/reader/Neo4jReader.kt b/src/main/kotlin/model/reader/Neo4jReader.kt index 42e4ca4..e42cef7 100644 --- a/src/main/kotlin/model/reader/Neo4jReader.kt +++ b/src/main/kotlin/model/reader/Neo4jReader.kt @@ -78,6 +78,7 @@ class Neo4jReader(uri: String, user: String, password: String) : Reader { } transaction.commit() + transaction.close() } override fun loadGraph(filepath: String, nameGraph: String): Graph { From 6da6362993b0d5571022cb5caa9ed9127d7602d6 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 23 Sep 2024 23:14:14 +0300 Subject: [PATCH 123/139] feat: add type to graph --- src/main/kotlin/model/reader/Neo4jReader.kt | 39 ++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/model/reader/Neo4jReader.kt b/src/main/kotlin/model/reader/Neo4jReader.kt index e42cef7..1b94402 100644 --- a/src/main/kotlin/model/reader/Neo4jReader.kt +++ b/src/main/kotlin/model/reader/Neo4jReader.kt @@ -1,9 +1,6 @@ package model.reader -import model.graph.Edge -import model.graph.Graph -import model.graph.UndirectedGraph -import model.graph.Vertex +import model.graph.* import org.neo4j.driver.AuthTokens import org.neo4j.driver.GraphDatabase import org.neo4j.driver.Transaction @@ -57,6 +54,12 @@ class Neo4jReader(uri: String, user: String, password: String) : Reader { "graphName" to graphName ) ) + tx.run( + "MATCH (g:Graph {graphName: \$graphName}) DETACH DELETE g", + mapOf( + "graphName" to graphName + ) + ) if (txInput == null) { tx.commit() @@ -69,6 +72,21 @@ class Neo4jReader(uri: String, user: String, password: String) : Reader { deleteGraph(nameGraph, transaction) + val graphType: String = when (graph) { + is WeightedDirectedGraph -> "WeightedUndirected" + is WeightedGraph -> "Weighted" + is DirectedGraph -> "Directed" + else -> "Undirected" + } + + transaction.run( + "MERGE (g:Graph {graphName: \$graphName, type: \$graphType})", + mapOf( + "graphName" to nameGraph, + "graphType" to graphType + ) + ) + graph.vertices.forEach { v -> createNode(v, nameGraph, transaction) @@ -82,9 +100,20 @@ class Neo4jReader(uri: String, user: String, password: String) : Reader { } override fun loadGraph(filepath: String, nameGraph: String): Graph { - val graph = UndirectedGraph() + var graph: Graph = UndirectedGraph() session.executeRead { tx -> + val graphType = + tx.run("MATCH (g:Graph {graphName: \$graphName}) return g", mapOf("graphName" to nameGraph)).single() + .get("g").get("type").asString() + + graph = when (graphType) { + "Undirected" -> UndirectedGraph() + "Directed" -> DirectedGraph() + "Weighted" -> WeightedGraph() + else -> WeightedDirectedGraph() + } + tx.run("MATCH (n:Node {graphName: \$graphName}) return n", mapOf("graphName" to nameGraph)) .forEach { v -> graph.addVertex((v.get("n").get("key").asInt())) } From 2587a9e98a7696bcbccf55101a8a9da3a2ac2db0 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 23 Sep 2024 23:21:17 +0300 Subject: [PATCH 124/139] docs: add comments to Neo4jReader --- src/main/kotlin/model/reader/Neo4jReader.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/kotlin/model/reader/Neo4jReader.kt b/src/main/kotlin/model/reader/Neo4jReader.kt index 1b94402..a37808e 100644 --- a/src/main/kotlin/model/reader/Neo4jReader.kt +++ b/src/main/kotlin/model/reader/Neo4jReader.kt @@ -67,6 +67,9 @@ class Neo4jReader(uri: String, user: String, password: String) : Reader { } } + /** + * Save graph to Neo4j Database + */ override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) { val transaction = session.beginTransaction() @@ -99,6 +102,12 @@ class Neo4jReader(uri: String, user: String, password: String) : Reader { transaction.close() } + /** + * Load graph to Neo4j Database + * + * @return the loaded graph + * @throws NoSuchRecordException if there is no graph with given graph name + */ override fun loadGraph(filepath: String, nameGraph: String): Graph { var graph: Graph = UndirectedGraph() From 439d948b89e1313d245cbef0a89034de109474c1 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Mon, 23 Sep 2024 23:27:59 +0300 Subject: [PATCH 125/139] feat: add tests for Neo4jReader --- .../kotlin/model/reader/Neo4jReaderTest.kt | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/test/kotlin/model/reader/Neo4jReaderTest.kt diff --git a/src/test/kotlin/model/reader/Neo4jReaderTest.kt b/src/test/kotlin/model/reader/Neo4jReaderTest.kt new file mode 100644 index 0000000..85992ef --- /dev/null +++ b/src/test/kotlin/model/reader/Neo4jReaderTest.kt @@ -0,0 +1,129 @@ +package model.reader + +import model.algorithm.Dijkstra +import model.graph.Graph +import model.graph.UndirectedGraph +import model.graph.Vertex +import model.graph.WeightedGraph +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.neo4j.driver.exceptions.NoSuchRecordException +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class Neo4jReaderTest { + lateinit var testGraph: Graph + val neo4jReader = Neo4jReader("bolt://localhost:7687", "neo4j", "qwertyui") + val testGraphName = "testGraph" + + private fun isSameNodes(graph1: Graph, graph2: Graph): Boolean = + graph1.vertices.sortedBy { v -> v.key } == graph2.vertices.sortedBy { v -> v.key } + + private fun isSameEdges(graph1: Graph, graph2: Graph): Boolean { + listOf(graph1, graph2).forEach { graph -> + graph.vertices.forEach { v -> + val graph1EdgeNodesWithWeights = + graph1.adjacencyList[v]?.map { edge -> Pair(edge.second, edge.weight) } + ?.sortedBy { node -> node.first.key } + + val graph2EdgeNodesWithWeights = + graph2.adjacencyList[v]?.map { edge -> Pair(edge.second, edge.weight) } + ?.sortedBy { node -> node.first.key } + + if (graph1EdgeNodesWithWeights != graph2EdgeNodesWithWeights) return false + } + } + + return true + } + + @Nested + inner class `Save and load graph` { + @BeforeEach + fun setup() { + testGraph = WeightedGraph() + } + + @Test + fun `save and load empty graph one time`() { + neo4jReader.saveGraph(testGraph, "", testGraphName) + val graph = neo4jReader.loadGraph("", testGraphName) + + assertEquals(graph.vertices.size, testGraph.vertices.size) + assertEquals(graph.adjacencyList.values.size, testGraph.adjacencyList.size) + } + + @Test + fun `save and load empty graph 100 times in a row`() { + var graph: Graph = testGraph + + for (i in 1..100) { + neo4jReader.saveGraph(testGraph, "", testGraphName) + graph = neo4jReader.loadGraph("", testGraphName) + } + + assertEquals(graph.vertices.size, testGraph.vertices.size) + assertEquals(graph.adjacencyList.values.size, testGraph.adjacencyList.size) + } + + @Test + fun `save and load non-empty graph one time`() { + for (i in 1..5) { + testGraph.addVertex(i) + } + testGraph.addEdge(1, 2, 2) + testGraph.addEdge(2, 5, 4) + testGraph.addEdge(1, 4, 4) + testGraph.addEdge(4, 2, 1) + testGraph.addEdge(1, 3, 3) + testGraph.addEdge(4, 5, 1) + testGraph.addEdge(3, 5, 5) + + neo4jReader.saveGraph(testGraph, "", testGraphName) + val graph = neo4jReader.loadGraph("", testGraphName) + + assertEquals(graph.vertices.size, testGraph.vertices.size) + assertEquals(graph.adjacencyList.size, testGraph.adjacencyList.size) + + assertTrue(isSameNodes(graph, testGraph)) + assertTrue(isSameEdges(graph, testGraph)) + } + + @Test + fun `save and load non-empty graph 100 times in a row`() { + for (i in 1..5) { + testGraph.addVertex(i) + } + testGraph.addEdge(1, 2, 2) + testGraph.addEdge(2, 5, 4) + testGraph.addEdge(1, 4, 4) + testGraph.addEdge(4, 2, 1) + testGraph.addEdge(1, 3, 3) + testGraph.addEdge(4, 5, 1) + testGraph.addEdge(3, 5, 5) + + var graph = testGraph + for (i in 1..100) { + neo4jReader.saveGraph(testGraph, "", testGraphName) + graph = neo4jReader.loadGraph("", testGraphName) + + } + assertEquals(graph.vertices.size, testGraph.vertices.size) + assertEquals(graph.adjacencyList.size, testGraph.adjacencyList.size) + + assertTrue(isSameNodes(graph, testGraph)) + assertTrue(isSameEdges(graph, testGraph)) + } + + @Test + fun `load graph that don't exist in DB`() { + try { + neo4jReader.loadGraph("", "Homka") + } catch (_: NoSuchRecordException) { + + } + } + } +} \ No newline at end of file From ffd989b81f374e60df1b692a4b5f54c0ec26daaf Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 24 Sep 2024 00:26:32 +0300 Subject: [PATCH 126/139] feat: add methods for change edge's color --- .../viewModel/canvas/CanvasViewModel.kt | 33 +++++++++++++++---- .../viewModel/canvas/EdgeCanvasViewModel.kt | 3 +- .../kotlin/viewModel/graph/EdgeViewModel.kt | 2 +- .../viewModel/graph/UndirectedViewModel.kt | 30 +++++++++++++++++ 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 669751c..537f4fb 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -13,9 +13,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerInputScope +import model.graph.Edge import model.graph.UndirectedGraph import view.HEADER_HEIGHT import view.MENU_WIDTH +import viewModel.graph.EdgeViewModel import viewModel.graph.UndirectedViewModel import viewModel.graph.VertexViewModel @@ -35,6 +37,10 @@ class CanvasViewModel( private val _vertices = mutableStateMapOf() + private fun getVertex(vm: VertexViewModel): VertexCanvasViewModel { + return _vertices[vm] ?: throw IllegalArgumentException("There is no VertexCanvasViewModel for $vm") + } + fun createNode(offset: Offset) { if (isNodeCreatingMode) { val coordinates = (offset - (canvasSize / 2.0F)) * (1 / zoom) + center @@ -50,20 +56,22 @@ class CanvasViewModel( } } - private val _edges = graphViewModel.adjacencyList.map { it.value }.flatten().map { - val vertex1 = - _vertices[it.first] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.first}") - val vertex2 = - _vertices[it.second] ?: throw IllegalStateException("There is no VertexCanvasViewModel for ${it.second}") + private val _edges = graphViewModel.adjacencyList.mapValues { + it.value.map { edgeViewModel -> + val vertex1 = getVertex(edgeViewModel.first) + val vertex2 = getVertex(edgeViewModel.second) - EdgeCanvasViewModel(vertex1, vertex2, it.color, it, this) + EdgeCanvasViewModel(vertex1, vertex2, edgeViewModel, this) + } + }.mapKeys { + getVertex(it.key) } val vertices get() = _vertices.values val edges - get() = _edges + get() = _edges.values.flatten() val onScroll: AwaitPointerEventScope.(PointerEvent) -> Unit = { if (it.changes.first().scrollDelta.y > 0) { @@ -102,4 +110,15 @@ class CanvasViewModel( fun onOrientatedChange(isOrientated: Boolean) { this.isOrientated = isOrientated } + + /* + * Change edges' color + * */ + fun changeEdgesColor(edges: List>) { + graphViewModel.changeEdgesColor(edges) + } + + fun resetEdgesColorToDefault() { + graphViewModel.resetEdgesColorToDefault() + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt index b94acdb..372a79b 100644 --- a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -1,16 +1,15 @@ package viewModel.canvas -import androidx.compose.ui.graphics.Color import viewModel.graph.EdgeViewModel class EdgeCanvasViewModel( val first: VertexCanvasViewModel, val second: VertexCanvasViewModel, - val color: Color, val edgeViewModel: EdgeViewModel, private val canvasViewModel: CanvasViewModel, ) { var showOrientation by canvasViewModel::isOrientated + var color by edgeViewModel::color val strokeWidth get() = edgeViewModel.strokeWidth * canvasViewModel.zoom diff --git a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt index 3b3027b..0ce2395 100644 --- a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt +++ b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt @@ -11,7 +11,7 @@ import model.graph.Edge class EdgeViewModel( val first: VertexViewModel, val second: VertexViewModel, - private val edge: Edge, + val edge: Edge, private val _weightVisibility: State, color: Color = Config.Edge.color, strokeWidth: Float = Config.Edge.strokeWidth diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index d7a26a3..5b63bff 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -1,5 +1,6 @@ package viewModel.graph +import Config import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -8,6 +9,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import model.algorithm.Clustering import model.algorithm.PageRank +import model.graph.Edge import model.graph.UndirectedGraph import model.graph.Vertex @@ -115,6 +117,34 @@ class UndirectedViewModel( return viewModel } + /* + * Change edges' color + * */ + fun changeEdgesColor(edges: List>) { + edges.forEach { p -> + val edge = p.first + val color = p.second + + val vertex1 = _vertices[edge.first] ?: return + val vertex2 = _vertices[edge.second] ?: return + + val edgeViewModelList1 = _adjacencyList[vertex1] ?: return + val edgeViewModel1 = edgeViewModelList1.find { it.second == vertex2 } ?: return + edgeViewModel1.color = color + + val edgeViewModelList2 = _adjacencyList[vertex2] ?: return + val edgeViewModel2 = edgeViewModelList2.find { it.second == vertex1 } ?: return + edgeViewModel2.color = color + } + } + + /* + * Reset current color on all edges to default in Config + * */ + fun resetEdgesColorToDefault() { + adjacencyList.values.flatten().forEach { it.color = Config.Edge.color } + } + init { graph.vertices.forEachIndexed { i, vertex -> val group = groups.getOrDefault(vertex, 0) From 2209f0f3271cd11ab27bf6b6eb49523e516c5390 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 24 Sep 2024 01:21:37 +0300 Subject: [PATCH 127/139] feat: add creating edge button --- src/main/kotlin/view/MenuView.kt | 3 +- src/main/kotlin/view/canvas/CanvasView.kt | 7 ++- .../kotlin/view/canvas/VertexCanvasView.kt | 8 ++- src/main/kotlin/viewModel/MenuViewModel.kt | 1 + .../viewModel/canvas/CanvasViewModel.kt | 54 +++++++++++++++---- .../viewModel/canvas/VertexCanvasViewModel.kt | 8 ++- .../viewModel/graph/UndirectedViewModel.kt | 12 +++++ .../kotlin/viewModel/graph/VertexViewModel.kt | 4 ++ 8 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 3a5befc..cb6af38 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -87,6 +87,7 @@ fun MenuView( viewModel: MenuViewModel ) { var isNodeCreating by viewModel::isNodeCreating + var isEdgeCreating by viewModel::isEdgeCreating var isClustering by viewModel::isClustering var isRanked by viewModel::isRanked var isAlgorithmMenuOpen by viewModel::isAlgorithmMenuOpen @@ -97,7 +98,7 @@ fun MenuView( ) { Spacer(Modifier.height(25f.dp)) MenuIcon("Nodes.svg", "AddNode.svg", Modifier.glow(isNodeCreating)) { isNodeCreating = !isNodeCreating } - MenuIcon("Ribs.svg", "AddEdge.svg", modifier = Modifier.alpha(0.2f)) + MenuIcon("Ribs.svg", "AddEdge.svg", Modifier.glow(isEdgeCreating)) { isEdgeCreating = !isEdgeCreating } MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glow(isClustering)) { isClustering = !isClustering } MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glow(viewModel.isRanked)) { isRanked = !isRanked } MenuIcon( diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt index a0f369b..a2c7207 100644 --- a/src/main/kotlin/view/canvas/CanvasView.kt +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -35,8 +35,11 @@ fun CanvasView( } .clipToBounds() ) { - viewModel.edges.forEach { - EdgeCanvasView(it) + // for rerender when update + if (viewModel.edgesCount > 0) { + viewModel.edges.flatten().forEach { + EdgeCanvasView(it) + } } viewModel.vertices.forEach { diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index 9c39f6c..34cf53a 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.gestures.onDrag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size +import androidx.compose.foundation.onClick import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -26,8 +27,13 @@ fun VertexCanvasView( modifier .size(viewModel.radius * 2) .offset(viewModel.offset.x.dp, viewModel.offset.y.dp) - .border(color = viewModel.color, width = viewModel.strokeWidth.dp, shape = CircleShape) + .border( + color = if (viewModel.canvasViewModel.pickedNodeForEdgeCreating != viewModel) viewModel.color else Color.Green, + width = viewModel.strokeWidth.dp, + shape = CircleShape + ) .background(color = Color(0xFF242424), shape = CircleShape) + .onClick { viewModel.onClickWhenEdgeCreating() } .onDrag(onDrag = viewModel::onDrag), contentAlignment = Alignment.Center ) { diff --git a/src/main/kotlin/viewModel/MenuViewModel.kt b/src/main/kotlin/viewModel/MenuViewModel.kt index de14aca..d548045 100644 --- a/src/main/kotlin/viewModel/MenuViewModel.kt +++ b/src/main/kotlin/viewModel/MenuViewModel.kt @@ -9,6 +9,7 @@ class MenuViewModel( val canvasViewModel: CanvasViewModel ) { var isNodeCreating by canvasViewModel::isNodeCreatingMode + var isEdgeCreating by canvasViewModel::isEdgeCreatingMode var isClustering by canvasViewModel::isClustering var isRanked by canvasViewModel::isRanked var isAlgorithmMenuOpen by mutableStateOf(false) diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 537f4fb..3807027 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent @@ -29,13 +30,18 @@ class CanvasViewModel( var isClustering by graphViewModel::clustering var isRanked by graphViewModel::ranked + var isEdgeCreatingMode by mutableStateOf(false) + var pickedNodeForEdgeCreating by mutableStateOf(null) + var isNodeCreatingMode by mutableStateOf(false) + var edgesCount by mutableStateOf(0) var zoom by mutableStateOf(1f) var center by mutableStateOf(Offset(0f, 0f)) var canvasSize by mutableStateOf(Offset(400f, 400f)) var isOrientated by mutableStateOf(false) private val _vertices = mutableStateMapOf() + private val _edges = mutableStateMapOf>() private fun getVertex(vm: VertexViewModel): VertexCanvasViewModel { return _vertices[vm] ?: throw IllegalArgumentException("There is no VertexCanvasViewModel for $vm") @@ -54,24 +60,24 @@ class CanvasViewModel( graphViewModel.vertices.forEach { v -> _vertices[v] = VertexCanvasViewModel(v, this) } - } - private val _edges = graphViewModel.adjacencyList.mapValues { - it.value.map { edgeViewModel -> - val vertex1 = getVertex(edgeViewModel.first) - val vertex2 = getVertex(edgeViewModel.second) + graphViewModel.adjacencyList.forEach { + _edges[getVertex(it.key)] = ArrayList(it.value.map { edgeViewModel -> + val vertex1 = getVertex(edgeViewModel.first) + val vertex2 = getVertex(edgeViewModel.second) - EdgeCanvasViewModel(vertex1, vertex2, edgeViewModel, this) + EdgeCanvasViewModel(vertex1, vertex2, edgeViewModel, this) + }.toList()) } - }.mapKeys { - getVertex(it.key) + + edgesCount = _edges.values.flatten().size } val vertices get() = _vertices.values val edges - get() = _edges.values.flatten() + get() = _edges.values val onScroll: AwaitPointerEventScope.(PointerEvent) -> Unit = { if (it.changes.first().scrollDelta.y > 0) { @@ -121,4 +127,34 @@ class CanvasViewModel( fun resetEdgesColorToDefault() { graphViewModel.resetEdgesColorToDefault() } + + fun createEdge(first: VertexCanvasViewModel, second: VertexCanvasViewModel) { + val edgesVM = graphViewModel.createEdge(first.vertexViewModel, second.vertexViewModel) + val firstCanvasEdgeList = _edges[first] ?: return + val secondCanvasEdgeList = _edges[second] ?: return + + if (edgesVM != null) { + firstCanvasEdgeList.add(EdgeCanvasViewModel(first, second, edgesVM.first, this)) + secondCanvasEdgeList.add(EdgeCanvasViewModel(second, first, edgesVM.second, this)) + } + + edgesCount++ + } + + fun onClickNodeEdgeCreating(vm: VertexCanvasViewModel) { + if (!isEdgeCreatingMode) return + + if (pickedNodeForEdgeCreating == vm) { + pickedNodeForEdgeCreating = null + return + } + + if (pickedNodeForEdgeCreating == null) { + pickedNodeForEdgeCreating = vm + return + } + + createEdge(pickedNodeForEdgeCreating ?: return, vm) + pickedNodeForEdgeCreating = null + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index b4f993a..b0831db 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -4,8 +4,8 @@ import androidx.compose.ui.geometry.Offset import viewModel.graph.VertexViewModel class VertexCanvasViewModel( - private val vertexViewModel: VertexViewModel, - private val canvasViewModel: CanvasViewModel, + val vertexViewModel: VertexViewModel, + val canvasViewModel: CanvasViewModel, ) { val color by vertexViewModel::color val label by vertexViewModel::label @@ -24,6 +24,10 @@ class VertexCanvasViewModel( vertexViewModel.onDrag(it * (1f / canvasViewModel.zoom)) } + fun onClickWhenEdgeCreating() { + canvasViewModel.onClickNodeEdgeCreating(this) + } + private fun calculateOffset() = Offset( (canvasViewModel.canvasSize.x / 2) + ((vertexViewModel.x - canvasViewModel.center.x) * canvasViewModel.zoom), (canvasViewModel.canvasSize.y / 2) + ((vertexViewModel.y - canvasViewModel.center.y) * canvasViewModel.zoom) diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 5b63bff..d778dda 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -51,6 +51,18 @@ class UndirectedViewModel( updateSizes() } + fun createEdge(first: VertexViewModel, second: VertexViewModel): Pair? { + val edge = graph.addEdge(first.getKey(), second.getKey()) ?: return null + + val firstEdge = EdgeViewModel(first, second, edge, mutableStateOf(false)) + val secondEdge = EdgeViewModel(second, first, edge, mutableStateOf(false)) + + _adjacencyList[first]?.add(EdgeViewModel(first, second, edge, mutableStateOf(false))) + _adjacencyList[second]?.add(EdgeViewModel(second, first, edge, mutableStateOf(false))) + + return Pair(firstEdge, secondEdge) + } + private fun getColor(group: Int): Color { if (clustering) { val color = groupColors[group] diff --git a/src/main/kotlin/viewModel/graph/VertexViewModel.kt b/src/main/kotlin/viewModel/graph/VertexViewModel.kt index cdde6bb..749f50a 100644 --- a/src/main/kotlin/viewModel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewModel/graph/VertexViewModel.kt @@ -28,6 +28,10 @@ class VertexViewModel( val labelVisibility get() = _labelVisible + fun getKey(): Int { + return vertex.key + } + fun onDrag(it: Offset): Unit { x += it.x y += it.y From f0a4cffd1345c55696bb68353545b5a991921700 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 24 Sep 2024 05:30:10 +0300 Subject: [PATCH 128/139] feat: linked the bridges algorithm to the button --- src/main/kotlin/view/MainView.kt | 53 ++++++++++++++----- src/main/kotlin/view/MenuView.kt | 10 ++-- src/main/kotlin/viewModel/MenuViewModel.kt | 3 +- .../viewModel/canvas/CanvasViewModel.kt | 3 +- .../viewModel/graph/UndirectedViewModel.kt | 25 ++++++++- 5 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 19c8b76..c27ed15 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -1,26 +1,28 @@ package view import Config -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import view.canvas.CanvasView import viewModel.MainViewModel +import viewModel.MenuViewModel val HEADER_HEIGHT = Config.headerHeight val MENU_WIDTH = Config.menuWidth @Composable -fun DisplayAlgorithmMenu(name: String) { +fun DisplayAlgorithmMenu(name: String, viewModel: MenuViewModel, onClick: () -> Unit = {}) { val imageResources = listOf( "FindBridge.svg", @@ -52,34 +54,49 @@ fun DisplayAlgorithmMenu(name: String) { ImageButton( imageResourceId = image, onClick = { - } + if(image == "FindBridge.svg"){ + onClick() + } + }, viewModel ) } } } } +@OptIn(ExperimentalFoundationApi::class) @Composable -fun ImageButton(imageResourceId: String, onClick: () -> Unit) { +fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuViewModel) { Box( modifier = Modifier .size(440.dp, 60.dp) .padding(1.dp) - .clickable { onClick() } .background(Color(0x00)) ) { - Image( - painter = painterResource(imageResourceId), - contentDescription = "Button Image", - modifier = Modifier.size(445.dp, 59.dp), - contentScale = ContentScale.Crop - ) + if (imageResourceId == "FindBridge.svg") { + Image( + painter = painterResource(imageResourceId), + contentDescription = "Button Image", + modifier = Modifier.glowRec(viewModel.isFinded).size(445.dp, 59.dp).onClick(onClick = onClick), + contentScale = ContentScale.Crop + ) + } + else { + Image( + painter = painterResource(imageResourceId), + contentDescription = "Button Image", + modifier = Modifier.alpha(0.2f).size(445.dp, 59.dp).onClick(onClick = onClick), + contentScale = ContentScale.Crop + ) + } } } @Composable fun MainView(mainViewModel: MainViewModel) { + var isBridgeFinded by mainViewModel.menuViewModel::isFinded + Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { MenuView(mainViewModel.menuViewModel) @@ -90,8 +107,16 @@ fun MainView(mainViewModel: MainViewModel) { } if (mainViewModel.menuViewModel.isAlgorithmMenuOpen) { - DisplayAlgorithmMenu("DownMenuAlgorithm.svg") + DisplayAlgorithmMenu("DownMenuAlgorithm.svg", + mainViewModel.menuViewModel + ){ isBridgeFinded = !isBridgeFinded } } SettingsView(mainViewModel.settingsViewModel) +} + +fun Modifier.glowRec(flag: Boolean): Modifier { + if (!flag) return Modifier + + return Modifier.border(1f.dp, color = Color(0xFFFF00FF), shape = RectangleShape) } \ No newline at end of file diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 3a5befc..9fd5643 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -96,19 +96,19 @@ fun MenuView( horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.height(25f.dp)) - MenuIcon("Nodes.svg", "AddNode.svg", Modifier.glow(isNodeCreating)) { isNodeCreating = !isNodeCreating } + MenuIcon("Nodes.svg", "AddNode.svg", Modifier.glowCircle(isNodeCreating)) { isNodeCreating = !isNodeCreating } MenuIcon("Ribs.svg", "AddEdge.svg", modifier = Modifier.alpha(0.2f)) - MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glow(isClustering)) { isClustering = !isClustering } - MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glow(viewModel.isRanked)) { isRanked = !isRanked } + MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glowCircle(isClustering)) { isClustering = !isClustering } + MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glowCircle(viewModel.isRanked)) { isRanked = !isRanked } MenuIcon( "Algorithm.svg", "Algorithms....svg", - Modifier.glow(viewModel.isAlgorithmMenuOpen) + Modifier.glowCircle(viewModel.isAlgorithmMenuOpen) ) { isAlgorithmMenuOpen = !isAlgorithmMenuOpen } } } -fun Modifier.glow(flag: Boolean): Modifier { +fun Modifier.glowCircle(flag: Boolean): Modifier { if (!flag) return Modifier return Modifier.border(4f.dp, color = Color(0xFFFF00FF), shape = CircleShape) diff --git a/src/main/kotlin/viewModel/MenuViewModel.kt b/src/main/kotlin/viewModel/MenuViewModel.kt index de14aca..697e010 100644 --- a/src/main/kotlin/viewModel/MenuViewModel.kt +++ b/src/main/kotlin/viewModel/MenuViewModel.kt @@ -11,5 +11,6 @@ class MenuViewModel( var isNodeCreating by canvasViewModel::isNodeCreatingMode var isClustering by canvasViewModel::isClustering var isRanked by canvasViewModel::isRanked + var isFinded by canvasViewModel::isFinded var isAlgorithmMenuOpen by mutableStateOf(false) -} \ No newline at end of file +} diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 537f4fb..415330b 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -28,6 +28,7 @@ class CanvasViewModel( var isClustering by graphViewModel::clustering var isRanked by graphViewModel::ranked + var isFinded by graphViewModel::bridgeFinded var isNodeCreatingMode by mutableStateOf(false) var zoom by mutableStateOf(1f) @@ -114,7 +115,7 @@ class CanvasViewModel( /* * Change edges' color * */ - fun changeEdgesColor(edges: List>) { + fun changeEdgesColor(edges: MutableList>) { graphViewModel.changeEdgesColor(edges) } diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index 5b63bff..bd42282 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import model.algorithm.Clustering +import model.algorithm.FindBridges import model.algorithm.PageRank import model.graph.Edge import model.graph.UndirectedGraph @@ -17,15 +18,18 @@ class UndirectedViewModel( private val graph: UndirectedGraph, val showVerticesLabels: Boolean, var groups: HashMap = hashMapOf(), - var ranks: List> = listOf() + var ranks: List> = listOf(), + var bridges: List = listOf() ) { private val _vertices = hashMapOf() private val _adjacencyList = hashMapOf>() private val groupColors = hashMapOf(0 to Color.Black) + private val BridgesWthColor = mutableListOf>() private val _color = mutableStateOf(Color.Black) private val _clustering = mutableStateOf(false) private val _ranked = mutableStateOf(false) + private val _bridgeFinded = mutableStateOf(false) private var size by mutableStateOf(10f) @@ -48,8 +52,25 @@ class UndirectedViewModel( set(value) { _ranked.value = value ranks = PageRank(graph).computePageRank(3) + println("хуй") updateSizes() } + + var bridgeFinded + get() = _bridgeFinded.value + set(value) { + _bridgeFinded.value = value + bridges = FindBridges(graph).findBridges() + bridges.forEach { + BridgesWthColor.add(it to Color.Red) + } + if (bridgeFinded) { + changeEdgesColor(BridgesWthColor) + } + else{ + resetEdgesColorToDefault() + } + } private fun getColor(group: Int): Color { if (clustering) { @@ -120,7 +141,7 @@ class UndirectedViewModel( /* * Change edges' color * */ - fun changeEdgesColor(edges: List>) { + fun changeEdgesColor(edges: MutableList>) { edges.forEach { p -> val edge = p.first val color = p.second From 0d8789b680b838e3534844fbbd4ed4811eb26dbc Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 24 Sep 2024 11:48:30 +0300 Subject: [PATCH 129/139] refactor: got rid of println and fixed branch conflict --- src/main/kotlin/view/MenuView.kt | 10 +++++----- src/main/kotlin/viewModel/graph/UndirectedViewModel.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/view/MenuView.kt b/src/main/kotlin/view/MenuView.kt index 9fd5643..3a5befc 100644 --- a/src/main/kotlin/view/MenuView.kt +++ b/src/main/kotlin/view/MenuView.kt @@ -96,19 +96,19 @@ fun MenuView( horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.height(25f.dp)) - MenuIcon("Nodes.svg", "AddNode.svg", Modifier.glowCircle(isNodeCreating)) { isNodeCreating = !isNodeCreating } + MenuIcon("Nodes.svg", "AddNode.svg", Modifier.glow(isNodeCreating)) { isNodeCreating = !isNodeCreating } MenuIcon("Ribs.svg", "AddEdge.svg", modifier = Modifier.alpha(0.2f)) - MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glowCircle(isClustering)) { isClustering = !isClustering } - MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glowCircle(viewModel.isRanked)) { isRanked = !isRanked } + MenuIcon("Clustering.svg", "ClusterD.svg", Modifier.glow(isClustering)) { isClustering = !isClustering } + MenuIcon("PageRank.svg", "AnalysisGraph.svg", Modifier.glow(viewModel.isRanked)) { isRanked = !isRanked } MenuIcon( "Algorithm.svg", "Algorithms....svg", - Modifier.glowCircle(viewModel.isAlgorithmMenuOpen) + Modifier.glow(viewModel.isAlgorithmMenuOpen) ) { isAlgorithmMenuOpen = !isAlgorithmMenuOpen } } } -fun Modifier.glowCircle(flag: Boolean): Modifier { +fun Modifier.glow(flag: Boolean): Modifier { if (!flag) return Modifier return Modifier.border(4f.dp, color = Color(0xFFFF00FF), shape = CircleShape) diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index bd42282..27e7ea0 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -52,7 +52,7 @@ class UndirectedViewModel( set(value) { _ranked.value = value ranks = PageRank(graph).computePageRank(3) - println("хуй") + updateSizes() } From 1014e627c37c9bcb21b1d04ccec7f574c3a83cbc Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 24 Sep 2024 12:17:28 +0300 Subject: [PATCH 130/139] fix: disable tests that interact with database --- src/test/kotlin/model/reader/Neo4jReaderTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/kotlin/model/reader/Neo4jReaderTest.kt b/src/test/kotlin/model/reader/Neo4jReaderTest.kt index 85992ef..b9b28b8 100644 --- a/src/test/kotlin/model/reader/Neo4jReaderTest.kt +++ b/src/test/kotlin/model/reader/Neo4jReaderTest.kt @@ -13,6 +13,10 @@ import org.neo4j.driver.exceptions.NoSuchRecordException import kotlin.test.assertEquals import kotlin.test.assertTrue +// Unfortunately tests don't work without local database, and mocking database it useless while you check work with DB +// TODO: use this tests for integrate tests +const val IS_ENABLED = false + class Neo4jReaderTest { lateinit var testGraph: Graph val neo4jReader = Neo4jReader("bolt://localhost:7687", "neo4j", "qwertyui") @@ -48,6 +52,8 @@ class Neo4jReaderTest { @Test fun `save and load empty graph one time`() { + if (!IS_ENABLED) return + neo4jReader.saveGraph(testGraph, "", testGraphName) val graph = neo4jReader.loadGraph("", testGraphName) @@ -57,6 +63,8 @@ class Neo4jReaderTest { @Test fun `save and load empty graph 100 times in a row`() { + if (!IS_ENABLED) return + var graph: Graph = testGraph for (i in 1..100) { @@ -70,6 +78,8 @@ class Neo4jReaderTest { @Test fun `save and load non-empty graph one time`() { + if (!IS_ENABLED) return + for (i in 1..5) { testGraph.addVertex(i) } @@ -93,6 +103,8 @@ class Neo4jReaderTest { @Test fun `save and load non-empty graph 100 times in a row`() { + if (!IS_ENABLED) return + for (i in 1..5) { testGraph.addVertex(i) } @@ -119,6 +131,8 @@ class Neo4jReaderTest { @Test fun `load graph that don't exist in DB`() { + if (!IS_ENABLED) return + try { neo4jReader.loadGraph("", "Homka") } catch (_: NoSuchRecordException) { From f99a20984d63cbb776ea9c75948d55b5b1f48d9a Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 24 Sep 2024 12:42:15 +0300 Subject: [PATCH 131/139] refactor: added some changes after the review --- src/main/kotlin/view/MainView.kt | 39 ++++++++++++-------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index c27ed15..2d61e5b 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -51,14 +51,7 @@ fun DisplayAlgorithmMenu(name: String, viewModel: MenuViewModel, onClick: () -> .padding(top = 150.dp, start = 30.dp) ) { items(imageResources) { image -> - ImageButton( - imageResourceId = image, - onClick = { - if(image == "FindBridge.svg"){ - onClick() - } - }, viewModel - ) + ImageButton(imageResourceId = image, onClick = onClick, viewModel) } } } @@ -73,22 +66,17 @@ fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuVie .padding(1.dp) .background(Color(0x00)) ) { - if (imageResourceId == "FindBridge.svg") { - Image( - painter = painterResource(imageResourceId), - contentDescription = "Button Image", - modifier = Modifier.glowRec(viewModel.isFinded).size(445.dp, 59.dp).onClick(onClick = onClick), - contentScale = ContentScale.Crop - ) - } - else { - Image( - painter = painterResource(imageResourceId), - contentDescription = "Button Image", - modifier = Modifier.alpha(0.2f).size(445.dp, 59.dp).onClick(onClick = onClick), - contentScale = ContentScale.Crop - ) + val modifier = when (imageResourceId) { + "FindBridge.svg" -> Modifier.glowRec(viewModel.isFinded).onClick(onClick = onClick) + else -> Modifier.alpha(0.2f) } + + Image( + painter = painterResource(imageResourceId), + contentDescription = "Button Image", + modifier = modifier.size(445.dp, 59.dp), + contentScale = ContentScale.Crop + ) } } @@ -107,9 +95,10 @@ fun MainView(mainViewModel: MainViewModel) { } if (mainViewModel.menuViewModel.isAlgorithmMenuOpen) { - DisplayAlgorithmMenu("DownMenuAlgorithm.svg", + DisplayAlgorithmMenu( + "DownMenuAlgorithm.svg", mainViewModel.menuViewModel - ){ isBridgeFinded = !isBridgeFinded } + ) { isBridgeFinded = !isBridgeFinded } } SettingsView(mainViewModel.settingsViewModel) From 4a5893bd67b8e2f5fbf554eb2a9323e785cfd938 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 24 Sep 2024 14:24:15 +0300 Subject: [PATCH 132/139] rafactor: made the algorithm list function more simple to connect the button and the algorithm --- src/main/kotlin/view/MainView.kt | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 2d61e5b..d401d9e 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -23,14 +23,15 @@ val MENU_WIDTH = Config.menuWidth @Composable fun DisplayAlgorithmMenu(name: String, viewModel: MenuViewModel, onClick: () -> Unit = {}) { + var isBridgeFinded by viewModel::isFinded val imageResources = listOf( - "FindBridge.svg", - "Dijkstra.svg", - "Bellman-Ford.svg", - "IslandTree.svg", - "StrongConnectivityComponent.svg", - "FindCycle.svg" + mapOf("icon" to "FindBridge.svg", "onClick" to { isBridgeFinded = !isBridgeFinded }), + mapOf("icon" to "Dijkstra.svg", "onClick" to {}), + mapOf("icon" to "Bellman-Ford.svg", "onClick" to {}), + mapOf("icon" to "IslandTree.svg", "onClick" to {}), + mapOf("icon" to "StrongConnectivityComponent.svg", "onClick" to {}), + mapOf("icon" to "FindCycle.svg", "onClick" to {}) ) Box( modifier = Modifier.padding(top = 240.dp, start = 80.dp) @@ -50,8 +51,10 @@ fun DisplayAlgorithmMenu(name: String, viewModel: MenuViewModel, onClick: () -> .background(Color.Transparent) .padding(top = 150.dp, start = 30.dp) ) { - items(imageResources) { image -> - ImageButton(imageResourceId = image, onClick = onClick, viewModel) + items(imageResources) { button -> + val icon = button["icon"] as String + val onClickAction = button["onClick"] as? () -> Unit ?: {} + ImageButton(imageResourceId = icon, onClick = onClickAction, viewModel) } } } @@ -67,14 +70,14 @@ fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuVie .background(Color(0x00)) ) { val modifier = when (imageResourceId) { - "FindBridge.svg" -> Modifier.glowRec(viewModel.isFinded).onClick(onClick = onClick) + "FindBridge.svg" -> Modifier.glowRec(viewModel.isFinded) else -> Modifier.alpha(0.2f) } Image( painter = painterResource(imageResourceId), contentDescription = "Button Image", - modifier = modifier.size(445.dp, 59.dp), + modifier = modifier.size(445.dp, 59.dp).onClick(onClick = onClick), contentScale = ContentScale.Crop ) } @@ -82,9 +85,7 @@ fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuVie @Composable fun MainView(mainViewModel: MainViewModel) { - var isBridgeFinded by mainViewModel.menuViewModel::isFinded - Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { MenuView(mainViewModel.menuViewModel) From 54f6fb4122ce6cb2818e38ed14690414963ec94e Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 24 Sep 2024 14:52:54 +0300 Subject: [PATCH 133/139] refactor: change map on data class --- src/main/kotlin/view/MainView.kt | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index d401d9e..b8fa8da 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -22,22 +21,23 @@ val HEADER_HEIGHT = Config.headerHeight val MENU_WIDTH = Config.menuWidth @Composable -fun DisplayAlgorithmMenu(name: String, viewModel: MenuViewModel, onClick: () -> Unit = {}) { +fun displayAlgorithmMenu(name: String, viewModel: MenuViewModel) { var isBridgeFinded by viewModel::isFinded + data class ImageResource(val icon: String, val onClick: () -> Unit) + val imageResources = listOf( - mapOf("icon" to "FindBridge.svg", "onClick" to { isBridgeFinded = !isBridgeFinded }), - mapOf("icon" to "Dijkstra.svg", "onClick" to {}), - mapOf("icon" to "Bellman-Ford.svg", "onClick" to {}), - mapOf("icon" to "IslandTree.svg", "onClick" to {}), - mapOf("icon" to "StrongConnectivityComponent.svg", "onClick" to {}), - mapOf("icon" to "FindCycle.svg", "onClick" to {}) + ImageResource("FindBridge.svg") { isBridgeFinded = !isBridgeFinded }, + ImageResource("Dijkstra.svg") {}, + ImageResource("Bellman-Ford.svg") {}, + ImageResource("IslandTree.svg") {}, + ImageResource("StrongConnectivityComponent.svg") {}, + ImageResource("FindCycle.svg") {} ) + Box( modifier = Modifier.padding(top = 240.dp, start = 80.dp) ) { - - // Изображение Image( painter = painterResource(name), contentDescription = "Padded Image", @@ -51,10 +51,8 @@ fun DisplayAlgorithmMenu(name: String, viewModel: MenuViewModel, onClick: () -> .background(Color.Transparent) .padding(top = 150.dp, start = 30.dp) ) { - items(imageResources) { button -> - val icon = button["icon"] as String - val onClickAction = button["onClick"] as? () -> Unit ?: {} - ImageButton(imageResourceId = icon, onClick = onClickAction, viewModel) + items(imageResources) { imageResource -> + ImageButton(imageResourceId = imageResource.icon, onClick = imageResource.onClick, viewModel) } } } @@ -85,7 +83,7 @@ fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuVie @Composable fun MainView(mainViewModel: MainViewModel) { - var isBridgeFinded by mainViewModel.menuViewModel::isFinded + Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { MenuView(mainViewModel.menuViewModel) @@ -96,10 +94,10 @@ fun MainView(mainViewModel: MainViewModel) { } if (mainViewModel.menuViewModel.isAlgorithmMenuOpen) { - DisplayAlgorithmMenu( + displayAlgorithmMenu( "DownMenuAlgorithm.svg", mainViewModel.menuViewModel - ) { isBridgeFinded = !isBridgeFinded } + ) } SettingsView(mainViewModel.settingsViewModel) From bacb10436085f16779c0ea01bc7d06e70f5244ff Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 24 Sep 2024 15:59:48 +0300 Subject: [PATCH 134/139] feat: add findEdge to Graph interface and implementation --- src/main/kotlin/model/graph/Graph.kt | 1 + src/main/kotlin/model/graph/UndirectedGraph.kt | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index e2e2888..0829c09 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -9,4 +9,5 @@ interface Graph { fun updateVertex(key: Int, newKey: Int): Vertex? fun addEdge(first: Int, second: Int, weight: Long = 1): Edge? fun removeEdge(first: Int, second: Int): Edge? + fun getEdge(first: Int, second: Int): Edge? } \ No newline at end of file diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index 9f80745..0a98769 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -73,6 +73,12 @@ open class UndirectedGraph : Graph { fun getEdges(vertex: Vertex) = _adjacencyList[vertex] + override fun getEdge(first: Int, second: Int): Edge? { + val firstVertex = findVertex(first) ?: return null + + return getEdges(firstVertex)?.find { it.second.key == second } + } + private data class UndirectedVertex(override var key: Int) : Vertex private data class UndirectedEdge(override val first: Vertex, override val second: Vertex) : Edge { From 40c7086f9db9e52430714a3a7376abeab60a97d2 Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 24 Sep 2024 16:44:00 +0300 Subject: [PATCH 135/139] feat: connected the Dijkstra algorithm button to the algorithm itself] --- src/main/kotlin/Config.kt | 1 + src/main/kotlin/model/algorithm/Dijkstra.kt | 7 ++++ src/main/kotlin/view/MainView.kt | 11 +++++-- .../kotlin/view/canvas/VertexCanvasView.kt | 4 +-- src/main/kotlin/viewModel/MenuViewModel.kt | 3 +- .../viewModel/canvas/CanvasViewModel.kt | 32 +++++++++++++++++++ .../viewModel/canvas/VertexCanvasViewModel.kt | 11 ++++++- .../viewModel/graph/UndirectedViewModel.kt | 12 ++++--- 8 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt index f0b4354..756f857 100644 --- a/src/main/kotlin/Config.kt +++ b/src/main/kotlin/Config.kt @@ -6,6 +6,7 @@ object Config { object Edge { val color = Color(0xFF00E0FF) + val dijkstraColor = Color.Green val strokeWidth = 8f } } \ No newline at end of file diff --git a/src/main/kotlin/model/algorithm/Dijkstra.kt b/src/main/kotlin/model/algorithm/Dijkstra.kt index 2dbded8..fe07ce0 100644 --- a/src/main/kotlin/model/algorithm/Dijkstra.kt +++ b/src/main/kotlin/model/algorithm/Dijkstra.kt @@ -1,9 +1,16 @@ package model.algorithm +import model.graph.Edge import model.graph.Graph import model.graph.Vertex class Dijkstra(private val graph: Graph) { + fun triplesToEdges(list: List>): List { + return list.map { + graph.getEdge(it.first, it.second) + ?: throw Error("There is no edge from ${it.first} node to ${it.second} node") + } + } fun findShortestPath(startKey: Int, endKey: Int): List>? { val startVertex = graph.vertices.find { it.key == startKey } ?: return null diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index b8fa8da..09e1106 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -22,13 +22,17 @@ val MENU_WIDTH = Config.menuWidth @Composable fun displayAlgorithmMenu(name: String, viewModel: MenuViewModel) { - var isBridgeFinded by viewModel::isFinded + var isBridgeFinded by viewModel::isBridgeFinded + var isDijkstraMode by viewModel::isDijkstraMode data class ImageResource(val icon: String, val onClick: () -> Unit) val imageResources = listOf( ImageResource("FindBridge.svg") { isBridgeFinded = !isBridgeFinded }, - ImageResource("Dijkstra.svg") {}, + ImageResource("Dijkstra.svg") { + isDijkstraMode = !isDijkstraMode + if (!isDijkstraMode) viewModel.canvasViewModel.resetEdgesColorToDefault() + }, ImageResource("Bellman-Ford.svg") {}, ImageResource("IslandTree.svg") {}, ImageResource("StrongConnectivityComponent.svg") {}, @@ -68,7 +72,8 @@ fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuVie .background(Color(0x00)) ) { val modifier = when (imageResourceId) { - "FindBridge.svg" -> Modifier.glowRec(viewModel.isFinded) + "FindBridge.svg" -> Modifier.glowRec(viewModel.isBridgeFinded) + "Dijkstra.svg" -> Modifier.glowRec(viewModel.isDijkstraMode) else -> Modifier.alpha(0.2f) } diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index 34cf53a..82d02bf 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -28,12 +28,12 @@ fun VertexCanvasView( .size(viewModel.radius * 2) .offset(viewModel.offset.x.dp, viewModel.offset.y.dp) .border( - color = if (viewModel.canvasViewModel.pickedNodeForEdgeCreating != viewModel) viewModel.color else Color.Green, + color = if (viewModel.canvasViewModel.pickedNodeForDijkstra != viewModel) viewModel.color else Color.Green, width = viewModel.strokeWidth.dp, shape = CircleShape ) .background(color = Color(0xFF242424), shape = CircleShape) - .onClick { viewModel.onClickWhenEdgeCreating() } + .onClick { viewModel.onClick() } .onDrag(onDrag = viewModel::onDrag), contentAlignment = Alignment.Center ) { diff --git a/src/main/kotlin/viewModel/MenuViewModel.kt b/src/main/kotlin/viewModel/MenuViewModel.kt index 66ad09a..208cfcf 100644 --- a/src/main/kotlin/viewModel/MenuViewModel.kt +++ b/src/main/kotlin/viewModel/MenuViewModel.kt @@ -12,6 +12,7 @@ class MenuViewModel( var isEdgeCreating by canvasViewModel::isEdgeCreatingMode var isClustering by canvasViewModel::isClustering var isRanked by canvasViewModel::isRanked - var isFinded by canvasViewModel::isFinded + var isBridgeFinded by canvasViewModel::isFinded + var isDijkstraMode by canvasViewModel::isDijkstraMode var isAlgorithmMenuOpen by mutableStateOf(false) } diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 8a5b828..a5ba556 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -1,5 +1,6 @@ package viewModel.canvas +import Config import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.gestures.detectDragGestures @@ -14,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerInputScope +import model.algorithm.Dijkstra import model.graph.Edge import model.graph.UndirectedGraph import view.HEADER_HEIGHT @@ -32,7 +34,9 @@ class CanvasViewModel( var isFinded by graphViewModel::bridgeFinded var isEdgeCreatingMode by mutableStateOf(false) + var isDijkstraMode by mutableStateOf(false) var pickedNodeForEdgeCreating by mutableStateOf(null) + var pickedNodeForDijkstra by mutableStateOf(null) var isNodeCreatingMode by mutableStateOf(false) var edgesCount by mutableStateOf(0) @@ -158,4 +162,32 @@ class CanvasViewModel( createEdge(pickedNodeForEdgeCreating ?: return, vm) pickedNodeForEdgeCreating = null } + + fun onClickNodeDijkstraOn(vm: VertexCanvasViewModel) { + if (!isDijkstraMode) { + resetEdgesColorToDefault() + return + } + + if (pickedNodeForDijkstra == vm) { + pickedNodeForDijkstra = null + return + } + + if (pickedNodeForDijkstra == null) { + pickedNodeForDijkstra = vm + return + } + + val firstVertex = + pickedNodeForDijkstra ?: throw IllegalStateException("there is no node in pickedNodeForDijkstra method") + + val dijksta = Dijkstra(graph) + val path = dijksta.findShortestPath(firstVertex.vertexViewModel.getKey(), vm.vertexViewModel.getKey()) ?: return + val edges = dijksta.triplesToEdges(path) + + val PathWthColor = edges.map { it to Config.Edge.dijkstraColor } + changeEdgesColor(PathWthColor.toMutableList()) + pickedNodeForDijkstra = null + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index b0831db..99b9695 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -24,10 +24,19 @@ class VertexCanvasViewModel( vertexViewModel.onDrag(it * (1f / canvasViewModel.zoom)) } - fun onClickWhenEdgeCreating() { + fun onClick() { + onClickWhenDijkstraOn() + onClickWhenEdgeCreating() + } + + private fun onClickWhenEdgeCreating() { canvasViewModel.onClickNodeEdgeCreating(this) } + private fun onClickWhenDijkstraOn() { + canvasViewModel.onClickNodeDijkstraOn(this) + } + private fun calculateOffset() = Offset( (canvasViewModel.canvasSize.x / 2) + ((vertexViewModel.x - canvasViewModel.center.x) * canvasViewModel.zoom), (canvasViewModel.canvasSize.y / 2) + ((vertexViewModel.y - canvasViewModel.center.y) * canvasViewModel.zoom) diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index c9524f9..08dfaf9 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import model.algorithm.Clustering +import model.algorithm.Dijkstra import model.algorithm.FindBridges import model.algorithm.PageRank import model.graph.Edge @@ -19,17 +20,19 @@ class UndirectedViewModel( val showVerticesLabels: Boolean, var groups: HashMap = hashMapOf(), var ranks: List> = listOf(), - var bridges: List = listOf() + var bridges: List = listOf(), ) { private val _vertices = hashMapOf() private val _adjacencyList = hashMapOf>() private val groupColors = hashMapOf(0 to Color.Black) private val BridgesWthColor = mutableListOf>() + private val PathWthColor = mutableListOf>() private val _color = mutableStateOf(Color.Black) private val _clustering = mutableStateOf(false) private val _ranked = mutableStateOf(false) private val _bridgeFinded = mutableStateOf(false) + private val _shortestPathFinded = mutableStateOf(false) private var size by mutableStateOf(10f) @@ -55,7 +58,7 @@ class UndirectedViewModel( updateSizes() } - + var bridgeFinded get() = _bridgeFinded.value set(value) { @@ -66,12 +69,13 @@ class UndirectedViewModel( } if (bridgeFinded) { changeEdgesColor(BridgesWthColor) - } - else{ + } else { resetEdgesColorToDefault() } } + var shortestPathFinded by mutableStateOf(false) + fun createEdge(first: VertexViewModel, second: VertexViewModel): Pair? { val edge = graph.addEdge(first.getKey(), second.getKey()) ?: return null From 4b0c99d1040ce8683eddd1804f1ae17be2dde5f2 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 24 Sep 2024 16:51:50 +0300 Subject: [PATCH 136/139] feat: add find cycle button --- src/main/kotlin/view/MainView.kt | 5 ++++- src/main/kotlin/view/canvas/VertexCanvasView.kt | 2 +- .../kotlin/viewModel/canvas/CanvasViewModel.kt | 17 +++++++++++++++++ .../viewModel/canvas/VertexCanvasViewModel.kt | 4 ++-- .../viewModel/graph/UndirectedViewModel.kt | 6 +++--- .../kotlin/viewModel/graph/VertexViewModel.kt | 2 +- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index b8fa8da..7c74906 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -32,7 +32,10 @@ fun displayAlgorithmMenu(name: String, viewModel: MenuViewModel) { ImageResource("Bellman-Ford.svg") {}, ImageResource("IslandTree.svg") {}, ImageResource("StrongConnectivityComponent.svg") {}, - ImageResource("FindCycle.svg") {} + ImageResource("FindCycle.svg") { + viewModel.canvasViewModel.isEdgeFindCycleMode = !viewModel.canvasViewModel.isEdgeFindCycleMode + viewModel.canvasViewModel.resetEdgesColorToDefault() + } ) Box( diff --git a/src/main/kotlin/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt index 34cf53a..d046022 100644 --- a/src/main/kotlin/view/canvas/VertexCanvasView.kt +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -33,7 +33,7 @@ fun VertexCanvasView( shape = CircleShape ) .background(color = Color(0xFF242424), shape = CircleShape) - .onClick { viewModel.onClickWhenEdgeCreating() } + .onClick { viewModel.onClick(viewModel) } .onDrag(onDrag = viewModel::onDrag), contentAlignment = Alignment.Center ) { diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index 8a5b828..a933190 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerInputScope +import model.algorithm.FindCycle import model.graph.Edge import model.graph.UndirectedGraph import view.HEADER_HEIGHT @@ -34,6 +35,8 @@ class CanvasViewModel( var isEdgeCreatingMode by mutableStateOf(false) var pickedNodeForEdgeCreating by mutableStateOf(null) + var isEdgeFindCycleMode by mutableStateOf(false) + var isNodeCreatingMode by mutableStateOf(false) var edgesCount by mutableStateOf(0) var zoom by mutableStateOf(1f) @@ -54,6 +57,7 @@ class CanvasViewModel( val viewModel = graphViewModel.createVertex(coordinates) ?: return _vertices[viewModel] = VertexCanvasViewModel(viewModel, this) + _edges[getVertex(viewModel)] = ArrayList() } } @@ -142,6 +146,11 @@ class CanvasViewModel( edgesCount++ } + fun onClick(vm: VertexCanvasViewModel) { + onClickNodeEdgeCreating(vm) + onClickNodeFindCycle(vm) + } + fun onClickNodeEdgeCreating(vm: VertexCanvasViewModel) { if (!isEdgeCreatingMode) return @@ -158,4 +167,12 @@ class CanvasViewModel( createEdge(pickedNodeForEdgeCreating ?: return, vm) pickedNodeForEdgeCreating = null } + + fun onClickNodeFindCycle(vm: VertexCanvasViewModel) { + if (!isEdgeFindCycleMode) return + + changeEdgesColor(FindCycle(graph).calculate(vm.vertexViewModel.vertex).map { Pair(it, Color.Red) } + .toMutableList()) + isEdgeFindCycleMode = false + } } \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt index b0831db..81bc3bb 100644 --- a/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -24,8 +24,8 @@ class VertexCanvasViewModel( vertexViewModel.onDrag(it * (1f / canvasViewModel.zoom)) } - fun onClickWhenEdgeCreating() { - canvasViewModel.onClickNodeEdgeCreating(this) + fun onClick(vm: VertexCanvasViewModel) { + canvasViewModel.onClick(this) } private fun calculateOffset() = Offset( diff --git a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt index c9524f9..e413838 100644 --- a/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -55,7 +55,7 @@ class UndirectedViewModel( updateSizes() } - + var bridgeFinded get() = _bridgeFinded.value set(value) { @@ -66,8 +66,7 @@ class UndirectedViewModel( } if (bridgeFinded) { changeEdgesColor(BridgesWthColor) - } - else{ + } else { resetEdgesColorToDefault() } } @@ -146,6 +145,7 @@ class UndirectedViewModel( ) _vertices[vertex] = viewModel + _adjacencyList[viewModel] = ArrayList() return viewModel } diff --git a/src/main/kotlin/viewModel/graph/VertexViewModel.kt b/src/main/kotlin/viewModel/graph/VertexViewModel.kt index 749f50a..16d6a8b 100644 --- a/src/main/kotlin/viewModel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewModel/graph/VertexViewModel.kt @@ -11,7 +11,7 @@ import model.graph.Vertex class VertexViewModel( private val _labelVisible: Boolean, - private val vertex: Vertex, + val vertex: Vertex, x: Float = 0f, y: Float = 0f, color: Color = Color.Black, From 0bd93741fce8f4507f2038b3a91fe952eb2e31dd Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 24 Sep 2024 16:57:57 +0300 Subject: [PATCH 137/139] fix: now color reset when you click on another node --- src/main/kotlin/model/reader/SQLiteReader.kt | 412 +++++++++--------- src/main/kotlin/view/MainView.kt | 1 + .../viewModel/canvas/CanvasViewModel.kt | 2 +- 3 files changed, 208 insertions(+), 207 deletions(-) diff --git a/src/main/kotlin/model/reader/SQLiteReader.kt b/src/main/kotlin/model/reader/SQLiteReader.kt index 46b0b82..f72838f 100644 --- a/src/main/kotlin/model/reader/SQLiteReader.kt +++ b/src/main/kotlin/model/reader/SQLiteReader.kt @@ -1,206 +1,206 @@ -package model.reader - -import model.graph.* - -import java.sql.Connection -import java.sql.DriverManager -import java.sql.PreparedStatement - -class SQLiteReader: Reader { - - private fun createTable(connection: Connection) { - val statement = connection.createStatement() - val createTableVertex = """ - CREATE TABLE IF NOT EXISTS vertex ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - vertex_key INTEGER, - graph_id INTEGER, - FOREIGN KEY (graph_id) REFERENCES graph(graph_id) - ) - """ - val createTableEdge = """ - CREATE TABLE IF NOT EXISTS edge ( - start_vertex_id INTEGER, - end_vertex_id INTEGER, - weight INTEGER, - FOREIGN KEY (end_vertex_id) REFERENCES graph(graph_id), - FOREIGN KEY (start_vertex_id) REFERENCES graph(graph_id) - ) - """ - val createTableGraph = """ - CREATE TABLE IF NOT EXISTS graph ( - graph_id INTEGER PRIMARY KEY AUTOINCREMENT, - graph_name TEXT NOT NULL UNIQUE, - graph_type_flag INTEGER - ) - """ - statement.execute(createTableGraph) - statement.execute(createTableVertex) - statement.execute(createTableEdge) - statement.close() - } - - private fun insertGraph(connect: Connection, graph: Graph, nameGraph: String){ - - val insertName = "INSERT INTO graph (graph_name, graph_type_flag) VALUES (?, ?)" - val insertNameStmt: PreparedStatement = connect.prepareStatement(insertName) - insertNameStmt.setString(1, nameGraph) - - if (graph is WeightedGraph){ - insertNameStmt.setInt(2, 1) - } - if (graph is UndirectedGraph){ - insertNameStmt.setInt(2, 2) - } - if (graph is DirectedGraph){ - insertNameStmt.setInt(2, 3) - } - if (graph is WeightedDirectedGraph){ - insertNameStmt.setInt(2, 4) - } - - insertNameStmt.executeUpdate() - - val graphId = insertNameStmt.generatedKeys.getInt(1) - insertNameStmt.close() - - val insertVertexSql = "INSERT INTO vertex (vertex_key, graph_id) VALUES (?, ?)" - val insertVertexStmt: PreparedStatement = connect.prepareStatement(insertVertexSql) - - val vertexIdMap = mutableMapOf() - - for (vertex in graph.vertices){ - insertVertexStmt.setInt(1, vertex.key) - insertVertexStmt.setInt(2, graphId) - - insertVertexStmt.executeUpdate() - - val vertexIdResult = insertVertexStmt.generatedKeys - if (vertexIdResult.next()) { - val vertexId = vertexIdResult.getInt(1) - vertexIdMap[vertex] = vertexId - } - } - insertVertexStmt.close() - - val insertEdgeSql = "INSERT INTO edge (start_vertex_id, end_vertex_id, weight) VALUES (?, ?, ?)" - val insertEdgeStmt: PreparedStatement = connect.prepareStatement(insertEdgeSql) - - for ((vertex, edges) in graph.adjacencyList) { - val startVertexId = vertexIdMap[vertex] ?: throw Exception("Vertex not found in vertexIdMap") - for (edge in edges) { - val endVertexId = vertexIdMap[edge.second] ?: throw Exception("End vertex not found in vertexIdMap") - insertEdgeStmt.setInt(1, startVertexId) - insertEdgeStmt.setInt(2, endVertexId) - insertEdgeStmt.setLong(3, edge.weight) - insertEdgeStmt.executeUpdate() - } - } - insertEdgeStmt.close() - } - - private fun connect(filepath: String): Connection = DriverManager.getConnection("jdbc:sqlite:$filepath") - - - override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) { - - //Сконектились с базой - val connection = connect(filepath) - - //Создали таблицы и связи между ними - createTable(connection) - - //Сохранили граф по полочкам:) - insertGraph(connection, graph, nameGraph) - } - - override fun loadGraph(filepath: String, nameGraph: String): Graph { - - //Сконектились с базой - val connection = connect(filepath) - - val graph: Graph - - //Сделали запрос на получение id графа - val graphStmt = connection.prepareStatement( - "SELECT graph_id, graph_type_flag FROM graph WHERE graph_name = ?" - ) - - graphStmt.setString(1, nameGraph) - val graphResultSet = graphStmt.executeQuery() - - if (!graphResultSet.next()) { - throw IllegalArgumentException("Graph with name $nameGraph not found") - } - - val graphId = graphResultSet.getInt("graph_id") - val graphType = graphResultSet.getInt("graph_type_flag") - - graph = when (graphType) { - 1 -> WeightedGraph() - 2 -> UndirectedGraph() - 3 -> DirectedGraph() - 4 -> WeightedDirectedGraph() - else -> throw IllegalArgumentException("Unknown graph type: $graphType") - } - - graphResultSet.close() - graphStmt.close() - - //Сделали запрос на получение id и ключа вершины - val vertexStmt = connection.prepareStatement( - "SELECT id, vertex_key FROM vertex WHERE graph_id = ?" - ) - - vertexStmt.setInt(1, graphId) - val vertexResultSet = vertexStmt.executeQuery() - - // Нужна для нахождение вершин ребра через их id - val vertexMap = mutableMapOf() - - while (vertexResultSet.next()){ - val vertexId = vertexResultSet.getInt("id") - val vertexKey = vertexResultSet.getInt("vertex_key") - val vertex = graph.addVertex(vertexKey) - - if (vertex != null){ - vertexMap[vertexId] = vertex - } - } - - vertexResultSet.close() - vertexStmt.close() - - /* - Сделали запрос на получение id начальной и конечной вершины, а также веса, ребра, - через id вершины полученной от graph_id - */ - val edgeStmt = connection.prepareStatement( - "SELECT start_vertex_id, end_vertex_id, weight FROM edge WHERE start_vertex_id" + - " IN (SELECT id FROM vertex WHERE graph_id = ?)" - ) - - edgeStmt.setInt(1, graphId) - val edgeResultSet = edgeStmt.executeQuery() - - while (edgeResultSet.next()) { - val startVertexId = edgeResultSet.getInt("start_vertex_id") - val endVertexId = edgeResultSet.getInt("end_vertex_id") - val weight = edgeResultSet.getLong("weight") - - val startVertex = vertexMap[startVertexId] - val endVertex = vertexMap[endVertexId] - - if (startVertex != null && endVertex != null) { - graph.addEdge(startVertex.key, endVertex.key, weight) - } - } - edgeResultSet.close() - edgeStmt.close() - - connection.close() - return graph - } -} - - +//package model.reader +// +//import model.graph.* +// +//import java.sql.Connection +//import java.sql.DriverManager +//import java.sql.PreparedStatement +// +//class SQLiteReader: Reader { +// +// private fun createTable(connection: Connection) { +// val statement = connection.createStatement() +// val createTableVertex = """ +// CREATE TABLE IF NOT EXISTS vertex ( +// id INTEGER PRIMARY KEY AUTOINCREMENT, +// vertex_key INTEGER, +// graph_id INTEGER, +// FOREIGN KEY (graph_id) REFERENCES graph(graph_id) +// ) +// """ +// val createTableEdge = """ +// CREATE TABLE IF NOT EXISTS edge ( +// start_vertex_id INTEGER, +// end_vertex_id INTEGER, +// weight INTEGER, +// FOREIGN KEY (end_vertex_id) REFERENCES graph(graph_id), +// FOREIGN KEY (start_vertex_id) REFERENCES graph(graph_id) +// ) +// """ +// val createTableGraph = """ +// CREATE TABLE IF NOT EXISTS graph ( +// graph_id INTEGER PRIMARY KEY AUTOINCREMENT, +// graph_name TEXT NOT NULL UNIQUE, +// graph_type_flag INTEGER +// ) +// """ +// statement.execute(createTableGraph) +// statement.execute(createTableVertex) +// statement.execute(createTableEdge) +// statement.close() +// } +// +// private fun insertGraph(connect: Connection, graph: Graph, nameGraph: String){ +// +// val insertName = "INSERT INTO graph (graph_name, graph_type_flag) VALUES (?, ?)" +// val insertNameStmt: PreparedStatement = connect.prepareStatement(insertName) +// insertNameStmt.setString(1, nameGraph) +// +// if (graph is WeightedGraph){ +// insertNameStmt.setInt(2, 1) +// } +// if (graph is UndirectedGraph){ +// insertNameStmt.setInt(2, 2) +// } +// if (graph is DirectedGraph){ +// insertNameStmt.setInt(2, 3) +// } +// if (graph is WeightedDirectedGraph){ +// insertNameStmt.setInt(2, 4) +// } +// +// insertNameStmt.executeUpdate() +// +// val graphId = insertNameStmt.generatedKeys.getInt(1) +// insertNameStmt.close() +// +// val insertVertexSql = "INSERT INTO vertex (vertex_key, graph_id) VALUES (?, ?)" +// val insertVertexStmt: PreparedStatement = connect.prepareStatement(insertVertexSql) +// +// val vertexIdMap = mutableMapOf() +// +// for (vertex in graph.vertices){ +// insertVertexStmt.setInt(1, vertex.key) +// insertVertexStmt.setInt(2, graphId) +// +// insertVertexStmt.executeUpdate() +// +// val vertexIdResult = insertVertexStmt.generatedKeys +// if (vertexIdResult.next()) { +// val vertexId = vertexIdResult.getInt(1) +// vertexIdMap[vertex] = vertexId +// } +// } +// insertVertexStmt.close() +// +// val insertEdgeSql = "INSERT INTO edge (start_vertex_id, end_vertex_id, weight) VALUES (?, ?, ?)" +// val insertEdgeStmt: PreparedStatement = connect.prepareStatement(insertEdgeSql) +// +// for ((vertex, edges) in graph.adjacencyList) { +// val startVertexId = vertexIdMap[vertex] ?: throw Exception("Vertex not found in vertexIdMap") +// for (edge in edges) { +// val endVertexId = vertexIdMap[edge.second] ?: throw Exception("End vertex not found in vertexIdMap") +// insertEdgeStmt.setInt(1, startVertexId) +// insertEdgeStmt.setInt(2, endVertexId) +// insertEdgeStmt.setLong(3, edge.weight) +// insertEdgeStmt.executeUpdate() +// } +// } +// insertEdgeStmt.close() +// } +// +// private fun connect(filepath: String): Connection = DriverManager.getConnection("jdbc:sqlite:$filepath") +// +// +// override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) { +// +// //Сконектились с базой +// val connection = connect(filepath) +// +// //Создали таблицы и связи между ними +// createTable(connection) +// +// //Сохранили граф по полочкам:) +// insertGraph(connection, graph, nameGraph) +// } +// +// override fun loadGraph(filepath: String, nameGraph: String): Graph { +// +// //Сконектились с базой +// val connection = connect(filepath) +// +// val graph: Graph +// +// //Сделали запрос на получение id графа +// val graphStmt = connection.prepareStatement( +// "SELECT graph_id, graph_type_flag FROM graph WHERE graph_name = ?" +// ) +// +// graphStmt.setString(1, nameGraph) +// val graphResultSet = graphStmt.executeQuery() +// +// if (!graphResultSet.next()) { +// throw IllegalArgumentException("Graph with name $nameGraph not found") +// } +// +// val graphId = graphResultSet.getInt("graph_id") +// val graphType = graphResultSet.getInt("graph_type_flag") +// +// graph = when (graphType) { +// 1 -> WeightedGraph() +// 2 -> UndirectedGraph() +// 3 -> DirectedGraph() +// 4 -> WeightedDirectedGraph() +// else -> throw IllegalArgumentException("Unknown graph type: $graphType") +// } +// +// graphResultSet.close() +// graphStmt.close() +// +// //Сделали запрос на получение id и ключа вершины +// val vertexStmt = connection.prepareStatement( +// "SELECT id, vertex_key FROM vertex WHERE graph_id = ?" +// ) +// +// vertexStmt.setInt(1, graphId) +// val vertexResultSet = vertexStmt.executeQuery() +// +// // Нужна для нахождение вершин ребра через их id +// val vertexMap = mutableMapOf() +// +// while (vertexResultSet.next()){ +// val vertexId = vertexResultSet.getInt("id") +// val vertexKey = vertexResultSet.getInt("vertex_key") +// val vertex = graph.addVertex(vertexKey) +// +// if (vertex != null){ +// vertexMap[vertexId] = vertex +// } +// } +// +// vertexResultSet.close() +// vertexStmt.close() +// +// /* +// Сделали запрос на получение id начальной и конечной вершины, а также веса, ребра, +// через id вершины полученной от graph_id +// */ +// val edgeStmt = connection.prepareStatement( +// "SELECT start_vertex_id, end_vertex_id, weight FROM edge WHERE start_vertex_id" + +// " IN (SELECT id FROM vertex WHERE graph_id = ?)" +// ) +// +// edgeStmt.setInt(1, graphId) +// val edgeResultSet = edgeStmt.executeQuery() +// +// while (edgeResultSet.next()) { +// val startVertexId = edgeResultSet.getInt("start_vertex_id") +// val endVertexId = edgeResultSet.getInt("end_vertex_id") +// val weight = edgeResultSet.getLong("weight") +// +// val startVertex = vertexMap[startVertexId] +// val endVertex = vertexMap[endVertexId] +// +// if (startVertex != null && endVertex != null) { +// graph.addEdge(startVertex.key, endVertex.key, weight) +// } +// } +// edgeResultSet.close() +// edgeStmt.close() +// +// connection.close() +// return graph +// } +//} +// +// diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt index 7c74906..1bc68f0 100644 --- a/src/main/kotlin/view/MainView.kt +++ b/src/main/kotlin/view/MainView.kt @@ -72,6 +72,7 @@ fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuVie ) { val modifier = when (imageResourceId) { "FindBridge.svg" -> Modifier.glowRec(viewModel.isFinded) + "FindCycle.svg" -> Modifier.glowRec(viewModel.canvasViewModel.isEdgeFindCycleMode) else -> Modifier.alpha(0.2f) } diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index a933190..638f233 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -171,8 +171,8 @@ class CanvasViewModel( fun onClickNodeFindCycle(vm: VertexCanvasViewModel) { if (!isEdgeFindCycleMode) return + resetEdgesColorToDefault() changeEdgesColor(FindCycle(graph).calculate(vm.vertexViewModel.vertex).map { Pair(it, Color.Red) } .toMutableList()) - isEdgeFindCycleMode = false } } \ No newline at end of file From e28f8020b6a32ebb701be9a21fb6720c73441eeb Mon Sep 17 00:00:00 2001 From: Demon32123 Date: Tue, 24 Sep 2024 17:02:07 +0300 Subject: [PATCH 138/139] refactor: added reset color of edges in the algorithm before reuse --- src/main/kotlin/viewModel/canvas/CanvasViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt index a5ba556..6650a8f 100644 --- a/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -187,6 +187,7 @@ class CanvasViewModel( val edges = dijksta.triplesToEdges(path) val PathWthColor = edges.map { it to Config.Edge.dijkstraColor } + resetEdgesColorToDefault() changeEdgesColor(PathWthColor.toMutableList()) pickedNodeForDijkstra = null } From 6dce6f40861c8d283e69091082adb2c700a13d42 Mon Sep 17 00:00:00 2001 From: Homka122 Date: Tue, 24 Sep 2024 19:02:39 +0300 Subject: [PATCH 139/139] feat: add integrate test --- src/test/kotlin/model/IntegrateTests.kt | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/test/kotlin/model/IntegrateTests.kt diff --git a/src/test/kotlin/model/IntegrateTests.kt b/src/test/kotlin/model/IntegrateTests.kt new file mode 100644 index 0000000..077c0ae --- /dev/null +++ b/src/test/kotlin/model/IntegrateTests.kt @@ -0,0 +1,63 @@ +package model + +import androidx.compose.ui.geometry.Offset +import model.graph.UndirectedGraph +import org.junit.jupiter.api.Test +import viewModel.MainViewModel +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class IntegrateTests { + @Test + fun scenario1() { + // User launch app, mainViewModel is creating + val AMOUNT_NODES = 16 + val EDGE_CHANGE = 5.0 + + val graph = UndirectedGraph().apply { + for (i in (0 until AMOUNT_NODES)) { + addVertex(i) + } + + for (i in (0 until AMOUNT_NODES)) { + for (j in (0 until AMOUNT_NODES)) { + if (Math.random() < EDGE_CHANGE / 100) { + addEdge(i, j) + } + } + } + } + + val mainViewModel = MainViewModel(graph) + + // User create a few nodes + val oldSize = graph.vertices.size + + mainViewModel.canvasViewModel.isNodeCreatingMode = true + mainViewModel.canvasViewModel.createNode(offset = Offset(100f, 100f)) + mainViewModel.canvasViewModel.createNode(offset = Offset(300f, 100f)) + mainViewModel.canvasViewModel.createNode(offset = Offset(200f, 100f)) + mainViewModel.canvasViewModel.createNode(offset = Offset(100f, 100f)) + + // Graph changed + assertTrue(graph.vertices.size != oldSize) + + mainViewModel.canvasViewModel.isNodeCreatingMode = false + + // User add edge + mainViewModel.canvasViewModel.isEdgeCreatingMode = true + val firstVertex = mainViewModel.canvasViewModel.vertices.find { it.vertexViewModel.getKey() == 17 } + ?: throw Error("There is no vertex with id 17") + val secondVertex = mainViewModel.canvasViewModel.vertices.find { it.vertexViewModel.getKey() == 18 } + ?: throw Error("There is no vertex with id 18") + + // User click on two vertecies + mainViewModel.canvasViewModel.onClick(firstVertex) + mainViewModel.canvasViewModel.onClick(secondVertex) + + // Edge created + val edges = mainViewModel.canvasViewModel.edges.flatten() + val edge = edges.find { it.first == firstVertex && it.second == secondVertex } + assertNotNull(edge) + } +} \ No newline at end of file