diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..65a39e0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ruslan Nafikov, Matvey Nazdruhin, Oleg Chabykin + +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..cd178a8 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +## About the Application + +Welcome to our Kotlin application, a comprehensive toolkit designed to facilitate the visualization, analysis, and storage of graph data. This application provides a user-friendly interface that empowers users to explore and manipulate graphs with ease. + +## Key Features + +- **Graph Plane Layout (Spring Embedder):** Efficiently arrange graph nodes for optimal visualization. +- **Highlighting Key Vertices:** Easily identify and emphasize critical nodes within the graph. +- **Search for Communities (Clustering):** Discover and group nodes into communities based on their connectivity. +- **Isolation of Strongly Connected Components (Kosarayu algorithm):** Identify and isolate subgraphs where every node is reachable from every other node. +- **Finding Bridges:** Locate and highlight the edges that, when removed, would increase the number of disconnected components. +- **Search for Cycles for a Given Vertex:** Trace and visualize cycles originating from a specified vertex. +- **Constructing a Minimal Spanning Tree (SpanningTree STP):** Build a tree that spans all vertices with the minimum total edge weight. +- **Finding the Shortest Path (Dijkstra's Algorithm):** Determine the shortest path between two vertices using Dijkstra's algorithm. +- **Finding the Shortest Path (Ford-Bellman Algorithm):** Similarly, find the shortest path but with the Ford-Bellman algorithm. + +## Important Notes + +Please be aware that the following features are currently unavailable and have not been integrated into the current version of the application: + +- **Search for Communities (Clustering)** +- **Constructing a Minimal Spanning Tree (SpanningTree STP)** + +We are actively working on these features and plan to include them in future updates. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7ad29d1 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + kotlin("jvm") version "1.9.22" + kotlin("plugin.serialization") version "1.5.31" + id("org.jetbrains.compose") version "1.6.2" + id("jacoco") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + google() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +} + +dependencies { + testImplementation(kotlin("test")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") + implementation(compose.desktop.currentOs) + implementation("io.github.microutils:kotlin-logging-jvm:2.0.11") + implementation ("org.slf4j:slf4j-simple:1.7.30") + // https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc + implementation("org.xerial:sqlite-jdbc:3.47.0.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1") +} + +tasks.test { + useJUnitPlatform() + finalizedBy("jacocoTestReport") +} + +tasks.jacocoTestReport{ + dependsOn(tasks.test) + reports{ + xml.required.set(true) + html.required.set(true) + } +} + +jacoco { + toolVersion = "0.8.12" +} + +kotlin { + jvmToolchain(21) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official 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..c020cf0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed May 15 03:47:01 MSK 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..98f31e9 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} +rootProject.name = "untitled3" + diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..ffd9148 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,1320 @@ +import algos.DiGraph +import mu.KotlinLogging +import algos.Graph +import algos.WGraph +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.input.key.* +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.* +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.random.Random +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import kotlin.math.min +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import java.io.File +import java.awt.FileDialog +import java.awt.Frame +import java.sql.DriverManager +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.math.roundToInt + +fun DrawScope.drawArrow(color: Color, start: Offset, end: Offset, n: Dp) { + val arrowHeadSize = 10.dp.toPx() + val angle = Math.atan2((end.y - start.y).toDouble(), (end.x - start.x).toDouble()) + + // Преобразуем n в пиксели + val nPx = n.toPx() + + // Вычисляем новую конечную точку с учетом отступа n + val newEnd = Offset( + (end.x - nPx * Math.cos(angle)).toFloat(), + (end.y - nPx * Math.sin(angle)).toFloat() + ) + + drawLine( + color = color, + start = start, + end = newEnd, + strokeWidth = 2.dp.toPx() + ) + + val arrowHead1 = Offset( + (newEnd.x - arrowHeadSize * Math.cos(angle - Math.PI / 6)).toFloat(), + (newEnd.y - arrowHeadSize * Math.sin(angle - Math.PI / 6)).toFloat() + ) + val arrowHead2 = Offset( + (newEnd.x - arrowHeadSize * Math.cos(angle + Math.PI / 6)).toFloat(), + (newEnd.y - arrowHeadSize * Math.sin(angle + Math.PI / 6)).toFloat() + ) + + drawLine( + color = color, + start = newEnd, + end = arrowHead1, + strokeWidth = 2.dp.toPx() + ) + drawLine( + color = color, + start = newEnd, + end = arrowHead2, + strokeWidth = 2.dp.toPx() + ) +} + +fun generateDistinctColors(n: Int): List { + val colors = mutableListOf() + val hueStep = 360.0 / n + + for (i in 0 until n) { + val hue = (i * hueStep).roundToInt() + val color = hslToColor(hue, 100, 50) + colors.add(color) + } + + return colors +} + +fun hslToColor(h: Int, s: Int, l: Int): Color { + val h = h / 360.0 + val s = s / 100.0 + val l = l / 100.0 + + val q = if (l < 0.5) l * (1 + s) else l + s - l * s + val p = 2 * l - q + + val r = (hueToRgb(p, q, h + 1.0 / 3) * 255).roundToInt() + val g = (hueToRgb(p, q, h) * 255).roundToInt() + val b = (hueToRgb(p, q, h - 1.0 / 3) * 255).roundToInt() + + return Color(r, g, b) +} + +fun hueToRgb(p: Double, q: Double, t: Double): Double { + var t = t + if (t < 0) t += 1 + if (t > 1) t -= 1 + if (t < 1.0 / 6) return p + (q - p) * 6 * t + if (t < 1.0 / 2) return q + if (t < 2.0 / 3) return p + (q - p) * (2.0 / 3 - t) * 6 + return p +} + +fun communityColoring(mp: MutableMap): MutableMap { + val s: MutableSet = mutableSetOf() + for (i in mp.keys) { + s.add(mp[i]!!) + } + val colors = generateDistinctColors(s.size) + val res: MutableMap = mutableMapOf() + val comToCol = s.zip(colors).toMap() + for (i in mp.keys) { + res[i] = comToCol[mp[i]!!]!! + } + return res +} + +fun Float.toDp(density: Density): Dp { + return with(density) { this@toDp.toDp() } +} + +fun makeLineKeysFromList(nodes: List): List> { + return nodes.zipWithNext() +} + +//function to find in Map key by the value +fun findInMap(dict: MutableMap>, circleRadius: Dp, offset: Offset): Int? { + for ((key, value) in dict) { + val distance = sqrt(((offset.x) - value.first.value).pow(2) + ((offset.y) - value.second.value).pow(2)) + if (distance <= circleRadius.value) { + return key + } + } + return null +} + +// Класс для отслеживания дейсивий пользователя для отмены (1 значение - код действия, 2 - прилагающиеся данные) +data class Action(val type: Int, val data: Any?) + + +// штука, чтобы не ломалось при различных разрешениях экрана +fun Dp.toPixels(density: Density): Float = this.value * density.density + +// инфа про кружочек +@Serializable +data class CircleData( + val x: Float, + val y: Float, +) + +//инфа про текущее состояние графа (для сохранения) (можно добавить еще какой-то инфы) +@Serializable +data class WindowStateData( + val graphMode: Boolean, + val circlesToDraw: Map, + val linesToDraw: Map, Pair>, + val switchState: Boolean, + val nodeCounter: Int +) + +//ну собсна сохранение в формат .json +fun saveToFile( + graphMode: Boolean, + circlesToDraw: Map, + linesToDraw: Map, Pair>, + switchState: Boolean, + nodeCounter: Int, + fileName: String?, + flag: Int +) { + if (flag == 1) { + val data = WindowStateData(graphMode, circlesToDraw, linesToDraw, switchState, nodeCounter) + val json = Json { + allowStructuredMapKeys = true + }.encodeToString(value = data) + val directory = File("src/main/resources/save/") + directory.mkdirs() + + if (fileName == null) { + // Получаем текущее время + val currentTime = LocalDateTime.now() + // Форматируем время в строку + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss") + val formattedTime = currentTime.format(formatter) + + val newFileName = "$formattedTime.json" + val file = File(directory, newFileName) + file.writeText(json) + } else { + if (".db" in fileName) { + val file = File(directory, fileName.substring(0, fileName.length - 3) + ".json") + file.writeText(json) + } else { + val file = File(directory, fileName) + file.writeText(json) + } + } + } + if (flag == 2) { + val data = WindowStateData(graphMode, circlesToDraw, linesToDraw, switchState, nodeCounter) + val json = Json { + allowStructuredMapKeys = true + }.encodeToString(value = data) + val directory = File("src/main/resources/save/") + directory.mkdirs() + var url: String + if (fileName == null) { + val currentTime = LocalDateTime.now() + // Форматируем время в строку + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss") + val formattedTime = currentTime.format(formatter) + + val newFileName = "$formattedTime.db" + val file = File(directory, newFileName) + url = "jdbc:sqlite:src/main/resources/save/$newFileName" + } else { + if (".json" in fileName) { + val newFileName = fileName.substring(0, fileName.length - 5) + url = "jdbc:sqlite:src/main/resources/save/$newFileName.db" + } else { + url = "jdbc:sqlite:src/main/resources/save/$fileName" + } + } + val conn = DriverManager.getConnection(url) + val stmt = conn.createStatement() + val sql = ("CREATE TABLE IF NOT EXISTS graph (" + + " json text" + + ");"); + stmt.execute(sql) + val sql1 = "INSERT INTO graph(json) VALUES(?)" + val pstmt = conn.prepareStatement(sql1) + pstmt.setString(1, json) + pstmt.executeUpdate() + conn.close() + } +} + +fun chooseFile(initialDirectory: File): File? { + val fileDialog = FileDialog(Frame(), "Выберите файл", FileDialog.LOAD) + fileDialog.isMultipleMode = false + fileDialog.directory = initialDirectory.absolutePath + fileDialog.file = "*.db;*.json" + fileDialog.isVisible = true + + return if (fileDialog.file != null) { + File(fileDialog.directory, fileDialog.file) + } else { + null + } +} + +fun loadFromFile(fileName: String): WindowStateData { + val directory = File("src/main/resources/save/") + val file = File(directory, fileName) + + if (!file.exists()) { + throw IllegalArgumentException("File not found: $fileName") + } + var jsonContent: String = "" + if (".json" in fileName) { + jsonContent = file.readText() + } + if (".db" in fileName) { + val url = "jdbc:sqlite:" + directory + "/" + fileName + println(url) + val conn = DriverManager.getConnection(url) + val stmt = conn.createStatement() + val r = "SELECT json\n" + + "FROM graph\n" + + "ORDER BY ROWID DESC\n" + + "LIMIT 1;" + val res = stmt.executeQuery(r) + jsonContent = res.getString("json") + conn.close() + } + return Json { + allowStructuredMapKeys = true + }.decodeFromString(jsonContent) +} + +//отладочная информация в консоль (вместо отладочных принтов) +private val logger = KotlinLogging.logger {} + + +//основной код +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun app(savesData: WindowStateData, selectedFile: String?) { + val firstTime = remember { mutableStateOf(true) } + val saves by remember { mutableStateOf(savesData) } + // Вся инфа которую нужно хранить + val windowState = rememberWindowState() + val density = LocalDensity.current + var windowSize by remember { mutableStateOf(windowState.size) } + var windowHeight by remember { mutableStateOf(windowState.size.height) } + var windowWidth by remember { mutableStateOf(windowState.size.width) } + var circlesToDraw by remember { mutableStateOf(mutableMapOf>()) } + var colorsForBeetweenes by remember { mutableStateOf(mapOf()) } + var louvainCommunity by remember { mutableStateOf(mapOf()) } + var linesToDraw by remember { mutableStateOf(mutableMapOf, Pair, Pair>>()) } + var graph by remember { mutableStateOf(WGraph()) } + + var louvainFlag by remember { mutableStateOf(false) } + + val bridges = remember { mutableStateOf(listOf>()) } + val isNodesToFindWay = remember { mutableStateOf(false) } + val isNodesToFindWayD = remember { mutableStateOf(false) } + val shortestWay = remember { mutableStateOf(listOf()) } + val cyclesFromNode = remember { mutableStateOf(listOf>()) } + var isCyclesFromNode by remember { mutableStateOf(false) } + var isNodesClustering by remember { mutableStateOf(false) } + var nodesInClusters by remember { mutableStateOf(mapOf()) } + + var selectedCircle by remember { mutableStateOf(null) } + var isDragging by remember { mutableStateOf(false) } + var selectedCircleToMove by remember { mutableStateOf(null) } + var startConnectingPoint by remember { mutableStateOf(null) } + var endConnectingPoint by remember { mutableStateOf(null) } + var circleRadius by remember { mutableStateOf(20.dp) } + var expanded by remember { mutableStateOf(false) } + var additionalOptionsGroup1 by remember { mutableStateOf(false) } + var additionalOptionsGroup2 by remember { mutableStateOf(false) } + var additionalOptionsGroup3 by remember { mutableStateOf(false) } + var openSettings by remember { mutableStateOf(false) } + var isColorsForBeetweenes by remember { mutableStateOf(false) } + var sccs by remember { mutableStateOf(mapOf, Color>()) } + var switchState by remember { mutableStateOf(false) } + var turnBack by remember { mutableStateOf(false) } + var iconStates by remember { mutableStateOf(false) } + var sccsFlag by remember { mutableStateOf(false) } + var spanningTreeFlag by remember { mutableStateOf(false) } + var spanningTreeEdges by remember { mutableStateOf(mutableListOf>()) } + val scaleFactor = windowHeight / 600.dp + + val colorStates by remember { // цвет темы + mutableStateOf( + mutableListOf( + Color.White, + Color.Red, + Color.Blue, + Color.Gray, + Color.Black, + ) + ) + } + + fun findSCCsInGraph(): Map, Color> { + val diGraph = DiGraph() + circlesToDraw.keys.forEach { diGraph.addNode(it) } + linesToDraw.keys.forEach { diGraph.addEdge(it.first, it.second) } + linesToDraw.keys.forEach { diGraph.addEdge(it.second, it.first) } + + logger.info { "Nodes: ${diGraph.nodes}" } + logger.info { "Edges: ${diGraph.edges}" } + + val sccst = diGraph.findSCCs().filter { it.size > 1 }.sortedBy { it.size }.associateWith { + Color( + Random.nextInt(50, 200), + Random.nextInt(50, 200), + Random.nextInt(50, 200) + ) + } + + logger.info { "SCCs: ${sccst.keys}" } + return sccst + } + + if (switchState) { // тут в зависимости от переключателя в настройках выбирается тема + colorStates[0] = Color.Black + colorStates[1] = Color.Red + colorStates[2] = Color.Yellow + colorStates[3] = Color.LightGray + colorStates[4] = Color.White + iconStates = false + } else { + colorStates[0] = Color.White + colorStates[1] = Color.Red + colorStates[2] = Color.Blue + colorStates[3] = Color.Gray + colorStates[4] = Color.Black + iconStates = true + } + + val actionStack = remember { mutableStateListOf() } + + var selectedOption by remember { mutableStateOf(1) } + var nodeCounter by remember { mutableStateOf(0) } + var dragOffset by remember { mutableStateOf(Offset.Zero) } + + if (firstTime.value) { + if (saves.graphMode) { + graph = DiGraph() + } + switchState = saves.switchState + nodeCounter = saves.nodeCounter + saves.circlesToDraw.forEach { (key, data) -> + circlesToDraw[key] = Pair(data.x.toDp(density), data.y.toDp(density)) + graph.addNode(key) + } + saves.linesToDraw.forEach { (key, data) -> + linesToDraw[key] = Pair( + Pair(data.first.x.toDp(density), data.first.y.toDp(density)), + Pair(data.second.x.toDp(density), data.second.y.toDp(density)) + ) + graph.addEdge(key.first, key.second, 1) + + } + firstTime.value = false + } + Column { // начало UI + Row( // всякие модификаторы для того чтобы было красиво + modifier = Modifier + .fillMaxWidth() + .background(colorStates[0]) + .padding(8.dp), + horizontalArrangement = Arrangement.Start + ) { //тут уже всякие нажимаемые элементы + IconButton(onClick = { // в частности это кнопка настроек + openSettings = false + isNodesToFindWay.value = false + isNodesToFindWayD.value = false + expanded = true // штука отслеживающая открытие DropDownMenu + additionalOptionsGroup1 = false + additionalOptionsGroup2 = false + additionalOptionsGroup3 = false + louvainFlag = false + spanningTreeFlag = false + bridges.value = listOf() + shortestWay.value = listOf() + + }) { + val imageResource = if (iconStates) { + painterResource("img/logo(Black).png") + } else { + painterResource("img/logo(white).png") + } + Image( // Кортинка + painter = imageResource, + contentDescription = "Параметры", + modifier = Modifier.size(30.dp * scaleFactor) // Размер изображения + ) + } // конец кнопки настроек + + DropdownMenu( // само меню после нажатия кнопки настроек + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.background(colorStates[0]).padding(1.dp).waterfallPadding().shadow( + elevation = 8.dp, + spotColor = colorStates[4], + ambientColor = colorStates[0], + ) + + ) { + DropdownMenuItem(onClick = { + logger.info { "Option 1 clicked" } + additionalOptionsGroup1 = true + additionalOptionsGroup2 = false + additionalOptionsGroup3 = false + }) { + Text("Первая группа алгоритмов", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + if (additionalOptionsGroup1) { + DropdownMenuItem(onClick = { + logger.info { "Addtional Option" } + expanded = false + isNodesToFindWayD.value = true + additionalOptionsGroup1 = false + }) { + Text("Дейкстра", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + DropdownMenuItem(onClick = { + logger.info { "Addtional Option" } + expanded = false + isNodesToFindWay.value = true + additionalOptionsGroup1 = false + }) { + Text("Форд-Беллман", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + DropdownMenuItem(onClick = { + // случайная раскладка графа на плоскости + circlesToDraw.forEach { (key, _) -> + circlesToDraw[key] = Pair( + Dp(Random.nextFloat() * windowWidth.value.toInt()), + Dp(Random.nextInt(0, windowHeight.value.toInt()).toFloat()) + ) + } + linesToDraw.forEach { (key, _) -> + linesToDraw[key] = Pair(circlesToDraw[key.first]!!, circlesToDraw[key.second]!!) + } + expanded = false + additionalOptionsGroup1 = false + }) { + Text("Разложить граф случайно", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + DropdownMenuItem(onClick = { + val qwerty = Graph.SpringEmbedder().layout(graph) + //раскладка графа алгоритмом от Руслана + circlesToDraw.forEach { (key, _) -> + circlesToDraw[key] = Pair( + Dp(qwerty[key]!!.first.toFloat() * windowWidth.value / 30 + windowWidth.value / 2), + Dp(qwerty[key]!!.second.toFloat() * windowHeight.value / 30 + windowHeight.value / 2) + ) + } + linesToDraw.forEach { (key, _) -> + linesToDraw[key] = Pair(circlesToDraw[key.first]!!, circlesToDraw[key.second]!!) + } + expanded = false + additionalOptionsGroup1 = false + }) {// название поменять, на название алгоритма + Text( + "Spring Embedder (Раскладка графа)", + color = colorStates[4], + fontSize = 12.sp * scaleFactor + ) + } + DropdownMenuItem(onClick = { + logger.info { "Additional Option 2 clicked" } + louvainCommunity = communityColoring(graph.LouvainAlgorithm()) + logger.info { louvainCommunity } + + louvainFlag = true + expanded = false + additionalOptionsGroup1 = false + + }) { + Text("Выделение сообществ", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + } + DropdownMenuItem(onClick = { + logger.info { "Option 2 clicked" } + additionalOptionsGroup2 = true + additionalOptionsGroup1 = false + additionalOptionsGroup3 = false + }) { + Text("Вторая группа алгоритмов", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + if (additionalOptionsGroup2) { + DropdownMenuItem(onClick = {// выделение компоненты сильной связности + logger.info { "Additional Option 1 clicked" } + expanded = false + additionalOptionsGroup2 = false + sccs = findSCCsInGraph() + sccsFlag = true + }) { + Text( + "Выделение компонент сильной связности", + color = colorStates[4], + fontSize = 12.sp * scaleFactor + ) + } + DropdownMenuItem(onClick = { //выделение сообществ + logger.info { "Additional Option 3 clicked" } + expanded = false + nodesInClusters = graph.betweennessCentrality() + isNodesClustering = true + logger.info { nodesInClusters } + }) { + Text("Ключевые вершины", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + DropdownMenuItem(onClick = { // поиск мостов (как работают - смотреть в /algos) + logger.info { "Additional Option 2 clicked" } + expanded = false + additionalOptionsGroup2 = false + bridges.value = graph.findBridges() + selectedOption = 0 + }) { + Text("Поиск мостов", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + DropdownMenuItem(onClick = { // поиск циклов (Сделать!) Сделал))) + logger.info { "Additional Option 3 clicked" } + expanded = false + additionalOptionsGroup2 = false + isCyclesFromNode = true + selectedOption = 0 + + }) { + Text( + "Поиск циклов для заданной вершины", + color = colorStates[4], + fontSize = 12.sp * scaleFactor + ) + } + DropdownMenuItem(onClick = { // мин. остовное дерево (сделать!) Это пусть тоже Олег делает + logger.info { "Additional Option 1 clicked" } + expanded = false + additionalOptionsGroup3 = false + selectedOption = 0 + spanningTreeFlag = true + spanningTreeEdges = graph.getSpanningTree().edges + logger.info { spanningTreeEdges } + }) { + Text( + "Построение минимального остовного дерева", + color = colorStates[4], + fontSize = 12.sp * scaleFactor + ) + } + } + + DropdownMenuItem(onClick = { // просто открывается маленькое меню настроек (там смена темы и сохранение) + logger.info { "settings" } + expanded = false + openSettings = true + }) { + Text("Параметры", color = colorStates[4], fontSize = 12.sp * scaleFactor) + } + + } + RadioButton( // это переключатели на главном окне (соединение / создание / редактирование узлов и отмена) + selected = selectedOption == 1, // это создание + onClick = { selectedOption = 1 }, + colors = RadioButtonDefaults.colors( + unselectedColor = colorStates[4], // Цвет неактивного радиобаттона + selectedColor = Color.Cyan // Цвет активного радиобаттона + ) + ) + Text( + "Создать узлы", + modifier = Modifier.align(Alignment.CenterVertically), + color = colorStates[4], + fontSize = 12.sp * scaleFactor + ) + + RadioButton( + selected = selectedOption == 2, // соединение + onClick = { selectedOption = 2 }, + colors = RadioButtonDefaults.colors( + unselectedColor = colorStates[4], // Цвет неактивного радиобаттона + selectedColor = Color.Magenta // Цвет активного радиобаттона + ) + + ) + Text( + "Соединить узлы", + modifier = Modifier.align(Alignment.CenterVertically), + color = colorStates[4], + fontSize = 12.sp * scaleFactor + ) + + RadioButton( // это редактирование узлов + selected = selectedOption == 4, + onClick = { selectedOption = 4 }, + colors = RadioButtonDefaults.colors( + unselectedColor = colorStates[4], // Цвет неактивного радиобаттона + selectedColor = Color.Cyan // Цвет активного радиобаттона + ) + ) + Text( + "Редактировать", + modifier = Modifier.align(Alignment.CenterVertically), + color = colorStates[4], + fontSize = 12.sp * scaleFactor + ) + + IconButton(onClick = { // Отмена + turnBack = true + }) { + + val imageResource = if (iconStates) { + painterResource("img/nazad(Black).png") + } else + painterResource("img/nazad(White).png") + + Image( + painter = imageResource, // Замените на путь к вашему изображению + contentDescription = "Back", + modifier = Modifier.size(30.dp * scaleFactor) // Размер изображения + ) + } + if (turnBack) { // здесь откат происходит с помощью хранения действий в стеке + // Pro tips: сделай ограничение размера стека, типа самое глубокое чтобы удалялось при размере стека >50? + turnBack = false + if (actionStack.isNotEmpty()) { + val lastAction = actionStack.removeLast() + when (lastAction.type) { + 1 -> { // в стеке действий код 1 значит создание узла (значит мы удаляем) + circlesToDraw.remove(lastAction.data as Int) + nodeCounter-- + graph.removeNode(lastAction.data) + } + + 2 -> { // отмена линии + val (start, end) = lastAction.data as Pair<*, *> + linesToDraw.remove(Pair(start, end)) + graph.removeEdge(start as Int, end as Int) + } + + 3 -> { // отмена передвижения + val (key, pos, lines) = lastAction.data as Triple, List>> + circlesToDraw[key] = pos + graph.addNode(key) + for (i in lines) { + graph.addEdge(i.first, i.second, 1) + linesToDraw[i] = Pair(circlesToDraw[i.first]!!, circlesToDraw[i.second]!!) + } + } + } + } + } + } + Box(modifier = Modifier + .fillMaxSize() + + .background(colorStates[0]) + + .onPreviewKeyEvent { event -> // обработка действий пользователя на клавиатуре (не работает) + logger.info { event.key } + logger.info { event.isCtrlPressed } + if (event.key == Key.Z && event.isCtrlPressed) { + logger.info { actionStack } + if (actionStack.isNotEmpty()) { + val lastAction = actionStack.removeLast() + when (lastAction.type) { // повторение функций кнопки отмена + 1 -> { + circlesToDraw.remove(lastAction.data as Int) + nodeCounter-- + graph.removeNode(lastAction.data) + } + + 2 -> { + val (start, end) = lastAction.data as Pair<*, *> + linesToDraw.remove(Pair(start, end)) + graph.removeEdge(start as Int, end as Int) + } + + 3 -> { + val (key, pos, lines) = lastAction.data as Triple, List>> + circlesToDraw[key] = pos + graph.addNode(key) + for (i in lines) { + graph.addEdge(i.first, i.second, 1) + linesToDraw[i] = Pair(circlesToDraw[i.first]!!, circlesToDraw[i.second]!!) + } + } + } + } + true + } else { + false + } + } + .onPointerEvent(PointerEventType.Scroll) { // кручение колесика (уменьшает / увеличивает кружочки) + if (it.changes.first().scrollDelta.y > 0) { + circleRadius = (circleRadius.value - 0.3F).toDp() + if (circleRadius.value < 4) { + circleRadius = 4.dp + } + } else { + circleRadius = (circleRadius.value + 0.3F).toDp() + if (circleRadius.value > 25) { + circleRadius = 25.dp + } + } + } + .pointerInput(Unit) { // нажатие на поле (в зависимости от выбранного радиобаттона либо создаст круг, либо подсветит кружок) + detectTapGestures(onTap = { offset -> + openSettings = false + bridges.value = listOf() + isColorsForBeetweenes = false + louvainFlag = false + spanningTreeFlag = false + shortestWay.value = listOf() + val hitCircle = findInMap(circlesToDraw, circleRadius, offset) + if (hitCircle != null && isCyclesFromNode) { + cyclesFromNode.value = graph.findCyclesFromNode(hitCircle) + logger.info { graph.findCyclesFromNode(hitCircle) } + } else + isCyclesFromNode = false + if (isNodesToFindWay.value || isNodesToFindWayD.value) { + if (hitCircle != null) { + if (startConnectingPoint == null) { + startConnectingPoint = hitCircle + } else if (endConnectingPoint == null && startConnectingPoint != hitCircle) { + endConnectingPoint = hitCircle + val temp: List? = if (isNodesToFindWayD.value) { + graph.shortestPathD(startConnectingPoint!!, endConnectingPoint!!) + } else { + graph.shortestPathBF(startConnectingPoint!!, endConnectingPoint!!) + } + if (temp != null) { + shortestWay.value = temp + startConnectingPoint = null + endConnectingPoint = null + isNodesToFindWay.value = false + isNodesToFindWayD.value = false + } else { + startConnectingPoint = null + endConnectingPoint = null + } + } else { + startConnectingPoint = null + endConnectingPoint = null + } + } else { + isNodesToFindWay.value = false + isNodesToFindWayD.value = false + } + } else { + when (selectedOption) { + 1 -> { + sccsFlag = false + isNodesClustering = false + actionStack.add(Action(1, nodeCounter)) + circlesToDraw = circlesToDraw.toMutableMap() + .apply { this[nodeCounter] = Pair(offset.x.toDp(), offset.y.toDp()) } + logger.info { offset } + graph.addNode(nodeCounter) + nodeCounter += 1 + + } + + 2 -> { + sccsFlag = false + isNodesClustering = false + + val hitCircle = findInMap(circlesToDraw, circleRadius, offset) + if (hitCircle != null) { + if (startConnectingPoint == null) { + startConnectingPoint = hitCircle + } else if (endConnectingPoint == null && startConnectingPoint != hitCircle) { + endConnectingPoint = hitCircle + if ((!(Pair( + endConnectingPoint, + startConnectingPoint + ) in linesToDraw && !saves.graphMode) || Pair( + startConnectingPoint, + endConnectingPoint + ) in linesToDraw) + ) { + actionStack.add( + Action( + 2, + Pair(startConnectingPoint!!, endConnectingPoint!!) + ) + ) + linesToDraw = linesToDraw.toMutableMap().apply { + this[Pair(startConnectingPoint!!, endConnectingPoint!!)] = + Pair( + circlesToDraw[startConnectingPoint]!!, + circlesToDraw[endConnectingPoint]!! + ) + } + graph.addEdge(startConnectingPoint!!, endConnectingPoint!!, 1) + + } + startConnectingPoint = null + endConnectingPoint = null + } else { + startConnectingPoint = null + endConnectingPoint = null + } + } + } + + 4 -> { + selectedCircle = findInMap(circlesToDraw, circleRadius, offset) + } + } + } + }) + } + .onSizeChanged { newSize -> // обработка изменения размеров приложения + val temp = with(density) { DpSize(newSize.width.toDp(), newSize.height.toDp()) } + if (temp != windowSize) { + windowSize = temp + windowWidth = with(density) { newSize.width.toDp() } + windowHeight = with(density) { newSize.height.toDp() } + } + } + .pointerInput(Unit) { + detectDragGestures( // обработка перемещательных действий пользователя (кружка или всей плоскости) + onDragStart = { offset -> + selectedCircle = null + selectedCircleToMove = findInMap(circlesToDraw, circleRadius, offset) + isDragging = selectedCircleToMove == null + + }, + onDragEnd = { + selectedCircle = null + selectedCircleToMove = null + isDragging = false + }, + onDragCancel = { + selectedCircle = null + selectedCircleToMove = null + isDragging = false + }, + onDrag = { change, dragAmount -> + if (isDragging) { + selectedCircle = null + // Перемещаем все круги и линии + circlesToDraw = circlesToDraw.mapValues { (_, value) -> + Pair(value.first + dragAmount.x.toDp(), value.second + dragAmount.y.toDp()) + } as MutableMap> + linesToDraw = linesToDraw.mapValues { (_, value) -> + Pair( + Pair( + value.first.first + dragAmount.x.toDp(), + value.first.second + dragAmount.y.toDp() + ), + Pair( + value.second.first + dragAmount.x.toDp(), + value.second.second + dragAmount.y.toDp() + ) + ) + } as MutableMap, Pair, Pair>> + } + selectedCircleToMove?.let { circleId -> + val newX = circlesToDraw[circleId]!!.first + dragAmount.x.toDp() + val newY = circlesToDraw[circleId]!!.second + dragAmount.y.toDp() + circlesToDraw = circlesToDraw.toMutableMap().apply { this[circleId] = Pair(newX, newY) } + dragOffset += change.positionChange() + + // Update connected lines + linesToDraw = linesToDraw.toMutableMap().apply { + for ((key, value) in this) { + if (key.first == circleId) { + this[key] = Pair(Pair(newX, newY), value.second) + } else if (key.second == circleId) { + this[key] = Pair(value.first, Pair(newX, newY)) + } + } + } + } + } + ) + }.onSizeChanged { newSize -> + val temp = with(density) { DpSize(newSize.width.toDp(), newSize.height.toDp()) } + if (temp != windowSize) { + windowSize = temp + windowWidth = with(density) { newSize.width.toDp() } + windowHeight = with(density) { newSize.height.toDp() } + } + }) + { + val shortway = makeLineKeysFromList(shortestWay.value) + + Canvas(modifier = Modifier.align(Alignment.TopStart)) { // тут начинается отрисовка всего непотребства на экране + val canvasWidth = size.width + val canvasHeight = size.height + // Отрисовка линий + val allPairs = cyclesFromNode.value.toMutableList().flatMap { cycle -> + val pairs = cycle.zipWithNext().map { (a, b) -> Pair(a, b) } + if (cycle.size > 1) { + pairs + Pair(cycle.last(), cycle.first()) + } else { + pairs + } + } + for ((key, value) in linesToDraw) { + var col = colorStates[4] + if (spanningTreeFlag) { + if (key in spanningTreeEdges || Pair(key.second, key.first) in spanningTreeEdges) { + col = Color.Green + } + } + if (key in allPairs || Pair(key.second, key.first) in allPairs) { + col = Color.Magenta + } + if (Pair(key.first, key.second) in bridges.value || Pair(key.second, key.first) in bridges.value) { + col = Color.Magenta + } + if (Pair(key.first, key.second) in shortway || Pair(key.second, key.first) in shortway) { + col = Color.Green + } + if (!saves.graphMode) { + drawLine( + color = col, + start = Offset( + value.first.first.value - canvasWidth / 2, + value.first.second.value - canvasHeight / 2 + ), + end = Offset( + value.second.first.value - canvasWidth / 2, + value.second.second.value - canvasHeight / 2 + ), + strokeWidth = 2f + ) + } else { + drawArrow( + color = col, + start = Offset( + value.first.first.value - canvasWidth / 2, + value.first.second.value - canvasHeight / 2 + ), + end = Offset( + value.second.first.value - canvasWidth / 2, + value.second.second.value - canvasHeight / 2 + ), + circleRadius + ) + } + } + + // Отрисовка кругов + val uniqueVertices = cyclesFromNode.value.flatten().toSet() + for ((key, value) in circlesToDraw) { + var col = colorStates[1] + + if (louvainFlag) { + col = louvainCommunity[key]!! + } + + if (isNodesClustering) { + val maxValue = nodesInClusters.values.maxOf { it } + val onePartValue = maxValue / 200 + col = Color( + red = min((75F + nodesInClusters[key]!! / onePartValue).toInt(), 255), + min((10F + nodesInClusters[key]!! / onePartValue).toInt(), 255), + min((10F + nodesInClusters[key]!! / onePartValue).toInt(), 255) + ) + } + if (isCyclesFromNode && cyclesFromNode.value.isNotEmpty()) { + if (key in uniqueVertices) { + col = Color.Blue + } + if (key == cyclesFromNode.value.first()[0]) + col = Color.Cyan + } else if (shortestWay.value.isNotEmpty() && (key == shortestWay.value.first() || key == shortestWay.value.last())) { + col = Color.Cyan + } else if (key in shortestWay.value) { + col = Color.Blue + } + drawCircle( + color = col, + radius = circleRadius.value, + center = Offset(value.first.value - canvasWidth / 2, value.second.value - canvasHeight / 2), + style = Fill + ) + // Проверка, является ли круг выбранным или перемещаемым + if (selectedCircle == key || selectedCircleToMove == key || startConnectingPoint == key) { + drawCircle( + color = colorStates[2], + radius = circleRadius.value + 1, + center = Offset( + value.first.value - canvasWidth / 2, + value.second.value - canvasHeight / 2 + ), + style = Stroke(width = 2.dp.toPx()) + ) + } // если придумаешь как делать текст на кружочках будешь крутым (я не смог) + //drawText( + // textLayoutResult = TextLayoutResult(layoutInput=(),) + //) + + } + if (sccsFlag) { + sccs.keys.forEach { scc -> + val rColor = sccs[scc]!! + scc.forEach { node -> + val position = circlesToDraw[node]!! + drawCircle( + color = rColor, + radius = circleRadius.value + 1, + center = Offset( + position.first.value - canvasWidth / 2, + position.second.value - canvasHeight / 2 + ), + style = Fill + ) + } + } + } + } + cyclesFromNode.value = listOf() + +// Отображаем всплывающее окно, если круг выбран + selectedCircle?.let { key -> + Window(onCloseRequest = { selectedCircle = null }, + state = WindowState( + position = WindowPosition( + (circlesToDraw[key]!!.first), + (circlesToDraw[key]!!.second) + ), size = DpSize(250.dp, 250.dp) + ), + content = { + Box( + modifier = Modifier.padding(1.dp).fillMaxSize().background(colorStates[0]), + contentAlignment = Alignment.Center + ) { + Text("Редактирование узла", color = colorStates[4]) + // вот сюда можно добавить еще всякого полезного, типа настройки веса ребра как нибудь, я не придумал) + Button(onClick = { + graph.removeNode(key) + val listToRemove = mutableListOf>() + for (i in linesToDraw.keys) { + if (i.first == key || i.second == key) { + listToRemove.add(i) + } + } + for (i in listToRemove) { + linesToDraw.remove(i) + } + actionStack.add( + Action( + 3, + Triple(key, circlesToDraw[key], listToRemove) + ) + ) + circlesToDraw.remove(key) + selectedCircle = null + + + //linesToDraw = newLinesToDraw as MutableMap, Pair, Pair>> + }) { + Text("Удалить вершину", color = colorStates[4]) + } + } + } + ) + } + } + } + if (openSettings) { // всплывающее окно настроек + + Window(onCloseRequest = { openSettings = false }, + focusable = true, + alwaysOnTop = false, + title = "settings", + state = WindowState( + position = WindowPosition(windowWidth / 2, windowHeight / 2), + size = DpSize(300.dp, 300.dp) + ), + content = { + Box( + modifier = Modifier.padding(1.dp).fillMaxSize().background(colorStates[0]), + contentAlignment = Alignment.Center + ) { + Column { + Text("Выбрать тему", color = colorStates[4]) + Spacer(modifier = Modifier.height(16.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Тёмная тема:", color = colorStates[4]) + Spacer(modifier = Modifier.width(8.dp)) + Switch( + checked = switchState, + onCheckedChange = { switchState = it } + ) + } + Button(onClick = { + val circlesToDrawInPixels = circlesToDraw.mapValues { (_, position) -> + CircleData( + x = position.first.toPixels(density), + y = position.second.toPixels(density), + ) + } + val linesToDrawInPixels = linesToDraw.mapValues { (_, position) -> + Pair( + CircleData( + x = position.first.first.toPixels(density), + y = position.first.second.toPixels(density) + ), CircleData( + x = position.second.first.toPixels(density), + y = position.second.second.toPixels(density) + ) + ) + } + saveToFile( + graphMode = saves.graphMode, + circlesToDrawInPixels, + linesToDrawInPixels, + switchState, + nodeCounter, + selectedFile, + flag = 1 + ) + openSettings = false + }) { + Text("Сохранить граф в .json") + } + Button(onClick = { + val circlesToDrawInPixels = circlesToDraw.mapValues { (_, position) -> + CircleData( + x = position.first.toPixels(density), + y = position.second.toPixels(density), + ) + } + val linesToDrawInPixels = linesToDraw.mapValues { (_, position) -> + Pair( + CircleData( + x = position.first.first.toPixels(density), + y = position.first.second.toPixels(density) + ), CircleData( + x = position.second.first.toPixels(density), + y = position.second.second.toPixels(density) + ) + ) + } + saveToFile( + graphMode = saves.graphMode, circlesToDrawInPixels, + linesToDrawInPixels, + switchState, + nodeCounter, + selectedFile, + flag = 2 + ) + openSettings = false + }) { + Text("Сохранить граф в .db") + } + } + } + } + ) + + } +} + +@Composable +fun mainScreen(onStartClick: () -> Unit, onFileSelected: (File?) -> Unit, onGraphModeSelected: (Boolean) -> Unit) { + var selectedFile by remember { mutableStateOf(null) } + val saveDirectory = File("src/main/resources/save/") + var graphMode by remember { mutableStateOf(false) } + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Spacer(modifier = Modifier.height(16.dp)) + + Text("Выберите режим графа для создания") + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = graphMode, + onClick = { + graphMode = true + onGraphModeSelected(true) + } + ) + Text("Ориентированный") + Spacer(modifier = Modifier.width(16.dp)) + RadioButton( + selected = !graphMode, + onClick = { + graphMode = false + onGraphModeSelected(false) + } + ) + Text("Неориентированный") + } + + Button(onClick = onStartClick) { + Text("Построить граф") + } + Button(onClick = { selectedFile = chooseFile(saveDirectory); onFileSelected(selectedFile) }) { + Text("Загрузить сохранение") + } + selectedFile?.let { file -> + onFileSelected(file) + } + } +} + + +fun main() = application { + val density = LocalDensity.current + val windowSize = with(density) { DpSize(800.dp.value.toInt().toDp(), 600.dp.value.toInt().toDp()) } + + var showMainScreen by remember { mutableStateOf(true) } + var selectedFile by remember { mutableStateOf(null) } + var graphMode by remember { mutableStateOf(false) } + + if (showMainScreen) { + Window(onCloseRequest = ::exitApplication) { + mainScreen( + onStartClick = { showMainScreen = false }, + onFileSelected = { file -> selectedFile = file; showMainScreen = false }, + onGraphModeSelected = { mode -> graphMode = mode } + ) + } + } else { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(size = windowSize), + title = "Connect-a-Lot", + focusable = true + ) { + val initialData = if (selectedFile != null) { + loadFromFile(selectedFile!!.name) + } else { + WindowStateData( + graphMode = graphMode, + circlesToDraw = mapOf(), + linesToDraw = mapOf(), + switchState = false, + nodeCounter = 0 + ) + } + app(initialData, selectedFile?.name) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/algos/DGraph.kt b/src/main/kotlin/algos/DGraph.kt new file mode 100644 index 0000000..ca0e292 --- /dev/null +++ b/src/main/kotlin/algos/DGraph.kt @@ -0,0 +1,157 @@ +package algos + +import java.util.* + +open class DiGraph() : WGraph(){ + + override fun addEdge(n: Int, m: Int, weight: Int) { + if (Pair(n, m) !in edges) { + edges.add(Pair(n, m)) + weights[Pair(n, m)] = weight + adjacencyList.getOrPut(n) { mutableListOf() }.add(m) + } + } + + override fun findCyclesFromNode(node: Int): List> { + val cycles = mutableListOf>() + val visited = mutableSetOf() + val path = LinkedHashSet() + + fun dfs(currentNode: Int) { + if (path.contains(currentNode)) { + // Cycle detected + val cycle = path.toList().subList(path.indexOf(currentNode), path.size) + cycles.add(cycle) + return + } + + if (visited.contains(currentNode)) { + return + } + + visited.add(currentNode) + path.add(currentNode) + + for (neighbor in adjacencyList[currentNode] ?: emptyList()) { + dfs(neighbor) + } + + path.remove(currentNode) + } + + dfs(node) + return cycles + } + + +override fun findBridges(): List> { + val bridges = mutableListOf>() + val visited = mutableSetOf() + val discoveryTime = mutableMapOf() + val low = mutableMapOf() + val stack = Stack() + var time = 0 + + fun dfs(node: Int, parent: Int) { + visited.add(node) + discoveryTime[node] = time + low[node] = time + time++ + stack.push(node) + + for (neighbor in adjacencyList[node] ?: emptyList()) { + if (!visited.contains(neighbor)) { + dfs(neighbor, node) + low[node] = minOf(low[node]!!, low[neighbor]!!) + } else if (stack.contains(neighbor)) { + low[node] = minOf(low[node]!!, discoveryTime[neighbor]!!) + } + } + + if (low[node] == discoveryTime[node]) { + val scc = mutableListOf() + while (true) { + val v = stack.pop() + scc.add(v) + if (v == node) break + } + if (scc.size == 1) { + bridges.add(Pair(parent, node)) + } + } + } + + for (node in nodes) { + if (!visited.contains(node)) { + dfs(node, -1) + } + } + + return bridges + } + +// Выделение компонент сильной связности + + fun transpose(): DiGraph { + val transposedGraph = DiGraph() + for (node in nodes) { + transposedGraph.addNode(node) + } + for ((from, neighbors) in adjacencyList) { + for (to in neighbors) { + transposedGraph.addEdge(to, from, 1) + } + } + return transposedGraph + } + fun findSCCs(): List> { + val visited = mutableSetOf() + val stack = Stack() + + // Первый проход DFS + fun fillOrder(v: Int) { + visited.add(v) + for (neighbor in adjacencyList[v] ?: emptyList()) { + if (!visited.contains(neighbor)) { + fillOrder(neighbor) + } + } + stack.push(v) + } + + for (node in nodes) { + if (!visited.contains(node)) { + fillOrder(node) + } + } + + // Транспонирование графа + val transposedGraph = transpose() + + // Сброс посещенных вершин для второго прохода + visited.clear() + + // Второй проход DFS + val sccs = mutableListOf>() + fun dfs(v: Int, scc: MutableList) { + visited.add(v) + scc.add(v) + for (neighbor in transposedGraph.adjacencyList[v] ?: emptyList()) { + if (!visited.contains(neighbor)) { + dfs(neighbor, scc) + } + } + } + + while (!stack.isEmpty()) { + val v = stack.pop() + if (!visited.contains(v)) { + val scc = mutableListOf() + dfs(v, scc) + sccs.add(scc) + } + } + + return sccs + } +} diff --git a/src/main/kotlin/algos/Graph.kt b/src/main/kotlin/algos/Graph.kt new file mode 100644 index 0000000..b53307e --- /dev/null +++ b/src/main/kotlin/algos/Graph.kt @@ -0,0 +1,215 @@ +package algos +import androidx.compose.ui.graphics.Color +import java.util.* +import kotlin.collections.ArrayDeque +import kotlin.math.sqrt + +open class Graph { + val nodes = mutableListOf() + val edges = mutableListOf>() + val adjacencyList = mutableMapOf>() + + fun addNode(n: Int) { + nodes.add(n) + adjacencyList[n] = mutableListOf() + } + + open fun addEdge(n: Int, m: Int) { + if (Pair(n, m) !in edges) { + edges.add(Pair(n, m)) + adjacencyList[n]?.add(m) + adjacencyList[m]?.add(n) + } + } + + fun removeNode(n: Int) { + nodes.remove(n) + adjacencyList.remove(n) + edges.removeAll { it.first == n || it.second == n } + for ((_, neighbors) in adjacencyList) { + neighbors.remove(n) + } + } + + fun removeEdge(n: Int, m: Int) { + edges.remove(Pair(n, m)) + edges.remove(Pair(m, n)) + adjacencyList[n]?.remove(m) + adjacencyList[m]?.remove(n) + } + + open fun findBridges(): List> { + val bridges = mutableListOf>() + val visited = mutableSetOf() + val discoveryTime = mutableMapOf() + val low = mutableMapOf() + var time = 0 + + fun dfs(node: Int, parent: Int) { + visited.add(node) + discoveryTime[node] = time + low[node] = time + time++ + + for (neighbor in adjacencyList[node] ?: emptyList()) { + if (!visited.contains(neighbor)) { + dfs(neighbor, node) + low[node] = minOf(low[node]!!, low[neighbor]!!) + if (low[neighbor] == discoveryTime[neighbor]) { + bridges.add(Pair(node, neighbor)) + } + } else if (neighbor != parent) { + low[node] = minOf(low[node]!!, discoveryTime[neighbor]!!) + } + } + } + + for (node in nodes) { + if (!visited.contains(node)) { + dfs(node, -1) + } + } + + return bridges + } + + open class SpringEmbedder { + private val smoothingFactor = 0.1 + private val iterations = 500 + private val temperature = 0.9 + + fun layout(graph: Graph): Map> { + val layout = mutableMapOf>() + val forces = mutableMapOf>() + val temperature = mutableMapOf() + + // Initialize the location of vertices with random coordinates + for (node in graph.nodes) { + layout[node] = Pair(Math.random(), Math.random()) + forces[node] = Pair(0.0, 0.0) + temperature[node] = 1.0 + } + + // Calculate the forces acting on the vertices + for (i in 0 until iterations) { + for (node in graph.nodes) { + forces[node] = Pair(0.0, 0.0) + for (neighbor in graph.adjacencyList[node] ?: emptyList()) { + val deltaX = layout[node]!!.first - layout[neighbor]!!.first + val deltaY = layout[node]!!.second - layout[neighbor]!!.second + val distance = sqrt(deltaX * deltaX + deltaY * deltaY) + val repulsion = smoothingFactor * smoothingFactor / distance // Сила отталкивания + forces[node] = + Pair(forces[node]!!.first + repulsion * deltaX, forces[node]!!.second + repulsion * deltaY) + } + } + + // Update vertex locations + for (node in graph.nodes) { + val forceX = forces[node]!!.first + val forceY = forces[node]!!.second + val length = sqrt(forceX * forceX + forceY * forceY) + if (length > 0) { + val deltaX = forceX * temperature[node]!! / length + val deltaY = forceY * temperature[node]!! / length + layout[node] = Pair(layout[node]!!.first + deltaX, layout[node]!!.second + deltaY) + } + // Decrease temperature + temperature[node] = temperature[node]!! * this.temperature + } + } + + return layout + } + } + + fun betweennessCentrality(): Map { + val betweenness = mutableMapOf() + val stack = mutableListOf() + val predecessors = mutableMapOf>() + val sigma = mutableMapOf() + val delta = mutableMapOf() + + fun shortestPaths(source: Int) { + val dist = mutableMapOf() + val queue = ArrayDeque() + dist[source] = 0 + sigma[source] = 1 + queue.add(source) + + while (queue.isNotEmpty()) { + val v = queue.removeFirst() + stack.add(v) + for (w in adjacencyList[v] ?: emptyList()) { + if (dist.getOrDefault(w, Int.MAX_VALUE) == Int.MAX_VALUE) { + queue.add(w) + dist[w] = dist.getOrDefault(v, 0) + 1 + } + if (dist[w] == dist.getOrDefault(v, 0) + 1) { + sigma[w] = sigma.getOrDefault(w, 0) + sigma.getOrDefault(v, 0) + predecessors.getOrPut(w) { mutableListOf() }.add(v) + } + } + } + } + + fun accumulateBetweenness(source: Int) { + val dependency = mutableMapOf() + while (stack.isNotEmpty()) { + val w = stack.removeLast() + for (v in predecessors[w] ?: emptyList()) { + dependency[v] = + dependency.getOrDefault(v, 0.0F) + (sigma.getOrDefault(v, 0) / sigma.getOrDefault(w, 1) + .toFloat()) * (1 + dependency.getOrDefault(w, 0.0F)) + } + if (w != source) { + betweenness[w] = betweenness.getOrDefault(w, 0.0F) + dependency.getOrDefault(w, 0.0F) + } + } + } + + for (source in nodes) { + shortestPaths(source) + accumulateBetweenness(source) + sigma.clear() + predecessors.clear() + } + + val n = nodes.size + for ((node, bc) in betweenness) { + betweenness[node] = (bc / ((n - 1) * (n - 2) / 2.0)).toFloat() + } + + return betweenness + } + + open fun findCyclesFromNode(node: Int): List> { + val cycles = mutableListOf>() + val visited = mutableSetOf() + val path = mutableListOf() + + fun dfs(currentNode: Int) { + visited.add(currentNode) + path.add(currentNode) + + for (neighbor in adjacencyList[currentNode] ?: emptyList()) { + if (!visited.contains(neighbor)) { + dfs(neighbor) + } else if (path.contains(neighbor)) { + // Cycle detected + val cycle = path.slice(path.indexOf(neighbor) until path.size) + if (cycle.contains(node)) { + cycles.add(cycle) + } + } + } + + path.removeAt(path.size - 1) + } + + dfs(node) + + return cycles.filter { it.size > 2 } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/algos/WGraph.kt b/src/main/kotlin/algos/WGraph.kt new file mode 100644 index 0000000..bbf6a38 --- /dev/null +++ b/src/main/kotlin/algos/WGraph.kt @@ -0,0 +1,298 @@ +package algos + +import java.util.* + +open class WGraph : Graph() { + val weights = mutableMapOf, Int>() + + open fun addEdge(n: Int, m: Int, weight: Int) { + super.addEdge(n, m) + weights[Pair(n, m)] = weight + weights[Pair(m, n)] = weight // Если граф ненаправленный + } + + fun shortestPathBF(source: Int, target: Int): List? { + val distance = mutableMapOf() + val predecessor = mutableMapOf() + + for (node in adjacencyList.keys) { + distance[node] = Int.MAX_VALUE + predecessor[node] = null + } + distance[source] = 0 + + for (i in 1 until adjacencyList.size) { + for ((u, neighbors) in adjacencyList) { + for (v in neighbors) { + val edgeWeight = weights[Pair(u, v)] ?: Int.MAX_VALUE + if (distance[u] != Int.MAX_VALUE && distance[u]!! + edgeWeight < distance[v]!!) { + distance[v] = distance[u]!! + edgeWeight + predecessor[v] = u + } + } + } + } + + // Проверка на наличие отрицательных циклов + for ((u, neighbors) in adjacencyList) { + for (v in neighbors) { + val edgeWeight = weights[Pair(u, v)] ?: Int.MAX_VALUE + if (distance[u] != Int.MAX_VALUE && distance[u]!! + edgeWeight < distance[v]!!) { + throw IllegalArgumentException("Граф содержит отрицательный цикл") + } + } + } + + // Восстановление пути + val path = mutableListOf() + var currentNode: Int? = target + while (currentNode != null) { + path.add(currentNode) + currentNode = predecessor[currentNode] + } + return if (path.last() == source) path.asReversed() else null + } + fun shortestPathD(source: Int, target: Int): List? { + val distance = mutableMapOf() + val predecessor = mutableMapOf() + val queue = PriorityQueue(compareBy { distance[it] }) + + for (node in adjacencyList.keys) { + distance[node] = Int.MAX_VALUE + predecessor[node] = null + queue.add(node) + } + distance[source] = 0 + + while (queue.isNotEmpty()) { + val u = queue.poll() + for (v in adjacencyList[u] ?: emptyList()) { + val edgeWeight = weights[Pair(u, v)] ?: Int.MAX_VALUE + if (distance[u] != Int.MAX_VALUE && distance[u]!! + edgeWeight < distance[v]!!) { + distance[v] = distance[u]!! + edgeWeight + predecessor[v] = u + queue.remove(v) // Delete and restore back to update the order in the queue + queue.add(v) + } + } + } + + // Restore path + val path = mutableListOf() + var currentNode: Int? = target + while (currentNode != null) { + path.add(currentNode) + currentNode = predecessor[currentNode] + } + return if (path.last() == source) path.asReversed() else null + } + + fun getSpanningTree(): WGraph { + val g = this + val edges: MutableList>> = mutableListOf() + val res: MutableList>> = mutableListOf() + for (i in g.weights) { + edges.add(Pair(i.value, Pair(i.key.first, i.key.second))); + } + edges.sortBy { it.first } + val treeId: MutableMap = mutableMapOf() + for (i in g.nodes) { + treeId[i] = i + } + for (i in edges) { + val a = i.second.first + val b = i.second.second + val l = i.first + if (treeId[a] != treeId[b]) { + res.add(Pair(l, Pair(a, b))) + val oldId = treeId[b] + val newId = treeId[a] + for (j in treeId.keys) { + if (treeId[j] == oldId) { + treeId[j] = newId!! + } + } + } + } + val ans: WGraph = WGraph() + for (i in g.nodes) { + ans.addNode(i) + } + for (i in res) { + ans.addEdge(i.second.first, i.second.second, i.first) + } + return ans + } + + fun LouvainAlgorithm(): MutableMap { + val communities = mutableMapOf() + fun execute(g: WGraph) { + for (i in g.nodes) { + communities[i] = i + } + fun getCommunityMembers(community: Int): MutableList { + val members = mutableListOf() + for (i in g.nodes) { + if (communities[i] == community) { + members.add(i) + } + } + return members + } + fun deltaQ(node: Int, community: Int): Double { + var m = 0 + for (i in g.weights.values) { + m += i + } + val curId = communities[node] + val newId = community + val currentCommunity = getCommunityMembers(communities[node]!!) + val newCommunity = getCommunityMembers(community) + var sigmaIn1 = 0 + var sigmaTot1 = 0 + for (i in newCommunity) { + for (j in g.adjacencyList[i]!!) { + sigmaTot1 += g.weights[Pair(i, j)]!! + if (communities[j] == newId) { + sigmaIn1 += g.weights[Pair(i, j)]!! + } + } + } + var kiin1 = 0 + var ki1 = 0 + for (i in g.adjacencyList[node]!!) { + ki1 += g.weights[Pair(node, i)]!! + if (communities[i] == newId) { + kiin1 += g.weights[Pair(node, i)]!! + } + } + kiin1 *= 2 + val qBef1: Double = (sigmaIn1.toDouble() / (2 * m).toDouble()) - Math.pow(sigmaTot1.toDouble() / (2 * m).toDouble(), 2.0) - Math.pow(ki1.toDouble() / (2 * m).toDouble(), 2.0) + val qAft1: Double = ((sigmaIn1 + kiin1).toDouble() / (2 * m).toDouble()) - Math.pow(((sigmaTot1 + ki1).toDouble() / (2 * m).toDouble()).toDouble(), 2.0) + val dQNew = qAft1 - qBef1 + var sigmaIn2 = 0 + var sigmaTot2 = 0 + for (i in currentCommunity) { + if (i == node) { + continue + } + for (j in g.adjacencyList[i]!!) { + sigmaTot2 += g.weights[Pair(i, j)]!! + if (communities[j] == curId && j != node) { + sigmaIn2 += g.weights[Pair(i, j)]!! + } + } + + } + var kiin2 = 0 + var ki2 = 0 + for (i in g.adjacencyList[node]!!) { + ki2 += g.weights[Pair(node, i)]!! + if (i == node) { + continue + } + if (communities[i] == curId) { + kiin2 += g.weights[Pair(node, i)]!! + } + } + kiin2 *= 2 + val qAft2: Double = (sigmaIn2.toDouble() / (2 * m).toDouble()) - Math.pow(sigmaTot2.toDouble() / (2 * m).toDouble(), 2.0) - Math.pow(ki2.toDouble() / (2 * m).toDouble(), 2.0) + val qBef2: Double = ((sigmaIn2 + kiin2).toDouble() / (2 * m).toDouble()) - Math.pow(((sigmaTot2 + ki2).toDouble() / (2 * m).toDouble()).toDouble(), 2.0) + val dQCur = qAft2 - qBef2 + return dQCur + dQNew + } + fun findBest(node: Int): Int { + var bestCommunity = communities[node] + var bestDQ: Double = 0.0 + for (adjacentNode in g.adjacencyList[node]!!) { + if (communities[adjacentNode] != communities[node]) { + var dQ = deltaQ(node, communities[adjacentNode]!!) + if (dQ > bestDQ) { + bestCommunity = communities[adjacentNode] + bestDQ =dQ + } + } + } + return bestCommunity!! + } + fun moveToCommunity(node: Int, community: Int) { + for (i in communities.keys) { + if (communities[i] == node) { + communities[i] = community + } + } + } + var good = true + var totalGood = false + while (good) { + good = false + for (node in g.nodes) { + val bestCommunity = findBest(node); + if (bestCommunity != communities[node]) { + good = true + totalGood = true + moveToCommunity(node, bestCommunity) + } + } + } + fun reduceGraph(graph: WGraph): WGraph { + val newG = WGraph() + val mp = mutableMapOf>() + for (i in communities.keys) { + if (mp.containsKey(communities[i])) { + mp[communities[i]!!]?.add(i) + } else { + mp[communities[i]!!] = mutableSetOf(); + mp[communities[i]!!]?.add(i) + } + } + for (i in mp.keys) { + newG.addNode(i) + } + for (community in mp.keys) { + var s = 0 + for (i in mp[community]!!) { + if (!graph.nodes.contains(i)) { + continue + } + for (j in graph.adjacencyList[i]!!) { + if (communities[j] == community) { + s += graph.weights[Pair(i, j)]!! + } + } + } + newG.addEdge(community, community, s) + } + for (community in mp.keys) { + for (community1 in mp.keys) { + if (community != community1) { + var s = 0 + var bad = true + for (i in mp[community]!!) { + if (!graph.nodes.contains(i)) { + continue + } + for (j in graph.adjacencyList[i]!!) { + if (communities[j] == community1) { + s += graph.weights[Pair(i, j)]!! + bad = false + } + } + } + if (!bad && !newG.weights.containsKey(Pair(community, community1))) { + newG.addEdge(community, community1, s) + } + } + } + } + return newG + } + val newG = reduceGraph(g) + if (totalGood) { + execute(newG) + } + } + execute(this) + return communities + } +} \ No newline at end of file diff --git a/src/main/resources/img/logo(Black).png b/src/main/resources/img/logo(Black).png new file mode 100644 index 0000000..1222629 Binary files /dev/null and b/src/main/resources/img/logo(Black).png differ diff --git a/src/main/resources/img/logo(white).png b/src/main/resources/img/logo(white).png new file mode 100644 index 0000000..d3eb0b1 Binary files /dev/null and b/src/main/resources/img/logo(white).png differ diff --git a/src/main/resources/img/nazad(Black).png b/src/main/resources/img/nazad(Black).png new file mode 100644 index 0000000..e354357 Binary files /dev/null and b/src/main/resources/img/nazad(Black).png differ diff --git a/src/main/resources/img/nazad(White).png b/src/main/resources/img/nazad(White).png new file mode 100644 index 0000000..33694ea Binary files /dev/null and b/src/main/resources/img/nazad(White).png differ diff --git a/src/test/kotlin/TestBetweennessAlgo.kt b/src/test/kotlin/TestBetweennessAlgo.kt new file mode 100644 index 0000000..4ff4fe0 --- /dev/null +++ b/src/test/kotlin/TestBetweennessAlgo.kt @@ -0,0 +1,73 @@ +import algos.Graph + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class GraphTest { + + @Test + fun testBetweennessCentrality() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(1, 2) + graph.addEdge(1, 3) + graph.addEdge(2, 3) + graph.addEdge(3, 4) + graph.addEdge(4, 5) + + val betweenness = graph.betweennessCentrality() + + assertEquals(0.0f, betweenness[1]) + assertEquals(0.0f, betweenness[2]) + assertEquals(1 + 1/3f, betweenness[3]) + assertEquals(1f, betweenness[4]) + assertEquals(0.0f, betweenness[5]) + } + + @Test + fun testBetweennessCentralityWithCycle() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 4) + graph.addEdge(4, 1) + + val betweenness = graph.betweennessCentrality() + assertEquals(1f/3.0f, betweenness[1]) + assertEquals(1f/3.0f, betweenness[2]) + assertEquals(1f/3.0f, betweenness[3]) + assertEquals(1f/3.0f, betweenness[4]) + } + + @Test + fun testBetweennessCentralityWithDisconnectedGraph() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(4, 5) + + val betweenness = graph.betweennessCentrality() + + assertEquals(0.0f, betweenness[1]) + assertEquals(1f/3.0f, betweenness[2]) + assertEquals(0.0f, betweenness[3]) + assertEquals(0.0f, betweenness[4]) + assertEquals(0.0f, betweenness[5]) + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestFindBridges.kt b/src/test/kotlin/TestFindBridges.kt new file mode 100644 index 0000000..3911f72 --- /dev/null +++ b/src/test/kotlin/TestFindBridges.kt @@ -0,0 +1,96 @@ +import algos.Graph +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class TestFindBridges { + + @Test + fun `test findBridges with no bridges`() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 1) + + val bridges = graph.findBridges() + assertTrue(bridges.isEmpty()) + } + + @Test + fun `test findBridges with a single bridge`() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 4) + graph.addEdge(4, 2) + + val bridges = graph.findBridges() + assertEquals(1, bridges.size) + assertTrue(bridges.contains(Pair(1, 2))) + } + + @Test + fun `test findBridges with multiple bridges`() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 4) + graph.addEdge(4, 5) + graph.addEdge(2, 4) + + val bridges = graph.findBridges() + assertEquals(2, bridges.size) + assertTrue(bridges.contains(Pair(1, 2))) + assertTrue(bridges.contains(Pair(4, 5))) + } + + @Test + fun `test findBridges with isolated nodes`() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addEdge(1, 2) + + val bridges = graph.findBridges() + assertEquals(1, bridges.size) + assertTrue(bridges.contains(Pair(1, 2))) + } + + @Test + fun `test findBridges with self-loop`() { + val graph = Graph() + graph.addNode(1) + graph.addEdge(1, 1) + + val bridges = graph.findBridges() + assertTrue(bridges.isEmpty()) + } + + @Test + fun `test findBridges with disconnected graph`() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addEdge(1, 2) + graph.addEdge(3, 4) + + val bridges = graph.findBridges() + assertEquals(2, bridges.size) + assertTrue(bridges.contains(Pair(1, 2))) + assertTrue(bridges.contains(Pair(3, 4))) + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestFindingCyclesInGraph.kt b/src/test/kotlin/TestFindingCyclesInGraph.kt new file mode 100644 index 0000000..34e16fe --- /dev/null +++ b/src/test/kotlin/TestFindingCyclesInGraph.kt @@ -0,0 +1,82 @@ +import algos.DiGraph +import algos.Graph + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TestFindingCyclesInGraph { + + @Test + fun `test findCyclesFromNode with a simple cycle`() { + val graph = Graph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 4) + graph.addEdge(4, 2) // Создаем цикл 2 -> 3 -> 4 -> 2 + + val cycles = graph.findCyclesFromNode(2) + assertEquals(1, cycles.size) + assertEquals(listOf(2, 3, 4), cycles.sortedBy { -it.size }[0]) + } + + @Test + fun `test findCyclesFromNode with multiple cycles`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addEdge(1, 2, 1) // Забыл добавить веса для ребер. Бывает) + graph.addEdge(2, 3, 1) + graph.addEdge(3, 1, 1) // Создаем цикл 1 -> 2 -> 3 -> 1 + graph.addEdge(3, 4, 1) + graph.addEdge(4, 2, 1) // Создаем цикл 2 -> 3 -> 4 -> 2 + + val cycles = graph.findCyclesFromNode(2) + assertEquals(2, cycles.size) + assertEquals(listOf(2, 3, 1), cycles.sortedBy { it.size }[0]) + assertEquals(listOf(2, 3, 4), cycles.sortedBy { it.size }[1]) + } + + @Test + fun `test findCyclesFromNode with no cycles`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 1) + graph.addEdge(3, 4, 1) + + val cycles = graph.findCyclesFromNode(1) + assertEquals(0, cycles.filter { it[0] == 1 }.size) + } + + @Test + fun `test findCyclesFromNode with a single node`() { + val graph = Graph() + graph.addNode(1) + + val cycles = graph.findCyclesFromNode(1) + assertEquals(0, cycles.size) + } + + @Test + fun `test findCyclesFromNode with a disconnected graph`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addEdge(1, 2, 1) + graph.addEdge(3, 4, 1) + + val cycles = graph.findCyclesFromNode(1) + assertEquals(0, cycles.size) + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestLouvain.kt b/src/test/kotlin/TestLouvain.kt new file mode 100644 index 0000000..5ab41ac --- /dev/null +++ b/src/test/kotlin/TestLouvain.kt @@ -0,0 +1,155 @@ +import algos.WGraph +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class TestLouvain { + + private fun getSetOfCommunities(mp: MutableMap): MutableSet> { + val t: MutableMap> = mutableMapOf() + for (i in mp.keys) { + if (!t.containsKey(mp[i])) { + t[mp[i]!!] = mutableSetOf() + } + t[mp[i]!!]?.add(i) + } + val tt: MutableSet> = mutableSetOf() + for (i in t.keys) { + val s: MutableSet = mutableSetOf() + for (j in t[i]!!) { + s.add(j) + } + tt.add(s) + } + return tt + } + + @Test + fun testWithDisjointCommunities() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + graph.addNode(6) + graph.addNode(7) + graph.addNode(8) + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 1) + graph.addEdge(3, 4, 1) + graph.addEdge(4, 5, 1) + graph.addEdge(5, 1, 1) + graph.addEdge(8, 7, 1) + graph.addEdge(7, 6, 1) + graph.addEdge(6, 8, 1) + + val communities = graph.LouvainAlgorithm() + val t = getSetOfCommunities(communities) + val right: MutableSet> = mutableSetOf(mutableSetOf(1, 2, 3, 4, 5), mutableSetOf(6, 7, 8)) + assertEquals(t, right) + } + + @Test + fun testWithJoinedCommunities() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + graph.addNode(6) + graph.addEdge(1, 2, 1) + graph.addEdge(1, 3, 1) + graph.addEdge(2, 3, 1) + graph.addEdge(4, 5, 1) + graph.addEdge(5, 6, 1) + graph.addEdge(4, 6, 1) + graph.addEdge(2, 5, 1) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right: MutableSet> = mutableSetOf(mutableSetOf(1, 2, 3), mutableSetOf(4, 5, 6)) + assertEquals(s, right) + } + + @Test + fun testWithOneNode() { + val graph = WGraph() + graph.addNode(1) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right = mutableSetOf(mutableSetOf(1)) + assertEquals(s, right) + } + + @Test + fun testWithWeightedEdges() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + graph.addEdge(1, 2, 6) + graph.addEdge(1, 3, 1) + graph.addEdge(2, 3, 1) + graph.addEdge(3, 5, 1) + graph.addEdge(3, 4, 1) + graph.addEdge(5, 4, 1) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right = mutableSetOf(mutableSetOf(1, 2), mutableSetOf(3, 4, 5)) + assertEquals(s, right) + } + + @Test + fun testWithTwoNodesWithoutEdges() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right = mutableSetOf(mutableSetOf(1), mutableSetOf(2)) + assertEquals(s, right) + } + + @Test + fun testWithSelfLoop() { + val graph = WGraph() + graph.addNode(1) + graph.addEdge(1, 1, 1) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right = mutableSetOf(mutableSetOf(1)) + assertEquals(s, right) + } + + @Test + fun testWithSelfLoop1() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addEdge(1, 1, 1) + graph.addEdge(1, 2, 1) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right = mutableSetOf(mutableSetOf(1, 2)) + assertEquals(s, right) + } + + @Test + fun testWithSelfLoop2() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addEdge(1, 1, 9) + graph.addEdge(1, 2, 1) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right = mutableSetOf(mutableSetOf(1, 2)) + assertEquals(s, right) + } + + @Test + fun testWithZeroEdge() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addEdge(1, 2, 0) + val s = getSetOfCommunities(graph.LouvainAlgorithm()) + val right = mutableSetOf(mutableSetOf(1), mutableSetOf(2)) + assertEquals(s, right) + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestShortestPathByDj.kt b/src/test/kotlin/TestShortestPathByDj.kt new file mode 100644 index 0000000..ed33a81 --- /dev/null +++ b/src/test/kotlin/TestShortestPathByDj.kt @@ -0,0 +1,284 @@ +import algos.WGraph + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.test.assertNull + +class DjTest { + + @Test + fun `test Shortest Path By Dj`() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(1, 2, 1) + graph.addEdge(1, 3, 2) + graph.addEdge(2, 3, 3) + graph.addEdge(3, 4, 4) + graph.addEdge(4, 5, 5) + + val shortestPath = graph.shortestPathBF(1, 5) + assertEquals(listOf(1, 3, 4, 5), shortestPath) + } + + @Test + fun `test Shortest Path By Dj With Negative Cycle`() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + + graph.addEdge(1, 2, -1) + graph.addEdge(2, 3, -2) + graph.addEdge(3, 1, -3) + + try { + graph.shortestPathBF(1, 3) + } catch (e: IllegalArgumentException) { + assertEquals("Граф содержит отрицательный цикл", e.message) + } + } + + @Test + fun `test Shortest Path By Dj With Disconnected Graph`() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 2) + graph.addEdge(4, 5, 3) + + val shortestPath = graph.shortestPathBF(1, 5) + assertEquals(null, shortestPath) + } + + @Test + fun `test shortest path from 0 to 5 with multiple paths`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + graph.addEdge(3, 5, 2) + graph.addEdge(4, 5, 1) + + val result = graph.shortestPathD(0, 5) + assertEquals(listOf(0, 1, 2, 3, 5), result) + } + + @Test + fun `test shortest path from 0 to 4 with different weights`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + + val result = graph.shortestPathD(0, 4) + assertEquals(listOf(0, 1, 2, 3, 4), result) + } + + @Test + fun `test no path from 5 to 0`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(4, 5, 1) + + val result = graph.shortestPathD(5, 0) + assertNull(result) + } + + @Test + fun `test shortest path from 2 to 5 with cycle`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + graph.addEdge(3, 5, 2) + graph.addEdge(4, 5, 1) + + val result = graph.shortestPathD(2, 5) + assertEquals(listOf(2, 3, 5), result) + } + + @Test + fun `test shortest path from 1 to 5 with different paths`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + graph.addEdge(3, 5, 2) + graph.addEdge(4, 5, 1) + + val result = graph.shortestPathD(1, 5) + assertEquals(listOf(1, 2, 3, 5), result) + } + + @Test + fun `test shortest path from 0 to 3 with multiple edges`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + + val result = graph.shortestPathD(0, 3) + assertEquals(listOf(0, 1, 2, 3), result) + } + + @Test + fun `test shortest path from 3 to 5 with direct edge`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + graph.addEdge(3, 5, 2) + graph.addEdge(4, 5, 1) + + val result = graph.shortestPathD(3, 5) + assertEquals(listOf(3, 5), result) + } + + @Test + fun `test no path from 4 to 0`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(3, 4, 1) + graph.addEdge(3, 5, 2) + graph.addEdge(4, 5, 1) + + val result = graph.shortestPathD(4, 0) + assertNull(result) + } + + @Test + fun `test shortest path from 0 to 2 with direct edge`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + + val result = graph.shortestPathD(0, 2) + assertEquals(listOf(0, 1, 2), result) + } + + @Test + fun `test shortest path from 2 to 4 with direct edge`() { + val graph = WGraph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + + graph.addEdge(0, 1, 1) + graph.addEdge(0, 2, 4) + graph.addEdge(1, 2, 2) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 1) + graph.addEdge(2, 4, 3) + graph.addEdge(3, 4, 1) + + val result = graph.shortestPathD(2, 4) + assertEquals(listOf(2, 3, 4), result) + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestShortestPathFordBellman.kt b/src/test/kotlin/TestShortestPathFordBellman.kt new file mode 100644 index 0000000..b35be7f --- /dev/null +++ b/src/test/kotlin/TestShortestPathFordBellman.kt @@ -0,0 +1,60 @@ +import algos.WGraph + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +class WGraphTest { + + @Test + fun testShortestPathBF() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(1, 2, 1) + graph.addEdge(1, 3, 2) + graph.addEdge(2, 3, 3) + graph.addEdge(3, 4, 4) + graph.addEdge(4, 5, 5) + + val shortestPath = graph.shortestPathBF(1, 5) + assertEquals(listOf(1, 3, 4, 5), shortestPath) + } + + @Test + fun testShortestPathBFWithNegativeCycle() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + + graph.addEdge(1, 2, -1) + graph.addEdge(2, 3, -2) + graph.addEdge(3, 1, -3) + + try { + graph.shortestPathBF(1, 3) + } catch (e: IllegalArgumentException) { + assertEquals("Граф содержит отрицательный цикл", e.message) + } + } + + @Test + fun testShortestPathBFWithDisconnectedGraph() { + val graph = WGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 2) + graph.addEdge(4, 5, 3) + + val shortestPath = graph.shortestPathBF(1, 5) + assertEquals(null, shortestPath) + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestSpanningTree.kt b/src/test/kotlin/TestSpanningTree.kt new file mode 100644 index 0000000..ea0bc9b --- /dev/null +++ b/src/test/kotlin/TestSpanningTree.kt @@ -0,0 +1,105 @@ +import algos.WGraph +import org.junit.jupiter.api.Test + +class TestSpanningTree { + @Test + fun easyTest() { + val graph = WGraph() + for (i in 1..4) { + graph.addNode(i) + } + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 1) + graph.addEdge(3, 4, 1) + graph.addEdge(1, 4, 1) + var good = true + val t = graph.getSpanningTree().edges + val right: MutableList> = mutableListOf(Pair(1, 2), Pair(2, 3), Pair(3, 4)) + for (i in right) { + if (Pair(i.first, i.second) in t || Pair(i.second, i.first) in t) { + continue + } + good = false + } + assert(good) + } + + @Test + fun weightedTest() { + val graph = WGraph() + for (i in 1..3) { + graph.addNode(i) + } + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 2) + graph.addEdge(3, 1, 3) + var good = true + val t = graph.getSpanningTree().edges + val right: MutableList> = mutableListOf(Pair(1, 2), Pair(2, 3)) + for (i in right) { + if (Pair(i.first, i.second) in t || Pair(i.second, i.first) in t) { + continue + } + good = false + } + assert(good) + } + + @Test + fun weightedDisjointedTest() { + val graph = WGraph() + for (i in 1..6) { + graph.addNode(i) + } + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 2) + graph.addEdge(3, 1, 3) + graph.addEdge(4, 5, 1) + graph.addEdge(5, 6, 1) + var good = true + val t = graph.getSpanningTree().edges + val right: MutableList> = mutableListOf(Pair(1, 2), Pair(2, 3), Pair(4, 5), Pair(5, 6)) + for (i in right) { + if (Pair(i.first, i.second) in t || Pair(i.second, i.first) in t) { + continue + } + good = false + } + assert(good) + } + + @Test + fun oneNodeTest() { + val graph = WGraph() + graph.addNode(1) + var good = true + val t = graph.getSpanningTree() + if (!(1 in t.nodes && t.nodes.size == 1)) { + good = false + } + assert(good) + } + + @Test + fun negativeEdgesTest() { + val graph = WGraph() + for (i in 1..4) { + graph.addNode(i) + } + graph.addEdge(1, 2, -10) + graph.addEdge(2, 4, 2) + graph.addEdge(1, 4, 5) + graph.addEdge(1, 3, 4) + graph.addEdge(3, 4, 3) + var good = true + val t = graph.getSpanningTree().edges + val right: MutableList> = mutableListOf(Pair(1, 2), Pair(2, 4), Pair(4, 3)) + for (i in right) { + if (Pair(i.first, i.second) in t || Pair(i.second, i.first) in t) { + continue + } + good = false + } + assert(good) + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestSpringEmbedder.kt b/src/test/kotlin/TestSpringEmbedder.kt new file mode 100644 index 0000000..d0436b0 --- /dev/null +++ b/src/test/kotlin/TestSpringEmbedder.kt @@ -0,0 +1,126 @@ +import algos.Graph + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.test.assertTrue + +class TestSpringEmbedder { + + @Test + fun `test layout for a simple graph`() { + val graph = Graph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addEdge(0, 1) + graph.addEdge(1, 2) + + val springEmbedder = Graph.SpringEmbedder() + val layout = springEmbedder.layout(graph) + + // Проверяем, что все вершины имеют координаты + assertEquals(3, layout.size) + assertTrue(layout.containsKey(0)) + assertTrue(layout.containsKey(1)) + assertTrue(layout.containsKey(2)) + + for (coordinate in layout.values) { + assertTrue(coordinate.first >= -20.0 && coordinate.first <= 20.0) + assertTrue(coordinate.second >= -20.0 && coordinate.second <= 20.0) + } + } + + @Test + fun `test layout for a graph with disconnected components`() { + val graph = Graph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addEdge(0, 1) + graph.addEdge(2, 3) + + val springEmbedder = Graph.SpringEmbedder() + val layout = springEmbedder.layout(graph) + + // Проверяем, что все вершины имеют координаты + assertEquals(4, layout.size) + assertTrue(layout.containsKey(0)) + assertTrue(layout.containsKey(1)) + assertTrue(layout.containsKey(2)) + assertTrue(layout.containsKey(3)) + + // Проверяем, что координаты находятся в пределах [0, 1] + for (coordinate in layout.values) { + assertTrue(coordinate.first >= -20.0 && coordinate.first <= 20.0) + assertTrue(coordinate.second >= -20.0 && coordinate.second <= 20.0) + } + } + + @Test + fun `test layout for a fully connected graph`() { + val graph = Graph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + graph.addEdge(0, 1) + graph.addEdge(1, 2) + graph.addEdge(2, 0) + + val springEmbedder = Graph.SpringEmbedder() + val layout = springEmbedder.layout(graph) + + // Проверяем, что все вершины имеют координаты + assertEquals(3, layout.size) + assertTrue(layout.containsKey(0)) + assertTrue(layout.containsKey(1)) + assertTrue(layout.containsKey(2)) + + // Проверяем, что координаты находятся в пределах [0, 1] + for (coordinate in layout.values) { + assertTrue(coordinate.first >= -20.0 && coordinate.first <= 20.0) + assertTrue(coordinate.second >= -20.0 && coordinate.second <= 20.0) + } + } + + @Test + fun `test layout for a graph with a single node`() { + val graph = Graph() + graph.addNode(0) + + val springEmbedder = Graph.SpringEmbedder() + val layout = springEmbedder.layout(graph) + + // Проверяем, что вершина имеет координаты + assertEquals(1, layout.size) + assertTrue(layout.containsKey(0)) + + // Проверяем, что координаты находятся в пределах [0, 1] + val coordinate = layout[0]!! + assertTrue(coordinate.first >= -20.0 && coordinate.first <= 20.0) + assertTrue(coordinate.second >= -20.0 && coordinate.second <= 20.0) + } + + @Test + fun `test layout for a graph with no edges`() { + val graph = Graph() + graph.addNode(0) + graph.addNode(1) + graph.addNode(2) + + val springEmbedder = Graph.SpringEmbedder() + val layout = springEmbedder.layout(graph) + + // Проверяем, что все вершины имеют координаты + assertEquals(3, layout.size) + assertTrue(layout.containsKey(0)) + assertTrue(layout.containsKey(1)) + assertTrue(layout.containsKey(2)) + + // Проверяем, что координаты находятся в пределах [0, 1] + for (coordinate in layout.values) { + assertTrue(coordinate.first >= -20.0 && coordinate.first <= 20.0) + assertTrue(coordinate.second >= -20.0 && coordinate.second <= 20.0) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/TestStronglyConnectedComponents.kt b/src/test/kotlin/TestStronglyConnectedComponents.kt new file mode 100644 index 0000000..d127e5c --- /dev/null +++ b/src/test/kotlin/TestStronglyConnectedComponents.kt @@ -0,0 +1,141 @@ +package algos + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TestStronglyConnectedComponents{ + + @Test + fun `test Find SCCs`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addEdge(1, 2, 1) + graph.addEdge(2, 3, 1) + graph.addEdge(3, 1, 1) + graph.addEdge(3, 4, 1) + graph.addEdge(4, 4, 1) + + val sccs = graph.findSCCs() + assertEquals(2, sccs.size) + assertEquals(true, sccs.any { it.containsAll(listOf(1, 2, 3)) }) + assertEquals(true, sccs.any { it.containsAll(listOf(4)) }) + } + + @Test + fun `test Find SCCs Empty Graph`() { + val graph = DiGraph() + val sccs = graph.findSCCs() + assertEquals(0, sccs.size) + } + + @Test + fun `test Find SCCs Single Node`() { + val graph = DiGraph() + graph.addNode(1) + val sccs = graph.findSCCs() + assertEquals(1, sccs.size) + assertEquals(true, sccs.any { it.contains(1) }) + } + + @Test + fun `test Find SCCs Disconnected Graph`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addEdge(1, 2) + graph.addEdge(2, 1) + graph.addEdge(3, 3) + + val sccs = graph.findSCCs() + assertEquals(2, sccs.size) + assertEquals(true, sccs.any { it.containsAll(listOf(1, 2)) }) + assertEquals(true, sccs.any { it.containsAll(listOf(3)) }) + } + + @Test + fun `test Find SCCs With Multiple SCCs`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addNode(4) + graph.addNode(5) + graph.addNode(6) + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 1) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(6, 4) + + val sccs = graph.findSCCs() + assertEquals(2, sccs.size) + assertEquals(true, sccs.any { it.containsAll(listOf(1, 2, 3)) }) + assertEquals(true, sccs.any { it.containsAll(listOf(4, 5, 6)) }) + } + + @Test + fun `test Find SCCs With Cycle`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 1) + + val sccs = graph.findSCCs() + assertEquals(1, sccs.size) + assertEquals(true, sccs.any { it.containsAll(listOf(1, 2, 3)) }) + } + + @Test + fun `test Find SCCs With No Edges`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + + val sccs = graph.findSCCs() + assertEquals(3, sccs.size) + assertEquals(true, sccs.any { it.contains(1) }) + assertEquals(true, sccs.any { it.contains(2) }) + assertEquals(true, sccs.any { it.contains(3) }) + } + + @Test + fun `test Find SCCs With Self Loops`() { + val graph = DiGraph() + graph.addNode(1) + graph.addNode(2) + graph.addNode(3) + graph.addEdge(1, 1) + graph.addEdge(2, 2) + graph.addEdge(3, 3) + + val sccs = graph.findSCCs() + assertEquals(3, sccs.size) + assertEquals(true, sccs.any { it.contains(1) }) + assertEquals(true, sccs.any { it.contains(2) }) + assertEquals(true, sccs.any { it.contains(3) }) + } + + @Test + fun `test Find SCCs With Large Graph`() { + val graph = DiGraph() + val nodes = (1..100).toList() + nodes.forEach { graph.addNode(it) } + for (i in 0 until nodes.size - 1) { + graph.addEdge(nodes[i], nodes[i + 1]) + } + graph.addEdge(nodes.last(), nodes.first()) + + val sccs = graph.findSCCs() + assertEquals(1, sccs.size) + assertEquals(true, sccs.any { it.containsAll(nodes) }) + } +} \ No newline at end of file