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 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/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. 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) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7032eb0 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,62 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") + id("jacoco") +} + +group = "com.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + flatDir { + dirs("libs") + } +} + +dependencies { + // Note, if you develop a library, you should use compose.desktop.common. + // compose.desktop.currentOs should be used in launcher-sourceSet + // (in a separate module for demo project and in testMain). + // With compose.desktop.common you will also lose @Preview functionality + implementation(compose.desktop.currentOs) + implementation(":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") + implementation("org.xerial:sqlite-jdbc:3.41.2.2") + implementation("org.neo4j.driver", "neo4j-java-driver", "5.6.0") +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "graphs-3" + packageVersion = "1.0.0" + } + } +} + +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 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 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..db9a6b8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libs/louvain-1.0-SNAPSHOT-sources.jar b/libs/louvain-1.0-SNAPSHOT-sources.jar new file mode 100644 index 0000000..d4c77a9 Binary files /dev/null and b/libs/louvain-1.0-SNAPSHOT-sources.jar differ diff --git a/libs/louvain-1.0-SNAPSHOT.jar b/libs/louvain-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..ece3327 Binary files /dev/null and b/libs/louvain-1.0-SNAPSHOT.jar differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..92d5e40 --- /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 = "graphs-graphs-3" diff --git a/src/main/kotlin/Config.kt b/src/main/kotlin/Config.kt new file mode 100644 index 0000000..756f857 --- /dev/null +++ b/src/main/kotlin/Config.kt @@ -0,0 +1,12 @@ +import androidx.compose.ui.graphics.Color + +object Config { + val headerHeight = 40f + val menuWidth = 80f + + 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/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..b4355e3 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,61 @@ +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.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 viewModel.MainViewModel + +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) + +fun main() = application { + var isOpen by remember { mutableStateOf(true) } + var isMaximized by remember { mutableStateOf(true) } + val 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( + 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/components/MyText.kt b/src/main/kotlin/components/MyText.kt new file mode 100644 index 0000000..a8f4d86 --- /dev/null +++ b/src/main/kotlin/components/MyText.kt @@ -0,0 +1,14 @@ +package components + +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/model/algorithm/BellmanFord.kt b/src/main/kotlin/model/algorithm/BellmanFord.kt new file mode 100644 index 0000000..ef5079d --- /dev/null +++ b/src/main/kotlin/model/algorithm/BellmanFord.kt @@ -0,0 +1,84 @@ +package model.algorithm + +import model.graph.Edge +import model.graph.Graph +import model.graph.Vertex + +class BellmanFord(private val graph: Graph) { + 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) + flag = true + } + } + } + + 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/main/kotlin/model/algorithm/Clustering.kt b/src/main/kotlin/model/algorithm/Clustering.kt new file mode 100644 index 0000000..fe20caf --- /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() + 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 { + val map = getPartition(links, 0) + val result = map.mapKeys { vIds[it.key] ?: throw IllegalStateException() } + 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 + } + } +} + diff --git a/src/main/kotlin/model/algorithm/Dijkstra.kt b/src/main/kotlin/model/algorithm/Dijkstra.kt new file mode 100644 index 0000000..fe07ce0 --- /dev/null +++ b/src/main/kotlin/model/algorithm/Dijkstra.kt @@ -0,0 +1,62 @@ +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 + 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(Triple(edge.first.key, edge.second.key, edge.weight)) + } + currentVertex = prevVertex + } + + return if (path.isEmpty()) null else path.reversed() + } +} diff --git a/src/main/kotlin/model/algorithm/FindBridges.kt b/src/main/kotlin/model/algorithm/FindBridges.kt new file mode 100644 index 0000000..afef70b --- /dev/null +++ b/src/main/kotlin/model/algorithm/FindBridges.kt @@ -0,0 +1,60 @@ +package model.algorithm + + +import model.graph.Edge +import model.graph.Graph +import model.graph.Vertex + + +class FindBridges( + private val graph: Graph +) { + 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 -> + if (visited[vertex] != true) { + findBridgesUtil(vertex, visited, disc, low, parent, bridges, time) + } + } + + return bridges + } + + private fun findBridgesUtil( + u: Vertex, + visited: MutableMap, + disc: MutableMap, + low: MutableMap, + parent: MutableMap, + 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]!!) + } + } + } +} diff --git a/src/main/kotlin/model/algorithm/FindCycle.kt b/src/main/kotlin/model/algorithm/FindCycle.kt new file mode 100644 index 0000000..3a320bc --- /dev/null +++ b/src/main/kotlin/model/algorithm/FindCycle.kt @@ -0,0 +1,66 @@ +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) + + if (isFound) { + return path + } + + return emptyList() + } + + 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 -> continue + } + } + + color[vertex] = Color.BLACK + if (!isFound) { + path.removeLastOrNull() + } + } + + 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 diff --git a/src/main/kotlin/model/algorithm/PageRank.kt b/src/main/kotlin/model/algorithm/PageRank.kt new file mode 100644 index 0000000..78d79df --- /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> { + val ranks = mutableMapOf() + val vertices = graph.vertices + + // Initialize ranks + vertices.forEach { vertex -> + ranks[vertex] = 1.0 / vertices.size + } + + repeat(iterations) { + val newRanks = mutableMapOf() + + 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/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt new file mode 100644 index 0000000..938f9eb --- /dev/null +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -0,0 +1,25 @@ +package model.graph + +open class DirectedGraph : UndirectedGraph() { + + 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(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/Edge.kt b/src/main/kotlin/model/graph/Edge.kt new file mode 100644 index 0000000..4b39b1f --- /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..0829c09 --- /dev/null +++ b/src/main/kotlin/model/graph/Graph.kt @@ -0,0 +1,13 @@ +package model.graph + +interface Graph { + val vertices: Collection + val adjacencyList: HashMap> + + 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? + 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 new file mode 100644 index 0000000..0a98769 --- /dev/null +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -0,0 +1,89 @@ +package model.graph + +open class UndirectedGraph : Graph { + protected val _vertices = hashMapOf() + protected val _adjacencyList = hashMapOf>() + + override val vertices: Collection + get() = _vertices.values + + override val adjacencyList: HashMap> + get() = _adjacencyList + + override fun addVertex(key: Int): Vertex? { + if (_vertices[key] != null) return null + + val vertex = UndirectedVertex(key) + + _vertices[key] = vertex + _adjacencyList[vertex] = arrayListOf() + + return vertex + } + + override fun removeVertex(key: Int): Vertex? { + val vertex = _vertices[key] ?: return null + + _vertices.remove(key) + _adjacencyList.remove(vertex) + + return vertex + } + + 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(UndirectedEdge(vertex1, vertex2)) + _adjacencyList[vertex2]?.add(UndirectedEdge(vertex2, vertex1)) + + return _adjacencyList[vertex1]?.last() + } + + override fun removeEdge(first: Int, second: Int): Edge? { + 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[vertex1]?.remove(edge1) + _adjacencyList[vertex2]?.remove(edge1) + + return edge1 + } + + override fun updateVertex(key: Int, newKey: Int): Vertex? { + val vertex = _vertices[key] ?: return null + if (_vertices[newKey] != null) return null + + vertex.key = newKey + return vertex + } + + fun findVertex(key: Int) = _vertices[key] + + 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 { + override var weight: Long + get() = 1 + set(value) {} + } +} \ 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..27a3d38 --- /dev/null +++ b/src/main/kotlin/model/graph/Vertex.kt @@ -0,0 +1,5 @@ +package model.graph + +interface Vertex { + var key: Int +} \ No newline at end of file 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 diff --git a/src/main/kotlin/model/graph/WeightedGraph.kt b/src/main/kotlin/model/graph/WeightedGraph.kt new file mode 100644 index 0000000..67711f9 --- /dev/null +++ b/src/main/kotlin/model/graph/WeightedGraph.kt @@ -0,0 +1,43 @@ +package model.graph + +class WeightedGraph : UndirectedGraph() { + + override fun addEdge(first: Int, second: Int, 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/main/kotlin/model/reader/Neo4jReader.kt b/src/main/kotlin/model/reader/Neo4jReader.kt new file mode 100644 index 0000000..a37808e --- /dev/null +++ b/src/main/kotlin/model/reader/Neo4jReader.kt @@ -0,0 +1,144 @@ +package model.reader + +import model.graph.* +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 + ) + ) + tx.run( + "MATCH (g:Graph {graphName: \$graphName}) DETACH DELETE g", + mapOf( + "graphName" to graphName + ) + ) + + if (txInput == null) { + tx.commit() + tx.close() + } + } + + /** + * Save graph to Neo4j Database + */ + override fun saveGraph(graph: Graph, filepath: String, nameGraph: String) { + val transaction = session.beginTransaction() + + 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) + + graph.adjacencyList[v]?.forEach { e -> + createEdge(e, nameGraph, transaction) + } + } + + transaction.commit() + 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() + + 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())) } + + 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() + 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 diff --git a/src/main/kotlin/model/reader/Reader.kt b/src/main/kotlin/model/reader/Reader.kt new file mode 100644 index 0000000..9d41a66 --- /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, nameGraph: String): Unit + fun loadGraph(filepath: String, nameGraph: String): Graph +} \ No newline at end of file diff --git a/src/main/kotlin/model/reader/SQLiteReader.kt b/src/main/kotlin/model/reader/SQLiteReader.kt new file mode 100644 index 0000000..f72838f --- /dev/null +++ b/src/main/kotlin/model/reader/SQLiteReader.kt @@ -0,0 +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 +// } +//} +// +// 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 diff --git a/src/main/kotlin/view/HeaderView.kt b/src/main/kotlin/view/HeaderView.kt new file mode 100644 index 0000000..8ca2327 --- /dev/null +++ b/src/main/kotlin/view/HeaderView.kt @@ -0,0 +1,174 @@ +package view + +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.icons.Icons +import androidx.compose.material.icons.filled.Close +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.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import components.MyText + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HeaderView(name: String, close: () -> Unit, maximize: () -> Unit, isMaximize: Boolean, minimize: () -> Unit) { + Row( + Modifier.fillMaxWidth().height(Config.headerHeight.dp) + .background(color = Color(0xFF3D3D3D)) + .onDrag { + if (isMaximize) { + maximize() + } + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(Modifier) { + Row(Modifier.padding(start = 7f.dp, top = 7f.dp)) { + Logo() + FileButton() + } + } + Row { + MyText(name) + } + Row { + 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 +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, + spotColor = Color.Black + ).background(Color(0xFF3D3D3D)), + 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 diff --git a/src/main/kotlin/view/MainView.kt b/src/main/kotlin/view/MainView.kt new file mode 100644 index 0000000..bbc96a5 --- /dev/null +++ b/src/main/kotlin/view/MainView.kt @@ -0,0 +1,119 @@ +package view + +import Config +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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, viewModel: MenuViewModel) { + 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") { + isDijkstraMode = !isDijkstraMode + if (!isDijkstraMode) viewModel.canvasViewModel.resetEdgesColorToDefault() + }, + ImageResource("Bellman-Ford.svg") {}, + ImageResource("IslandTree.svg") {}, + ImageResource("StrongConnectivityComponent.svg") {}, + ImageResource("FindCycle.svg") { + viewModel.canvasViewModel.isEdgeFindCycleMode = !viewModel.canvasViewModel.isEdgeFindCycleMode + viewModel.canvasViewModel.resetEdgesColorToDefault() + } + ) + + 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) { imageResource -> + ImageButton(imageResourceId = imageResource.icon, onClick = imageResource.onClick, viewModel) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ImageButton(imageResourceId: String, onClick: () -> Unit, viewModel: MenuViewModel) { + Box( + modifier = Modifier + .size(440.dp, 60.dp) + .padding(1.dp) + .background(Color(0x00)) + ) { + val modifier = when (imageResourceId) { + "FindBridge.svg" -> Modifier.glowRec(viewModel.isBridgeFinded) + "Dijkstra.svg" -> Modifier.glowRec(viewModel.isDijkstraMode) + "FindCycle.svg" -> Modifier.glowRec(viewModel.canvasViewModel.isEdgeFindCycleMode) + else -> Modifier.alpha(0.2f) + } + + Image( + painter = painterResource(imageResourceId), + contentDescription = "Button Image", + modifier = modifier.size(445.dp, 59.dp).onClick(onClick = onClick), + contentScale = ContentScale.Crop + ) + } +} + +@Composable +fun MainView(mainViewModel: MainViewModel) { + + Row(Modifier.offset(0f.dp, Config.headerHeight.dp)) { + MenuView(mainViewModel.menuViewModel) + + CanvasView( + mainViewModel.canvasViewModel, + Modifier.fillMaxSize() + ) + } + + if (mainViewModel.menuViewModel.isAlgorithmMenuOpen) { + displayAlgorithmMenu( + "DownMenuAlgorithm.svg", + mainViewModel.menuViewModel + ) + } + + 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 new file mode 100644 index 0000000..cb6af38 --- /dev/null +++ b/src/main/kotlin/view/MenuView.kt @@ -0,0 +1,116 @@ +package view + +import Config +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +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.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 viewModel.MenuViewModel +import kotlin.math.roundToInt + +@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 +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 + + 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(isNodeCreating)) { isNodeCreating = !isNodeCreating } + 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( + "Algorithm.svg", + "Algorithms....svg", + Modifier.glow(viewModel.isAlgorithmMenuOpen) + ) { isAlgorithmMenuOpen = !isAlgorithmMenuOpen } + } +} + +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 diff --git a/src/main/kotlin/view/SettingsView.kt b/src/main/kotlin/view/SettingsView.kt new file mode 100644 index 0000000..755d200 --- /dev/null +++ b/src/main/kotlin/view/SettingsView.kt @@ -0,0 +1,69 @@ +package view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +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 +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) + + 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 { + content() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/canvas/CanvasView.kt b/src/main/kotlin/view/canvas/CanvasView.kt new file mode 100644 index 0000000..a2c7207 --- /dev/null +++ b/src/main/kotlin/view/canvas/CanvasView.kt @@ -0,0 +1,49 @@ +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 +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 + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Composable +fun CanvasView( + viewModel: CanvasViewModel, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.background(Color(0xFF242424)) + .onPointerEvent(PointerEventType.Scroll, onEvent = viewModel.onScroll) + .pointerInput(Unit, viewModel.onDrag) + .pointerInput(Unit) { + detectTapGestures { + viewModel.createNode(it) + } + } + .pointerHoverIcon(PointerIcon.Hand) + .onSizeChanged { + viewModel.canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) + } + .clipToBounds() + ) { + // for rerender when update + if (viewModel.edgesCount > 0) { + viewModel.edges.flatten().forEach { + EdgeCanvasView(it) + } + } + + viewModel.vertices.forEach { + VertexCanvasView(it) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/canvas/EdgeCanvasView.kt b/src/main/kotlin/view/canvas/EdgeCanvasView.kt new file mode 100644 index 0000000..e4679a1 --- /dev/null +++ b/src/main/kotlin/view/canvas/EdgeCanvasView.kt @@ -0,0 +1,70 @@ +package view.canvas + +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.unit.dp +import viewModel.canvas.EdgeCanvasViewModel + +@Composable +fun EdgeCanvasView( + viewModel: EdgeCanvasViewModel, + modifier: Modifier = Modifier +) { + + Canvas(Modifier.fillMaxSize()) { + // 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 radiusVectorFirst = vectorNorm * viewModel.first.radius.value + val radiusVectorSecond = vectorNorm * viewModel.second.radius.value + + val start = firstCenter + radiusVectorFirst + val end = secondCenter - radiusVectorSecond + + 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(radiusVectorSecond * 0.8f, 30.0), + color = viewModel.color, + strokeWidth = viewModel.strokeWidth * 0.8f + ) + + drawLine( + start = end, + end = end - rotateVector(radiusVectorSecond * 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/view/canvas/VertexCanvasView.kt b/src/main/kotlin/view/canvas/VertexCanvasView.kt new file mode 100644 index 0000000..b8c52f1 --- /dev/null +++ b/src/main/kotlin/view/canvas/VertexCanvasView.kt @@ -0,0 +1,42 @@ +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.onClick +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 components.MyText +import viewModel.canvas.VertexCanvasViewModel + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun VertexCanvasView( + viewModel: VertexCanvasViewModel, + modifier: Modifier = Modifier +) { + Box( + modifier + .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, + width = viewModel.strokeWidth.dp, + shape = CircleShape + ) + .background(color = Color(0xFF242424), shape = CircleShape) + .onClick { viewModel.onClick() } + .onDrag(onDrag = viewModel::onDrag), + contentAlignment = Alignment.Center + ) { + MyText(viewModel.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..b6ea42f --- /dev/null +++ b/src/main/kotlin/viewModel/MainViewModel.kt @@ -0,0 +1,18 @@ +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 + ) + + 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..208cfcf --- /dev/null +++ b/src/main/kotlin/viewModel/MenuViewModel.kt @@ -0,0 +1,18 @@ +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 by canvasViewModel::isNodeCreatingMode + var isEdgeCreating by canvasViewModel::isEdgeCreatingMode + var isClustering by canvasViewModel::isClustering + var isRanked by canvasViewModel::isRanked + var isBridgeFinded by canvasViewModel::isFinded + var isDijkstraMode by canvasViewModel::isDijkstraMode + var isAlgorithmMenuOpen by mutableStateOf(false) +} 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 new file mode 100644 index 0000000..e12abe1 --- /dev/null +++ b/src/main/kotlin/viewModel/canvas/CanvasViewModel.kt @@ -0,0 +1,214 @@ +package viewModel.canvas + +import Config +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.Canvas +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.algorithm.FindCycle +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 + +class CanvasViewModel( + val graph: UndirectedGraph, +) { + private val graphViewModel = UndirectedViewModel(graph, true) + + var isClustering by graphViewModel::clustering + var isRanked by graphViewModel::ranked + 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 isEdgeFindCycleMode by mutableStateOf(false) + + 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") + } + + fun createNode(offset: Offset) { + if (isNodeCreatingMode) { + val coordinates = (offset - (canvasSize / 2.0F)) * (1 / zoom) + center + val viewModel = graphViewModel.createVertex(coordinates) ?: return + + _vertices[viewModel] = VertexCanvasViewModel(viewModel, this) + _edges[getVertex(viewModel)] = ArrayList() + } + } + + init { + graphViewModel.vertices.forEach { v -> + _vertices[v] = VertexCanvasViewModel(v, this) + } + + 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) + }.toList()) + } + + edgesCount = _edges.values.flatten().size + } + + val vertices + get() = _vertices.values + + val edges + get() = _edges.values + + 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 + } + } + } + + @OptIn(ExperimentalFoundationApi::class) + val onDrag: suspend PointerInputScope.() -> Unit = { + detectDragGestures( + matcher = PointerMatcher.Primary + ) { + center -= it * (1 / zoom) + } + } + + fun onColorChange(color: Color) { + graphViewModel.onColorChange(color) + } + + fun onSizeChange(newSize: Float) { + graphViewModel.onSizeChange(newSize) + } + + fun onOrientatedChange(isOrientated: Boolean) { + this.isOrientated = isOrientated + } + + /* + * Change edges' color + * */ + fun changeEdgesColor(edges: MutableList>) { + println(edges) + graphViewModel.changeEdgesColor(edges) + } + + fun resetEdgesColorToDefault() { + println("reset") + 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 onClick(vm: VertexCanvasViewModel) { + onClickNodeEdgeCreating(vm) + onClickNodeFindCycle(vm) + onClickNodeDijkstraOn(vm) + } + + 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 + } + + fun onClickNodeFindCycle(vm: VertexCanvasViewModel) { + if (!isEdgeFindCycleMode) return + + resetEdgesColorToDefault() + println(graph) + changeEdgesColor(FindCycle(graph).calculate(vm.vertexViewModel.vertex).map { Pair(it, Color.Red) } + .toMutableList()) + } + + fun onClickNodeDijkstraOn(vm: VertexCanvasViewModel) { + if (!isDijkstraMode) { + 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 } + resetEdgesColorToDefault() + changeEdgesColor(PathWthColor.toMutableList()) + pickedNodeForDijkstra = null + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt new file mode 100644 index 0000000..372a79b --- /dev/null +++ b/src/main/kotlin/viewModel/canvas/EdgeCanvasViewModel.kt @@ -0,0 +1,16 @@ +package viewModel.canvas + +import viewModel.graph.EdgeViewModel + +class EdgeCanvasViewModel( + val first: VertexCanvasViewModel, + val second: VertexCanvasViewModel, + val edgeViewModel: EdgeViewModel, + private val canvasViewModel: CanvasViewModel, +) { + var showOrientation by canvasViewModel::isOrientated + var color by edgeViewModel::color + + 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 new file mode 100644 index 0000000..c5c8c37 --- /dev/null +++ b/src/main/kotlin/viewModel/canvas/VertexCanvasViewModel.kt @@ -0,0 +1,36 @@ +package viewModel.canvas + +import androidx.compose.ui.geometry.Offset +import viewModel.graph.VertexViewModel + +class VertexCanvasViewModel( + val vertexViewModel: VertexViewModel, + val canvasViewModel: CanvasViewModel, +) { + val color by vertexViewModel::color + val label by vertexViewModel::label + + val strokeWidth + get() = 8f * canvasViewModel.zoom + val radius + get() = vertexViewModel.radius * canvasViewModel.zoom + val offset + get() = calculateOffset() + + val textSize + get() = vertexViewModel.radius * 0.6f * canvasViewModel.zoom + + fun onDrag(it: Offset): Unit { + vertexViewModel.onDrag(it * (1f / canvasViewModel.zoom)) + } + + + fun onClick() { + canvasViewModel.onClick(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) + ) +} \ No newline at end of file diff --git a/src/main/kotlin/viewModel/graph/EdgeViewModel.kt b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt new file mode 100644 index 0000000..0ce2395 --- /dev/null +++ b/src/main/kotlin/viewModel/graph/EdgeViewModel.kt @@ -0,0 +1,29 @@ +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 + +class EdgeViewModel( + val first: VertexViewModel, + val second: VertexViewModel, + val edge: Edge, + private val _weightVisibility: State, + color: Color = Config.Edge.color, + strokeWidth: Float = Config.Edge.strokeWidth +) { + private var _weight = mutableStateOf(edge.weight) + var weight + get() = _weight.value + set(value) { + _weight.value = value + edge.weight = 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 new file mode 100644 index 0000000..f6be330 --- /dev/null +++ b/src/main/kotlin/viewModel/graph/UndirectedViewModel.kt @@ -0,0 +1,233 @@ +package viewModel.graph + +import Config +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 +import model.algorithm.Clustering +import model.algorithm.Dijkstra +import model.algorithm.FindBridges +import model.algorithm.PageRank +import model.graph.Edge +import model.graph.UndirectedGraph +import model.graph.Vertex + +class UndirectedViewModel( + private val graph: UndirectedGraph, + val showVerticesLabels: Boolean, + var groups: HashMap = hashMapOf(), + 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) + + val vertices + get() = _vertices.values + + val adjacencyList + get() = _adjacencyList + + var clustering + get() = _clustering.value + set(value) { + _clustering.value = value + groups = Clustering(graph).calculate() + updateColor() + } + + var ranked + get() = _ranked.value + set(value) { + _ranked.value = value + ranks = PageRank(graph).computePageRank(3) + + 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() + } + } + + 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] + + 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 updateColor() { + _vertices.forEach { + it.value.color = getColor(groups.getOrDefault(it.key, 0)) + } + } + + 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() + } + + fun onSizeChange(newSize: Float) { + size = newSize + _vertices.forEach { + it.value.radius = size.dp + } + updateSizes() + } + + fun createVertex(coordinates: Offset): VertexViewModel? { + val vertex = graph.addVertex(graph.vertices.last().key + 1) ?: return null + + val viewModel = VertexViewModel( + showVerticesLabels, + vertex, + coordinates.x - size, + coordinates.y - size, + getColor(groups.getOrDefault(vertex, 0)), + radius = size.dp + ) + + _vertices[vertex] = viewModel + _adjacencyList[viewModel] = ArrayList() + + return viewModel + } + + /* + * Change edges' color + * */ + fun changeEdgesColor(edges: MutableList>) { + 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) + + if (_vertices[vertex] != null) return@forEachIndexed + + val vertexViewModel = VertexViewModel( + showVerticesLabels, + vertex, + (-1000..1000).random().toFloat(), + (-1000..1000).random().toFloat(), + getColor(group), + radius = size.dp + ) + _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, + second, + (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)), + ) + + _vertices[second] = secondVertexViewModel + setOffsetEdges(second, Offset(secondVertexViewModel.x, secondVertexViewModel.y)) + } + } + + setOffsetEdges(vertex, Offset(vertexViewModel.x, vertexViewModel.y)) + } + + 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/graph/VertexViewModel.kt b/src/main/kotlin/viewModel/graph/VertexViewModel.kt new file mode 100644 index 0000000..a83ce32 --- /dev/null +++ b/src/main/kotlin/viewModel/graph/VertexViewModel.kt @@ -0,0 +1,44 @@ +package viewModel.graph + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +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 +import androidx.compose.ui.unit.dp +import model.graph.Vertex + +class VertexViewModel( + private val _labelVisible: Boolean, + val vertex: Vertex, + x: Float = 0f, + y: Float = 0f, + color: Color = Color.Black, + radius: Dp = 8f.dp, +) { + 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() + + val labelVisibility + get() = _labelVisible + + fun getKey(): Int { + return vertex.key + } + + fun onDrag(it: Offset): Unit { + x += it.x + y += it.y + } + + override fun toString(): String { + return "VertexViewModel ${vertex.key}" + } +} \ No newline at end of file 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/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 @@ + + + + + + + + + + + + + + + + + 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/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/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 @@ + + + + + + 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/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/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/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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/Inter-Regular.ttf b/src/main/resources/Inter-Regular.ttf new file mode 100644 index 0000000..5e4851f Binary files /dev/null and b/src/main/resources/Inter-Regular.ttf differ 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/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 @@ + + + + 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 @@ + + + + + + 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 @@ + + + + + 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 diff --git a/src/test/kotlin/model/algorithm/BellmanFordTest.kt b/src/test/kotlin/model/algorithm/BellmanFordTest.kt new file mode 100644 index 0000000..e8bd388 --- /dev/null +++ b/src/test/kotlin/model/algorithm/BellmanFordTest.kt @@ -0,0 +1,143 @@ +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) + } + + 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 `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 + + graph.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(), 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 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 diff --git a/src/test/kotlin/model/algorithm/FindBridgesTest.kt b/src/test/kotlin/model/algorithm/FindBridgesTest.kt new file mode 100644 index 0000000..d03c617 --- /dev/null +++ b/src/test/kotlin/model/algorithm/FindBridgesTest.kt @@ -0,0 +1,127 @@ +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 diff --git a/src/test/kotlin/model/algorithm/FindCycleTest.kt b/src/test/kotlin/model/algorithm/FindCycleTest.kt new file mode 100644 index 0000000..5cf6cdb --- /dev/null +++ b/src/test/kotlin/model/algorithm/FindCycleTest.kt @@ -0,0 +1,173 @@ +package model.algorithm + +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) + } + + 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 }) + } + + 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; + + graph.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; + + 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) + } + + 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; + + 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) + } + + 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; + + graph.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; + + 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) + } + + val result = FindCycle(graph).calculate(vertex) + 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 diff --git a/src/test/kotlin/model/algorithm/PageRankTest.kt b/src/test/kotlin/model/algorithm/PageRankTest.kt new file mode 100644 index 0000000..1baa9e3 --- /dev/null +++ b/src/test/kotlin/model/algorithm/PageRankTest.kt @@ -0,0 +1,45 @@ +package model.algorithm + +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 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(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 diff --git a/src/test/kotlin/model/graph/DirectedGraphTest.kt b/src/test/kotlin/model/graph/DirectedGraphTest.kt new file mode 100644 index 0000000..7e5c948 --- /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 diff --git a/src/test/kotlin/model/graph/UndirectedGraphTest.kt b/src/test/kotlin/model/graph/UndirectedGraphTest.kt new file mode 100644 index 0000000..2577a79 --- /dev/null +++ b/src/test/kotlin/model/graph/UndirectedGraphTest.kt @@ -0,0 +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 + +@Suppress("ClassName") +class UndirectedGraphTest { + 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.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) { + 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[vertex]) + } + + @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) + + 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`() { + 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) + } + } +} \ No newline at end of file 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 diff --git a/src/test/kotlin/model/graph/WeightedGraphTest.kt b/src/test/kotlin/model/graph/WeightedGraphTest.kt new file mode 100644 index 0000000..1f17120 --- /dev/null +++ b/src/test/kotlin/model/graph/WeightedGraphTest.kt @@ -0,0 +1,76 @@ +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 { + lateinit var graph: WeightedGraph + + @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 diff --git a/src/test/kotlin/model/reader/Neo4jReaderTest.kt b/src/test/kotlin/model/reader/Neo4jReaderTest.kt new file mode 100644 index 0000000..b9b28b8 --- /dev/null +++ b/src/test/kotlin/model/reader/Neo4jReaderTest.kt @@ -0,0 +1,143 @@ +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 + +// 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") + 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`() { + if (!IS_ENABLED) return + + 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`() { + if (!IS_ENABLED) return + + 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`() { + if (!IS_ENABLED) return + + 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`() { + if (!IS_ENABLED) return + + 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`() { + if (!IS_ENABLED) return + + try { + neo4jReader.loadGraph("", "Homka") + } catch (_: NoSuchRecordException) { + + } + } + } +} \ No newline at end of file