diff --git a/.github/.keep b/.github/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..b0d4430 --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,11 @@ +version: 2 +mergeable: + - when: pull_request.*, pull_request_review.* + branches-ignore: + -"main" + validate: + - do: approvals + min: + count: 1 + required: + assignees: true diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..bbd1fcc --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,36 @@ +name: Measure coverage + +on: + [push, pull_request] + +jobs: + build: + permissions: + id-token: write + contents: read + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '21' + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Generate kover coverage report + run: ./gradlew koverXmlReport + + - name: Add coverage report to PR + id: kover + uses: mi-kas/kover-report@v1 + with: + path: | + ${{ github.workspace }}/build/reports/kover/report.xml + title: Code Coverage + update-comment: true + min-coverage-overall: 0 + min-coverage-changed-files: 0 + coverage-counter-type: LINE \ No newline at end of file diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml new file mode 100644 index 0000000..9210217 --- /dev/null +++ b/.github/workflows/gradle-build.yml @@ -0,0 +1,22 @@ +name: Build + +on: + [push, pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-latest ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: '21' + - name: Clean and Build + run: ./gradlew clean build -x test \ No newline at end of file diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml new file mode 100644 index 0000000..dad9e7c --- /dev/null +++ b/.github/workflows/gradle-test.yml @@ -0,0 +1,22 @@ +name: Test + +on: + [push, pull_request] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-latest ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: '21' + - name: Clean and Test + run: ./gradlew clean test \ No newline at end of file diff --git a/.github/workflows/megalinter.yml b/.github/workflows/megalinter.yml new file mode 100644 index 0000000..bbe4a75 --- /dev/null +++ b/.github/workflows/megalinter.yml @@ -0,0 +1,89 @@ +#--- +## MegaLinter GitHub Action configuration file +## More info at https://megalinter.io +#name: MegaLinter +# +#on: +# # Trigger mega-linter at every push. Action will also be visible from Pull Requests to main +# push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) +# +#env: # Comment env block if you don't want to apply fixes +# # Apply linter fixes configuration +# APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) +# APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all) +# APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request) +# ENABLE_LINTERS: KOTLIN_KTLINT +#concurrency: +# group: ${{ github.ref }}-${{ github.workflow }} +# cancel-in-progress: true +# +#jobs: +# megalinter: +# name: MegaLinter +# runs-on: ubuntu-latest +# permissions: +# # Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR +# # Remove the ones you do not need +# contents: write +# issues: write +# pull-requests: write +# steps: +# # Git Checkout +# - name: Checkout Code +# uses: actions/checkout@v3 +# with: +# token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} +# fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances +# +# # MegaLinter +# - name: MegaLinter +# id: ml +# # You can override MegaLinter flavor used to have faster performances +# # More info at https://megalinter.io/flavors/ +# uses: oxsecurity/megalinter@v7 +# env: +# # All available variables are described in documentation +# # https://megalinter.io/configuration/ +# VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY +# # DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks +# +# # Upload MegaLinter artifacts +# - name: Archive production artifacts +# if: success() || failure() +# uses: actions/upload-artifact@v4 +# with: +# name: MegaLinter reports +# path: | +# megalinter-reports +# mega-linter.log +# +# # Create pull request if applicable (for now works only on PR from same repository, not from forks) +# - name: Create Pull Request with applied fixes +# id: cpr +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# uses: peter-evans/create-pull-request@v6 +# with: +# token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} +# commit-message: "[MegaLinter] Apply linters automatic fixes" +# title: "[MegaLinter] Apply linters automatic fixes" +# labels: bot +# - name: Create PR output +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# run: | +# echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" +# echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" +# +# # Push new commit if applicable (for now works only on PR from same repository, not from forks) +# - name: Prepare commit +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# run: sudo chown -Rc $UID .git/ +# - name: Commit and push applied linter fixes +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# uses: stefanzweifel/git-auto-commit-action@v4 +# with: +# branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} +# commit_message: "[MegaLinter] Apply linters fixes" +# commit_user_name: megalinter-bot +# commit_user_email: nicolas.vuillamy@ox.security diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c69e5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +src/main/kotlin/**/localisationError.log +src/main/kotlin/**/settings.json +storage.db + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +.AppleDouble +.LSOverride + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a56e591 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: name-tests-test + - id: detect-private-key + - id: check-merge-conflict diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..d13bf86 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Graph Visualizer +## About +Application for creating graphs and visualisation graph algorithms. + +[dsfds.webm](https://github.com/spbu-coding-2023/graphs-graphs-8/assets/135718038/5fbf7226-122b-412e-b1c4-bb05ad998f0e) + + +## Quick start +### Requirements +- Kotlin 1.9.23 +- JDK 21 +#### Start +Run it by +``` +./gradlew run +``` +### Saving graphs +You can save graphs graphs using SQLite and Neo4j +- When using Neo4j you have to enter uri, username and password to establish a connection. +- SQLite is a local database, you don't need to do anything, just select it in the application settings. + +## Features +#### You can use basic algorithms +- Searching for clusters. +- Betweenness centrality. +- Graph force-directed drawing algorithm. +#### There are also other algorithms +- Find bridges. +- Find cycles. +- Find the shortest path using Ford-Bellman and Dijkstra algorithms. +- Find strongly connected components. +- Constructing a minimal spanning tree. + +## Licence +The app is distributed under [Unlicence](https://unlicense.org/), meaning we are putting this project into the public domain +## Contributing + We do not support contributing, so please write to the authors with your suggestions +## Authors +- [Aleksey Dmitrievtsev](https://github.com/admitrievtsev) +- [Gleb Nasretdinov](https://github.com/Ycyken) +- [Azamat Ishbaev](https://github.com/odiumuniverse) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..f651fed --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,58 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + kotlin("jvm") version "1.9.22" + id("org.jetbrains.compose") + id("org.jetbrains.kotlinx.kover") version "0.8.0" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" +} + +group = "visualizer" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://jitpack.io") + google() +} + + +dependencies { + // Note, if you develop a library, you should use compose.desktop.common. + // compose.desktop.currentOs should be used in launcher-sourceSet + // (in a separate module for demo project and in testMain). + // With compose.desktop.common you will also lose @Preview functionality + implementation(compose.desktop.currentOs) + val nav_version = "2.8.0-alpha02" + implementation("org.xerial", "sqlite-jdbc", "3.41.2.1") + implementation("org.jetbrains.androidx.navigation:navigation-compose:$nav_version") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("com.github.uhh-lt:chinese-whispers:-SNAPSHOT") + + //neo4j + implementation("org.neo4j.driver", "neo4j-java-driver", "5.6.0") + + // logging + implementation("io.github.microutils", "kotlin-logging-jvm", "2.0.6") + implementation("org.slf4j", "slf4j-simple", "1.7.29") + + testImplementation(kotlin("test")) +} + +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "GraphVisualizer" + packageVersion = "1.0.0" + } + } +} + +tasks { + test { + useJUnitPlatform() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..98aed13 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +kotlin.code.style=official +kotlin.version=1.9.22 +compose.version=1.6.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7e6084b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri May 17 20:03:05 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..bce8038 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + gradlePluginPortal() + mavenCentral() + } + + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) + } +} + +rootProject.name = "GraphVisualizer" diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..f23f5a6 --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,44 @@ +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.* +import java.awt.Dimension +import java.awt.GraphicsEnvironment + +val width = try { + GraphicsEnvironment.getLocalGraphicsEnvironment().maximumWindowBounds.width +} catch (e: Exception) { + 100 +} +val height = try { + GraphicsEnvironment.getLocalGraphicsEnvironment().maximumWindowBounds.height +} catch (e: Exception) { + 100 +} + +fun main() { + application { + val state = WindowState( + width = width.dp, + height = height.dp, + position = WindowPosition(alignment = Alignment.Center), + ) + Window( + state = state, + onCloseRequest = ::exitApplication, + title = "Graph Visualizer", + ) { + window.minimumSize = Dimension(100, 100) + App() + } + } +} + +@Composable +fun App() { + MaterialTheme() { + Navigation() + } +} diff --git a/src/main/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt new file mode 100644 index 0000000..9ada410 --- /dev/null +++ b/src/main/kotlin/Navigation.kt @@ -0,0 +1,55 @@ +import androidx.compose.runtime.Composable +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import view.screens.* +import viewmodel.DirectedGraphViewModel +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedGraphViewModel + +@Composable +fun Navigation() { + val navController = rememberNavController() + val mainScreenViewModel = MainScreenViewModel() + NavHost( + navController = navController, + startDestination = Screen.MainScreen.route + ) { + composable(route = Screen.MainScreen.route) { + MainScreen(navController = navController, mainScreenViewModel) + } + + composable( + route = "${Screen.UndirectedGraphScreen.route}/{graphName}", + arguments = listOf(navArgument("graphName") { type = NavType.StringType }) + ) { navBackStackEntry -> + + val graphName = navBackStackEntry.arguments?.getString("graphName") + ?: throw IllegalArgumentException("graphName must be provided when navigate to UndirectedGraphScreen") + val graphVM = + mainScreenViewModel.getGraph(graphName) as? UndirectedGraphViewModel + ?: throw IllegalStateException("Can't find graph with given in navigation name") + + UndirectedGraphScreen(mainScreenViewModel, navController, graphVM) + } + + composable( + route = "${Screen.DirectedGraphScreen.route}/{graphName}", + arguments = listOf(navArgument("graphName") { type = NavType.StringType }) + ) { navBackStackEntry -> + + val graphName = navBackStackEntry.arguments?.getString("graphName") + ?: throw IllegalArgumentException("graphName must be provided when navigate to DirectedGraphScreen") + val graphVM = mainScreenViewModel.getGraph(graphName) as? DirectedGraphViewModel + ?: throw IllegalStateException("Can't find graph with given in navigation name") + + DirectedGraphScreen(mainScreenViewModel, navController, graphVM) + } + + composable(route = Screen.SettingsScreen.route) { + SettingsScreen(navController = navController, mainScreenViewModel) + } + } +} diff --git a/src/main/kotlin/localisation/Localisation.kt b/src/main/kotlin/localisation/Localisation.kt new file mode 100644 index 0000000..897bf21 --- /dev/null +++ b/src/main/kotlin/localisation/Localisation.kt @@ -0,0 +1,41 @@ +package localisation + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import view.screens.SettingsJSON +import java.io.File +import java.time.LocalDateTime + +@kotlinx.serialization.Serializable +class TranslationPair(val code: String, val localisation: String) + +@Serializable +class TranslationList(val transList: List) + +fun localisation(text: String): String{ + val language = getLocalisation() + try { + val data = Json.decodeFromString(File("src/main/resources/localisation/$language.json").readText()) + for (wordPair in data.transList) { + if (wordPair.code == text){ + return wordPair.localisation + } + } + return text + } + catch (ex: Exception){ + File("src/main/kotlin/localisation/logs/localisationError.log").appendText("LOCALISATION ERROR ${LocalDateTime.now()} -- Key $text is not found in $language\nEXCEPTION IS $ex\n") + return text + } +} + +fun getLocalisation(): String{ + try { + val language = Json.decodeFromString(File("src/main/kotlin/settings.json").readText()).language + return language + } + catch(ex: Exception){ + File("src/main/kotlin/localisation/logs/localisationError.log").appendText("FILE OF LOC NOT FOUND ${LocalDateTime.now()} -- EXCEPTION IS $ex\n") + return "en-US" + } +} \ No newline at end of file diff --git a/src/main/kotlin/localisation/logs/localisationError.log b/src/main/kotlin/localisation/logs/localisationError.log new file mode 100644 index 0000000..8aa04b4 --- /dev/null +++ b/src/main/kotlin/localisation/logs/localisationError.log @@ -0,0 +1,41 @@ +[Текст песни «+7(952)812»] + +[Интро] +Это второй +А +That's a Krishtall +Ау-у, YEEI, а + +[Припев] +52 (Алло) +Да здравствует Санкт-Петербург (А), и это город наш (YEEI) +Я каждый свой новый куплет валю как никогда (YEEI, а) +Альбом, он чисто мой, никому его не продам (Он мой) +Не думаю о том (YEEI), как хорошо было вчера (А-а; мне пох) + +[Куплет] +Меняю города (А) +Представляю район — у меня есть репертуар (YEEI, 2-3) +Никогда не просил, но всегда где-то доставал (Где?) +Чем больше денег (А), тем больше мне нравится Москва (А) +Но в Питере душа (YEEI), в Питере семья (YEEI) +В Питере братва (А, а), там знают наши имена (52) ++7(952)8-1-2 (Алло) +Это второй альбом (А), вторая глава (Второй) +Не думал, не гадал, всё, что я делал, — рэповал (Всегда) +Андеграунд — это не броуки в протёртых штанах (Пошёл на хуй) +Нужно прожить мою жизнь, чтоб так же, как я, слагать (Ага) +Нужно мой рэп услышать (YEEI), чтоб точно его понять + +[Припев] +52 (Алло) +Да здравствует Санкт-Петербург (А), и это город наш (YEEI) +Я каждый свой новый куплет валю как никогда (YEEI, а) +Альбом, он чисто мой, никому его не продам (Он мой) +Не думаю о том (YEEI), как хорошо было вчера (Ага) + +[Аутро] +Да здравствует 52 +Да здравствует Петербург, да здравствует 52 +Да здравствует Петербург, да здравствует 52 (Ау; YEEI, а) +Да здравствует 52 (Ау), YEEI, long live (Это второй) diff --git a/src/main/kotlin/model/algos/BetweenesCentrality.kt b/src/main/kotlin/model/algos/BetweenesCentrality.kt new file mode 100644 index 0000000..5b80219 --- /dev/null +++ b/src/main/kotlin/model/algos/BetweenesCentrality.kt @@ -0,0 +1,95 @@ +package model.algos + +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import java.util.LinkedList +import java.util.Queue +import java.util.Stack + +object BetweenesCentralityDirected { + fun pagerank(graph: DirectedGraph, top: Int): Map { + val ranks = mutableMapOf() + val dampingFactor = 0.8 + val vertices = graph.vertices + + vertices.forEach { vertex -> ranks[vertex] = 1.0 / vertices.size } + repeat(100) { + val newRanks = mutableMapOf() + vertices.forEach(fun(vertex: V) { + var rankSum = 0.0 + vertices.forEach { neighbor -> + val edges = graph.edgesOf(neighbor) + if (neighbor != vertex) { + if (edges.any { it.to == vertex }) { + rankSum += ranks[neighbor]?.div(edges.size) ?: 0.0 + } + } + } + newRanks[vertex] = (1 - dampingFactor) / vertices.size + dampingFactor * rankSum + }) + newRanks.forEach { (vertex, value) -> + ranks[vertex] = value + } + } + return ranks.entries.sortedByDescending { it.value }.take(top).associate { it.toPair() } + } +} + +object BetweenesCentralityUndirected { + fun compute(graph: UndirectedGraph, top: Int): Map { + val centrality = mutableMapOf() + for (v in graph.vertices) { + centrality[v] = 0.0 + } + + for (s in graph.vertices) { + val stack = Stack() + val predecessors = mutableMapOf>() + val shortestPaths = mutableMapOf() + val distance = mutableMapOf() + val dependency = mutableMapOf() + + for (v in graph.vertices) { + predecessors[v] = mutableListOf() + shortestPaths[v] = 0 + distance[v] = -1 + dependency[v] = 0.0 + } + + shortestPaths[s] = 1 + distance[s] = 0 + val queue: Queue = LinkedList() + queue.add(s) + + while (queue.isNotEmpty()) { + val v = queue.poll() + stack.push(v) + for (edge in graph.edgesOf(v)) { + val w = edge.to + if (distance[w]!! < 0) { + queue.add(w) + distance[w] = distance[v]!! + 1 + } + if (distance[w] == distance[v]!! + 1) { + shortestPaths[w] = shortestPaths[w]!! + shortestPaths[v]!! + predecessors[w]!!.add(v) + } + } + } + + while (stack.isNotEmpty()) { + val w = stack.pop() + for (v in predecessors[w]!!) { + dependency[v] = + dependency[v]!! + (shortestPaths[v]!!.toDouble() / shortestPaths[w]!!) * (1 + dependency[w]!!) + } + if (w != s) { + centrality[w] = centrality[w]!! + dependency[w]!! + } + } + } + + return centrality.entries.sortedByDescending { it.value }.take(top) + .associate { it.toPair() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/algos/Dijkstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt new file mode 100644 index 0000000..9345178 --- /dev/null +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -0,0 +1,51 @@ +import model.graph.Edge +import model.graph.Graph +import java.util.* + +class Dijkstra(val graph: Graph, private val totalNodes: Int) { + private val vertexValues: MutableMap = emptyMap().toMutableMap() + private val visitedSet: MutableSet = HashSet() + private val prioraQueue = PriorityQueue(totalNodes) + private val pathMap: MutableMap>> = + emptyMap>>().toMutableMap() + + fun dijkstra(start: V, end: V): MutableList> { + for (vertex in graph.vertices) { + vertexValues.put(vertex, Int.MAX_VALUE) + pathMap.put(vertex, emptyList>().toMutableList()) + } + prioraQueue.add(start) + vertexValues[start] = 0 + + while (visitedSet.size != totalNodes) { + if (prioraQueue.isEmpty()) { + return pathMap[end]!! + } + val ux = prioraQueue.remove() + if (visitedSet.contains(ux)) { + continue + } + if (ux != null) { + visitedSet.add(ux) + refreshSearch(ux) + } + } + return pathMap[end]!! + } + + private fun refreshSearch(currentVertex: V) { + var newRange = -1 + for (edge in graph.edgesOf(currentVertex)) { + if (!visitedSet.contains(edge.to)) { + newRange = vertexValues[edge.from]!! + edge.weight + if (newRange < vertexValues[edge.to]!!) { + vertexValues[edge.to] = newRange + val k = pathMap[edge.from]?.toMutableList() + k?.add(edge) + pathMap[edge.to] = k!! + } + prioraQueue.add(edge.to) + } + } + } +} diff --git a/src/main/kotlin/model/algos/FindBridges.kt b/src/main/kotlin/model/algos/FindBridges.kt new file mode 100644 index 0000000..acc86b9 --- /dev/null +++ b/src/main/kotlin/model/algos/FindBridges.kt @@ -0,0 +1,50 @@ +package model.algos + +import model.graph.UndirectedGraph +import model.graph.Edge + +fun findBridges(graph: UndirectedGraph): Set> { + val timeIn = mutableMapOf() + for (vertex in graph.vertices) { + timeIn[vertex] = -1 + } + val ret = mutableMapOf() + var time = 0 + val bridges = mutableSetOf>() + val notVisited = graph.vertices.toMutableSet() + + fun dfs(vertex: V, prevVertex: V) { + timeIn[vertex] = time++ + notVisited.remove(vertex) + ret[vertex] = timeIn[vertex]!! + val edges = graph.edgesOf(vertex) + for (edge in edges) { + val destination = edge.to + val timeInDestination = timeIn[destination]!! + if (timeInDestination != -1 && + destination != prevVertex && + timeInDestination < ret[vertex]!! + ) { // if back edge + ret[vertex] = timeInDestination + } + if (timeInDestination != -1) { // if visited vertex + continue + } + dfs(destination, vertex) + val retDestination = ret[destination]!! + if (retDestination < ret[vertex]!!) { + ret[vertex] = retDestination + } + if (timeIn[vertex]!! < retDestination) { + bridges.add(edge) + } + } + } + + + while (notVisited.isNotEmpty()) { + dfs(notVisited.elementAt(0), notVisited.elementAt(0)) + } + + return bridges.toSet() +} diff --git a/src/main/kotlin/model/algos/FindCycle.kt b/src/main/kotlin/model/algos/FindCycle.kt new file mode 100644 index 0000000..6803b00 --- /dev/null +++ b/src/main/kotlin/model/algos/FindCycle.kt @@ -0,0 +1,103 @@ +package model.algos + +import model.graph.Graph + +object FindCycle { + fun findCycles(graph: Graph, startVertex: V): List> { + val blockedSet = mutableSetOf() + val blockedMap = mutableMapOf>() + val stack = mutableListOf() + val preResult = mutableListOf>() + val sccs = StrongConnections() + val sccResult = sccs.findStrongConnections(graph) + + for (subGraph in sccResult) { + if (subGraph.size > 1) { + val startNode = subGraph.first() + findCyclesInSCC( + startNode, + startNode, + graph, + blockedSet, + blockedMap, + stack, + preResult + ) + blockedSet.clear() + blockedMap.clear() + } + } + + val result = mutableListOf>() + for (res in preResult) { + if (res.contains(startVertex)) { + result.add(res) + } + } + return result + } + + private fun findCyclesInSCC( + start: V, + current: V, + graph: Graph, + blockedSet: MutableSet, + blockedMap: MutableMap>, + stack: MutableList, + result: MutableList> + ): Boolean { + var foundCycle = false + stack.add(current) + blockedSet.add(current) + + for (edge in graph.edgesOf(current)) { + val neighbor = edge.to + if (neighbor == start) { + val cycle = stack.toList() + result.add(cycle) + foundCycle = true + } else if (neighbor !in blockedSet) { + if (findCyclesInSCC( + start, + neighbor, + graph, + blockedSet, + blockedMap, + stack, + result + ) + ) { + foundCycle = true + } + } + } + + if (foundCycle) { + unblock(current, blockedSet, blockedMap) + } else { + for (edge in graph.edgesOf(current)) { + val neighbor = edge.to + blockedMap.computeIfAbsent(neighbor) { mutableSetOf() }.add(current) + } + } + + stack.removeAt(stack.size - 1) + return foundCycle + } + + private fun unblock( + node: V, + blockedSet: MutableSet, + blockedMap: MutableMap> + ) { + val stack = mutableListOf(node) + while (stack.isNotEmpty()) { + val current = stack.removeAt(stack.size - 1) + if (current in blockedSet) { + blockedSet.remove(current) + blockedMap[current]?.forEach { stack.add(it) } + blockedMap.remove(current) + } + } + } +} diff --git a/src/main/kotlin/model/algos/ForceAtlas2.kt b/src/main/kotlin/model/algos/ForceAtlas2.kt new file mode 100644 index 0000000..acd6673 --- /dev/null +++ b/src/main/kotlin/model/algos/ForceAtlas2.kt @@ -0,0 +1,95 @@ +package model.algos + +import height +import kotlinx.coroutines.yield +import viewmodel.graph.AbstractGraphViewModel +import viewmodel.graph.VertexViewModel +import width +import kotlin.math.sign +import kotlin.math.sqrt + +const val repulsionK: Double = 150.0 +const val attractionK: Double = 250.0 +const val gravityK: Double = 5.0 + +object ForceAtlas2 { + suspend fun forceDrawing(graphVM: AbstractGraphViewModel) { + val vertices = graphVM.verticesVM + while (true) { + yield() + val forces = mutableMapOf, Pair>() + for (vertex in vertices) { + val edges = vertex.edges + val isConnected = mutableMapOf() + for (edge in edges) { + isConnected[edge.to] = true + } + var forceX = 0.0 + var forceY = 0.0 + + val gravityForces = getGravity(vertex) + forceX += gravityForces.first + forceY += gravityForces.second + + for (vertexInner in vertices) { + if (vertexInner == vertex) continue + val dx = vertexInner.x.toDouble() - vertex.x.toDouble() + val dy = vertexInner.y.toDouble() - vertex.y.toDouble() + val repulsion = getRepulsion(dx, dy, vertex.degree, vertexInner.degree) + forceX -= sign(dx) * repulsion + forceY -= sign(dy) * repulsion + + if (isConnected[vertexInner.vertex] ?: false) { + val attraction = getAttraction(dx, dy) + forceX += sign(dx) * attraction + forceY += sign(dy) * attraction + } + } + forces[vertex] = Pair(forceX.toFloat(), forceY.toFloat()) + } + for (vertex in forces.keys) { + val forcesPair = forces[vertex]!! + if (!forcesPair.first.isNaN()) { + val newX = (vertex.x + forcesPair.first) + vertex.x = newX + } + if (!forcesPair.second.isNaN()) { + val newY = + (vertex.y + forcesPair.second) + vertex.y = newY + } + } + } + } + + private fun getRepulsion(dx: Double, dy: Double, degree1: Int, degree2: Int): Double { + val distance = getDistance(dx, dy) +// val repulsion = repulsionK * (degree1 + 1) * (1 + degree2) / distance + val repulsion = repulsionK / distance + return repulsion + } + + private fun getAttraction(dx: Double, dy: Double): Double { + val distance = getDistance(dx, dy) + val attraction = distance / attractionK + return attraction + } + + private fun getDistance(dx: Double, dy: Double): Double { + val distance = sqrt(dx * dx + dy * dy) + return distance + } + + private fun getGravity(vertex: VertexViewModel): Pair { + val x = vertex.x.toDouble() + val y = vertex.y.toDouble() + val centerX = (width - 250) / 2 + val centerY = height / 2 + + val dx = centerX - x + val dy = centerY - y + val forceX = dx.sign * gravityK + val forceY = dy.sign * gravityK + return Pair(forceX, forceY) + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt new file mode 100644 index 0000000..9e9e241 --- /dev/null +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -0,0 +1,70 @@ +package model.algos + +import model.graph.Graph +import model.graph.Edge + +typealias Path = List> +typealias Paths = Map> + +object FordBellman { + /** + * @return + * If it is possible to reach the destination , then return Pair(length of shortest path, path) + * + * If it is possible to reach the destination, but graph contains negative cycle, + * than return Pair(null, some path to destination) + * + * If it is not possible to reach the destination, then return Pair(null, null) + * + */ + fun findShortestPath(from: V, to: V, graph: Graph): Pair?> { + val distances = mutableMapOf() + val minSources = mutableMapOf>() + for (vertex in graph.vertices) { + distances[vertex] = null + } + distances[from] = 0 + + var lastTimeRelaxed = false + repeat(graph.size) { + lastTimeRelaxed = false + for (edge in graph.edges) { + val from = edge.from + val to = edge.to + if (distances[from] == null) continue + + val newWeight = distances[from]!! + edge.weight + val oldWeight: Int + if (distances[to] == null) { + distances[to] = newWeight + minSources[to] = edge + lastTimeRelaxed = true + continue + } + + oldWeight = distances[to]!! + if (oldWeight > newWeight) { + distances[to] = newWeight + minSources[to] = edge + lastTimeRelaxed = true + } + } + } + var path: MutableList>? = null + var curVert = to + if (distances[curVert] != null) { + path = mutableListOf() + while (curVert != from) { + val prevEdge = minSources[curVert] + ?: throw IllegalStateException("Can't find previous edge of path") + path.add(prevEdge) + curVert = prevEdge.from + } + } + + if (lastTimeRelaxed) distances[to] = null + val pathAnswer = path?.reversed()?.toList() + return Pair(distances[to], pathAnswer) + } +} + diff --git a/src/main/kotlin/model/algos/Prim.kt b/src/main/kotlin/model/algos/Prim.kt new file mode 100644 index 0000000..944845d --- /dev/null +++ b/src/main/kotlin/model/algos/Prim.kt @@ -0,0 +1,35 @@ +package model.algos + +import model.graph.Edge +import model.graph.UndirectedGraph +import java.util.PriorityQueue + +object Prim { + // find minimum spanning tree + fun findMst(graph: UndirectedGraph, startVertex: V): List> { + val mst = mutableListOf>() + val visited = mutableSetOf() + val edgeQueue = PriorityQueue>() + + fun addEdges(vertex: V) { + visited.add(vertex) + for (edge in graph.edgesOf(vertex)) { + if (edge.to !in visited) { + edgeQueue.add(edge) + } + } + } + + addEdges(startVertex) + + while (edgeQueue.isNotEmpty()) { + val edge = edgeQueue.poll() + if (edge.to !in visited) { + mst.add(edge) + addEdges(edge.to) + } + } + + return mst + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/algos/StrongConnections.kt b/src/main/kotlin/model/algos/StrongConnections.kt new file mode 100644 index 0000000..e20e281 --- /dev/null +++ b/src/main/kotlin/model/algos/StrongConnections.kt @@ -0,0 +1,72 @@ +package model.algos + +import model.graph.DirectedGraph +import model.graph.Edge +import model.graph.Graph + +class StrongConnections { + private val comparatorItoV = emptyMap().toMutableMap() + private val comparatorVtoI = emptyMap().toMutableMap() + + fun findStrongConnections(graph: Graph): List> { + for (vertex in graph.vertices) { + comparatorItoV[comparatorItoV.size] = vertex + comparatorVtoI[vertex] = comparatorVtoI.size + } + val adjustment = emptyMap>().toMutableMap() + val dim = comparatorItoV.size + val result: MutableList> = ArrayList() + val listStrongCon: MutableList = List(dim + 1) { false }.toMutableList() + for (i in comparatorVtoI.keys) adjustment[i] = emptyList().toMutableList() + for (vertex in graph.vertices) + for (edge in graph.edgesOf(vertex)) + adjustment[edge.from]?.add(edge.to) + for (indexV in 0.. = ArrayList() + connections.add(comparatorItoV[indexV]!!) + for (indexN in indexV + 1..> + ): Boolean { + val visited: MutableList = List(comparatorItoV.size + 1) { 0 }.toMutableList() + return dfs(source, top, adjustment, visited) + } + + private fun dfs( + current: Int, + top: Int, + adjustment: MutableMap>, + visited: MutableList + ): Boolean { + if (current == top) { + return true + } + visited[current] = 1 + for (x in adjustment[comparatorItoV[current]]!!) { + if (visited[comparatorVtoI[x]!!] == 0) { + if (dfs(comparatorVtoI[x]!!, top, adjustment, visited)) { + return true + } + } + } + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt new file mode 100644 index 0000000..9c1f751 --- /dev/null +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -0,0 +1,10 @@ +package model.graph + +class DirectedGraph : Graph() { + override fun addEdge(from: V, to: V, weight: Int) { + if (weight != 1) isWeighted = true + if (weight < 0) negativeWeights = true + val edge = Edge(from, to, weight) + graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } + } +} diff --git a/src/main/kotlin/model/graph/Edge.kt b/src/main/kotlin/model/graph/Edge.kt new file mode 100644 index 0000000..9145767 --- /dev/null +++ b/src/main/kotlin/model/graph/Edge.kt @@ -0,0 +1,7 @@ +package model.graph + +data class Edge(val from: V, val to: V, val weight: Int = 1) : Comparable> { + override fun compareTo(other: Edge): Int { + return this.weight - other.weight + } +} diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt new file mode 100644 index 0000000..b690dcd --- /dev/null +++ b/src/main/kotlin/model/graph/Graph.kt @@ -0,0 +1,146 @@ +package model.graph + +import java.sql.DriverManager +import java.sql.SQLException + +abstract class Graph() { + protected val graph = mutableMapOf>>() + private val DB_DRIVER = "jdbc:sqlite" + + val entries + get() = graph.entries + var isWeighted = false + protected set + var negativeWeights = false + protected set + + val vertices + get() = graph.keys + + val edges: List> + get() { + val edges = mutableListOf>() + for (vertex in vertices) { + val edgesOf = edgesOf(vertex) + edges.addAll(edgesOf) + } + return edges.toList() + } + + var size = graph.size + private set + + fun addVertex(vertex: V) { + graph.putIfAbsent(vertex, mutableListOf>()) + size++ + } + + fun degreeOfVertex(vertex: V): Int { + return graph[vertex]?.size ?: 0 + } + + fun saveSQLite(name: String, type: String, bdName: String) { + var parameterCreate = "( Vertexes String," + var parameterInput = "( Vertexes," + var create = ("CREATE TABLE '$name'") + val createIndex = ("CREATE TABLE IF NOT EXISTS Graphs (name TEXT, type TEXT);") + val insertIndex = ("INSERT INTO Graphs (name, type) VALUES('$name', '$type');") + for (i in graph.entries) { + parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " + parameterInput = "$parameterInput V${i.key.toString()}," + } + parameterCreate = parameterCreate.slice(0..parameterCreate.length - 3) + parameterCreate = "$parameterCreate )" + parameterInput = parameterInput.slice(0..parameterInput.length - 2) + parameterInput = "$parameterInput )" + create = create + parameterCreate + ";" + val connection = DriverManager.getConnection("$DB_DRIVER:$bdName.db") + ?: throw SQLException("Cannot connect to database") + val delTable = "DROP TABLE IF EXISTS '$name';" + val delIndexRec = "DELETE FROM Graphs WHERE name='$name';" + connection.createStatement().also { stmt -> + try { + stmt.execute(delTable) + } catch (ex: Exception) { + println("Can't delete old table of graph") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(delIndexRec) + } catch (ex: Exception) { + println("Can't delete graph entry from Graphs") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(createIndex) + stmt.execute(create) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(insertIndex) + } catch (ex: Exception) { + println("Unsuccessful") + println(ex) + } finally { + stmt.close() + } + } + + var request = "INSERT INTO '$name' $parameterInput VALUES " + for (i in graph.entries) { + var record = "( 'V${i.key}', " + val recList = emptyMap().toMutableMap() + for (j in graph.entries) { + recList[j.key] = "NULL" + } + for (j in i.value) { + recList[j.to] = j.weight.toString() + } + for (j in recList) { + record = "$record ${j.value}, " + } + record = record.slice(0..record.length - 3) + record = "$record )," + request = "$request $record" + } + request = request.slice(0..request.length - 2) + connection.createStatement().also { stmt -> + try { + stmt.execute(request) + } catch (ex: Exception) { + println("Unsuccessful") + println(ex) + } finally { + stmt.close() + } + } + + } + + abstract fun addEdge(from: V, to: V, weight: Int = 1) + + fun edgesOf(from: V): MutableList> { + return graph[from] ?: mutableListOf() + } + + fun forEach(action: (MutableList>) -> Unit) { + graph.forEach { number, list -> action(list) } + } + + operator fun iterator() = graph.entries.iterator() +} diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt new file mode 100644 index 0000000..f4d984c --- /dev/null +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -0,0 +1,12 @@ +package model.graph + +class UndirectedGraph : Graph() { + override fun addEdge(from: V, to: V, weight: Int) { + if (weight != 1) isWeighted = true + if (weight < 0) negativeWeights = true + val edge1 = Edge(from, to, weight) + val edge2 = Edge(to, from, weight) + graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } + graph[to]?.add(edge2) ?: { graph[to] = mutableListOf(edge2) } + } +} diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json new file mode 100644 index 0000000..53900dd --- /dev/null +++ b/src/main/kotlin/settings.json @@ -0,0 +1 @@ +{"language":"en-US","bd":"sqlite","neo4jUri":"","neo4jUser":"","neo4jPassword":""} \ No newline at end of file diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt new file mode 100644 index 0000000..bb6b173 --- /dev/null +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -0,0 +1,163 @@ +package view.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState +import localisation.getLocalisation +import localisation.localisation +import viewmodel.graph.AbstractGraphViewModel + +@Composable +fun AddEdgeDialog( + _visible: Boolean, + onClose: () -> Unit, + graphVM: AbstractGraphViewModel, + isDirected: Boolean = false +) { + var visible by mutableStateOf(_visible) + DialogWindow( + visible = visible, + title = "New Edge", + onCloseRequest = onClose, + state = rememberDialogState(height = 520.dp, width = 580.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + var notWeighted by remember { mutableStateOf(true) } + var weight by remember { mutableStateOf("1") } + val language = getLocalisation() + Column(modifier = Modifier.padding(30.dp, 24.dp).fillMaxSize()) { + val textWidth = 90.dp + val rightPadding = 200.dp + Row { + Text( + text = localisation(if (isDirected) "from" else "1st"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth), + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(rightPadding)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Text( + text = localisation(if (isDirected) "to" else ("2nd")), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth) + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }, + ) + Spacer(modifier = Modifier.width(rightPadding)) + } + Spacer(modifier = Modifier.height(20.dp)) + Row { + if (!notWeighted) { + Text( + text = localisation("weight"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + .width(textWidth + 30.dp) + ) + TextField( + enabled = !notWeighted, + modifier = Modifier + .fillMaxWidth() + .border( + 3.dp, + color = Color.Black, + shape = RoundedCornerShape(10.dp), + ) + .background(color = Color.White, shape = RoundedCornerShape(10.dp)), + shape = RoundedCornerShape(10.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + textStyle = defaultStyle, + value = weight, + onValueChange = { value -> + if (value.length < 10) weight = + value.filter { it.isDigit() || (it == '-' && it == value.first()) } + }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } + Spacer(modifier = Modifier.weight(1f)) + Row { + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = notWeighted, + onCheckedChange = { + notWeighted = it; + weight = if (notWeighted) "1" else "" + } + ) + Text( + text = localisation("unweighted"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(rightPadding)) + } + Spacer(modifier = Modifier.height(20.dp)) + Row { + val onClick = { + if (weight == "") weight = "1" + graphVM.addEdge(source, destination, weight.toInt()) + visible = false + + } + DefaultButton( + onClick, "add", when (language) { + "ru-RU" -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.width(30.dp)) + DefaultButton(onClose, "back", defaultStyle, Color.Red) + } + } + } +} diff --git a/src/main/kotlin/view/common/AddVertexDialog.kt b/src/main/kotlin/view/common/AddVertexDialog.kt new file mode 100644 index 0000000..5eb0d4a --- /dev/null +++ b/src/main/kotlin/view/common/AddVertexDialog.kt @@ -0,0 +1,93 @@ +package view.common + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState +import localisation.getLocalisation +import localisation.localisation +import viewmodel.graph.AbstractGraphViewModel + +@Composable +fun AddVertexDialog( + _visible: Boolean, + onClose: () -> Unit, + graphVM: AbstractGraphViewModel, +) { + var visible by mutableStateOf(_visible) + var centerCoordinates by remember { mutableStateOf(true) } + DialogWindow( + visible = visible, + title = "New Vertices", + onCloseRequest = onClose, + state = rememberDialogState(height = 340.dp, width = 560.dp) + ) { + var verticesNumber by remember { mutableStateOf("1") } + val language = getLocalisation() + Column(modifier = Modifier.padding(30.dp, 24.dp)) { + Row(modifier = Modifier.fillMaxWidth()) { + Text( + text = localisation("number"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(180.dp), + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = verticesNumber, + onValueChange = { newValue -> + if (newValue.length < 6) { + verticesNumber = newValue.filter { it.isDigit() } + } + }, + ) + } + Spacer(modifier = Modifier.height(30.dp)) + Row { + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = centerCoordinates, + onCheckedChange = { centerCoordinates = it } + ) + Text( + text = localisation("center_coordinates"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + Spacer(modifier = Modifier.height(30.dp)) + Row { + val onClick = { + if (verticesNumber == "") verticesNumber = "1" + repeat(verticesNumber.toInt()) { + graphVM.addVertex(graphVM.size.toString(), centerCoordinates) + } + graphVM.updateView() + visible = false + + } + DefaultButton(onClick, "add", defaultStyle) + Spacer(modifier = Modifier.width(30.dp)) + DefaultButton(onClose, "back", defaultStyle, Color.Red) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/common/BounceClick.kt b/src/main/kotlin/view/common/BounceClick.kt new file mode 100644 index 0000000..084158a --- /dev/null +++ b/src/main/kotlin/view/common/BounceClick.kt @@ -0,0 +1,44 @@ +package view.common + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput + +enum class ButtonState { Pressed, Idle } + +fun Modifier.bounceClick() = composed { + var buttonState by remember { mutableStateOf(ButtonState.Idle) } + val scale by animateFloatAsState(if (buttonState == ButtonState.Pressed) 0.70f else 1f) + + this + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { } + ) + .pointerInput(buttonState) { + awaitPointerEventScope { + buttonState = if (buttonState == ButtonState.Pressed) { + waitForUpOrCancellation() + ButtonState.Idle + } else { + awaitFirstDown(false) + ButtonState.Pressed + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/common/DefaultButton.kt b/src/main/kotlin/view/common/DefaultButton.kt new file mode 100644 index 0000000..c83960c --- /dev/null +++ b/src/main/kotlin/view/common/DefaultButton.kt @@ -0,0 +1,36 @@ +package view.common + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import localisation.localisation + +@Composable +fun DefaultButton( + onClick: () -> Unit, + localisationCode: String, + style: TextStyle = defaultStyle, + color: Color = DefaultColors.primaryBright, + width: androidx.compose.ui.unit.Dp = 240.dp, + height: androidx.compose.ui.unit.Dp = 80.dp, +) { + Button( + onClick = onClick, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(width, height), + colors = ButtonDefaults.buttonColors(backgroundColor = color) + ) { + Text(localisation(localisationCode), style = style) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/common/DefaultShortButton.kt b/src/main/kotlin/view/common/DefaultShortButton.kt new file mode 100644 index 0000000..dfda6f4 --- /dev/null +++ b/src/main/kotlin/view/common/DefaultShortButton.kt @@ -0,0 +1,17 @@ +package view.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp + +@Composable +fun DefaultShortButton( + onClick: () -> Unit, + localisationCode: String, + style: TextStyle = defaultStyle, + color: Color = DefaultColors.primaryBright, + + ) { + DefaultButton(onClick, localisationCode, style, color, 220.dp, 70.dp) +} \ No newline at end of file diff --git a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt new file mode 100644 index 0000000..a1dace1 --- /dev/null +++ b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt @@ -0,0 +1,99 @@ +package view.common + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState +import localisation.localisation +import viewmodel.graph.AbstractGraphViewModel + + +@Composable +fun DirectedAlgorithmDialog( + visible: Boolean, + title: String, + onCloseRequest: () -> Unit, + graphVM: AbstractGraphViewModel, + action: String, +) { + DialogWindow( + visible = visible, + title = title, + onCloseRequest = onCloseRequest, + state = rememberDialogState(height = 380.dp, width = 580.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + val textWidth = 90.dp + Column(modifier = Modifier.padding(30.dp, 24.dp)) { + Row { + Text( + text = localisation("from"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth) + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Text( + text = localisation("to"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth) + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + val dijkstra = { graphVM.drawDijkstra(source, destination) } + val fordBellman = { graphVM.drawFordBellman(source, destination) } + val onClick = if (action == "Dijkstra") { + dijkstra + } else if (action == "FordBellman") { + fordBellman + } else { + {} + } + DefaultButton(onClick, "start", defaultStyle) + Spacer(modifier = Modifier.width(30.dp)) + DefaultButton(onCloseRequest, "back", defaultStyle, Color.Red) + } + } + } +} diff --git a/src/main/kotlin/view/common/styling.kt b/src/main/kotlin/view/common/styling.kt new file mode 100644 index 0000000..b277944 --- /dev/null +++ b/src/main/kotlin/view/common/styling.kt @@ -0,0 +1,47 @@ +package view.common + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import view.screens.SettingType +import view.screens.getSetting + +val defaultStyle = TextStyle(fontSize = 28.sp) +val microStyle = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Center) +val smallStyle = TextStyle(fontSize = 22.sp, textAlign = TextAlign.Center) +val mediumStyle = TextStyle(fontSize = 26.sp, textAlign = TextAlign.Center) + +val bigStyle = TextStyle(fontSize = 50.sp) + +object DefaultColors { + val greenBright = Color(0xff00E400) + + val yellowBright = Color(0xffFFF14A) + val yellowDark = Color(0xffCFC007) + val blueBright = Color(0xff00BDFF) + val blueDark = Color(0xff076FBE) + val pinkBright = Color(0xffFFD3D3) + val pinkDark = Color(0xffFCABAB) + val background = Color.White + + var primaryBright by mutableStateOf( + when (getSetting(SettingType.BD)) { + "sqlite" -> pinkBright + "neo4j" -> blueBright + "local" -> yellowBright + else -> throw IllegalStateException("BD Setting is invalid") + } + ) + var primaryDark by mutableStateOf( + when (getSetting(SettingType.BD)) { + "sqlite" -> pinkDark + "neo4j" -> blueDark + "local" -> yellowDark + else -> throw IllegalStateException("BD Setting is invalid") + } + ) +} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/GraphView.kt b/src/main/kotlin/view/graph/GraphView.kt new file mode 100644 index 0000000..35a000e --- /dev/null +++ b/src/main/kotlin/view/graph/GraphView.kt @@ -0,0 +1,23 @@ +package view.graph + +import androidx.compose.runtime.Composable +import view.graph.vertex.DirectedVertexView +import view.graph.vertex.UndirectedVertexView +import viewmodel.DirectedGraphViewModel +import viewmodel.UndirectedGraphViewModel + +@Composable +fun UndirectedGraphView(graphVM: UndirectedGraphViewModel) { + for (vertexVM in graphVM.verticesVM) { + UndirectedVertexView(vertexVM, graphVM) + + } +} + + +@Composable +fun DirectedGraphView(graphVM: DirectedGraphViewModel) { + for (vertexVM in graphVM.verticesVM) { + DirectedVertexView(vertexVM, graphVM) + } +} diff --git a/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt b/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt new file mode 100644 index 0000000..c602265 --- /dev/null +++ b/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt @@ -0,0 +1,108 @@ +package view.graph.edge + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import viewmodel.DirectedGraphViewModel +import viewmodel.graph.EdgeViewModel +import kotlin.math.atan2 + +@Composable +fun DirectedEdgeView( + graphVM: DirectedGraphViewModel, + edgeVM: EdgeViewModel, + isWeighted: Boolean, +) { + + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + val vertexSizeZoomed = edgeVM.fromVM.vertexSize * graphVM.zoom + val first = edgeVM.fromVM + val second = edgeVM.toVM + drawLine( + start = Offset( + (first.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (first.offsetY + vertexSizeZoomed / 2).dp.toPx() + ), + end = Offset( + (second.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2).dp.toPx() + ), + strokeWidth = 6f * graphVM.zoom, + color = edgeVM.color, + ) + rotate( + degrees = ((57.2958 * (atan2( + ((first.offsetY - second.offsetY).toDouble()).dp.toPx(), + ((first.offsetX - second.offsetX).toDouble()).dp.toPx() + ))).toFloat()), + pivot = Offset( + (second.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2).dp.toPx() + ) + ) { + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 16f * graphVM.zoom), + topLeft = Offset( + (second.offsetX + vertexSizeZoomed / 2 + 65 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 8f * graphVM.zoom).dp.toPx() + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 14f * graphVM.zoom), + topLeft = Offset( + (second.offsetX + vertexSizeZoomed / 2 + 60 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 7f * graphVM.zoom).dp.toPx() + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 12f * graphVM.zoom), + topLeft = Offset( + (second.offsetX + vertexSizeZoomed / 2 + 55 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 6f * graphVM.zoom).dp.toPx() + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 10f * graphVM.zoom), + topLeft = Offset( + (second.offsetX + vertexSizeZoomed / 2 + 50 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 5f * graphVM.zoom).dp.toPx() + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 8f * graphVM.zoom), + topLeft = Offset( + (second.offsetX + vertexSizeZoomed / 2 + 45 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 4f * graphVM.zoom).dp.toPx() + ), + ) + } + if (isWeighted) { + drawText( + textMeasurer, edgeVM.weight.toString(), + topLeft = Offset( + ((first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom).dp.toPx(), + ((first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom).dp.toPx() + ), + style = TextStyle(background = Color.White, fontSize = 20.sp) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt b/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt new file mode 100644 index 0000000..e7b9042 --- /dev/null +++ b/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt @@ -0,0 +1,53 @@ +package view.graph.edge + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import viewmodel.UndirectedGraphViewModel +import viewmodel.graph.EdgeViewModel + +@Composable +fun UndirectedEdgeView( + graphVM: UndirectedGraphViewModel, + edgeVM: EdgeViewModel, + isWeighted: Boolean, +) { + + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + val vertexSizeZoomed = edgeVM.fromVM.vertexSize * graphVM.zoom + val first = edgeVM.fromVM + val second = edgeVM.toVM + drawLine( + start = Offset( + (first.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (first.offsetY + vertexSizeZoomed / 2).dp.toPx() + ), + end = Offset( + (second.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2).dp.toPx() + ), + strokeWidth = 5f * graphVM.zoom, + color = edgeVM.color, + ) + if (isWeighted) + drawText( + textMeasurer, edgeVM.weight.toString(), + topLeft = Offset( + ((first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom).dp.toPx(), + ((first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom).dp.toPx() + ), + style = TextStyle(background = Color.White, fontSize = 20.sp) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt b/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt new file mode 100644 index 0000000..64af669 --- /dev/null +++ b/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt @@ -0,0 +1,77 @@ +package view.graph.vertex + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import view.graph.edge.DirectedEdgeView +import viewmodel.DirectedGraphViewModel +import viewmodel.graph.VertexViewModel +import kotlin.math.min + +@Composable +fun DirectedVertexView( + vertexVM: VertexViewModel, + graphVM: DirectedGraphViewModel, +) { + val vertex = vertexVM.vertex + + Box( + modifier = Modifier + .offset( + vertexVM.offsetX.dp, + vertexVM.offsetY.dp + ) + .clip(shape = CircleShape) + .size((vertexVM.vertexSize * graphVM.zoom).dp) + .background(vertexVM.color) + .border((5 * graphVM.zoom).dp, Color.Black, CircleShape) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + vertexVM.x += dragAmount.x / graphVM.zoom + vertexVM.y += dragAmount.y / graphVM.zoom + } + + } + ) { + Text( + text = "$vertex", + fontSize = (graphVM.zoom * 28).sp, + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + .offset(y = (-vertexVM.vertexSize / 10).dp), + ) + } + if (graphVM.visibleCentrality) { + Box(modifier = Modifier.zIndex(-3f)) { + var centrality = vertexVM.centrality.toString() + centrality = centrality.substring(0, min(4, centrality.length)) + Text( + centrality, fontSize = (28 * graphVM.zoom).sp, + color = Color.LightGray, + modifier = Modifier + .wrapContentSize() + .offset( + x = (vertexVM.offsetX).dp, + y = (vertexVM.offsetY - vertexVM.vertexSize * 0.7 * graphVM.zoom).dp + ) + ) + } + } + + for (edgeVM in vertexVM.edges) { + DirectedEdgeView(graphVM, edgeVM, graphVM.isWeighted) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt new file mode 100644 index 0000000..522af96 --- /dev/null +++ b/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt @@ -0,0 +1,80 @@ +package view.graph.vertex + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import view.graph.edge.UndirectedEdgeView +import viewmodel.UndirectedGraphViewModel +import viewmodel.graph.VertexViewModel +import kotlin.math.min + +@Composable +fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { + val vertex = vertexVM.vertex + + Box( + modifier = Modifier + .offset( + vertexVM.offsetX.dp, + vertexVM.offsetY.dp + ) + .clip(shape = CircleShape) + .size((vertexVM.vertexSize * graphVM.zoom).dp) + .background(vertexVM.color) + .border((5 * graphVM.zoom).dp, Color.Black, CircleShape) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + vertexVM.x += dragAmount.x / graphVM.zoom + vertexVM.y += dragAmount.y / graphVM.zoom + } + + } + ) { + Text( + text = "$vertex", + fontSize = (graphVM.zoom * 28).sp, + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + .offset(y = (-vertexVM.vertexSize / 10).dp), + ) + if (graphVM.visibleCentrality) { + Text( + "${vertexVM.centrality}", fontSize = (28 * graphVM.zoom).sp, + modifier = Modifier.wrapContentSize().offset(y = (-vertexVM.vertexSize - 10).dp) + ) + } + } + if (graphVM.visibleCentrality) { + Box(modifier = Modifier.zIndex(-3f)) { + var centrality = vertexVM.centrality.toString() + centrality = centrality.substring(0, min(4, centrality.length)) + Text( + centrality, fontSize = (28 * graphVM.zoom).sp, + color = Color.LightGray, + modifier = Modifier + .wrapContentSize() + .offset( + x = (vertexVM.offsetX + vertexVM.vertexSize / 7 * graphVM.zoom).dp, + y = (vertexVM.offsetY - vertexVM.vertexSize * 0.7 * graphVM.zoom).dp + ) + ) + } + } + + for (edgeVM in vertexVM.edges) { + UndirectedEdgeView(graphVM, edgeVM, graphVM.isWeighted) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt new file mode 100644 index 0000000..8c1ead9 --- /dev/null +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -0,0 +1,231 @@ +package view.screens + + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.PointerMatcher +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import localisation.getLocalisation +import model.algos.ForceAtlas2 +import view.common.* +import view.graph.DirectedGraphView +import viewmodel.DirectedGraphViewModel +import viewmodel.MainScreenViewModel + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Composable +fun DirectedGraphScreen( + mainScreenViewModel: MainScreenViewModel, + navController: NavController, + graphVM: DirectedGraphViewModel +) { + val language = getLocalisation() + Box( + modifier = Modifier + .fillMaxSize() + .onPointerEvent(PointerEventType.Scroll) { + if (it.changes.first().scrollDelta.y > 0) { + graphVM.zoom = (graphVM.zoom - graphVM.zoom / 8).coerceIn(0.01f, 15f).dp.toPx() + } else { + graphVM.zoom = (graphVM.zoom + graphVM.zoom / 8).coerceIn(0.01f, 15f).dp.toPx() + + val awtEvent = it.awtEventOrNull + if (awtEvent != null) { + val xPosition = awtEvent.x.toFloat() + val yPosition = awtEvent.y.toFloat() + val pointerVector = + (Offset( + xPosition.dp.toPx(), + yPosition.dp.toPx() + ) - (graphVM.canvasSize / 2f)) * (1 / graphVM.zoom) + graphVM.center += pointerVector * (0.15f.dp.toPx()) + } + } + }.pointerInput(Unit) { + detectDragGestures( + matcher = PointerMatcher.Primary + ) { + graphVM.center -= it * (1 / graphVM.zoom).dp.toPx() + } + }.pointerHoverIcon(PointerIcon.Hand) + .onSizeChanged { + graphVM.canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) + } + .clipToBounds() + ) { + DirectedGraphView(graphVM) + } + + Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { + var isOpenedVertexMenu by remember { mutableStateOf(false) } + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + var isOpenedDijkstraMenu by remember { mutableStateOf(false) } + var isOpenedFordBellmanMenu by remember { mutableStateOf(false) } + var isVisualizationRunning by remember { mutableStateOf(false) } + + // To MainScreen + DefaultShortButton({ navController.popBackStack() }, "home", defaultStyle) + Spacer(modifier = Modifier.height(10.dp)) + + // Add vertex Button + DefaultShortButton( + { isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when (language) { + ("ru-RU") -> microStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Add edge Button + DefaultShortButton( + { isOpenedEdgeMenu = !isOpenedEdgeMenu }, "add_edge", when (language) { + ("ru-RU") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Save button + DefaultShortButton( + { mainScreenViewModel.saveGraph(graphVM.name) }, + "save", + color = DefaultColors.greenBright + ) + Spacer(modifier = Modifier.height(16.dp)) + + // Visualization Button + val scope = rememberCoroutineScope { Dispatchers.Default } + DefaultShortButton( + { + isVisualizationRunning = !isVisualizationRunning + if (isVisualizationRunning) { + scope.launch { + ForceAtlas2.forceDrawing(graphVM) + } + } else { + scope.coroutineContext.cancelChildren() + } + }, "visualize", defaultStyle, + if (isVisualizationRunning) Color.Red else Color(0xffFFB300) + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + { graphVM.resetColors() }, "reset", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallStyle + else -> defaultStyle + }, Color.LightGray + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + { graphVM.drawBetweennessCentrality() }, + "betweenness_centrality", + microStyle + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + { graphVM.chinaWhisperCluster() }, "find_clusters", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + { graphVM.drawStrongConnections() }, "find_strong_connections", when (language) { + ("en-US") -> smallStyle + ("ru-RU") -> microStyle + ("cn-CN") -> microStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Dijkstra Button + DefaultShortButton( + { isOpenedDijkstraMenu = !isOpenedDijkstraMenu }, "dijkstra", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // FordBellman Button + DefaultShortButton( + { isOpenedFordBellmanMenu = !isOpenedFordBellmanMenu }, + "ford_bellman", + when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Cycles Button + DefaultShortButton( + onClick = { graphVM.drawCycles("1") }, "find_cycles", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> mediumStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Add vertex Dialog + AddVertexDialog( + isOpenedVertexMenu && isVisualizationRunning.not(), + { isOpenedVertexMenu = false }, + graphVM, + ) + + // Add edge Dialog + AddEdgeDialog( + isOpenedEdgeMenu, + { isOpenedEdgeMenu = !isOpenedEdgeMenu }, + graphVM, + isDirected = true + ) + + // Dijkstra Dialog + DirectedAlgorithmDialog( + isOpenedDijkstraMenu, + "Dijkstra Algorithm", + { isOpenedDijkstraMenu = false }, + graphVM, + "Dijkstra" + ) + + // Ford-Bellman Dialog + DirectedAlgorithmDialog( + isOpenedFordBellmanMenu, + "Ford Bellman Algorithm", + { isOpenedFordBellmanMenu = false }, + graphVM, + "FordBellman" + ) + } +} + diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt new file mode 100644 index 0000000..aa519d5 --- /dev/null +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -0,0 +1,332 @@ +package view.screens + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogState +import androidx.compose.ui.window.DialogWindow +import androidx.navigation.NavController +import localisation.localisation +import view.common.DefaultColors +import view.common.bigStyle +import view.common.bounceClick +import view.common.defaultStyle +import viewmodel.GraphType +import viewmodel.MainScreenViewModel +import java.io.File + + +@Composable +fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { + var search by remember { mutableStateOf("") } + var graphName by remember { mutableStateOf("") } + val dialogState = remember { mutableStateOf(false) } + val expandedDropDown = remember { mutableStateOf(false) } + var selectedOptionTextDropDown = remember { GraphType.Undirected } + + if (!mainScreenViewModel.inited) { + mainScreenViewModel.initGraphList() + } + Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { + Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { + // Search tab + TextField( + value = search, + textStyle = bigStyle, + placeholder = { Text(text = localisation("enter_graph_name"), style = bigStyle) }, + onValueChange = { search = it }, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .border( + width = 4.dp, + color = DefaultColors.primaryBright, + shape = RoundedCornerShape(45.dp) + ), + shape = RoundedCornerShape(45.dp), + trailingIcon = { + Icon( + Icons.Filled.Search, contentDescription = "SearchIcon", modifier = Modifier + .size(100.dp) + .padding(10.dp) + ) + }, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + + Spacer(modifier = Modifier.width(40.dp)) + + // Add graph + IconButton( + onClick = { + dialogState.value = true + }, + modifier = Modifier + .padding(horizontal = 10.dp) + .size(100.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .clickable { } + .background(DefaultColors.primaryBright) + .border( + width = 5.dp, + color = Color.Black, + shape = RoundedCornerShape(45.dp) + ) + .bounceClick(), + ) { + Icon( + Icons.Filled.Add, + contentDescription = "Add graph", + modifier = Modifier.size(100.dp) + ) + } + + // To settings + IconButton( + onClick = { navController.navigate(Screen.SettingsScreen.route) }, + modifier = Modifier + .padding(horizontal = 10.dp) + .size(100.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .clickable { } + .background(DefaultColors.primaryBright) + .border( + width = 5.dp, + color = Color.Black, + shape = RoundedCornerShape(45.dp) + ) + .bounceClick(), + + ) { + Icon( + Icons.Filled.Settings, + contentDescription = "Settings", + modifier = Modifier.size(100.dp) + ) + } + } + + DialogWindow( + visible = dialogState.value, + title = "New Graph", + resizable = false, + onCloseRequest = { dialogState.value = false }, + state = DialogState(size = DpSize(960.dp, 680.dp)) + ) + { + Text( + text = localisation("enter_new_graph_name"), + modifier = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + style = defaultStyle + ) + TextField( + value = graphName, + textStyle = bigStyle, + placeholder = { Text(text = localisation("write_name"), style = bigStyle) }, + onValueChange = { graphName = it }, + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 70.dp) + .size(800.dp, 90.dp) + .weight(1f) + .border( + width = 4.dp, + color = Color.Cyan, + shape = RoundedCornerShape(25.dp) + ), + shape = RoundedCornerShape(25.dp), + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + Button( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(horizontal = 20.dp, vertical = 180.dp) + .border( + width = 2.dp, + color = Color.Black, + shape = RoundedCornerShape(25.dp) + ) + .width(300.dp) + .height(60.dp), + shape = RoundedCornerShape(25.dp), + colors = if (graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.pinkBright) else ButtonDefaults.buttonColors( + backgroundColor = DefaultColors.pinkDark + ), + onClick = { + if (graphName != "") { + mainScreenViewModel.addGraph( + graphName, + selectedOptionTextDropDown, + ) + mainScreenViewModel.saveGraph(graphName) + graphName = "" + dialogState.value = false + } + }, + ) { + Text( + text = localisation("add"), + color = if (graphName != "") Color.White else Color.Black, + fontSize = 28.sp + ) + } + + Button( + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 260.dp) + .border( + width = 2.dp, + color = Color.Black, + shape = RoundedCornerShape(25.dp) + ) + .width(300.dp) + .height(60.dp), + + shape = RoundedCornerShape(25.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red), + onClick = { + dialogState.value = false + }, + ) { + Text(text = localisation("back"), color = Color.White, fontSize = 28.sp) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .padding(horizontal = 350.dp, vertical = 180.dp) + .width(300.dp) + .height(60.dp) + .clip(RoundedCornerShape(25.dp)) + .border(BorderStroke(2.dp, Color.LightGray), RoundedCornerShape(25.dp)) + .clickable { expandedDropDown.value = !expandedDropDown.value }, + ) { + Text( + text = selectedOptionTextDropDown.toString(), + fontSize = 20.sp, + modifier = Modifier.padding(start = 20.dp) + ) + Icon( + Icons.Filled.ArrowDropDown, "contentDescription", + Modifier.align(Alignment.CenterEnd) + ) + DropdownMenu( + expanded = expandedDropDown.value, + onDismissRequest = { expandedDropDown.value = false } + ) { + GraphType.entries.forEach { selectedOption -> + DropdownMenuItem( + onClick = { + selectedOptionTextDropDown = selectedOption + expandedDropDown.value = false + } + ) { + Text(text = localisation(selectedOption.toString().lowercase())) + } + } + } + } + } + + Spacer(modifier = Modifier.height(30.dp)) + + LazyColumn(modifier = Modifier.fillMaxWidth()) { + itemsIndexed(mainScreenViewModel.graphNames) { _, name -> + if (!name.startsWith(search)) return@itemsIndexed + // To GraphScreen + val graphVM = mainScreenViewModel.getGraph(name) + Row(modifier = Modifier.padding(vertical = 15.dp)) { + Button( + onClick = { + mainScreenViewModel.loadGraph(name, "storage") + if (graphVM.graphType == GraphType.Undirected) { + navController.navigate( + "${Screen.UndirectedGraphScreen.route}/$name" + ) + } else navController.navigate( + "${Screen.DirectedGraphScreen.route}/$name" + ) + }, + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .weight(1f) + .clip(shape = RoundedCornerShape(45.dp)) + .border( + width = 5.dp, + color = Color.Black, + shape = RoundedCornerShape(45.dp) + ), + colors = ButtonDefaults.buttonColors( + backgroundColor = DefaultColors.primaryBright + ) + ) { + Row { + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + Image( + bitmap = if (graphVM.graphType == GraphType.Directed) loadImageBitmap( + File("src/main/resources/directed.png").inputStream() + ) + else loadImageBitmap(File("src/main/resources/undirected.png").inputStream()), + contentDescription = "Type", + + modifier = Modifier + .padding(15.dp) + .align(Alignment.End), + ) + } + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + Text( + text = name, + style = bigStyle, + modifier = Modifier.clip(RoundedCornerShape(45.dp)) + ) + } + } + + } + + Spacer(modifier = Modifier.width(10.dp)) + + // Remove Graph + IconButton( + onClick = { mainScreenViewModel.removeGraph(name) }, + modifier = Modifier + .padding(horizontal = 10.dp) + .size(100.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .background(Color(0xe8, 0x08, 0x3e)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .bounceClick(), + ) { + Icon( + Icons.Filled.Delete, + contentDescription = "Remove graph", + modifier = Modifier + .padding(5.dp) + .fillMaxSize() + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/SealedScreens.kt b/src/main/kotlin/view/screens/SealedScreens.kt new file mode 100644 index 0000000..bd4b613 --- /dev/null +++ b/src/main/kotlin/view/screens/SealedScreens.kt @@ -0,0 +1,8 @@ +package view.screens + +sealed class Screen(val route: String){ + object MainScreen: Screen("main_screen") + object UndirectedGraphScreen: Screen("undirected_graph_screen") + object DirectedGraphScreen: Screen("directed_graph_screen") + object SettingsScreen: Screen("settings_screen") +} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/SettingsScreen.kt b/src/main/kotlin/view/screens/SettingsScreen.kt new file mode 100644 index 0000000..05f3bc3 --- /dev/null +++ b/src/main/kotlin/view/screens/SettingsScreen.kt @@ -0,0 +1,359 @@ +package view.screens + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import localisation.localisation +import view.common.DefaultColors +import view.common.bounceClick +import view.common.defaultStyle +import viewmodel.MainScreenViewModel +import java.io.File + +const val pathToSettings = "src/main/kotlin/settings.json" +const val boldLine = 8 +const val defaultLine = 3 + +@Serializable +class SettingsJSON( + var language: String, + var bd: String, + var neo4jUri: String = "", + var neo4jUser: String = "", + var neo4jPassword: String = "", +) + +enum class SettingType { + LANGUAGE, + BD, + NEO4JURI, + NEO4JUSER, + NEO4JPASSWORD, +} + +fun resetSettings() { + File(pathToSettings).writeText( + Json.encodeToString( + SettingsJSON( + "en-US", + "sqlite" + ) + ) + ) +} + +fun makeSetting(type: SettingType, value: String) { + try { + val data = Json.decodeFromString(File(pathToSettings).readText()) + when (type) { + SettingType.LANGUAGE -> data.language = value + SettingType.BD -> data.bd = value + SettingType.NEO4JURI -> data.neo4jUri = value + SettingType.NEO4JUSER -> data.neo4jUser = value + SettingType.NEO4JPASSWORD -> data.neo4jPassword = value + } + File(pathToSettings).writeText(Json.encodeToString(data)) + } catch (exception: Exception) { + resetSettings() + return + } +} + +fun getSetting(type: SettingType): String { + try { + val data = Json.decodeFromString(File(pathToSettings).readText()) + return when (type) { + SettingType.LANGUAGE -> data.language + SettingType.BD -> data.bd + SettingType.NEO4JURI -> data.neo4jUri + SettingType.NEO4JUSER -> data.neo4jUser + SettingType.NEO4JPASSWORD -> data.neo4jPassword + } + } catch (e: Exception) { + resetSettings() + return getSetting(type) + } +} + +@Composable +fun SettingsScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { + Column(modifier = Modifier.padding(20.dp, 10.dp)) { + Row(modifier = Modifier.fillMaxSize()) { + Language(navController) + Spacer(Modifier.width(10.dp)) + Saving(mainScreenViewModel) + Spacer(Modifier.width(10.dp)) + } + } +} + +@Composable +fun Language(navController: NavController) { + Column { + var language by mutableStateOf(getSetting(SettingType.LANGUAGE)) + Text( + text = localisation("language"), + fontSize = 28.sp, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp) + ) + Button( + onClick = { + makeSetting(SettingType.LANGUAGE, "cn-CN") + language = "cn-CN" + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border( + width = if (language == "cn-CN") boldLine.dp else defaultLine.dp, + color = Color.Black + ) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = + if (language == "cn-CN") DefaultColors.primaryBright else DefaultColors.primaryDark + ) + ) { + Text("汉语", style = defaultStyle) + } + Button( + onClick = { + makeSetting(SettingType.LANGUAGE, "ru-RU") + language = "ru-RU" + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border( + width = if (language == "ru-RU") boldLine.dp else defaultLine.dp, + color = Color.Black + ) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = + if (language == "ru-RU") DefaultColors.primaryBright else DefaultColors.primaryDark + ) + ) { + Text("Русский", style = defaultStyle) + } + Button( + onClick = { + makeSetting(SettingType.LANGUAGE, "en-US") + language = "en-US" + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border( + width = if (language == "en-US") boldLine.dp else defaultLine.dp, + color = Color.Black + ) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = + if (language == "en-US") DefaultColors.primaryBright else DefaultColors.primaryDark + ) + ) { + Text("English", style = defaultStyle) + } + Button( + onClick = { navController.navigate(Screen.MainScreen.route) }, + modifier = Modifier + .padding(16.dp) + .border(width = defaultLine.dp, color = Color.Black) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text(localisation("back"), style = defaultStyle, color = Color.White) + } + } +} + +@Composable +fun Saving(mainScreenViewModel: MainScreenViewModel) { + Column { + Text( + text = localisation("saving"), + fontSize = 28.sp, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp) + ) + Button( + onClick = { + makeSetting(SettingType.BD, "sqlite") + mainScreenViewModel.saveType = "sqlite" + mainScreenViewModel.inited = false + DefaultColors.primaryBright = DefaultColors.pinkBright + DefaultColors.primaryDark = DefaultColors.pinkDark + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border( + width = if (mainScreenViewModel.saveType == "sqlite") boldLine.dp else defaultLine.dp, + color = Color.Black + ) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = if (mainScreenViewModel.saveType == "sqlite") + DefaultColors.pinkBright else DefaultColors.pinkDark + ) + ) { + Text("SQLite", style = defaultStyle) + } + Button( + onClick = { + makeSetting(SettingType.BD, "neo4j") + mainScreenViewModel.saveType = "neo4j" + mainScreenViewModel.inited = false + DefaultColors.primaryBright = DefaultColors.blueBright + DefaultColors.primaryDark = DefaultColors.blueDark + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border( + width = if (mainScreenViewModel.saveType == "neo4j") boldLine.dp else defaultLine.dp, + color = Color.Black + ) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = if (mainScreenViewModel.saveType == "neo4j") + DefaultColors.blueBright else DefaultColors.blueDark + ) + ) { + Text("Neo4j", style = defaultStyle) + } + Button( + onClick = { + makeSetting(SettingType.BD, "local") + mainScreenViewModel.saveType = "local" + mainScreenViewModel.inited = false + DefaultColors.primaryBright = DefaultColors.yellowBright + DefaultColors.primaryDark = DefaultColors.yellowDark + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border( + width = if (mainScreenViewModel.saveType == "local") boldLine.dp else defaultLine.dp, + color = Color.Black + ) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = if (mainScreenViewModel.saveType == "local") + DefaultColors.yellowBright else DefaultColors.yellowDark + ) + ) { + Text("Local file", style = defaultStyle) + } + } + Spacer(modifier = Modifier.width(30.dp)) + var uri by remember { mutableStateOf(getSetting(SettingType.NEO4JURI)) } + var user by remember { mutableStateOf(getSetting(SettingType.NEO4JUSER)) } + var password by remember { mutableStateOf(getSetting(SettingType.NEO4JPASSWORD)) } + if (mainScreenViewModel.saveType == "neo4j") { + Column(modifier = Modifier.fillMaxSize()) { + Text( + text = localisation("neo4j_data"), + fontSize = 28.sp, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + TextField( + modifier = Modifier + .width(1000.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)) + .size(400.dp, 80.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + placeholder = { + Text( + "Enter URI: for example, bolt://localhost:7687", + fontSize = 32.sp + ) + }, + textStyle = TextStyle(fontSize = 32.sp), + shape = RoundedCornerShape(25.dp), + value = uri, + onValueChange = { newValue -> uri = newValue }, + ) + Spacer(modifier = Modifier.height(40.dp)) + TextField( + modifier = Modifier + .width(1000.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)) + .size(300.dp, 80.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + placeholder = { + Text( + "Enter username:", + fontSize = 32.sp + ) + }, + textStyle = TextStyle(fontSize = 32.sp), + value = user, + onValueChange = { newValue -> user = newValue }, + ) + Spacer(modifier = Modifier.height(40.dp)) + TextField( + modifier = Modifier + .width(1000.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)) + .size(400.dp, 80.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + placeholder = { + Text( + "Enter password", + fontSize = 32.sp + ) + }, + shape = RoundedCornerShape(25.dp), + textStyle = TextStyle(fontSize = 32.sp), + value = password, + onValueChange = { newValue -> password = newValue }, + ) + Spacer(modifier = Modifier.height(40.dp)) + Button( + onClick = { + makeSetting(SettingType.NEO4JURI, uri) + makeSetting(SettingType.NEO4JUSER, user) + makeSetting(SettingType.NEO4JPASSWORD, password) + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = DefaultColors.blueBright + ) + ) { + Text("Connect", style = defaultStyle) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt new file mode 100644 index 0000000..6327ee5 --- /dev/null +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -0,0 +1,214 @@ +package view.screens + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.PointerMatcher +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import localisation.getLocalisation +import model.algos.ForceAtlas2 +import view.common.* +import view.graph.UndirectedGraphView +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedGraphViewModel + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Composable +fun UndirectedGraphScreen( + mainScreenViewModel: MainScreenViewModel, + navController: NavController, + graphVM: UndirectedGraphViewModel, +) { + val language = getLocalisation() + Box(modifier = Modifier + .fillMaxSize() + .onPointerEvent(PointerEventType.Scroll) { + + if (it.changes.first().scrollDelta.y > 0) { + graphVM.zoom = (graphVM.zoom - graphVM.zoom / 8).coerceIn(0.01f, 15f) + } else { + graphVM.zoom = (graphVM.zoom + graphVM.zoom / 8).coerceIn(0.01f, 15f) + + val awtEvent = it.awtEventOrNull + if (awtEvent != null) { + val xPosition = awtEvent.x.toFloat() + val yPosition = awtEvent.y.toFloat() + val pointerVector = + (Offset( + xPosition, + yPosition + ) - (graphVM.canvasSize / 2f)) * (1 / graphVM.zoom) + graphVM.center += pointerVector * 0.15f + } + } + }.pointerInput(Unit) { + detectDragGestures( + matcher = PointerMatcher.Primary + ) { + graphVM.center -= it * (1 / graphVM.zoom) + } + }.pointerHoverIcon(PointerIcon.Hand) + .onSizeChanged { + graphVM.canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) + } + .clipToBounds() + ) { + UndirectedGraphView(graphVM) + } + + Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { + var isOpenedVertexMenu by remember { mutableStateOf(false) } + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + var isDijkstraMenu by remember { mutableStateOf(false) } + var isFordBellmanMenu by remember { mutableStateOf(false) } + var isVisualizationRunning by remember { mutableStateOf(false) } + + + // To MainScreen + DefaultShortButton({ navController.popBackStack() }, "home", defaultStyle) + Spacer(modifier = Modifier.height(10.dp)) + + // Add vertex Button + DefaultShortButton( + { isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when (language) { + ("ru-RU") -> microStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Add edge button + DefaultShortButton( + { isOpenedEdgeMenu = !isOpenedEdgeMenu }, "add_edge", when (language) { + ("ru-RU") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(16.dp)) + + // Save button + DefaultShortButton( + { mainScreenViewModel.saveGraph(graphVM.name) }, + "save", + color = DefaultColors.greenBright + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Visualization Button + val scope = rememberCoroutineScope { Dispatchers.Default } + DefaultShortButton( + { + isVisualizationRunning = !isVisualizationRunning + if (isVisualizationRunning) { + scope.launch { + ForceAtlas2.forceDrawing(graphVM) + } + } else { + scope.coroutineContext.cancelChildren() + } + }, "visualize", defaultStyle, + if (isVisualizationRunning) Color.Red else Color(0xffFFB300) + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Reset colors Button + DefaultShortButton( + { graphVM.resetColors() }, "reset", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallStyle + else -> defaultStyle + }, Color.LightGray + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + { graphVM.drawBetweennessCentrality() }, + "betweenness_centrality", + microStyle + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Dijkstra Button + DefaultShortButton( + { isDijkstraMenu = !isDijkstraMenu }, "dijkstra", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // FordBellman Button + DefaultShortButton( + { isFordBellmanMenu = !isFordBellmanMenu }, "ford_bellman", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + onClick = { graphVM.drawMst() }, "find_mst", when (language) { + ("en-US") -> smallStyle + ("ru-RU") -> microStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + onClick = { graphVM.drawBridges() }, "find_bridges", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallStyle + else -> defaultStyle + } + ) + Spacer(modifier = Modifier.height(10.dp)) + + // Add vertex Dialog + AddVertexDialog( + isOpenedVertexMenu && isVisualizationRunning.not(), + { isOpenedVertexMenu = false }, + graphVM, + ) + + // Add edge Dialog + AddEdgeDialog(isOpenedEdgeMenu, { isOpenedEdgeMenu = false }, graphVM) + + // Dijkstra Dialog + DirectedAlgorithmDialog( + isDijkstraMenu, + "Dijkstra Algorithm", + { isDijkstraMenu = false }, + graphVM, + "Dijkstra" + ) + + // Ford-Bellman Dialog + DirectedAlgorithmDialog( + isFordBellmanMenu, + "Ford Bellman Algorithm", + { isFordBellmanMenu = false }, + graphVM, + "FordBellman" + ) + + } +} diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt new file mode 100644 index 0000000..1b02507 --- /dev/null +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -0,0 +1,131 @@ +package viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import mu.KotlinLogging +import view.screens.SettingType +import view.screens.getSetting +import viewmodel.graph.AbstractGraphViewModel +import viewmodel.io.Neo4jRepository +import viewmodel.io.SQLiteRepository + +enum class GraphType() { + Undirected, + Directed, +} + +private val logger = KotlinLogging.logger { } + +class MainScreenViewModel() : ViewModel() { + var saveType by mutableStateOf(getSetting(SettingType.BD)) + val graphs by mutableStateOf(mutableMapOf>()) + val graphNames = mutableStateListOf() + internal var inited = false + + fun addGraph(name: String, type: GraphType) { + if (graphNames.contains(name)) { + return + } + val graphVM: AbstractGraphViewModel + when (type) { + GraphType.Undirected -> { + graphVM = UndirectedGraphViewModel(name) + + } + + GraphType.Directed -> { + graphVM = DirectedGraphViewModel(name) + } + } + graphs[name] = graphVM + graphNames.add(name) + } + + fun saveGraph(name: String, bdName: String = "storage") { + try { + val graphVM = getGraph(name) + if (saveType == "sqlite") { + graphVM.model.saveSQLite(name, graphVM.graphType.toString(), bdName) + } else if (saveType == "neo4j") { + val rep = Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + rep.saveGraph(graphVM) + } + } catch (e: Exception) { + logger.error { "Can't save graph: $name" } + } + } + + fun getGraph(name: String): AbstractGraphViewModel { + return graphs[name] + ?: throw IllegalStateException("Can't find graph with name $name") + } + + fun loadGraph(name: String, bdName: String) { + val graphVM = getGraph(name) + if (graphVM.isInited) return + if (saveType == "sqlite") { + SQLiteRepository.loadGraph(graphVM, bdName) + } else if (saveType == "neo4j") { + val rep = Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + graphs[name] = rep.getGraph(name) + } + graphVM.isInited = true + + } + + fun initGraphList(bdName: String = "storage") { + saveType = getSetting(SettingType.BD) + clear() + if (saveType == "sqlite") { + SQLiteRepository.initGraphList(bdName, this) + } else if (saveType == "neo4j") { + val rep = try { + Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + } catch (e: Exception) { + logger.info { "Could not start a neo4j session in repository with given data" } + return + } + rep.initGraphList(this) + } + inited = true + } + + fun removeGraph(name: String) { + if (saveType == "sqlite") { + try { + SQLiteRepository.removeGraph(name) + } catch (e: Exception) { + } + + } else if (saveType == "neo4j") { + val rep = Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + rep.removeGraph(name) + } + graphs.remove(name) + graphNames.remove(name) + } + + fun clear() { + graphs.clear() + graphNames.clear() + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt new file mode 100644 index 0000000..57be5bb --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt @@ -0,0 +1,133 @@ +package viewmodel.graph + +import Dijkstra +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import height +import model.algos.FindCycle +import model.algos.FordBellman +import model.graph.Graph +import model.graph.Edge +import view.common.DefaultColors +import viewmodel.GraphType +import width +import kotlin.random.Random + +abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { + val name = _name + protected var graphVM by mutableStateOf(mutableMapOf>()) + protected val graphModel = graph + var size = 0 + val isWeighted + get() = graphModel.isWeighted + val negativeWeights + get() = graphModel.negativeWeights + var isInited = false + var visibleCentrality by mutableStateOf(false) + val model + get() = graphModel + val verticesVM + get() = graphVM.values + val edgesVM: List> + get() { + val result = mutableListOf>() + for (edgesVM in graphVM.values) { + for (edgeVM in edgesVM.edges) { + result.add(edgeVM) + } + } + return result.toList() + } + abstract val graphType: GraphType + var zoom by mutableStateOf(1f) + var canvasSize by mutableStateOf(Offset(400f, 400f)) + var center by mutableStateOf(Offset((width / 2).toFloat(), (height / 2).toFloat())) + + init { + for (vertex in graphModel.entries) { + graphVM[vertex.key] = VertexViewModel(vertex.key, graphVM = this) + } + for (vertex in graphModel.entries) { + for (edge in vertex.value) { + val sourceVertexVM: VertexViewModel + val destinationVertexVM: VertexViewModel + try { + sourceVertexVM = graphVM[edge.from]!! + destinationVertexVM = graphVM[edge.to]!! + } catch (e: Exception) { + println("Can't set edge: source or destination is not exist") + break + } + val edgeVM = EdgeViewModel(edge, sourceVertexVM, destinationVertexVM) + sourceVertexVM.edges.add(edgeVM) + } + } + } + + abstract fun addEdge(from: V, to: V, weight: Int = 1) + + abstract fun drawEdges(edges: Collection>, color: Color) + + fun updateView() { + val keep = graphVM + graphVM = mutableMapOf>() + graphVM = keep + } + + abstract fun drawBetweennessCentrality() + + fun drawDijkstra(start: V, end: V) { + if (this.negativeWeights) return + val result = Dijkstra(graphModel, graphModel.size).dijkstra(start, end) + drawEdges(result, Color.Red) + } + + fun drawFordBellman(from: V, to: V) { + val path = FordBellman.findShortestPath(from, to, this.graphModel).second ?: emptyList() + drawEdges(path, Color.Cyan) + } + + fun drawCycles(startVertex: V) { + val findCycle = FindCycle + for (cycle in findCycle.findCycles(graphModel, startVertex)) { + val col = + Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + for (edge in cycle) { + if (edge in graphModel.vertices) { + graphVM[edge]?.color = col + } + } + } + } + + fun vertexVmOf(vertex: V): VertexViewModel? { + return graphVM[vertex] + } + + fun edgesVmOf(vertex: V): List> { + return graphVM[vertex]?.edges?.toList() ?: emptyList() + } + + fun addVertex(vertex: V, centerCoordinates: Boolean = true) { + size += 1 + graphVM.putIfAbsent( + vertex, + VertexViewModel(vertex, graphVM = this, centerCoordinates = centerCoordinates), + ) + graphModel.addVertex(vertex) + } + + fun resetColors() { + for (edgeVM in edgesVM) { + edgeVM.color = Color.Black + } + for (vertexVM in verticesVM) { + vertexVM.color = DefaultColors.primaryBright + } + visibleCentrality = false + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt new file mode 100644 index 0000000..f3964dd --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt @@ -0,0 +1,107 @@ +package viewmodel + +import androidx.compose.ui.graphics.Color +import model.algos.StrongConnections +import de.tudarmstadt.lt.cw.graph.ArrayBackedGraph +import de.tudarmstadt.lt.cw.graph.ArrayBackedGraphCW +import de.tudarmstadt.lt.cw.graph.Graph +import model.algos.BetweenesCentralityDirected +import model.graph.DirectedGraph +import model.graph.Edge +import mu.KotlinLogging +import viewmodel.graph.AbstractGraphViewModel +import viewmodel.graph.EdgeViewModel +import viewmodel.graph.VertexViewModel +import kotlin.random.Random + +private val logger = KotlinLogging.logger { } + +class DirectedGraphViewModel( + name: String, + val graph: DirectedGraph = DirectedGraph() +) : AbstractGraphViewModel(name, graph) { + + override val graphType = GraphType.Directed + + override fun addEdge(from: V, to: V, weight: Int) { + val source: VertexViewModel + val destination: VertexViewModel + try { + source = graphVM[from]!! + destination = graphVM[to]!! + } catch (e: Exception) { + println("Can't add edge between $from and $to: one of them don't exist") + return + } + for (edge in source.edges) if (edge.to == to) return + + val edge = Edge(from, to, weight) + val edgeVM = EdgeViewModel(edge, source, destination) + source.edges.add(edgeVM) + graphModel.addEdge(from, to, weight) + } + + override fun drawEdges(edges: Collection>, color: Color) { + for (edge in edges) { + for (edgeVM in this.edgesVmOf(edge.from)) { + if (edgeVM.to == edge.to) edgeVM.color = color + } + } + } + + fun chinaWhisperCluster() { + val comparatorItoV = emptyMap().toMutableMap() + val comparatorVtoI = emptyMap().toMutableMap() + for (i in graph.vertices) { + comparatorItoV[comparatorItoV.size] = i + comparatorVtoI[i] = comparatorVtoI.size + } + val cwGraph: Graph = ArrayBackedGraph(comparatorVtoI.size, comparatorVtoI.size) + for (i in comparatorItoV) { + cwGraph.addNode(i.key) + } + for (i in graph.edges) { + cwGraph.addEdge(comparatorVtoI[i.from], comparatorVtoI[i.to], i.weight.toFloat()) + } + + val cw = ArrayBackedGraphCW(comparatorItoV.size) + + val findClusters = cw.findClusters(cwGraph) + for (k in findClusters.values) { + val col = + Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + for (j in k) { + + graphVM[comparatorItoV[j]]?.color = col + } + } + } + + override fun drawBetweennessCentrality() { + val result = BetweenesCentralityDirected.pagerank(graphModel as DirectedGraph, size) + for (vertexVM in verticesVM) { + vertexVM.centrality = (result[vertexVM.vertex] ?: run { + logger.error { "Can't find centrality value for vertex in graph" } + 0.0 + }) * 100 + } + this.visibleCentrality = true + } + + fun drawStrongConnections() { + val strongConnections = StrongConnections() + for (component in strongConnections.findStrongConnections(graphModel)) { + val color = + Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + for (vertex in component) { + if (vertex in graphModel.vertices) { + graphVM[vertex]?.color = color + } + } + } + } + + fun saveSQLite() { + graph.saveSQLite(name, "Directed", "storage") + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt new file mode 100644 index 0000000..bab4290 --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -0,0 +1,23 @@ +package viewmodel.graph + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import model.graph.Edge + +class EdgeViewModel( + edge: Edge, + vertexFromVM: VertexViewModel, + vertexToVM: VertexViewModel, +) : + ViewModel() { + val fromVM = vertexFromVM + val toVM = vertexToVM + + val weight by mutableStateOf(edge.weight) + val from by mutableStateOf(edge.from) + val to by mutableStateOf(edge.to) + var color by mutableStateOf(Color.Black) +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt new file mode 100644 index 0000000..597600b --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt @@ -0,0 +1,84 @@ +package viewmodel + +import androidx.compose.ui.graphics.Color +import model.algos.BetweenesCentralityDirected +import model.algos.BetweenesCentralityUndirected +import model.algos.Prim +import model.algos.findBridges +import model.graph.DirectedGraph +import model.graph.Edge +import model.graph.UndirectedGraph +import mu.KotlinLogging +import viewmodel.GraphType +import viewmodel.graph.AbstractGraphViewModel +import viewmodel.graph.EdgeViewModel +import viewmodel.graph.VertexViewModel + +private val logger = KotlinLogging.logger { } + +class UndirectedGraphViewModel( + name: String, + val graph: UndirectedGraph = UndirectedGraph() +) : AbstractGraphViewModel(name, graph) { + override val graphType = GraphType.Undirected + + override fun addEdge(from: V, to: V, weight: Int) { + val source: VertexViewModel + val destination: VertexViewModel + try { + source = graphVM[from]!! + destination = graphVM[to]!! + } catch (e: Exception) { + println("Can't add edge between $from and $to: one of them don't exist") + return + } + for (edge in source.edges) if (edge.to == to) return + for (edge in destination.edges) if (edge.from == from) return + + val edgeFromSource = Edge(from, to, weight) + val edgeFromDestination = Edge(to, from, weight) + val edgeFromSourceVM = EdgeViewModel(edgeFromSource, source, destination) + val edgeFromDestinationVM = EdgeViewModel(edgeFromDestination, destination, source) + source.edges.add(edgeFromSourceVM) + destination.edges.add(edgeFromDestinationVM) + graphModel.addEdge(from, to, weight) + } + + override fun drawEdges(edges: Collection>, color: Color) { + for (edge in edges) { + for (edgeVM in this.edgesVmOf(edge.from)) { + if (edgeVM.to == edge.to) edgeVM.color = color + } + for (edgeVM in this.edgesVmOf(edge.to)) { + if (edgeVM.to == edge.from) edgeVM.color = color + } + } + } + + fun drawMst() { + if (size == 0) return + val startVertex = graphModel.vertices.first() + val result = Prim.findMst(graphModel as UndirectedGraph, startVertex) + drawEdges(result, Color.Magenta) + } + + override fun drawBetweennessCentrality() { + val result = BetweenesCentralityUndirected.compute(graphModel as UndirectedGraph, size) + for (vertexVM in verticesVM) { + vertexVM.centrality = (result[vertexVM.vertex] ?: run { + logger.error { "Can't find centrality value for vertex in graph" } + 0.0 + }) + } + this.visibleCentrality = true + } + + fun drawBridges() { + val result = findBridges(graphModel as UndirectedGraph) + drawEdges(result, Color.Yellow) + } + + fun saveSQLite() { + graph.saveSQLite(name, "Undirected", "storage") + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt new file mode 100644 index 0000000..eac230e --- /dev/null +++ b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -0,0 +1,49 @@ +package viewmodel.graph + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import height +import width +import view.common.DefaultColors +import kotlin.random.Random + +class VertexViewModel( + _vertex: V, + _edges: MutableList> = mutableListOf(), + graphVM: AbstractGraphViewModel, + centerCoordinates: Boolean = true +) : + ViewModel() { + val vertex: V = _vertex + var edges = mutableStateListOf>() + var x by mutableStateOf(0f) + var y by mutableStateOf(0f) + val graphVM = graphVM + + init { + for (edge in _edges) { + edges.add(edge) + } + if (centerCoordinates) { + x = Random.nextInt(width / 2 - 300, width / 2 + 300).toFloat() + y = Random.nextInt(height / 2 - 300, height / 2 + 300).toFloat() + } else { + x = Random.nextInt(0, 30000).toFloat() + y = Random.nextInt(0, 30000).toFloat() + } + } + + val offsetX + get() = (graphVM.canvasSize.x / 2) + ((x - graphVM.center.x) * graphVM.zoom) + val offsetY + get() = (graphVM.canvasSize.y / 2) + ((y - graphVM.center.y) * graphVM.zoom) + + var vertexSize by mutableStateOf(60f) + var centrality by mutableStateOf(0.0) + var color by mutableStateOf(DefaultColors.primaryBright) + val degree + get() = edges.size +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/io/Neo4jRepository.kt b/src/main/kotlin/viewmodel/io/Neo4jRepository.kt new file mode 100644 index 0000000..0c8e5dd --- /dev/null +++ b/src/main/kotlin/viewmodel/io/Neo4jRepository.kt @@ -0,0 +1,162 @@ +package viewmodel.io + + +import mu.KotlinLogging +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase +import org.neo4j.driver.TransactionContext +import viewmodel.DirectedGraphViewModel +import viewmodel.GraphType +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedGraphViewModel +import viewmodel.graph.AbstractGraphViewModel +import java.io.Closeable + +// penguin-carlo-ceramic-invite-wheel-2163 + +private val logger = KotlinLogging.logger { } + +class Neo4jRepository(uri: String, user: String, password: String) : Closeable { + + val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + val session = driver.session() + + fun saveGraph(graphVM: AbstractGraphViewModel) { + val graphName = graphVM.name + val graphType = graphVM.graphType + // remove old graph if exist + session.executeWrite { tx -> + removeGraph(tx, graphName) + } + // create graph + session.executeWrite { tx -> + tx.run("CREATE (graph: Graph {name: '$graphName', type: '$graphType'});") + } + session.executeWrite { tx -> + tx.run("CREATE CONSTRAINT uniqueGraphName IF NOT EXISTS FOR (graph: Graph) REQUIRE (graph.value) IS UNIQUE;") + tx.run("CREATE INDEX IF NOT EXISTS FOR (graph: Graph) ON (graph.name);") + } + // create vertices in a graph + session.executeWrite { tx -> + for (vertexVM in graphVM.verticesVM) { + tx.run( + "CREATE (v: `$graphName` {value : \$vertexValue});", + mapOf("vertexValue" to vertexVM.vertex.toString()) + ) + } + } + session.executeWrite { tx -> + tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (n: `$graphName`) REQUIRE (n.value) IS UNIQUE;") + tx.run("CREATE INDEX IF NOT EXISTS FOR (n: `$graphName`) ON (n.value);") + } + // create edges in a graph + session.executeWrite { tx -> + for (edgeVM in graphVM.edgesVM) { + tx.run( + "MATCH (v1: `$graphName`) WHERE v1.value = \$vertex1 \n" + + "MATCH (v2: `$graphName`) WHERE v2.value = \$vertex2 \n" + + "CREATE (v1)-[:Edge {weight: \$edgeWeight}]->(v2)", + mapOf( + "vertex1" to edgeVM.from.toString(), + "vertex2" to edgeVM.to.toString(), + "edgeWeight" to edgeVM.weight.toString() + ) + ) + } + } + } + + fun removeGraph(name: String) { + session.executeWrite { tx -> + removeGraph(tx, name) + } + } + + private fun removeGraph( + tx: TransactionContext, + name: String, + ) { + val check = tx.run( + "MATCH (graph: Graph) WHERE graph.name = '$name' RETURN graph.name as name;" + ).list() + + if (check.size == 0) return + if (check.size > 1) { + logger.error { "More than 1 graph with same name and type exist in neo4j" } + return + } + tx.run( + "OPTIONAL MATCH (n: `$name`)" + + "OPTIONAL MATCH (graph: Graph) WHERE graph.name = '$name'" + + "DETACH DELETE n, graph;" + ) + } + + fun getAllGraphs(): List> { + val graphs = mutableListOf>() + val names = session.executeRead { tx -> + val result = tx.run("MATCH (graph: Graph) RETURN graph.name as name") + return@executeRead result.list() { it.asMap()["name"].toString() } + } + for (name in names) { + graphs.add(getGraph(name)) + } + return graphs + } + + fun getGraph(graphName: String): AbstractGraphViewModel { + val graphData = session.executeRead() { tx -> + val result = + tx.run("MATCH (graph: Graph) WHERE graph.name = '$graphName' RETURN graph.name as name, graph.type as type") + return@executeRead result.list().first().asMap() + } + val graph: AbstractGraphViewModel + if (graphData["type"] == "Undirected") { + graph = UndirectedGraphViewModel(graphData["name"].toString()) + } else if (graphData["type"] == "Directed") { + graph = DirectedGraphViewModel(graphData["name"].toString()) + } else throw IllegalArgumentException("graph type in db isn't correct") + val vertices = session.executeRead() { tx -> + val result = tx.run("MATCH (v: `$graphName`) RETURN v.value as value") + return@executeRead result.list() { it.asMap()["value"].toString() } + } + for (vertex in vertices) { + graph.addVertex(vertex) + } + session.executeRead { tx -> + for (vertex in vertices) { + val destinations = + tx.run("MATCH (v: `$graphName` {value: '$vertex'})-[:Edge]->(n) RETURN n.value as destination") + .list() { it.asMap()["destination"].toString() } + for (destination in destinations) { + graph.addEdge(vertex, destination) + } + } + } + return graph + } + + fun initGraphList(mainScreenViewModel: MainScreenViewModel) { + val graphs = session.executeRead { tx -> + val result = + tx.run("MATCH (graph: Graph) RETURN graph.name as name, graph.type as type") + return@executeRead result.list() { it.asMap() } + } + for (graph in graphs) { + val graphType = + if (graph["type"] == "Undirected") GraphType.Undirected else GraphType.Directed + mainScreenViewModel.addGraph(graph["name"].toString(), graphType) + } + } + + fun clearDB() { + session.executeWrite { tx -> + tx.run("MATCH (n) DETACH DELETE n") + } + } + + override fun close() { + session.close() + driver.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/io/SQLiteRepository.kt b/src/main/kotlin/viewmodel/io/SQLiteRepository.kt new file mode 100644 index 0000000..644ab04 --- /dev/null +++ b/src/main/kotlin/viewmodel/io/SQLiteRepository.kt @@ -0,0 +1,119 @@ +package viewmodel.io + +import viewmodel.DirectedGraphViewModel +import viewmodel.GraphType +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedGraphViewModel +import viewmodel.graph.AbstractGraphViewModel +import java.sql.DriverManager +import java.sql.SQLException + +object SQLiteRepository { + private val DB_DRIVER = "jdbc:sqlite" + fun initGraphList(source: String, mainScreenVM: MainScreenViewModel) { + val DB_DRIVER = "jdbc:sqlite" + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") + ?: throw SQLException("Cannot connect to database") + val createIndex = ("CREATE TABLE IF NOT EXISTS Graphs (name TEXT, type TEXT);") + + connection.createStatement().also { stmt -> + try { + stmt.execute(createIndex) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM Graphs") } + val resSet = getGraphs.executeQuery() + while (resSet.next()) { + if (resSet.getString("type") == "Directed") { + mainScreenVM.addGraph(resSet.getString("name"), GraphType.Directed) + } else if (resSet.getString("type") == "Undirected") { + mainScreenVM.addGraph(resSet.getString("name"), GraphType.Undirected) + } + } + connection.close() + } + + fun loadGraph(graphVM: AbstractGraphViewModel, source: String) { + if (graphVM.graphType == GraphType.Directed) { + val graphVM = graphVM as DirectedGraphViewModel + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graphVM.name}") } + val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graphVM.name}") } + val resVertex = getVertex.executeQuery() + val resEdges = getGraphs.executeQuery() + while (resVertex.next()) { + var vertexName = resVertex.getString("Vertexes") + if (vertexName.length > 1) vertexName = + vertexName.slice(1..vertexName.length - 1) + graphVM.addVertex(vertexName) + } + while (resEdges.next()) { + for (i in graphVM.graph.vertices) { + val weight = resEdges.getString("V$i") + var to = resEdges.getString("Vertexes") + to = to.slice(1.. + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM '${graph.name}'") } + val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM '${graph.name}'") } + val resVertex = getVertex.executeQuery() + val resEdges = getGraphs.executeQuery() + while (resVertex.next()) { + var vertexName = resVertex.getString("Vertexes") + if (vertexName.length > 1) vertexName = + vertexName.slice(1..vertexName.length - 1) + graph.addVertex(vertexName) + } + while (resEdges.next()) { + for (i in graph.graph.vertices) { + val weight = resEdges.getString("V$i") + var to = resEdges.getString("Vertexes") + to = to.slice(1.. + try { + stmt.execute(delTable) + } catch (e: Exception) { + println("Can't remove table with name $name in sqlite: no such table") + println(e) + } finally { + stmt.close() + } + try { + stmt.execute(delIndexRec) + } catch (e: Exception) { + println("Can't graph entry with name $name in sqlite: no such entry") + println(e) + } finally { + stmt.close() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/requests/create.sql b/src/main/kotlin/viewmodel/requests/create.sql new file mode 100644 index 0000000..f79951c --- /dev/null +++ b/src/main/kotlin/viewmodel/requests/create.sql @@ -0,0 +1,5 @@ +CREATE TABLE if not exists cities +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name varchar(255) +); \ No newline at end of file diff --git a/src/main/resources/directed.png b/src/main/resources/directed.png new file mode 100644 index 0000000..46744ca Binary files /dev/null and b/src/main/resources/directed.png differ diff --git a/src/main/resources/localisation/cn-CN.json b/src/main/resources/localisation/cn-CN.json new file mode 100644 index 0000000..9157ba3 --- /dev/null +++ b/src/main/resources/localisation/cn-CN.json @@ -0,0 +1,100 @@ +{ + "transList": [ + { + "code": "settings", + "localisation": "设置" + }, + { + "code": "back", + "localisation": "回去" + }, + { + "code": "enter_graph_name", + "localisation": "输入图表名称" + }, + { + "code": "home", + "localisation": "返回主屏幕" + }, + { + "code": "add_vertex", + "localisation": "添加" + }, + { + "code": "add_edge", + "localisation": "添加边缘" + }, + { + "code": "add", + "localisation": "添加" + }, + { + "code": "write_name", + "localisation": "写名字" + }, + { + "code": "enter_new_graph_name", + "localisation": "创建新图表" + }, + { + "code": "save", + "localisation": "储蓄" + }, + { + "code": "visualize", + "localisation": "图形布局" + }, + { + "code": "reset", + "localisation": "重置颜色" + }, + { + "code": "find_strong_connections", + "localisation": "强连通性的组成部分" + }, + { + "code": "find_clusters", + "localisation": "查找集群" + }, + { + "code": "dijkstra", + "localisation": "Dijkstra的算法" + }, + { + "code": "ford_bellman", + "localisation": "福特-贝尔曼算法" + }, + { + "code": "find_cycles", + "localisation": "查找周期" + }, + { + "code": "find_mst", + "localisation": "最小生成树" + }, + { + "code": "find_bridges", + "localisation": "寻找桥梁" + }, + { + "code": "center_coordinates", + "localisation": "中心坐标" + }, + { + "code": "number", + "localisation": "数量" + }, + { + "code": "unweighted", + "localisation": "未加权" + }, + { + "code": "start", + "localisation": "开始" + }, + { + "code": "end", + "localisation": "结局" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/localisation/en-US.json b/src/main/resources/localisation/en-US.json new file mode 100644 index 0000000..e2e718a --- /dev/null +++ b/src/main/resources/localisation/en-US.json @@ -0,0 +1,120 @@ +{ + "transList": [ + { + "code": "settings", + "localisation": "Settings" + }, + { + "code": "back", + "localisation": "Back" + }, + { + "code": "enter_graph_name", + "localisation": "Enter graph name" + }, + { + "code": "home", + "localisation": "Home" + }, + { + "code": "add_vertex", + "localisation": "Add vertex" + }, + { + "code": "saving", + "localisation": "Saving" + }, + { + "code": "language", + "localisation": "Language" + }, + { + "code": "neo4j_data", + "localisation": "Neo4j data" + }, + { + "code": "add_edge", + "localisation": "Add edge" + }, + { + "code": "add", + "localisation": "Add" + }, + { + "code": "write_name", + "localisation": "Write name" + }, + { + "code": "enter_new_graph_name", + "localisation": "Create new graph" + }, + { + "code": "save", + "localisation": "Save" + }, + { + "code": "visualize", + "localisation": "Visualize" + }, + { + "code": "reset", + "localisation": "Reset" + }, + { + "code": "find_strong_connections", + "localisation": "Strong Connections" + }, + { + "code": "find_clusters", + "localisation": "Find Clusters" + }, + { + "code": "dijkstra", + "localisation": "Dijkstra Path" + }, + { + "code": "betweenness_centrality", + "localisation": "Betweenness Centrality" + }, + { + "code": "ford_bellman", + "localisation": "Ford Bellman" + }, + { + "code": "find_cycles", + "localisation": "Find Cycles" + }, + { + "code": "find_mst", + "localisation": "Minimal Spanning Tree" + }, + { + "code": "find_bridges", + "localisation": "Find Bridges" + }, + { + "code": "center_coordinates", + "localisation": "Center Coordinates" + }, + { + "code": "number", + "localisation": "Amount" + }, + { + "code": "unweighted", + "localisation": "Unweighted" + }, + { + "code": "start", + "localisation": "Start" + }, + { + "code": "end", + "localisation": "End" + }, + { + "code": "weight", + "localisation": "weight" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/localisation/ru-RU.json b/src/main/resources/localisation/ru-RU.json new file mode 100644 index 0000000..859a713 --- /dev/null +++ b/src/main/resources/localisation/ru-RU.json @@ -0,0 +1,120 @@ +{ + "transList": [ + { + "code": "settings", + "localisation": "Настройки" + }, + { + "code": "back", + "localisation": "Назад" + }, + { + "code": "enter_graph_name", + "localisation": "Введите название графа" + }, + { + "code": "home", + "localisation": "На главную" + }, + { + "code": "saving", + "localisation": "Сохранение" + }, + { + "code": "neo4j_data", + "localisation": "Данные для Neo4j" + }, + { + "code": "language", + "localisation": "Язык" + }, + { + "code": "add_vertex", + "localisation": "Добавить вершину" + }, + { + "code": "add_edge", + "localisation": "Добавить ребро" + }, + { + "code": "add", + "localisation": "Добавить" + }, + { + "code": "write_name", + "localisation": "Введите название" + }, + { + "code": "enter_new_graph_name", + "localisation": "Создание нового графа" + }, + { + "code": "betweenness_centrality", + "localisation": "Ключевые вершины" + }, + { + "code": "save", + "localisation": "Сохранить" + }, + { + "code": "visualize", + "localisation": "Раскладка" + }, + { + "code": "reset", + "localisation": "Сбросить цвета" + }, + { + "code": "find_strong_connections", + "localisation": "Компоненты Сильной Связности" + }, + { + "code": "find_clusters", + "localisation": "Найти Кластеры" + }, + { + "code": "dijkstra", + "localisation": "Алгоритм Дейкстры" + }, + { + "code": "ford_bellman", + "localisation": "Алгоритм Форда Беллмана" + }, + { + "code": "find_cycles", + "localisation": "Найти циклы" + }, + { + "code": "find_mst", + "localisation": "Минимальное остовное дерево" + }, + { + "code": "find_bridges", + "localisation": "Найти мосты" + }, + { + "code": "center_coordinates", + "localisation": "Центрировать координаты" + }, + { + "code": "number", + "localisation": "Количество" + }, + { + "code": "unweighted", + "localisation": "Невзвешенный" + }, + { + "code": "start", + "localisation": "Начало" + }, + { + "code": "end", + "localisation": "Конец" + }, + { + "code": "weight", + "localisation": "вес" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/undirected.png b/src/main/resources/undirected.png new file mode 100644 index 0000000..1ca9a84 Binary files /dev/null and b/src/main/resources/undirected.png differ diff --git a/src/test/kotlin/SQLiteIntegrationTest.kt b/src/test/kotlin/SQLiteIntegrationTest.kt new file mode 100644 index 0000000..461e25b --- /dev/null +++ b/src/test/kotlin/SQLiteIntegrationTest.kt @@ -0,0 +1,54 @@ +import model.graph.Edge +import viewmodel.GraphType +import viewmodel.MainScreenViewModel +import java.io.File +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class SQLiteIntegrationTest { + + @Test + fun `SQLite integrable test`() { + val mainScreenVM = MainScreenViewModel() + mainScreenVM.addGraph("someName", GraphType.Directed) + val graph = mainScreenVM.getGraph("someName") + for (i in 1..4) { + graph.addVertex("$i") + } + graph.run { + this.addEdge("1", "4", 20) + this.addEdge("1", "2", 2) + this.addEdge("2", "3", 3) + this.addEdge("3", "4", 1) + } + mainScreenVM.saveGraph("someName", "test") + + mainScreenVM.initGraphList("test") + mainScreenVM.saveType = "sqlite" + mainScreenVM.loadGraph("someName", "test") + val loadedGraph = mainScreenVM.getGraph("someName") + val result = Dijkstra(loadedGraph.model, 4).dijkstra("1", "4") + val shortestLengthExpected = 6 + var shortestLengthActual = 0 + for (i in result) { + shortestLengthActual += i.weight + } + assertNotNull(shortestLengthActual) + assertEquals( + shortestLengthExpected, shortestLengthActual, + "Dijkstra must return weight of the shortest path" + ) + val pathExpected = listOf( + Edge("1", "2", 2), + Edge("2", "3", 3), + Edge("3", "4", 1) + ) + assertContentEquals( + pathExpected, result, + "Dijkstra must return shortest path when it is possible to reach destination" + ) + File("test.db").delete() + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/BetweennesCentralityTest.kt b/src/test/kotlin/algos/BetweennesCentralityTest.kt new file mode 100644 index 0000000..7422dbb --- /dev/null +++ b/src/test/kotlin/algos/BetweennesCentralityTest.kt @@ -0,0 +1,37 @@ +package algos + +import model.algos.BetweenesCentralityUndirected +import model.graph.UndirectedGraph +import kotlin.test.Test +import kotlin.test.assertNotNull + +class BetweennesCentralityTest { + @Test + fun basic() { + val graph = UndirectedGraph() + for (i in 0..9) { + graph.addVertex(i) + } + graph.addEdge(1, 2) + graph.addEdge(1, 3) + graph.addEdge(1, 4) + graph.addEdge(2, 3) + graph.addEdge(2, 4) + graph.addEdge(3, 4) + graph.addEdge(2, 5) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(5, 7) + graph.addEdge(6, 7) + graph.addEdge(6, 8) + graph.addEdge(6, 9) + graph.addEdge(7, 8) + graph.addEdge(7, 9) + graph.addEdge(8, 9) + val centrality = BetweenesCentralityUndirected.compute(graph, 9) + for ((vertex, value) in centrality) { + println("Vertex: $vertex, Betweenness Centrality: $value") + } + assertNotNull(centrality) + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/DijkstraTest.kt b/src/test/kotlin/algos/DijkstraTest.kt new file mode 100644 index 0000000..a1bca6a --- /dev/null +++ b/src/test/kotlin/algos/DijkstraTest.kt @@ -0,0 +1,73 @@ +package algos + +import Dijkstra +import model.graph.DirectedGraph +import model.graph.Edge +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class DijkstraTest { + + @Test + fun `dijkstra basic find and possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 4, 20) + this.addEdge(1, 2, 2) + this.addEdge(2, 3, 3) + this.addEdge(3, 4, 1) + } + val result = Dijkstra(graph, 4).dijkstra(1, 4) + val shortestLengthExpected = 6 + var shortestLengthActual = 0 + for (i in result){ + shortestLengthActual += i.weight + } + assertNotNull(shortestLengthActual) + assertEquals( + shortestLengthExpected, shortestLengthActual, + "Dijkstra must return weight of the shortest path" + ) + val pathExpected = listOf( + Edge(1, 2, 2), + Edge(2, 3, 3), + Edge(3, 4, 1) + ) + assertContentEquals( + pathExpected, result, + "Dijkstra must return shortest path when it is possible to reach destination" + ) + } + + @Test + fun `dijkstra not possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..6) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2, 8) + this.addEdge(2, 3, 2) + this.addEdge(4, 5, 3) + this.addEdge(5, 6, 5) + this.addEdge(6, 4, 9) + this.addEdge(4, 6, 20) + } + + val result = Dijkstra(graph, 4).dijkstra(1, 4) + + val pathExpected = emptyList>() + val pathActual = result + assertContentEquals( + pathExpected, pathActual, + "Dijkstra must return empty list as shortest path if it is not possible to reach destination" + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/FindCycleTest.kt b/src/test/kotlin/algos/FindCycleTest.kt new file mode 100644 index 0000000..9c6749c --- /dev/null +++ b/src/test/kotlin/algos/FindCycleTest.kt @@ -0,0 +1,31 @@ +package algos + +import model.algos.FindCycle +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import model.graph.Edge +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotNull + +internal class FindCycleTest { + + @Test + fun `3 vertices directed cycle`() { + val graph = DirectedGraph() + for (i in 1..3) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2) + this.addEdge(2, 3) + this.addEdge(3, 1) + } + + val pathActual = FindCycle.findCycles(graph, 2).elementAt(0) + assertNotNull(pathActual) + val pathExpected = listOf(1, 2, 3) + assertContentEquals(pathExpected, pathActual, "TODO") + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/FordBellmanTest.kt b/src/test/kotlin/algos/FordBellmanTest.kt new file mode 100644 index 0000000..9a2f77c --- /dev/null +++ b/src/test/kotlin/algos/FordBellmanTest.kt @@ -0,0 +1,115 @@ +package algos + +import model.algos.FordBellman +import model.graph.DirectedGraph +import model.graph.Edge +import kotlin.test.* + +internal class FordBellmanTest { + + @Test + fun `basic find without negative cycle and possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 3, 2) + this.addEdge(1, 2, 7) + this.addEdge(3, 2, 3) + this.addEdge(2, 4, 1) + this.addEdge(4, 3, -1) + } + + val result = FordBellman.findShortestPath(1, 4, graph) + + val shortestLengthExpected = 6 + val shortestLengthActual = result.first + assertNotNull(shortestLengthActual) + assertEquals( + shortestLengthExpected, shortestLengthActual, + "FordBellman must return weight of the shortest path" + ) + + val pathExpected = listOf( + Edge(1, 3, 2), + Edge(3, 2, 3), + Edge(2, 4, 1) + ) + val pathActual = result.second + assertContentEquals( + pathExpected, pathActual, + "FordBellman must return shortest path when it is possible to reach destination and there is no negative cycles" + ) + } + + @Test + fun `not possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..6) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2, 8) + this.addEdge(2, 3, 2) + this.addEdge(4, 5, 3) + this.addEdge(5, 6, 5) + this.addEdge(6, 4, 9) + this.addEdge(4, 6, -20) + } + + val result = FordBellman.findShortestPath(1, 4, graph) + + val shortestLengthExpected = null + val shortestLengthActual = result.first + assertEquals( + shortestLengthExpected, shortestLengthActual, + "FordBellman must return null as length of path if it is not possible to reach destination" + ) + + val pathExpected = null + val pathActual = result.second + assertContentEquals( + pathExpected, pathActual, + "FordBellman must return null as shortest path if it is not possible to reach destination" + ) + } + + @Test + fun `shortest path with negative cycle`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2, 1) + this.addEdge(2, 3, 4) + this.addEdge(3, 1, -10) + this.addEdge(3, 4, 4) + } + + val result = FordBellman.findShortestPath(1, 4, graph) + + val shortestLengthExpected = null + val shortestLengthActual = result.first + assertEquals( + shortestLengthExpected, shortestLengthActual, + "FordBellman must return null as length of path if there is negative cycles" + ) + + val pathActual = result.second + assertNotNull( + pathActual, + "FordBellman must return not null path with negative cycles" + ) + //TODO: implement correctness of returned path with negative cycle + assertTrue( + pathActual.size >= 3, + "FordBellman must return some correct path to destination with negative cycles" + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/algos/PrimTest.kt b/src/test/kotlin/algos/PrimTest.kt new file mode 100644 index 0000000..b96a443 --- /dev/null +++ b/src/test/kotlin/algos/PrimTest.kt @@ -0,0 +1,44 @@ +package algos + +import model.algos.Prim +import model.graph.UndirectedGraph +import model.graph.Edge +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotNull + +internal class PrimTest { + @Test + fun basic() { + val graph = UndirectedGraph() + for (i in 0..7) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2, 1) + this.addEdge(2, 3, 2) + this.addEdge(3, 4, 30) + this.addEdge(4, 5, 12) + this.addEdge(5, 3, 8) + this.addEdge(6, 7, 7) + this.addEdge(6, 3, 2) + this.addEdge(7, 1, 20) + this.addEdge(1, 3, 10) + } + + val pathActual = Prim.findMst(graph, 1) + + val pathExpected = listOf( + Edge(1, 2, 1), + Edge(2, 3, 2), + Edge(3, 6, 2), + Edge(6, 7, 7), + Edge(3, 5, 8), + Edge(5, 4, 12) + ) + + assertNotNull(pathActual) + assertContentEquals(pathExpected, pathActual) + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/SearchBridgesTest.kt b/src/test/kotlin/algos/SearchBridgesTest.kt new file mode 100644 index 0000000..f87ebb0 --- /dev/null +++ b/src/test/kotlin/algos/SearchBridgesTest.kt @@ -0,0 +1,58 @@ +package algos + +import model.algos.findBridges +import model.graph.UndirectedGraph +import model.graph.Edge +import kotlin.test.Test +import kotlin.test.assertTrue + +internal class SearchBridgesTest { + + private fun bridgesEquals(bridges1: Set>, bridges2: Set>): Boolean { + for (bridge in bridges1) { + val bridgeReversed = Edge(bridge.to, bridge.from, bridge.weight) + if (bridges2.contains(bridge) || bridges2.contains(bridgeReversed)) + continue + return false + } + return true + } + + @Test + fun `empty graph`() { + val graph = UndirectedGraph() + + val expectedBridges = setOf>() + val actualBridges = findBridges(graph) + assertTrue( + bridgesEquals(expectedBridges, actualBridges), + "Empty graph must not contain bridges" + ) + } + + @Test + fun `basic bridges search`() { + val graph = UndirectedGraph() + for (i in 1..7) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2) + this.addEdge(1, 3) + this.addEdge(2, 3) + this.addEdge(3, 4) + this.addEdge(4, 5) + this.addEdge(4, 6) + this.addEdge(5, 6) + this.addEdge(6, 7) + } + + val expectedBridges = setOf(Edge(3, 4), Edge(6, 7)) + val actualBridges = findBridges(graph) + assertTrue( + bridgesEquals(expectedBridges, actualBridges), + "searchBridges must return set of graph bridges" + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/StrongConnectionsTest.kt b/src/test/kotlin/algos/StrongConnectionsTest.kt new file mode 100644 index 0000000..4d6aa68 --- /dev/null +++ b/src/test/kotlin/algos/StrongConnectionsTest.kt @@ -0,0 +1,71 @@ +package algos + +import model.algos.StrongConnections +import model.graph.DirectedGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class StrongConnectionsTest { + + @Test + fun `strong connections unconnected find`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + val resultActual = StrongConnections().findStrongConnections(graph) + val resultExpected = listOf(listOf(1), listOf(2), listOf(3), listOf(4)) + assertNotNull(resultActual) + assertEquals( + resultExpected, resultActual, + "Unconnected vertices should be in different strong connections" + ) + } + + @Test + fun `strong connections cylce`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + graph.run { + this.addEdge(1, 2, 8) + this.addEdge(2, 3, 2) + this.addEdge(3, 4, -3) + this.addEdge(4, 1, 3) + } + val resultActual = StrongConnections().findStrongConnections(graph) + val resultExpected = listOf(listOf(1, 2, 3, 4)) + assertNotNull(resultActual) + assertEquals( + resultExpected, resultActual, + "Cycle is a strong connection" + ) + } + + @Test + fun `strong connections cycles joined with a bridge`() { + val graph = DirectedGraph() + for (i in 1..6) { + graph.addVertex(i) + } + graph.run { + this.addEdge(1, 2, 52) + this.addEdge(2, 3, 52) + this.addEdge(3, 1, -52) + this.addEdge(4, 5, 52) + this.addEdge(5, 6, -52) + this.addEdge(6, 4, 52) + this.addEdge(1, 6, 52) + + } + val resultActual = StrongConnections().findStrongConnections(graph) + val resultExpected = listOf(listOf(1, 2, 3), listOf(4, 5, 6)) + assertNotNull(resultActual) + assertEquals( + resultExpected, resultActual, + "Cycles joined with a bridge is a 2 strong connections" + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/Neo4jTest.kt b/src/test/kotlin/io/Neo4jTest.kt new file mode 100644 index 0000000..71855c8 --- /dev/null +++ b/src/test/kotlin/io/Neo4jTest.kt @@ -0,0 +1,53 @@ +package io + +import model.graph.DirectedGraph +import org.junit.Test +import view.graph.UndirectedGraphView +import view.screens.UndirectedGraphScreen +import viewmodel.DirectedGraphViewModel +import viewmodel.UndirectedGraphViewModel +import viewmodel.io.Neo4jRepository +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +internal class Neo4jTest { + + @Test + fun `check`() { + val rep = + Neo4jRepository( + "bolt://localhost:7687", + "neo4j", + "penguin-carlo-ceramic-invite-wheel-2163" + ) + rep.clearDB() + val graph1 = UndirectedGraphViewModel("name1") + val graph2 = DirectedGraphViewModel("name2") + graph1.run { + this.addVertex("1") + this.addVertex("2") + this.addVertex("3") + this.addEdge("1", "2") + this.addEdge("2", "3") + } + graph2.run { + this.addVertex("14") + this.addVertex("15") + this.addVertex("16") + this.addEdge("14", "15") + this.addEdge("15", "16") + this.addEdge("16", "15") + } + rep.saveGraph(graph1) + rep.saveGraph(graph2) + graph1.addVertex("4") + rep.saveGraph(graph1) + val graphsSaved = rep.getAllGraphs() + val graph1Saved = graphsSaved.find { it.name == "name1" } + val graph2Saved = graphsSaved.find { it.name == "name2" } + assertEquals(graph1.model.vertices, graph1Saved?.model?.vertices) + assertEquals(graph1.model.edges, graph1Saved?.model?.edges) + assertEquals(graph2.model.vertices, graph2Saved?.model?.vertices) + assertEquals(graph2.model.edges, graph2Saved?.model?.edges) + } +} \ No newline at end of file