diff --git a/.github/.keep b/.github/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg new file mode 100644 index 0000000..9d05455 --- /dev/null +++ b/.github/badges/branches.svg @@ -0,0 +1 @@ +branches49.7% \ No newline at end of file diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg new file mode 100644 index 0000000..c126868 --- /dev/null +++ b/.github/badges/jacoco.svg @@ -0,0 +1 @@ +coverage60.9% \ No newline at end of file diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml new file mode 100644 index 0000000..daa371c --- /dev/null +++ b/.github/workflows/detekt.yml @@ -0,0 +1,29 @@ +name: Run detekt +on: + pull_request: + branches: + - main + workflow_dispatch: +jobs: + build: + permissions: + security-events: write + runs-on: [ ubuntu-latest ] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: temurin + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Build with Gradle + run: ./gradlew detekt + - name: Upload SARIF to GitHub using the upload-sarif action + uses: github/codeql-action/upload-sarif@v3 + if: success() || failure() + with: + sarif_file: build/reports/detekt/detekt.sarif + category: static-analysis diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..68ee09b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Run tests +on: + push: +permissions: write-all +jobs: + build: + runs-on: [ubuntu-latest] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: temurin + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Build with Gradle + run: ./gradlew build -x detekt + - name: Generate JaCoCo Badge + uses: cicirello/jacoco-badge-generator@v2 + with: + generate-branches-badge: true + badges-directory: .github/badges + jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv + - name: Log coverage percentage + run: | + echo "coverage = ${{ steps.jacoco.outputs.coverage }}" + echo "branch coverage = ${{ steps.jacoco.outputs.branches }}" + - name: Commit the badge (if it changed) + run: | + if [[ `git status --porcelain *.svg` ]]; then + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add *.svg + git commit -m "chore: update autogenerated JaCoCo coverage badge" *.svg + git push + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d2c4b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,218 @@ +# Created by https://www.toptal.com/developers/gitignore/api/linux,intellij+iml,windows,macos,gradle,kotlin +# Edit at https://www.toptal.com/developers/gitignore?templates=linux,intellij+iml,windows,macos,gradle,kotlin + +### Intellij+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea//workspace.xml +.idea//tasks.xml +.idea//usage.statistics.xml +.idea//dictionaries +.idea//shelf + +# AWS User-specific +.idea//aws.xml + +# Generated files +.idea//contentModel.xml + +# Sensitive or high-churn files +.idea//dataSources/ +.idea//dataSources.ids +.idea//dataSources.local.xml +.idea//sqlDataSources.xml +.idea//dynamic.xml +.idea//uiDesigner.xml +.idea//dbnavigator.xml + +# Gradle +.idea//gradle.xml +.idea//libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea//mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### Kotlin ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/linux,intellij+iml,windows,macos,gradle,kotlin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4d96c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Islam Magomedov, Damir Yunusov, Sofya Grishkova + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc716c4 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +![Coverage](.github/badges/jacoco.svg) ![Branches](.github/badges/branches.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +## GraphApp +![graphApp](images/graphApp.png) +Представляем наше приложение для работы с графами. +В нём представлены следующие возможности: +1. Визуализация графа, возможность масштабирования и навигации (в разработке) +2. Работа с 4 типами графов, в том числе направленными/ненаправленными и взвешенными/невзвешенными +3. Сохранение и чтение файлов в формате JSON +4. ForceAtlas2 - силовая модель раскладки графа (в разработке) +5. Поиск сообществ +6. Поиск мостов +7. Поиск минимального остова +8. Поиск компонент сильной связности + + +## Лицензия + +Приложение распространяется под MIT License. Смотрите `LICENSE.txt` для большей информации. + + +## Источники + +* [На чём основан наш графический интерфейс](https://github.com/spbu-coding-2023/gui-workshop?tab=readme-ov-file) +* [Статья про ForceAtlas2](https://journals.plos.org/plosone/article%3Fid=10.1371/journal.pone.0098679) +* [Статья на хабре про алгоритм Прима(и не только)](https://habr.com/ru/articles/569444/) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..bb5d12c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,92 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import io.gitlab.arturbosch.detekt.Detekt + +plugins { + kotlin("plugin.serialization") version "1.9.23" + kotlin("jvm") version "1.9.23" + id("io.gitlab.arturbosch.detekt").version("1.23.6") + id("org.jetbrains.compose") version "1.6.1" + jacoco +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation(compose.desktop.currentOs) + testImplementation(kotlin("test")) +} + +kotlin { + jvmToolchain(21) +} + +detekt { + toolVersion = "1.23.6" + + source.setFrom("$projectDir/src/main/kotlin", "$projectDir/src/test/kotlin") + + config.setFrom("$projectDir/config/detekt/detekt.yml") + buildUponDefaultConfig = true + allRules = false + + ignoreFailures = false + + basePath = rootProject.projectDir.absolutePath +} + +jacoco { + toolVersion = "0.8.12" + reportsDirectory = layout.buildDirectory.dir("reports/jacoco") +} + +tasks.test { + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} + +tasks.withType().configureEach { + reports { + html.required.set(true) + sarif.required.set(true) // SARIF to support integrations with GitHub Code Scanning + } +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) + + classDirectories.setFrom( + files(classDirectories.files.map { + fileTree(it) { + exclude("**/view/**", "**/viewmodel/**", "**/app/**") + } + }) + ) + + reports { + xml.required = false + csv.required = true + html.required = true + } + + +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "graphs-4" + packageVersion = "1.0.0" + } + } +} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..e6fbcfd --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,774 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 15 + thresholdInInterfaces: 15 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [] + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: false + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 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..1af9e09 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/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/HEAD/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 + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + 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 + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# 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..6689b85 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +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% equ 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% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/images/graphApp.png b/images/graphApp.png new file mode 100644 index 0000000..eb2b8c9 Binary files /dev/null and b/images/graphApp.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..0c1a7dd --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + gradlePluginPortal() + mavenCentral() + } +} + +rootProject.name = "graphs-graphs-4" \ No newline at end of file diff --git a/src/main/kotlin/app/Main.kt b/src/main/kotlin/app/Main.kt new file mode 100644 index 0000000..c5b518a --- /dev/null +++ b/src/main/kotlin/app/Main.kt @@ -0,0 +1,45 @@ +package app + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import model.graphs.Graph +import view.AppTheme +import view.screens.StartingScreen +import view.screens.mainScreen +import viewmodel.graphs.CircularPlacementStrategy +import viewmodel.screens.MainScreenViewModel +import viewmodel.screens.StartingScreenViewModel +import java.awt.Dimension + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "GraphApp" + ) { + window.minimumSize = Dimension(800, 600) + + app() + } +} + +@Composable +fun app() { + val darkTheme = remember { mutableStateOf(false) } + val currentGraph = remember { mutableStateOf?>(null) } + val mainScreenViewModel = remember(currentGraph.value) { + currentGraph.value?.let { MainScreenViewModel(it, CircularPlacementStrategy(), currentGraph, darkTheme) } + } + + AppTheme(darkTheme.value) { + if (currentGraph.value == null) { + StartingScreen(StartingScreenViewModel(currentGraph)) + } else { + mainScreenViewModel?.let { + mainScreen(viewModel = it) + } + } + } +} diff --git a/src/main/kotlin/model/functionality/BridgeFinder.kt b/src/main/kotlin/model/functionality/BridgeFinder.kt new file mode 100644 index 0000000..c85c91f --- /dev/null +++ b/src/main/kotlin/model/functionality/BridgeFinder.kt @@ -0,0 +1,61 @@ +package model.functionality + +import model.graphs.Edge +import model.graphs.GraphUndirected +import model.graphs.Vertex +import kotlin.math.min + +class BridgeFinder> { + private var discoveryTime = hashMapOf, Int>() + private var bridges: Set = emptySet() + private var parent = hashMapOf, Vertex?>() + private var low = hashMapOf, Int>() + private var timer: Int = 0 + + fun findBridges(graph: GraphUndirected): Set { + for (element in graph.vertices()) { + discoveryTime[element] = -1 + low[element] = -1 + parent[element] = null + } + + graph.vertices().forEach { + if (discoveryTime[it] == -1) { + timer = 0 + dfsRecursive(graph, it) + } + } + + return bridges + } + + private fun dfsRecursive(graph: GraphUndirected, vertex: Vertex) { + discoveryTime[vertex] = timer + low[vertex] = timer + timer += 1 + + graph.getNeighbors(vertex).forEach { + if (discoveryTime[it.to] == -1) { + parent[it.to] = vertex + dfsRecursive(graph, it.to) + + val lowVertex: Int = low[vertex] ?: -1 + val lowIt: Int = low[it.to] ?: -1 + val discVertex: Int = discoveryTime[vertex] ?: -1 + + low[vertex] = min(lowVertex, lowIt) + + if (lowIt > discVertex) { + bridges = bridges.plus(it) + } + } else { + if (parent[vertex] != it.to) { + val lowVertex: Int = low[vertex] ?: -1 + val discTimeIt: Int = discoveryTime[it.to] ?: -1 + + low[vertex] = min(lowVertex, discTimeIt) + } + } + } + } +} diff --git a/src/main/kotlin/model/functionality/CommunityDetector.kt b/src/main/kotlin/model/functionality/CommunityDetector.kt new file mode 100644 index 0000000..ea11be7 --- /dev/null +++ b/src/main/kotlin/model/functionality/CommunityDetector.kt @@ -0,0 +1,337 @@ +package model.functionality + +import model.graphs.Edge +import model.graphs.GraphUndirected +import model.graphs.UndirectedGraph +import model.graphs.UnweightedEdge +import model.graphs.Vertex +import java.lang.Math.random +import kotlin.math.exp +import kotlin.math.pow + +class CommunityDetector>( + var graph: GraphUndirected, + private var resolution: Double, + private var randomness: Double +) { + internal fun flatten( + partition: HashSet>> + ): HashSet>> { + val output = HashSet>>() + + for (community in partition) { + output.add(flatCommunity(community)) + } + + return output + } + + internal fun maintainPartition( + partition: List>>, + currGraph: GraphUndirected + ): HashSet>> { + // newPartition = {{v | v ⊆ C, v ∈ currGraph.vertices() } | C ∈ partition} + val newPartition: MutableList>> = MutableList(partition.size) { hashSetOf() } + + for (vertex in currGraph.vertices()) { + val index = partition.indexOf(partition.find { it.containsAll(vertex.key as HashSet<*>) }) + newPartition[index].add(vertex) + } + + return newPartition.toHashSet() + } + + + fun leiden(): HashSet>> { + var currentGraph = graph + var partition: HashSet>> = initPartition(graph) + var notDone = true + + while (notDone) { + moveNodesFast(currentGraph, partition) + + notDone = (partition.size) != (currentGraph.vertices().size) + + if (notDone) { + val refinedPartition = refinePartition(currentGraph, partition) + currentGraph = aggregateGraph(currentGraph, refinedPartition) + partition = maintainPartition(partition.toList(), currentGraph) + } + } + + return flatten(partition) + } + + private fun moveNodesFast( + graph: GraphUndirected, + partition: HashSet>> + ) { + val vertexQueue = graph.vertices().toMutableList() + vertexQueue.shuffle() + + while (vertexQueue.isNotEmpty()) { + val currentVertex = vertexQueue.first() + vertexQueue.remove(currentVertex) + + val startingQuality = quality(graph, partition) + var max = 0.0 + var bestCommunity = partition.find { it.contains(currentVertex) } + val originalCommunity = bestCommunity + + require(bestCommunity != null) { "Community that contains currentVertex must exist." } + bestCommunity.remove(currentVertex) + + partition.add(hashSetOf()) + + // Determine the best community for currentVertex + + for (community in partition) { + community.add(currentVertex) + + val currentQuality = quality(graph, partition) + community.remove(currentVertex) + + if (currentQuality - startingQuality >= max) { + max = currentQuality - startingQuality + bestCommunity = community + } + } + + bestCommunity?.add(currentVertex) + + if (bestCommunity != originalCommunity) { + for (edge in graph.getNeighbors(currentVertex)) { + if (bestCommunity?.contains(edge.to) == false) { + vertexQueue.add(edge.to) + } + } + } + } + + partition.removeIf { it.size == 0 } + } + + private fun quality( + graph: GraphUndirected, + partition: HashSet>> + ): Double { + var sum = 0.0 + + for (community in partition) { + val cS = flatCommunity(community).size + sum += countEdges(graph, community, community) - ((resolution * cS * (cS - 1)) / 2) + } + + return sum + } + + internal fun countEdges( + currGraph: GraphUndirected, + set1: HashSet>, set2: Set> + ): Int { + var count = 0 + + for (u in set1) { + for (v in currGraph.getNeighbors(u)) { + if (v.to in set2) { + count += v.copies + } + } + } + + if (set1 == set2) { + count /= 2 + } + + return count + } + + internal fun aggregateGraph( + graph: GraphUndirected, + partition: HashSet>> + ): GraphUndirected { + val newGraph = UndirectedGraph>>() + + for (community in partition) { + if (community.size != 0) { + newGraph.addVertex(community) + } + } + + val communities = newGraph.vertices() + + for (edge in graph.edges()) { + val v1 = edge.from + val v2 = edge.to + + val c1 = communities.find { it.key.contains(v1) } + val c2 = communities.find { it.key.contains(v2) } + + if (c1 != null && c2 != null) { + newGraph.addSingleEdge(UnweightedEdge(c1, c2)) + } + } + + // ANY UndirectedGraph is GraphUndirected + @Suppress("UNCHECKED_CAST") + return newGraph as GraphUndirected + } + + private fun refinePartition( + graph: GraphUndirected, + partition: HashSet>> + ): HashSet>> { + var refinedPartition = initPartition(graph) + + for (community in partition) { + refinedPartition = mergeNodesSubset(graph, refinedPartition, community) + } + + refinedPartition.removeAll { it.size == 0 } + + return refinedPartition + } + + internal fun flatVertex(vertex: Vertex): HashSet> { + if (vertex.key is Collection<*>) { + @Suppress("UNCHECKED_CAST") + return unpack(hashSetOf(), vertex as Vertex>) + } + + @Suppress("UNCHECKED_CAST") + return hashSetOf(vertex) as HashSet> + } + + private fun unpack( + vertices: HashSet>, + vertex: Vertex> + ): HashSet> { + for (element in vertex.key) { + element as Vertex<*> + if (element.key is Collection<*>) { + @Suppress("UNCHECKED_CAST") + unpack(vertices, element as Vertex>) + } else { + @Suppress("UNCHECKED_CAST") + vertices.add(element as Vertex) + } + } + + return vertices + } + + internal fun flatCommunity( + community: HashSet> + ): HashSet> { + val output: HashSet> = hashSetOf() + + for (vertex in community) { + output.addAll(flatVertex(vertex)) + } + + return output + } + + private fun mergeNodesSubset( + graph: GraphUndirected, + partition: HashSet>>, + subset: HashSet> + ): HashSet>> { + // Consider only nodes that are well-connected within subset + val r: MutableList> = mutableListOf() + + for (vertex in subset) { + val vertexSize: Double = flatVertex(vertex).size.toDouble() + + var edges = 0 + graph.getNeighbors(vertex).forEach { + if (subset.contains(it.to)) { + edges += 1 + } + } + + if (edges >= (resolution * vertexSize * (flatCommunity(subset).size - vertexSize))) { + r.add(vertex) + } + } + + for (vertex in r.shuffled()) { + // Consider only nodes that have not yet been merged + val originalCommunity = partition.find { it.contains(vertex) } + + if (originalCommunity?.size == 1) { + // Consider only well-connected communities + val wellConnectedCommunities: HashSet>> = hashSetOf() + + for (community in partition) { + if (subset.containsAll(community)) { + val communitySize = flatCommunity(community).size + val edges = countEdges(graph, community, subset.minus(community)) + + val communityRank = resolution * communitySize * + (flatCommunity(subset).size) - communitySize + if (edges >= communityRank) { + wellConnectedCommunities.add(community) + } + } + } + + originalCommunity.remove(vertex) + + val qualityProbability: HashMap>, Double> = hashMapOf() + val startingQuality = quality(graph, partition) + val temp: HashSet>> = HashSet() + + for (community in wellConnectedCommunities) { + if (community.size != 0) { + community.add(vertex) + + val currentQuality = quality(graph, partition) + + if (currentQuality - startingQuality < 0) { + temp.add(community.minus(vertex).toHashSet()) + } else { + qualityProbability[community.minus(vertex).toHashSet()] = + exp((currentQuality - startingQuality) * (randomness.pow(-1.0))) + } + + community.remove(vertex) + } + } + + wellConnectedCommunities.removeAll(temp) + wellConnectedCommunities.removeIf { it.size == 0 } + + if (wellConnectedCommunities.size != 0) { + // Choose random community for more broad exploration of possible partitions + var totalWeight = 0.0 + + for (community in wellConnectedCommunities) { + val x = qualityProbability[community] + + require(x != null) { "qualityProbability != null" } + totalWeight += x + } + + val randomNumber = random() * totalWeight + val keyList = qualityProbability.values.filter { it < randomNumber } + val key = keyList.maxOrNull() ?: qualityProbability.values.min() + val newCommunity = qualityProbability.entries.find { it.value == key }?.key + + require(newCommunity != null) { "Failed to assign newCommunity." } + partition.find { it == newCommunity }?.add(vertex) + } else { + originalCommunity.add(vertex) + } + } + } + + return partition + } + + internal fun initPartition( + graph: GraphUndirected + ): HashSet>> { + return graph.vertices().map { hashSetOf(it) }.toHashSet() + } +} diff --git a/src/main/kotlin/model/functionality/DistanceRank.kt b/src/main/kotlin/model/functionality/DistanceRank.kt new file mode 100644 index 0000000..abed5f3 --- /dev/null +++ b/src/main/kotlin/model/functionality/DistanceRank.kt @@ -0,0 +1,78 @@ +package model.functionality + +import model.graphs.DirectedGraph +import model.graphs.Edge +import model.graphs.Vertex +import java.util.* +import kotlin.math.exp +import kotlin.math.log10 + +@Suppress("MagicNumber") +class DistanceRank>(val graph: DirectedGraph) { + private val vertexQueue = PriorityQueue, Double>>(compareBy { it.second }) + private val dist = mutableMapOf, Double>().withDefault { 1e6 } + private var size = 0.0 + private var t: Double = 0.0 + private val beta = 0.1 + private val gamma = 0.65 + private var distance = 0.0 + private val visitedStartingVertices = mutableMapOf, Boolean>().withDefault { false } + + private fun enqueue(vertex: Vertex, distance: Double) { + vertexQueue.add(Pair(vertex, distance)) + } + + private fun dequeue(): Pair, Double> { + return vertexQueue.poll() + } + + private fun getOutDegree(vertex: Vertex): Int { + return graph.adjList[vertex]?.size ?: 0 + } + + @Suppress("NestedBlockDepth") + fun rank(): Map, Double> { + for (i in graph.adjList.keys) dist[i] = 1e10 + + //val allSCCs = TarjanSCC().findSCCs(graph) + val allSCCs = graph.findSCC() + val startingVertices = mutableSetOf>() + + + allSCCs.forEach { scc -> + val vertex = scc.random() + val outDegree = getOutDegree(vertex).toDouble() + val initialDist = 1 + log10(outDegree + 1) + enqueue(vertex, initialDist) + dist[vertex] = initialDist + startingVertices.add(vertex) + visitedStartingVertices[vertex] = false + } + + while (!vertexQueue.isEmpty()) { + val (vertex, currentDistance) = dequeue() + val newDistance = log10(getOutDegree(vertex).toDouble() + 1) + gamma * currentDistance + visitedStartingVertices[vertex] = true + + + size++ + t = (size / graph.adjList.keys.size) + val alpha = exp(-t * beta) + + graph.adjList[vertex]?.forEach { child -> + distance = (1 - alpha) * dist[vertex]!! + alpha * newDistance + if (startingVertices.contains(child.to) && !visitedStartingVertices[child.to]!!) { + dist[child.to] = distance + enqueue(child.to, distance) + } else if (distance < dist[child.to]!!) { + if (dist.getValue(child.to) == 1e10) { + enqueue(child.to, distance) + } + dist[child.to] = distance + } + } + + } + return dist + } +} diff --git a/src/main/kotlin/model/functionality/FindingCycles.kt b/src/main/kotlin/model/functionality/FindingCycles.kt new file mode 100644 index 0000000..563ac5f --- /dev/null +++ b/src/main/kotlin/model/functionality/FindingCycles.kt @@ -0,0 +1,147 @@ +package model.functionality + +import model.graphs.* +import java.util.* +import kotlin.math.min + +class JohnsonAlg>(val graph: GraphDirected) { + private val stack = Stack>() + private val blocked = mutableMapOf, Boolean>() + private val blockedMap = mutableMapOf, MutableSet>>() + private val allCycles = HashSet>>() + + + fun findCycles(startVertex: Vertex): HashSet>> { + val relevantSCC = TarjanSCC().findSCC(startVertex, graph) + startFindCycles(startVertex, relevantSCC) + return allCycles + } + + private fun startFindCycles(startVertex: Vertex, subgraph: HashSet>) { + val subGraphNodes = subgraph.associateWith { vertex -> + graph.getNeighbors(vertex).filter { subgraph.contains(it.to) } ?: listOf() + } + subgraph.forEach { node -> + blocked[node] = false + blockedMap[node] = mutableSetOf() + } + dfsCycleFind(startVertex, startVertex, subGraphNodes) + } + + private fun dfsCycleFind(start: Vertex, current: Vertex, subGraph: Map, List>>): Boolean { + stack.add(current) + blocked[current] = true + var foundCycle = false + + for (neighbor in subGraph[current] ?: emptyList()) { + if (neighbor.to == start && stack.size > 1) { + allCycles.add(ArrayList(stack)) + foundCycle = true + } else if (blocked[neighbor.to] == false) { + val gotCycle = dfsCycleFind(start, neighbor.to, subGraph) + foundCycle = foundCycle || gotCycle + } + } + + if (foundCycle) unblock(current) + else { + for (neighbor in subGraph[current] ?: emptyList()) { + if (!blockedMap[neighbor.to]!!.contains(current)) { + blockedMap[neighbor.to]!!.add(current) + } + } + } + stack.pop() + return foundCycle + } + + + /*private fun processUnblocking(current: Vertex) { + val queue = ArrayDeque>() + stack.pop() + queue.add(current) + + while (queue.isNotEmpty()) { + val vertex = queue.removeFirst() + blocked[vertex] = false + + blockedMap[vertex]?.forEach { dependent -> + if (blockedMap[dependent]?.all { blocked[it] == false } == true) { + queue.add(dependent) + } + } + blockedMap[vertex]?.clear() + }*/ + + private fun unblock(vertex: Vertex) { + blocked[vertex] = false + if (blockedMap[vertex]?.size != 0) { + blockedMap[vertex]?.forEach { + if (blocked[it] == true) unblock(it) + } + } + blockedMap[vertex]?.clear() + } + + +} + +class TarjanSCC> { + val stack = Stack>() + val num = mutableMapOf, Int>() + val lowest = mutableMapOf, Int>() + val visited = hashSetOf>() + val processed = hashSetOf>() + var curIndex = 1 + + fun findSCC(vertex: Vertex, graph: GraphDirected): HashSet> { + return dfsTarjan(vertex, graph) + } + + fun containsInAnySCC(allSCCs: HashSet>>, v: Vertex): Boolean { + for (scc in allSCCs) { + if (scc.contains(v)) return false + } + return true + } + + fun findSCCs(graph: GraphDirected): HashSet>> { + val allSCCs: HashSet>> = HashSet>>() + for (v in graph.vertices()) { + if (containsInAnySCC(allSCCs, v)) allSCCs.add(dfsTarjan(v, graph)) + } + return allSCCs + } + + + fun dfsTarjan(vertex: Vertex, graph: GraphDirected): HashSet> { + num[vertex] = curIndex + lowest[vertex] = curIndex + curIndex++ + stack.add(vertex) + visited.add(vertex) + + graph.getNeighbors(vertex).forEach { + if (!stack.contains(it.to)) { + dfsTarjan(it.to, graph) + lowest[vertex] = min(lowest[vertex]!!, lowest[it.to]!!) + //As they say it's not recommended + } else if (stack.contains(it.to)) { + lowest[vertex] = min(lowest[vertex]!!, num[it.to]!!) + //The same situation hier + } + } + processed.add(vertex) + + val scc: HashSet> = HashSet>() + if (lowest[vertex] == num[vertex]) { + var sccVertex: Vertex + do { + sccVertex = stack.pop() + scc.add(sccVertex) + } while (sccVertex != vertex) + } + + return scc + } +} diff --git a/src/main/kotlin/model/functionality/GraphAlgorithms.kt b/src/main/kotlin/model/functionality/GraphAlgorithms.kt new file mode 100644 index 0000000..498deb8 --- /dev/null +++ b/src/main/kotlin/model/functionality/GraphAlgorithms.kt @@ -0,0 +1,13 @@ +package model.functionality + +enum class GraphAlgorithms(val string: String) { + STRONG_CONNECTION_COMPONENTS("Find Strong Connection Components"), + LAYOUT("ForceAtlas2"), + COMMUNITIES("Find Communities"), + BRIDGES("Find Bridges"), + MINIMAL_SPANNING_TREE("Find Minimal Spanning Tree"), + SHORTEST_DISTANCE("Find Shortest Distance"), + DIJKSTRA("Find Shortest Positive Distance"), + DISTANCE_RANK("Importance of vertices"), + JOHN_ALGORITHM("Find Cycles for vertex") +} diff --git a/src/main/kotlin/model/functionality/MinSpanTreeFinder.kt b/src/main/kotlin/model/functionality/MinSpanTreeFinder.kt new file mode 100644 index 0000000..70901a4 --- /dev/null +++ b/src/main/kotlin/model/functionality/MinSpanTreeFinder.kt @@ -0,0 +1,51 @@ +package model.functionality + +import model.graphs.Edge +import model.graphs.GraphUndirected +import model.graphs.Vertex + +class MinSpanTreeFinder>(private val graph: GraphUndirected) { + fun mstSearch(): Set { + val spanningTreeEdges = mutableSetOf() + val spanningTreeVertices = mutableSetOf>() + + //step 1: add first vertex in spanning tree + val firstVertex = graph.firstOrNull() ?: return spanningTreeEdges + spanningTreeVertices.add(firstVertex) + + while (spanningTreeVertices.size != graph.size) { + //step 2: search edge that connects different connection components + val minEdge = findMinEdge(graph.edges(), spanningTreeVertices) + + //step 3: add this edge and adjacent vertex in spanning tree + if (minEdge != null) { + spanningTreeVertices.add(minEdge.from) + spanningTreeVertices.add(minEdge.to) + spanningTreeEdges.add(minEdge) + } else { //graph isn't connected + return emptySet() + } + } + + return spanningTreeEdges + } + + private fun findMinEdge(edges: Set, spanningTreeVertices: MutableSet>): E? { + var minEdge: E? = null + + for (vertex in spanningTreeVertices) { + for (edge in edges) { + val u = edge.from + val v = edge.to + + if (//we want edge that connects vertex presented in spanning tree and vertex that isn't in tree + ((vertex == u && !spanningTreeVertices.contains(v)) + || (vertex == v && !spanningTreeVertices.contains(u))) + && (minEdge == null || minEdge > edge) + ) minEdge = edge + } + } + + return minEdge + } +} diff --git a/src/main/kotlin/model/functionality/ShortestPathFinder.kt b/src/main/kotlin/model/functionality/ShortestPathFinder.kt new file mode 100644 index 0000000..e508bab --- /dev/null +++ b/src/main/kotlin/model/functionality/ShortestPathFinder.kt @@ -0,0 +1,81 @@ +package model.functionality + +import model.graphs.GraphWeighted +import model.graphs.Vertex +import model.graphs.WeightedEdge +import java.util.* +import kotlin.Double.Companion.NEGATIVE_INFINITY +import kotlin.Double.Companion.POSITIVE_INFINITY + +class ShortestPathFinder(private val graph: GraphWeighted) { + internal fun bellmanFord(start: Vertex): Map, Double> { + val dist: MutableMap, Double> = mutableMapOf() + graph.vertices().forEach { + dist[it] = POSITIVE_INFINITY + } + + dist[start] = 0.0 + + for (i in 1..graph.size + 1) { + for (vertex in graph.vertices()) { + for (edge in graph.getNeighbors(vertex)) { + edge as WeightedEdge + + val distVertex = dist[vertex] + val distNeighbor = dist[edge.to] ?: POSITIVE_INFINITY + + if ((distVertex != null) && (i <= graph.size)) { + if (distVertex + edge.weight < distNeighbor) { + dist[edge.to] = (distVertex + edge.weight) + } + } else if (i == graph.size + 1) { + if (distVertex != null) { + if (distVertex + edge.weight < distNeighbor) { + dist[edge.to] = NEGATIVE_INFINITY + } + } + } + } + } + } + + return dist + } + + fun dijkstra(start: Vertex): Map, Double> { + val dist: MutableMap, Double> = mutableMapOf() + graph.vertices().forEach { + dist[it] = POSITIVE_INFINITY + } + val priorityQueue = PriorityQueue, Double>>(compareBy { it.second }) + + dist[start] = 0.0 + priorityQueue.add(Pair(start, 0.0)) + + while (priorityQueue.isNotEmpty()) { + val (current, currentDist) = priorityQueue.poll() + + var weight: Number + var neighbor: Vertex + + for (child in graph.getNeighbors(current)) { + child as WeightedEdge + neighbor = child.to + weight = child.weight + require(weight >= 0) + + val next = neighbor + val nextDist: Double = currentDist.plus(weight).toDouble() + + dist[next]?.let { + if (nextDist < it) { + dist[next] = nextDist + priorityQueue.add(Pair(next, nextDist)) + } + } + } + } + + return dist + } +} diff --git a/src/main/kotlin/model/functionality/StrConCompFinder.kt b/src/main/kotlin/model/functionality/StrConCompFinder.kt new file mode 100644 index 0000000..270ded1 --- /dev/null +++ b/src/main/kotlin/model/functionality/StrConCompFinder.kt @@ -0,0 +1,72 @@ +package model.functionality + +import model.graphs.Edge +import model.graphs.GraphDirected +import model.graphs.Vertex +import java.util.Stack +import kotlin.math.min + +class StrConCompFinder>(private val graph: GraphDirected) { + private val strConCompSet = mutableSetOf>>() + + fun sccSearch(): Set>> { + var index = 1 + val stack = Stack>() + val sccSearchHelper = HashMap, TarjanAlgoVertexStats>() + for (vertex in graph) { + sccSearchHelper[vertex] = TarjanAlgoVertexStats() + } + + fun strongConnect(vertex: Vertex): Set> { + val vertexStats = sccSearchHelper[vertex] + ?: throw IllegalArgumentException("$vertex vertex does not presented in graph.") + vertexStats.sccIndex = index + vertexStats.lowLink = index + vertexStats.onStack = true + stack.push(vertex) + index++ + + val adjEdges = graph.getNeighbors(vertex) + for (edge in adjEdges) { + val neighbor = edge.to + val neighborStats = sccSearchHelper[neighbor] + ?: throw IllegalArgumentException("$edge vertex does not presented in graph.") + + if (sccSearchHelper[neighbor]?.sccIndex == 0) { + strongConnect(neighbor) + vertexStats.lowLink = min(vertexStats.lowLink, neighborStats.lowLink) + } else if (neighborStats.onStack) { + vertexStats.lowLink = min(vertexStats.lowLink, neighborStats.sccIndex) + } + } + + val scc = mutableSetOf>() + if (vertexStats.lowLink == vertexStats.sccIndex) { + do { + val visitedVertex = stack.pop() + val visitedVertexStats = sccSearchHelper[visitedVertex] + ?: throw IllegalArgumentException("$visitedVertex vertex does not presented in graph.") + visitedVertexStats.onStack = false + scc.add(visitedVertex) + } while (visitedVertex != vertex) + } + + return scc + } + + for (vertex in graph) { + val vertexStats = sccSearchHelper[vertex] + ?: throw IllegalArgumentException("$vertex vertex does not presented in graph.") + + if (vertexStats.sccIndex == 0) { + val scc = strongConnect(vertex) + + if (scc.isNotEmpty()) { + strConCompSet.add(scc) + } + } + } + + return strConCompSet + } +} diff --git a/src/main/kotlin/model/functionality/TarjanAlgoVertexStats.kt b/src/main/kotlin/model/functionality/TarjanAlgoVertexStats.kt new file mode 100644 index 0000000..01d566a --- /dev/null +++ b/src/main/kotlin/model/functionality/TarjanAlgoVertexStats.kt @@ -0,0 +1,7 @@ +package model.functionality + +class TarjanAlgoVertexStats( + var sccIndex: Int = 0, + var lowLink: Int = 0, + var onStack: Boolean = false, +) diff --git a/src/main/kotlin/model/functionality/iograph/GraphType.kt b/src/main/kotlin/model/functionality/iograph/GraphType.kt new file mode 100644 index 0000000..7986f1e --- /dev/null +++ b/src/main/kotlin/model/functionality/iograph/GraphType.kt @@ -0,0 +1,8 @@ +package model.functionality.iograph + +enum class GraphType(val string: String) { + UNDIRECTED_GRAPH("Undirected Graph"), + DIRECTED_GRAPH("Directed Graph"), + UNDIRECTED_WEIGHTED_GRAPH("Undirected Weighted Graph"), + DIRECTED_WEIGHTED_GRAPH("Directed Weighted Graph"), +} diff --git a/src/main/kotlin/model/functionality/iograph/ReadWriteIntGraph.kt b/src/main/kotlin/model/functionality/iograph/ReadWriteIntGraph.kt new file mode 100644 index 0000000..0f3bf2c --- /dev/null +++ b/src/main/kotlin/model/functionality/iograph/ReadWriteIntGraph.kt @@ -0,0 +1,123 @@ +package model.functionality.iograph + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import model.graphs.AbstractGraph +import model.graphs.DirectedGraph +import model.graphs.DirectedWeightedGraph +import model.graphs.Edge +import model.graphs.Graph +import model.graphs.UndirectedGraph +import model.graphs.UndirectedWeightedGraph +import java.awt.FileDialog +import java.awt.Frame +import java.io.File + +class ReadWriteIntGraph { + private val format = Json { + isLenient = true + prettyPrint = true + ignoreUnknownKeys = true + allowStructuredMapKeys = true + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun writeUGraph(file: File, graph: UndirectedGraph) { + val output = file.outputStream() + format.encodeToStream(graph, output) + output.close() + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun writeDGraph(file: File, graph: DirectedGraph) { + val output = file.outputStream() + format.encodeToStream(graph, output) + output.close() + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun writeUWGraph(file: File, graph: UndirectedWeightedGraph) { + val output = file.outputStream() + format.encodeToStream(graph, output) + output.close() + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun writeDWGraph(file: File, graph: DirectedWeightedGraph) { + val output = file.outputStream() + format.encodeToStream(graph, output) + output.close() + } + + fun > saveGraph(graph: Graph) { + val dialog = FileDialog(Frame(), "Select Graph File", FileDialog.SAVE) + dialog.isVisible = true + + dialog.file ?: return + + val file = File(dialog.directory, "${dialog.file}.json") + + when (graph) { + is DirectedGraph -> writeDGraph(file, graph as DirectedGraph) + is UndirectedGraph -> writeUGraph(file, graph as UndirectedGraph) + is UndirectedWeightedGraph -> writeUWGraph(file, graph as UndirectedWeightedGraph) + is DirectedWeightedGraph -> writeDWGraph(file, graph as DirectedWeightedGraph) + } + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun readUGraph(file: File): UndirectedGraph { + val input = file.inputStream() + val graph = format.decodeFromStream>(input) + input.close() + + return graph + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun readDGraph(file: File): DirectedGraph { + val input = file.inputStream() + val graph = format.decodeFromStream>(input) + input.close() + + return graph + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun readUWGraph(file: File): UndirectedWeightedGraph { + val input = file.inputStream() + val graph = format.decodeFromStream>(input) + input.close() + + return graph + } + + @OptIn(ExperimentalSerializationApi::class) + internal fun readDWGraph(file: File): DirectedWeightedGraph { + val input = file.inputStream() + val graph = format.decodeFromStream>(input) + input.close() + + return graph + } + + fun openGraph(type: GraphType): AbstractGraph>? { + val dialog = FileDialog(Frame(), "Select Graph File", FileDialog.LOAD) + dialog.isVisible = true + + dialog.file ?: return null + + val file = File(dialog.directory, dialog.file) + + val graph = when (type) { + GraphType.UNDIRECTED_WEIGHTED_GRAPH -> readUWGraph(file) + GraphType.UNDIRECTED_GRAPH -> readUGraph(file) + GraphType.DIRECTED_WEIGHTED_GRAPH -> readDWGraph(file) + GraphType.DIRECTED_GRAPH -> readDGraph(file) + } + + return graph + } +} diff --git a/src/main/kotlin/model/functionality/iograph/VertexSerializer.kt b/src/main/kotlin/model/functionality/iograph/VertexSerializer.kt new file mode 100644 index 0000000..3d8184e --- /dev/null +++ b/src/main/kotlin/model/functionality/iograph/VertexSerializer.kt @@ -0,0 +1,22 @@ +package model.functionality.iograph + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import model.graphs.Vertex + +class VertexSerializer : KSerializer> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Vertex", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Vertex) { + encoder.encodeString(value.key.toString()) + } + + override fun deserialize(decoder: Decoder): Vertex { + val key = decoder.decodeString() + return Vertex(key.toInt() as T) + } +} diff --git a/src/main/kotlin/model/graphs/AbstractGraph.kt b/src/main/kotlin/model/graphs/AbstractGraph.kt new file mode 100644 index 0000000..69be39b --- /dev/null +++ b/src/main/kotlin/model/graphs/AbstractGraph.kt @@ -0,0 +1,93 @@ +package model.graphs + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +abstract class AbstractGraph> : Graph { + @SerialName("graph") + var adjList: HashMap, HashSet> = HashMap() + internal set + + @SerialName("size") + protected var _size: Int = 0 + override val size: Int + get() = _size + + override fun areConnected(u: Vertex, v: Vertex): Boolean { + return (adjList[u]?.any { it.contains(v) } ?: false) + || (adjList[v]?.any { it.contains(u) } ?: false) + } + + override fun addVertex(key: T): Vertex { + for (v in adjList.keys) { + if (v.key == key) { + return v + } + } + + val vertex = Vertex(key) + adjList[vertex] = HashSet() + + _size += 1 + + return vertex + } + + override fun addVertex(vertex: Vertex): Vertex { + if (adjList.containsKey(vertex)) { + return vertex + } + + adjList[vertex] = HashSet() + + _size += 1 + + return vertex + } + + override fun addVertices(vararg keys: T) { + for (key in keys) { + addVertex(key) + } + } + + override fun addVertices(vararg vertices: Vertex) { + for (vertex in vertices) { + addVertex(vertex) + } + } + + override fun addEdges(vararg edges: E) { + for (edge in edges) { + this.addEdge(edge) + } + } + + override fun addEdge(edge: E) { + for (vertex in adjList.keys) { + if (edge.from == vertex) { + adjList[vertex]?.add(edge) + } + } + } + + override fun vertices(): Set> { + return adjList.keys + } + + override fun getNeighbors(vertex: Vertex): HashSet { + return adjList[vertex] ?: HashSet() + } + + override fun edges(): Set { + val edges = HashSet() + for (vertex in adjList.keys) { + for (edge in adjList[vertex] ?: HashSet()) { + edges.add(edge) + } + } + + return edges + } +} diff --git a/src/main/kotlin/model/graphs/DirectedGraph.kt b/src/main/kotlin/model/graphs/DirectedGraph.kt new file mode 100644 index 0000000..20f9ef8 --- /dev/null +++ b/src/main/kotlin/model/graphs/DirectedGraph.kt @@ -0,0 +1,37 @@ +package model.graphs + +import kotlinx.serialization.Serializable +import model.functionality.DistanceRank +import model.functionality.JohnsonAlg +import model.functionality.StrConCompFinder +import model.functionality.TarjanSCC + +@Serializable +class DirectedGraph : + AbstractGraph>(), + GraphDirected> { + fun addEdge(vertex1: Vertex, vertex2: Vertex) { + require(adjList.containsKey(vertex1)) + require(adjList.containsKey(vertex2)) + + adjList.getOrPut(vertex1) { HashSet() }.add(UnweightedEdge(vertex1, vertex2)) + } + + + override fun findCycles(startNode: Vertex): HashSet>> { + return JohnsonAlg(this).findCycles(startNode) + } + + + override fun addEdge(edge: UnweightedEdge) { + addEdge(edge.from, edge.to) + } + + override fun findSCC(): Set>> { + return StrConCompFinder(this).sccSearch() + } + + fun distanceRank(): Map, Double> { + return DistanceRank(this).rank() + } +} diff --git a/src/main/kotlin/model/graphs/DirectedWeightedGraph.kt b/src/main/kotlin/model/graphs/DirectedWeightedGraph.kt new file mode 100644 index 0000000..35e4316 --- /dev/null +++ b/src/main/kotlin/model/graphs/DirectedWeightedGraph.kt @@ -0,0 +1,31 @@ +package model.graphs + +import kotlinx.serialization.Serializable +import model.functionality.JohnsonAlg +import model.functionality.StrConCompFinder + +@Serializable +class DirectedWeightedGraph : + AbstractGraph>(), + GraphDirected>, + GraphWeighted { + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Double) { + require(adjList.containsKey(vertex1)) + require(adjList.containsKey(vertex2)) + + adjList.getOrPut(vertex1) { HashSet() }.add(WeightedEdge(vertex1, vertex2, weight)) + } + + override fun addEdge(edge: WeightedEdge) { + addEdge(edge.from, edge.to, edge.weight) + } + + override fun findCycles(startNode: Vertex): HashSet>> { + return JohnsonAlg(this).findCycles(startNode) + } + + + override fun findSCC(): Set>> { + return StrConCompFinder(this).sccSearch() + } +} diff --git a/src/main/kotlin/model/graphs/Edge.kt b/src/main/kotlin/model/graphs/Edge.kt new file mode 100644 index 0000000..d297ae7 --- /dev/null +++ b/src/main/kotlin/model/graphs/Edge.kt @@ -0,0 +1,13 @@ +package model.graphs + +interface Edge : Comparable> { + val from: Vertex + val to: Vertex + var copies: Int + + fun reverse(): Edge + + fun contains(v: Vertex): Boolean { + return from == v || to == v + } +} diff --git a/src/main/kotlin/model/graphs/Graph.kt b/src/main/kotlin/model/graphs/Graph.kt new file mode 100644 index 0000000..19fb169 --- /dev/null +++ b/src/main/kotlin/model/graphs/Graph.kt @@ -0,0 +1,34 @@ +package model.graphs + +interface Graph> : Iterable> { + val size: Int + + fun addVertex(key: T): Vertex + + fun addVertex(vertex: Vertex): Vertex + + fun addVertices(vararg keys: T) + + fun addVertices(vararg vertices: Vertex) + + fun addEdge(edge: E) + + fun addEdges(vararg edges: E) + + //Нужно было свойствами, а не методами делать :( + fun vertices(): Set> + + fun edges(): Set + + fun areConnected(u: Vertex, v: Vertex): Boolean + + override fun iterator(): Iterator> { + return this.vertices().iterator() + } + + fun getNeighbors(vertex: Vertex): HashSet + +// fun cyclesForVertex(vertex: Vertex): HashSet>> { +// return JohnsonAlg(this).findCycles(vertex) +// } +} diff --git a/src/main/kotlin/model/graphs/GraphDirected.kt b/src/main/kotlin/model/graphs/GraphDirected.kt new file mode 100644 index 0000000..7c2a860 --- /dev/null +++ b/src/main/kotlin/model/graphs/GraphDirected.kt @@ -0,0 +1,12 @@ +package model.graphs + +import model.functionality.TarjanSCC + + +interface GraphDirected> : Graph { + fun findSCC(): Set>> + fun findCycles(startNode: Vertex): HashSet>> + fun Tarjan(startNode: Vertex): HashSet> { + return TarjanSCC().findSCC(startNode, this) + } +} diff --git a/src/main/kotlin/model/graphs/GraphUndirected.kt b/src/main/kotlin/model/graphs/GraphUndirected.kt new file mode 100644 index 0000000..a98c19c --- /dev/null +++ b/src/main/kotlin/model/graphs/GraphUndirected.kt @@ -0,0 +1,17 @@ +package model.graphs + +import model.functionality.BridgeFinder + +interface GraphUndirected> : Graph { + fun findBridges(): Set { + return BridgeFinder().findBridges(this) + } + + fun findMinSpanTree(): Set>? + + + // Resolution parameter x > 0 for community detection + // Higher resolution -> more communities + // Higher randomness -> more random node movements + fun runLeidenMethod(randomness: Double, resolution: Double): HashSet>> +} diff --git a/src/main/kotlin/model/graphs/GraphWeighted.kt b/src/main/kotlin/model/graphs/GraphWeighted.kt new file mode 100644 index 0000000..2a46099 --- /dev/null +++ b/src/main/kotlin/model/graphs/GraphWeighted.kt @@ -0,0 +1,15 @@ +package model.graphs + +import model.functionality.ShortestPathFinder + +interface GraphWeighted : Graph> { + fun findDistancesBellman(start: Vertex): Map, Double> { + val output = ShortestPathFinder(this).bellmanFord(start) + return output + } + + + fun findDistancesDijkstra(start: Vertex): Map, Double> { + return ShortestPathFinder(this).dijkstra(start) + } +} diff --git a/src/main/kotlin/model/graphs/UndirectedGraph.kt b/src/main/kotlin/model/graphs/UndirectedGraph.kt new file mode 100644 index 0000000..807c8cc --- /dev/null +++ b/src/main/kotlin/model/graphs/UndirectedGraph.kt @@ -0,0 +1,48 @@ +package model.graphs + +import kotlinx.serialization.Serializable +import model.functionality.CommunityDetector +import model.functionality.MinSpanTreeFinder + +@Serializable +open class UndirectedGraph : + AbstractGraph>(), + GraphUndirected> { + fun addEdge(vertex1: Vertex, vertex2: Vertex) { + require(adjList.containsKey(vertex1)) + require(adjList.containsKey(vertex2)) + + val edge = adjList[vertex1]?.find { it.to == vertex2 } + + if (edge != null) { + edge.copies += 1 + adjList[vertex2]!!.find { it.to == vertex1 }!!.copies += 1 + } else { + adjList.getOrPut(vertex1) { HashSet() }.add(UnweightedEdge(vertex1, vertex2)) + adjList.getOrPut(vertex2) { HashSet() }.add(UnweightedEdge(vertex2, vertex1)) + } + } + + override fun addEdge(edge: UnweightedEdge) { + addEdge(edge.from, edge.to) + } + + // добавляет одно конкретное ребро, пока надо только алг поиска + // сообществ + fun addSingleEdge(edge: UnweightedEdge) { + require(adjList.containsKey(edge.from)) + require(adjList.containsKey(edge.to)) + + edge.copies += 1 + adjList.getOrPut(edge.from) { HashSet() }.add(edge) + } + + override fun findMinSpanTree(): Set>? { + return MinSpanTreeFinder(this).mstSearch() + } + + override fun runLeidenMethod(randomness: Double, resolution: Double): HashSet>> { + return CommunityDetector(this, resolution, randomness).leiden() + } + +} diff --git a/src/main/kotlin/model/graphs/UndirectedWeightedGraph.kt b/src/main/kotlin/model/graphs/UndirectedWeightedGraph.kt new file mode 100644 index 0000000..432acc4 --- /dev/null +++ b/src/main/kotlin/model/graphs/UndirectedWeightedGraph.kt @@ -0,0 +1,41 @@ +package model.graphs + +import kotlinx.serialization.Serializable +import model.functionality.CommunityDetector +import model.functionality.MinSpanTreeFinder + +@Serializable +open class UndirectedWeightedGraph : + AbstractGraph>(), + GraphUndirected>, + GraphWeighted { + fun addEdge(vertex1: Vertex, vertex2: Vertex, weight: Double) { + require(adjList.containsKey(vertex1)) + require(adjList.containsKey(vertex2)) + + // для орграфов тоже надо будет реализовать дубликаты + // избавиться от !! (?) + + val edge = adjList[vertex1]?.find { it.to == vertex2 } + + if (edge != null) { + edge.copies += 1 + adjList[vertex2]!!.find { it.to == vertex1 }!!.copies += 1 + } else { + adjList.getOrPut(vertex1) { HashSet() }.add(WeightedEdge(vertex1, vertex2, weight)) + adjList.getOrPut(vertex2) { HashSet() }.add(WeightedEdge(vertex2, vertex1, weight)) + } + } + + override fun addEdge(edge: WeightedEdge) { + addEdge(edge.from, edge.to, edge.weight) + } + + override fun findMinSpanTree(): Set>? { + return MinSpanTreeFinder(this).mstSearch() + } + + override fun runLeidenMethod(randomness: Double, resolution: Double): HashSet>> { + return CommunityDetector(this, resolution, randomness).leiden() + } +} diff --git a/src/main/kotlin/model/graphs/UnweightedEdge.kt b/src/main/kotlin/model/graphs/UnweightedEdge.kt new file mode 100644 index 0000000..3ac7803 --- /dev/null +++ b/src/main/kotlin/model/graphs/UnweightedEdge.kt @@ -0,0 +1,33 @@ +package model.graphs + +import kotlinx.serialization.Serializable + +@Serializable +data class UnweightedEdge( + override val from: Vertex, + override val to: Vertex +) : Edge { + override var copies: Int = 1 + override fun reverse(): Edge { + return UnweightedEdge(to, from) + } + + override fun compareTo(other: Edge): Int { + return if (this == other) 0 else -1 + } + + override fun equals(other: Any?): Boolean { + return other is UnweightedEdge<*> && + ((from == other.from && to == other.to) || + (from == other.to && to == other.from)) + } + + override fun toString(): String { + return "($from, $to)" + } + + override fun hashCode(): Int { + val result = from.hashCode() + 31 * to.hashCode() + return result + } +} diff --git a/src/main/kotlin/model/graphs/Vertex.kt b/src/main/kotlin/model/graphs/Vertex.kt new file mode 100644 index 0000000..1ded1c9 --- /dev/null +++ b/src/main/kotlin/model/graphs/Vertex.kt @@ -0,0 +1,22 @@ +package model.graphs + +import kotlinx.serialization.Serializable +import model.functionality.iograph.VertexSerializer + +@Serializable(with = VertexSerializer::class) +data class Vertex(val key: T) { + override fun equals(other: Any?): Boolean { + return when(other) { + is Vertex<*> -> key == other.key + else -> false + } + } + + override fun toString(): String { + return "Vertex($key)" + } + + override fun hashCode(): Int { + return key?.hashCode() ?: 0 + } +} diff --git a/src/main/kotlin/model/graphs/WeightedEdge.kt b/src/main/kotlin/model/graphs/WeightedEdge.kt new file mode 100644 index 0000000..9874bad --- /dev/null +++ b/src/main/kotlin/model/graphs/WeightedEdge.kt @@ -0,0 +1,34 @@ +package model.graphs + +import kotlinx.serialization.Serializable + +@Serializable +data class WeightedEdge( + override val from: Vertex, + override val to: Vertex, + var weight: Double = 0.0 +) : Edge { + override var copies: Int = 1 + override fun reverse(): Edge { + return WeightedEdge(to, from, weight) + } + + override fun compareTo(other: Edge): Int { + return if (other is WeightedEdge) weight.compareTo(other.weight) else 1 + } + + override fun equals(other: Any?): Boolean { + return other is WeightedEdge<*> && (weight == other.weight) && + ((from == other.from && to == other.to) || + (from == other.to && to == other.from)) + } + + override fun hashCode(): Int { + val result = weight.hashCode() + 31 * from.hashCode() + 31 * 31 * to.hashCode() + return result + } + + override fun toString(): String { + return "($from, $to, $weight)" + } +} diff --git a/src/main/kotlin/view/AppTheme.kt b/src/main/kotlin/view/AppTheme.kt new file mode 100644 index 0000000..cc2dc56 --- /dev/null +++ b/src/main/kotlin/view/AppTheme.kt @@ -0,0 +1,52 @@ +package view + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Shapes +import androidx.compose.material.Typography +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun AppTheme(darkTheme: Boolean, content: @Composable () -> Unit) { + val darkThemeColors = darkColors( + primary = Color(80, 60, 60), + secondary = Color(126, 99, 99), + secondaryVariant = Color(175, 143, 111), + background = Color(62, 50, 50), + surface = Color(84, 72, 72), + onBackground = Color(174, 93, 62), + onError = Color(255, 166, 0) + ) + + val lightThemeColors = lightColors( + primary = Color(23, 107, 135), + secondary = Color(180, 212, 255), + secondaryVariant = Color(134, 182, 246), + background = Color(238, 245, 255), + surface = Color.White, + onBackground = Color(52, 76, 100), + ) + + MaterialTheme( + colors = if (darkTheme) darkThemeColors else lightThemeColors, + typography = Typography( + defaultFontFamily = FontFamily.SansSerif, + h1 = TextStyle(fontWeight = FontWeight.Bold, fontSize = 30.sp), + body1 = TextStyle(fontSize = 16.sp) + ), + shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(8.dp), + large = RoundedCornerShape(16.dp) + ), + content = content + ) +} diff --git a/src/main/kotlin/view/graphs/EdgeView.kt b/src/main/kotlin/view/graphs/EdgeView.kt new file mode 100644 index 0000000..8b5629b --- /dev/null +++ b/src/main/kotlin/view/graphs/EdgeView.kt @@ -0,0 +1,37 @@ +package view.graphs + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import viewmodel.graphs.EdgeViewModel + +@Suppress("FunctionNaming") +@Composable +fun EdgeView( + viewModel: EdgeViewModel, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + Canvas(modifier = Modifier.fillMaxSize()) { + drawLine( + start = Offset( + viewModel.u.x.toPx() + viewModel.u.radius.toPx(), + viewModel.u.y.toPx() + viewModel.u.radius.toPx(), + ), + + end = Offset( + viewModel.v.x.toPx() + viewModel.v.radius.toPx(), + viewModel.v.y.toPx() + viewModel.v.radius.toPx(), + ), + + color = viewModel.color, + strokeWidth = viewModel.width, + cap = StrokeCap.Round + ) + } + } +} diff --git a/src/main/kotlin/view/graphs/GraphView.kt b/src/main/kotlin/view/graphs/GraphView.kt new file mode 100644 index 0000000..6c8fed3 --- /dev/null +++ b/src/main/kotlin/view/graphs/GraphView.kt @@ -0,0 +1,59 @@ +package view.graphs + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.onDrag +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.unit.dp +import model.graphs.Edge +import model.graphs.Vertex +import viewmodel.graphs.GraphViewModel + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Suppress("FunctionNaming") +@Composable +fun > GraphView( + viewModel: GraphViewModel, +) { + var currentVertex: Vertex? by remember { mutableStateOf(null) } + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colors.background) + .padding(16.dp) + .onPointerEvent(PointerEventType.Scroll, onEvent = viewModel.onScroll) + .onDrag { offset -> + viewModel.onDrag(offset) + } + ) { + + viewModel.edges.forEach { edge -> + EdgeView(edge) + } + + viewModel.vertices.forEach { vertex -> + VertexView( + viewModel = vertex, + onClick = { + currentVertex = vertex.value + viewModel.currentVertex?.isSelected = false + viewModel.currentVertex = vertex + vertex.isSelected = true + } + ) + } + } +} diff --git a/src/main/kotlin/view/graphs/VertexView.kt b/src/main/kotlin/view/graphs/VertexView.kt new file mode 100644 index 0000000..7ccfe74 --- /dev/null +++ b/src/main/kotlin/view/graphs/VertexView.kt @@ -0,0 +1,94 @@ +package view.graphs + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import model.graphs.Vertex +import viewmodel.graphs.VertexViewModel + +@Suppress("FunctionNaming") +@Composable +fun VertexView( + viewModel: VertexViewModel, + modifier: Modifier = Modifier, + onClick: (Vertex) -> Unit +) { + var color by remember { mutableStateOf(Color.Unspecified) } + + color = if (viewModel.isSelected) { + Color(255, 166, 0) + } else if (viewModel.color != Color.Unspecified) { + viewModel.color + } else { + MaterialTheme.colors.onBackground + } + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .offset(viewModel.x, viewModel.y) + .size(viewModel.radius * 2, viewModel.radius * 2) + .background(color, CircleShape) + .clickable { + onClick(viewModel.value) + } + .pointerInput(viewModel) { + detectDragGestures { change, dragAmount -> + change.consume() + viewModel.onDrag(dragAmount) + } + } + + ) { + if (viewModel.isKeyLabelVisible) { + Text( + text = viewModel.label, + overflow = TextOverflow.Visible, + style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onPrimary), + modifier = Modifier.padding(8.dp) + ) + } + + @Suppress("MagicNumber") + if (viewModel.isDistLabelVisible) { + Text( + modifier = Modifier + .offset( + 1.dp, + (48).dp // Twice the size of the font. + ), + softWrap = false, + text = viewModel.distanceLabel, + overflow = TextOverflow.Visible, + style = TextStyle( + color = MaterialTheme.colors.onBackground, + fontSize = 24.sp, + fontFamily = FontFamily.SansSerif, + textAlign = TextAlign.Left + ) + ) + } + } +} diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt new file mode 100644 index 0000000..c595e58 --- /dev/null +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -0,0 +1,317 @@ +package view.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Checkbox +import androidx.compose.material.CheckboxDefaults +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import model.functionality.GraphAlgorithms +import model.functionality.iograph.GraphType +import model.graphs.* +import view.graphs.GraphView +import viewmodel.screens.MainScreenViewModel + +@Composable +fun > mainScreen(viewModel: MainScreenViewModel) { + val showMenu by remember { viewModel.showDropdownMenu } + val showOpenDialog by remember { viewModel.showOpenExistingGraphDialog } + val showChooseDialog by remember { viewModel.showChooseGraphTypeDialog } + val toStartingScreen by remember { viewModel.toStartingScreen } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("GraphApp") }, + backgroundColor = MaterialTheme.colors.primary, + contentColor = MaterialTheme.colors.onPrimary, + navigationIcon = { + IconButton(onClick = viewModel::showMenu) { + Icon(Icons.Filled.Menu, contentDescription = "Main Menu") + } + DropdownMenu(expanded = showMenu, onDismissRequest = viewModel::closeMenu) { + DropdownMenuItem(onClick = viewModel::openOpenDialog) { Text("Open Graph") } + DropdownMenuItem(onClick = viewModel::saveGraph) { Text("Save Graph") } + DropdownMenuItem(onClick = viewModel::changeTheme) { Text("Toggle Theme") } + DropdownMenuItem(onClick = viewModel::openToStartingScreenDialog) { Text("Main Menu") } + DropdownMenuItem(onClick = viewModel::closeApp) { Text("Exit") } + } + } + ) + } + ) { + mainContent(viewModel) + } + + when { + showOpenDialog -> OpenExistingGraphDialog(viewModel) + showChooseDialog -> OpenChooseGraphTypeDialog(viewModel) + toStartingScreen -> ToStartingScreenAlertDialog(viewModel) + } +} + +@Composable +fun > ToStartingScreenAlertDialog(viewModel: MainScreenViewModel) { + AlertDialog( + onDismissRequest = viewModel::closeToStartingScreenDialog, + title = { Text("Are you sure?") }, + text = { Text("Graphs won't save automatically") }, + confirmButton = { + Button(onClick = viewModel::toStartingScreen) { Text("Yep") } + }, + dismissButton = { + Button(onClick = viewModel::closeToStartingScreenDialog) { Text("Nope") } + } + ) +} + +@Composable +fun > mainContent( + viewModel: MainScreenViewModel, +) { + Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) { + Surface( + modifier = Modifier + .weight(1f) + .fillMaxSize(), + color = MaterialTheme.colors.surface + ) { + GraphView(viewModel.graphViewModel) + } + Column( + modifier = Modifier.width(370.dp), + ) { + toolPanel( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background(MaterialTheme.colors.secondary), + viewModel = viewModel + ) + } + } +} + +@Composable +fun > toolPanel(modifier: Modifier, viewModel: MainScreenViewModel) { + Column( + modifier = modifier + .fillMaxHeight() + .background(MaterialTheme.colors.surface) + .clip(RoundedCornerShape(8.dp)) + .padding(16.dp) + ) { + Text( + text = "Tools", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp), + color = MaterialTheme.colors.onSurface + ) + + toolButton(GraphAlgorithms.LAYOUT, viewModel::useForceAtlas2Layout) + + if (viewModel.graph is GraphUndirected) { + val resolutionInput by remember { viewModel.resolutionInput } + val randomnessInput by remember { viewModel.randomnessInput } + + toolButton(GraphAlgorithms.BRIDGES, viewModel::showBridges) + + Row { + TextField( + modifier = Modifier.weight(2f), + value = randomnessInput, + placeholder = { Text("Enter x: Double > 0. Optimal value lies in [0.0005, 0.1]") }, + onValueChange = viewModel::setRandomness, + label = { Text("Randomness") } + ) + + TextField( + modifier = Modifier.weight(2f), + value = resolutionInput, + placeholder = { Text("Enter y: Double > 0. Higher resolution lead to more communities and lower resolutions lead to fewer communities.") }, + onValueChange = viewModel::setResolution, + label = { Text("Resolution") }, + ) + } + + toolButton(GraphAlgorithms.COMMUNITIES, viewModel::findCommunities) + + toolButton(GraphAlgorithms.MINIMAL_SPANNING_TREE, viewModel::highlightMinSpanTree) + } + + if (viewModel.graph is GraphDirected) { + toolButton(GraphAlgorithms.STRONG_CONNECTION_COMPONENTS, viewModel::highlightSCC) + } + + if (viewModel.graph is GraphWeighted<*>) { + toolButton(GraphAlgorithms.DIJKSTRA, viewModel::findDistanceDijkstra) + } + + if (viewModel.graph is GraphDirected) { + toolButton(GraphAlgorithms.JOHN_ALGORITHM, viewModel::findDistanceDijkstra) + } + + if (viewModel.graph is DirectedGraph<*>) { + toolButton(GraphAlgorithms.DISTANCE_RANK, viewModel::distanceRank) + } + + //toolButton(GraphAlgorithms.SHORTEST_DISTANCE, viewModel::findDistanceBellman) + if (viewModel.graph is GraphWeighted<*>) { + Button( + onClick = { + viewModel.findDistanceBellman() + viewModel.showVerticesDistanceLabels.value = !viewModel.showVerticesDistanceLabels.value + viewModel.showVerticesDistanceLabels.value = !viewModel.showVerticesDistanceLabels.value + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.secondary, + contentColor = MaterialTheme.colors.onSurface, + ), + enabled = true, + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) + ) { + Icon(Icons.Default.Search, contentDescription = "Find the shortest distance") + Spacer(modifier = Modifier.width(8.dp)) + Text(text = "Find Shortest Distance") + } + } + + //Эти тоглы засунуть в ModalDrawer + toggleRow( + label = "Show Vertices Labels", + checked = viewModel.showVerticesLabels.value, + onCheckedChange = { viewModel.showVerticesLabels.value = it } + ) + + toggleRow( + label = "Show Edges Labels", + checked = viewModel.showEdgesLabels.value, + onCheckedChange = { viewModel.showEdgesLabels.value = it } + ) + + toggleRow( + label = "Show Distance Labels", + checked = viewModel.showVerticesDistanceLabels.value, + onCheckedChange = { viewModel.showVerticesDistanceLabels.value = it } + ) + } +} + +@Composable +fun toolButton(type: GraphAlgorithms, algorithm: () -> Unit) { + Button( + onClick = algorithm, + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.secondary, + contentColor = MaterialTheme.colors.onSurface, + ), + enabled = true, + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) + ) { + Icon(Icons.Default.Search, contentDescription = type.string) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = type.string) + } +} + +@Suppress("FunctionNaming") +@Composable +fun toggleRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 8.dp) + ) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + colors = CheckboxDefaults.colors(MaterialTheme.colors.primary) + ) + Text( + text = label, + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(start = 8.dp), + color = MaterialTheme.colors.onSurface + ) + } +} + +//next UI element are copy-pastes from starting screen +//it's bad, I just don't have time for it +//moreover, there's more copy-paste in main screen VM +//to make it work with these functions +@Composable +fun > OpenExistingGraphDialog(viewModel: MainScreenViewModel) { + AlertDialog( + onDismissRequest = viewModel::closeOpenDialog, + title = { Text("Open Existing Graph") }, + text = { Text("Would you like to open an existing graph?") }, + confirmButton = { + Button(onClick = viewModel::openChooseDialog) { Text("Open") } + }, + dismissButton = { + Button(onClick = viewModel::closeOpenDialog) { Text("Cancel") } + } + ) +} + +@Composable +fun > OpenChooseGraphTypeDialog(viewModel: MainScreenViewModel) { + AlertDialog( + onDismissRequest = { viewModel.closeChooseDialog() }, + title = { Text(text = "Choose graph type") }, + text = { Text(text = "Please select one of the options below:") }, + buttons = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OpenGraphButton(GraphType.UNDIRECTED_GRAPH, viewModel) + OpenGraphButton(GraphType.DIRECTED_GRAPH, viewModel) + OpenGraphButton(GraphType.UNDIRECTED_WEIGHTED_GRAPH, viewModel) + OpenGraphButton(GraphType.DIRECTED_WEIGHTED_GRAPH, viewModel) + } + } + ) +} + +@Composable +fun > OpenGraphButton(type: GraphType, viewModel: MainScreenViewModel) { + Button( + onClick = { viewModel.openGraph(type) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = type.string) + } +} diff --git a/src/main/kotlin/view/screens/StartingScreen.kt b/src/main/kotlin/view/screens/StartingScreen.kt new file mode 100644 index 0000000..90efc2b --- /dev/null +++ b/src/main/kotlin/view/screens/StartingScreen.kt @@ -0,0 +1,123 @@ +package view.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import model.functionality.iograph.GraphType +import viewmodel.screens.StartingScreenViewModel + +@Composable +fun StartingScreen(viewModel: StartingScreenViewModel) { + val showCreateNewGraphDialog by remember { viewModel.showCreateNewGraphDialog } + val showChooseGraphTypeDialog by remember { viewModel.showChooseGraphTypeDialog } + val showOpenExistingGraphDialog by remember { viewModel.showOpenExistingGraphDialog } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Welcome to GraphApp", style = MaterialTheme.typography.h1, fontSize = 48.sp) + Spacer(modifier = Modifier.height(128.dp)) + Button(modifier = Modifier.width(360.dp).height(60.dp), onClick = viewModel::openCreateDialog) { + Text(text = "Create Graph", fontSize = 24.sp) + } + Spacer(modifier = Modifier.height(16.dp)) + Button(modifier = Modifier.width(360.dp).height(60.dp), onClick = viewModel::openOpenDialog) { + Text(text = "Open Graph", fontSize = 24.sp) + } + Spacer(modifier = Modifier.height(16.dp)) + Button(modifier = Modifier.width(360.dp).height(60.dp), onClick = viewModel::closeApp) { + Text(text = "Exit", fontSize = 24.sp) + } + } + } + + when { + showCreateNewGraphDialog -> CreateNewGraphDialog(viewModel) + showOpenExistingGraphDialog -> OpenExistingGraphDialog(viewModel) + showChooseGraphTypeDialog -> OpenChooseGraphTypeDialog(viewModel) + } +} + +@Composable +fun OpenChooseGraphTypeDialog(viewModel: StartingScreenViewModel) { + AlertDialog( + onDismissRequest = { viewModel.closeChooseDialog() }, + title = { Text(text = "Choose graph type") }, + text = { Text(text = "Please select one of the options below:") }, + buttons = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OpenGraphButton(GraphType.UNDIRECTED_GRAPH, viewModel) + OpenGraphButton(GraphType.DIRECTED_GRAPH, viewModel) + OpenGraphButton(GraphType.UNDIRECTED_WEIGHTED_GRAPH, viewModel) + OpenGraphButton(GraphType.DIRECTED_WEIGHTED_GRAPH, viewModel) + } + } + ) +} + +@Composable +fun OpenGraphButton(type: GraphType, viewModel: StartingScreenViewModel) { + Button( + onClick = { viewModel.openGraph(type) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = type.string) + } +} + +@Composable +fun CreateNewGraphDialog(viewModel: StartingScreenViewModel) { + AlertDialog( + modifier = Modifier.width(330.dp), + onDismissRequest = viewModel::closeCreateDialog, + title = { Text("Create New Graph") }, + text = { Text("Would you like to create a new graph?") }, + confirmButton = { + Button(onClick = viewModel::createGraph) { Text("Create") } + }, + dismissButton = { + Button(onClick = viewModel::closeCreateDialog) { Text("Cancel") } + } + ) +} + +@Composable +fun OpenExistingGraphDialog(viewModel: StartingScreenViewModel) { + AlertDialog( + modifier = Modifier.width(360.dp), + onDismissRequest = viewModel::closeOpenDialog, + title = { Text("Open Existing Graph") }, + text = { Text("Would you like to open an existing graph?") }, + confirmButton = { + Button(onClick = viewModel::openChooseDialog) { Text("Open") } + }, + dismissButton = { + Button(onClick = viewModel::closeOpenDialog) { Text("Cancel") } + } + ) +} diff --git a/src/main/kotlin/viewmodel/graphs/CircularPlacementStrategy.kt b/src/main/kotlin/viewmodel/graphs/CircularPlacementStrategy.kt new file mode 100644 index 0000000..010c347 --- /dev/null +++ b/src/main/kotlin/viewmodel/graphs/CircularPlacementStrategy.kt @@ -0,0 +1,131 @@ +package viewmodel.graphs + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import model.graphs.Edge +import model.graphs.Vertex +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin + +class CircularPlacementStrategy : RepresentationStrategy { + override fun place(width: Double, height: Double, vertices: Collection>) { + if (vertices.isEmpty()) { + println("viewmodel.graphs.CircularPlacementStrategy.place: there is nothing to place 👐🏻") + return + } + + val center = Pair(width / 2, height / 2) + val angle = 2 * Math.PI / vertices.size + val sorted = vertices.sortedBy { it.label } + val first = sorted.first() + var point = Pair(center.first, center.second - min(width, height) / 2) + + first.x = point.first.dp + first.y = point.second.dp + + sorted + .drop(1) + .onEach { + point = point.rotate(center, angle) + it.x = point.first.dp + it.y = point.second.dp + } + } + + override fun highlight(vertices: Collection>) { + TODO("Not yet implemented") + } + + override fun highlightBridges(edges: Collection>, bridges: Set>) { + for (bridge in bridges) { + val toColor = edges.find { ((it.v.value == bridge.to) && (it.u.value == bridge.from)) } + val toColorSecond = edges.find { ((it.u.value == bridge.to) && (it.v.value == bridge.from)) } + + if (toColor != null) { + //earlier there was MaterialTheme.colors.secondaryVariant + //but then this class needs to be Composable + //I think to myself that it is strange + //because of that, I replace it with this color + val color = Color(134, 182, 246) + + toColor.color = color + toColor.width = 10f + + toColorSecond?.color = color + toColorSecond?.width = 10f + } else throw NoSuchElementException("WE LOST AN EDGE!!!") + } + } + + override fun colorEdges(vararg edges: EdgeViewModel, color: Color) { + for (edge in edges) { + edge.color = color + } + } + + override fun distanceRank(vertices: Collection>, max: Double, min: Double) { + for(vertex in vertices){ + val vImp = vertex.importance + if(vImp <= ( min + ( (max - min) / 4) ) ) vertex.color = Color(0xFFFF0000) // КРАСНЫЙ / ROT + else if(vImp <= ( min + 2 * ( (max - min) / 4) ) ) vertex.color = Color(0xFF0000FF) // СИНИЙ / BLAU + else if(vImp <= ( min + 3 * ( (max - min) / 4) ) ) vertex.color = Color(0xFF800080) //ФИОЛЕТОВЫЙ + else vertex.color = Color(0xFF00FF00) // ЗЕЛЁНЫЙ / GRÜN + } + } + + override fun findCycles(vertices: Collection>, cycle: List>, color: Color) { + for(vertex in cycle) { + val ver = vertices.find { it.value == vertex } + ver?.color = color + } + } + + override fun highlightSCC(scc: Set>>, vararg vertices: VertexViewModel) { + for (component in scc) { + println(component) + val array = Array(256) { it } + val color = Color(array.random(), array.random(), array.random()) + + for (vertex in vertices) { + if (vertex.value in component) vertex.color = color + } + } + } + + override fun highlightMinSpanTree(minSpanTree: Set>, vararg edges: EdgeViewModel) { + val color = Color.Blue + for (edge in minSpanTree) { + val u1 = edge.from + val v1 = edge.to + + for (edgeVM in edges) { + val u2 = edgeVM.u.value + val v2 = edgeVM.v.value + + if ((u1 == u2) && (v1 == v2) || ((u1 == v2) && (v1 == u2))) { + edgeVM.color = color + edgeVM.width = 6f + } + } + } + } + + override fun colorVertices(vararg vertices: VertexViewModel, color: Color) { + for(vertex in vertices){ + vertex.color = color + } + } + + private fun Pair.rotate(pivot: Pair, angle: Double): Pair { + val sin = sin(angle) + val cos = cos(angle) + + val diff = first - pivot.first to second - pivot.second + val rotated = Pair( + diff.first * cos - diff.second * sin, + diff.first * sin + diff.second * cos, + ) + return rotated.first + pivot.first to rotated.second + pivot.second + } +} diff --git a/src/main/kotlin/viewmodel/graphs/EdgeViewModel.kt b/src/main/kotlin/viewmodel/graphs/EdgeViewModel.kt new file mode 100644 index 0000000..8c3072d --- /dev/null +++ b/src/main/kotlin/viewmodel/graphs/EdgeViewModel.kt @@ -0,0 +1,31 @@ +package viewmodel.graphs + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color + +private const val DEFAULT_WIDTH = 6f + +class EdgeViewModel( + val u: VertexViewModel, + val v: VertexViewModel, + color: Color, + width: Float = DEFAULT_WIDTH, +) { + private var _width = mutableStateOf(width) + var width: Float + get() = _width.value + set(value) { + _width.value = value + } + + private var _color = mutableStateOf(color) + var color: Color + get() = _color.value + set(value) { + _color.value = value + } + + fun onScroll(scale: Float = 1f) { + width = DEFAULT_WIDTH * scale + } +} diff --git a/src/main/kotlin/viewmodel/graphs/GraphViewModel.kt b/src/main/kotlin/viewmodel/graphs/GraphViewModel.kt new file mode 100644 index 0000000..ded778d --- /dev/null +++ b/src/main/kotlin/viewmodel/graphs/GraphViewModel.kt @@ -0,0 +1,91 @@ +package viewmodel.graphs + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import model.graphs.Edge +import model.graphs.Graph +import model.graphs.Vertex + +const val MAX_SCALE = 4f +const val MIN_SCALE = 1f / 8 +const val DLT_SCALE = 1f / 16 + +class GraphViewModel>( + val graph: Graph, + showVerticesLabels: State, + showVerticesDistanceLabels: State, +) { + val graphSize = mutableStateOf(IntSize.Zero) + private var scale = 1f + + val onScroll: AwaitPointerEventScope.(event: PointerEvent) -> Unit = { + val position = it.changes.first().position + val scaleSign = it.changes.first().scrollDelta.y + val scaleDlt = scaleSign * DLT_SCALE + + val scaleResult = scale - scaleDlt + if (scaleResult in MIN_SCALE..MAX_SCALE) { + scale = scaleResult + + vertices.forEach { v -> v.onScroll(scaleDlt, position, scale) } + edges.forEach { e -> e.onScroll(scale) } + } + } + + fun onDrag(offset: Offset) { + vertices.forEach { v -> v.onDrag(offset) } + } + + var currentVertex: VertexViewModel? = null + private var biggestIndexCommunity = 0 + + private val _vertices = graph.vertices().associateWith { v -> + VertexViewModel(0.dp, 0.dp, v, showVerticesLabels, showVerticesDistanceLabels) + } + + private val _edges = graph.edges().associateWith { e -> + val fst = _vertices[e.from] + ?: error("VertexView for ${e.from} not found") + val snd = _vertices[e.to] + ?: error("VertexView for ${e.to} not found") + + EdgeViewModel(fst, snd, Color.Black, 4.toFloat()) + } + + // Color(78, 86, 129), + // Color(122, 91, 148), + // Color(173, 91, 151), + // Color(219, 91, 136), + // Color(252, 101, 107), + // Color(255, 129, 68), + + fun indexCommunities(communities: HashSet>>) { + var count = 1 + + val biggestCommunities = communities.sortedBy { it.size }.reversed() + + for (community in biggestCommunities) { + val color = Color((0..255).random(), (0..255).random(), (0..255).random()) + + for (vertex in community) { + _vertices[vertex]?.color = color + } + + count += 1 + } + + biggestIndexCommunity = biggestCommunities[0].size + } + + val vertices: Collection> + get() = _vertices.values + + val edges: Collection> + get() = _edges.values +} diff --git a/src/main/kotlin/viewmodel/graphs/RepresentationStrategy.kt b/src/main/kotlin/viewmodel/graphs/RepresentationStrategy.kt new file mode 100644 index 0000000..c3a247b --- /dev/null +++ b/src/main/kotlin/viewmodel/graphs/RepresentationStrategy.kt @@ -0,0 +1,17 @@ +package viewmodel.graphs + +import androidx.compose.ui.graphics.Color +import model.graphs.Edge +import model.graphs.Vertex + +interface RepresentationStrategy { + fun place(width: Double, height: Double, vertices: Collection>) + fun highlight(vertices: Collection>) + fun highlightBridges(edges: Collection>, bridges: Set>) + fun highlightSCC(scc: Set>>, vararg vertices: VertexViewModel) + fun highlightMinSpanTree(minSpanTree: Set>, vararg edges: EdgeViewModel) + fun colorVertices(vararg vertices: VertexViewModel, color: Color) + fun colorEdges(vararg edges: EdgeViewModel, color: Color) + fun distanceRank(vertices: Collection>, max: Double, min: Double) + fun findCycles(vertices: Collection>, cycle: List>, color: Color) +} diff --git a/src/main/kotlin/viewmodel/graphs/VertexViewModel.kt b/src/main/kotlin/viewmodel/graphs/VertexViewModel.kt new file mode 100644 index 0000000..80f9a03 --- /dev/null +++ b/src/main/kotlin/viewmodel/graphs/VertexViewModel.kt @@ -0,0 +1,77 @@ +package viewmodel.graphs + +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import model.graphs.Vertex + +private const val DEFAULT_RADIUS = 24f + +@Suppress("LongParameterList") +class VertexViewModel( + x: Dp = 0.dp, + y: Dp = 0.dp, + internal val value: Vertex, + private val keyLabelVisibility: State, + private val distanceLabelVisibility: State, + radius: Dp = DEFAULT_RADIUS.dp +) { + var isSelected by mutableStateOf(false) + var color by mutableStateOf(Color.Unspecified) + + private var _radius = mutableStateOf(radius) + var radius: Dp + get() = _radius.value + set(value) { + _radius.value = value + } + + private var _x = mutableStateOf(x) + var x: Dp + get() = _x.value + set(value) { + _x.value = value + } + + private var _y = mutableStateOf(y) + var y: Dp + get() = _y.value + set(value) { + _y.value = value + } + + val label + get() = value.key.toString() + + val isKeyLabelVisible + get() = keyLabelVisibility.value + + var distanceLabel: String = "" + + var importance: Double = 0.0 + + val isDistLabelVisible + get() = distanceLabelVisibility.value + + fun onDrag(offset: Offset) { + _x.value += offset.x.dp + _y.value += offset.y.dp + } + + //есть некоторая проблема с тем, что Dlt расстояния + //обладает степенной зависимостью от дистанции + //между вершиной и центром, + //в то время как radius вершин + //растёт пропорционально относительно scale. + //короче говоря, это выглядит не эстетично :( + fun onScroll(scaleDlt: Float, center: Offset, scale: Float = 1f) { + radius = DEFAULT_RADIUS.dp * scale + x += (center.x.dp - x) * scaleDlt + y += (center.y.dp - y) * scaleDlt + } +} diff --git a/src/main/kotlin/viewmodel/placement/ForceAtlas2Placement.kt b/src/main/kotlin/viewmodel/placement/ForceAtlas2Placement.kt new file mode 100644 index 0000000..d605515 --- /dev/null +++ b/src/main/kotlin/viewmodel/placement/ForceAtlas2Placement.kt @@ -0,0 +1,168 @@ +package viewmodel.placement + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.center +import androidx.compose.ui.unit.dp +import model.graphs.Edge +import viewmodel.graphs.GraphViewModel +import viewmodel.graphs.VertexViewModel +import kotlin.math.log2 +import kotlin.math.sqrt + +class ForceAtlas2Placement>(graphVM: GraphViewModel) { + private val vertices = graphVM.vertices + private val edges = graphVM.edges + private val graph = graphVM.graph + + private val center = graphVM.graphSize.value.center + + fun place( + amount: Int = 1, + kGrav: Float = 1f, + kRep: Float = 1f, + ) { + repeat(amount) { + val placement = mutableSetOf>() + + for (u in vertices) { + val forceAtlas2Vertex = ForceAtlas2VertexLayout(u) + + for (v in vertices) { + if (u != v) { + val repulseForce = applyRepForce(u, v, kRep) + val attractionForce = applyAttForce(u, v) + + forceAtlas2Vertex.addForces(repulseForce, attractionForce) + } + println() + } + + val gravityForce = applyGravForce(u, kGrav) + forceAtlas2Vertex.addForces(gravityForce) + placement.add(forceAtlas2Vertex) + } + + placement.forEach { it.applyForces() } + } + } + + private fun findAttForce( + u: VertexViewModel, + v: VertexViewModel, + isLinLog: Boolean = true, + considerOverlapping: Boolean = true + ): Float { + if (!graph.areConnected(u.value, v.value)) return 0f + + val distance = findDistance(u, v, considerOverlapping) + val force = when { + distance < 0 -> 0f + isLinLog -> log2(1f + distance) + else -> distance + } + + return force + } + + private fun applyAttForce( + u: VertexViewModel, + v: VertexViewModel, + isLinLog: Boolean = true, + considerOverlapping: Boolean = true + ): Pair { + val force = findAttForce(u, v, isLinLog, considerOverlapping) + val dest = Pair(v.x, v.y) + + return applyForce(u, dest, force) + } + + private fun findGravForce( + v: VertexViewModel, + kGrav: Float, + ): Float { + val force = findVertexMass(v) + + return kGrav * force + } + + private fun applyGravForce( + v: VertexViewModel, + kGrav: Float, + ): Pair { + val force = findGravForce(v, kGrav) + val xCenter = center.x.dp + val yCenter = center.y.dp + + return applyForce(v, Pair(xCenter, yCenter), force) + } + + private fun applyForce( + v: VertexViewModel, + dest: Pair, + force: Float, + isNegative: Boolean = false, + ): Pair { + val xDest = dest.first + val yDest = dest.second + + val xDlt = if (isNegative) -(xDest - v.x) * force else (xDest - v.x) * force + val yDlt = if (isNegative) -(yDest - v.y) * force else (yDest - v.y) * force + + return Pair(xDlt.value, yDlt.value) + } + + private fun findRepForce( + u: VertexViewModel, + v: VertexViewModel, + kRep: Float, + ): Float { + val uMass = findVertexMass(u) + val vMass = findVertexMass(v) + val distance = findDistance(u, v) + + val force = if (distance > 0f) uMass * vMass / distance else uMass * vMass + + return kRep * force + } + + private fun applyRepForce( + u: VertexViewModel, + v: VertexViewModel, + kRep: Float, + ): Pair { + val force = findRepForce(u, v, kRep) + val destination = Pair(v.x, v.y) + + return applyForce(u, destination, force, isNegative = true) + } + + private fun findDistance( + u: VertexViewModel, + v: VertexViewModel, + considerOverlapping: Boolean = true + ): Float { + val xDist = (v.x - u.x).value + val yDist = (v.y - u.y).value + val distance = sqrt(xDist * xDist + yDist * yDist) + + return if (considerOverlapping) { + val uSize = u.radius.value + val vSize = v.radius.value + val distanceWithoutOverlapping = distance - uSize - vSize + + distanceWithoutOverlapping //can be negative! + } else { + distance + } + } + + private fun findVertexMass(vertex: VertexViewModel): Float { + var mass = 1f + + for (edge in edges) { + if (vertex == edge.u) mass++ + } + + return mass + } +} diff --git a/src/main/kotlin/viewmodel/placement/ForceAtlas2VertexLayout.kt b/src/main/kotlin/viewmodel/placement/ForceAtlas2VertexLayout.kt new file mode 100644 index 0000000..6c73e86 --- /dev/null +++ b/src/main/kotlin/viewmodel/placement/ForceAtlas2VertexLayout.kt @@ -0,0 +1,22 @@ +package viewmodel.placement + +import androidx.compose.ui.unit.dp +import viewmodel.graphs.VertexViewModel + +data class ForceAtlas2VertexLayout( + val vertex: VertexViewModel, + var dltX: Float = 0f, + var dltY: Float = 0f, +) { + fun addForces(vararg forces: Pair) { + for (force in forces) { + dltX += force.first + dltY += force.second + } + } + + fun applyForces() { + vertex.x += dltX.dp + vertex.y += dltY.dp + } +} diff --git a/src/main/kotlin/viewmodel/screens/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/screens/MainScreenViewModel.kt new file mode 100644 index 0000000..106e5f4 --- /dev/null +++ b/src/main/kotlin/viewmodel/screens/MainScreenViewModel.kt @@ -0,0 +1,210 @@ +package viewmodel.screens + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.delay +import model.functionality.iograph.GraphType +import model.functionality.iograph.ReadWriteIntGraph +import model.graphs.* +import viewmodel.graphs.GraphViewModel +import viewmodel.graphs.RepresentationStrategy +import viewmodel.placement.ForceAtlas2Placement +import kotlin.system.exitProcess + +const val WIDTH = 800.0 +const val HEIGHT = 600.0 + +class MainScreenViewModel>( + var graph: Graph, + private val representationStrategy: RepresentationStrategy, + private val currentGraph: MutableState?>, + private val darkTheme: MutableState +) { + val showVerticesLabels = mutableStateOf(false) + val showVerticesDistanceLabels = mutableStateOf(false) + val showEdgesLabels = mutableStateOf(false) + var showDropdownMenu = mutableStateOf(false) + val showChooseGraphTypeDialog = mutableStateOf(false) + val showOpenExistingGraphDialog = mutableStateOf(false) + val toStartingScreen = mutableStateOf(false) + var graphViewModel = GraphViewModel(graph, showVerticesLabels, showVerticesDistanceLabels) + var resolutionInput = mutableStateOf("") + var randomnessInput = mutableStateOf("") + + init { + representationStrategy.place(WIDTH, HEIGHT, graphViewModel.vertices) + } + + fun setResolution(value: String) { + if (value.toDoubleOrNull() != null || value.isEmpty()) { + resolutionInput.value = value + } + } + + fun setRandomness(value: String) { + if (value.toDoubleOrNull() != null || value.isEmpty()) { + randomnessInput.value = value + } + } + + fun openChooseDialog() { + showChooseGraphTypeDialog.value = true + closeOpenDialog() //openDialog -> chooseDialog + } + + fun openOpenDialog() { + showOpenExistingGraphDialog.value = true + } + + fun closeChooseDialog() { + showChooseGraphTypeDialog.value = false + } + + fun closeOpenDialog() { + showOpenExistingGraphDialog.value = false + } + + fun showMenu() { + showDropdownMenu.value = true + } + + fun closeMenu() { + showDropdownMenu.value = false + } + + fun changeTheme() { + darkTheme.value = !darkTheme.value + } + + fun openToStartingScreenDialog() { + toStartingScreen.value = true + } + + fun closeToStartingScreenDialog() { + toStartingScreen.value = false + } + + fun openGraph(type: GraphType) { + val graph = ReadWriteIntGraph().openGraph(type) + + currentGraph.value = graph + } + + fun saveGraph() { + ReadWriteIntGraph().saveGraph(graph) + } + + fun highlightSCC() { + require(graph is GraphDirected) { "Unexpected graph type: ${graph::class.simpleName}." } + val scc = (graph as GraphDirected).findSCC() + + representationStrategy.highlightSCC(scc, *graphViewModel.vertices.toTypedArray()) + } + + fun highlightMinSpanTree() { + require(graph is GraphUndirected) { "Unexpected graph type: ${graph::class.simpleName}." } + val minSpanTree = (graph as GraphUndirected).findMinSpanTree() ?: return + + representationStrategy.highlightMinSpanTree(minSpanTree, *graphViewModel.edges.toTypedArray()) + } + + fun closeApp() { + exitProcess(0) + } + + fun showBridges() { + require(graph is GraphUndirected) { "Unexpected graph type: ${graph::class.simpleName}." } + val bridges = (graph as GraphUndirected).findBridges() + + representationStrategy.highlightBridges(graphViewModel.edges, bridges) + } + + fun findCommunities() { + require(graph is GraphUndirected) { "leiden method does not support directed graphs." } + //need to make it more informative for user + val randomness = randomnessInput.value.toDoubleOrNull() ?: return + val resolution = resolutionInput.value.toDoubleOrNull() ?: return + val communities = (graph as GraphUndirected).runLeidenMethod(randomness, resolution) + + graphViewModel.indexCommunities(communities) + } + + fun useForceAtlas2Layout() { + ForceAtlas2Placement(graphViewModel).place() + } + + fun findDistanceBellman() { + if (graph is GraphWeighted) { + val labels = + graphViewModel.currentVertex?.let { (graph as GraphWeighted).findDistancesBellman(it.value) } + + graphViewModel.vertices.forEach { + it.distanceLabel = (labels?.get(it.value)).toString() + } + } + } + + + fun findDistanceDijkstra() { + if (graph is GraphWeighted) { + graphViewModel.edges.forEach{ + require(true) //Here must be checking weights for being >= 0, but there's a problem + } + + val labels = + graphViewModel.currentVertex?.let { (graph as GraphWeighted).findDistancesDijkstra(it.value)} + + graphViewModel.vertices.forEach { + it.distanceLabel = (labels?.get(it.value)).toString() + } + } + } + + fun distanceRank() { + if(graph is DirectedGraph) { + val importances = (graphViewModel.graph as DirectedGraph).distanceRank() + + graphViewModel.vertices.forEach { + it.importance = (importances.get(it.value)) ?: 0.0 + } + + var min: Double = Double.POSITIVE_INFINITY + var max: Double = Double.NEGATIVE_INFINITY + + for((vertex, imp) in importances) { + if(imp > max) max = imp + else if(imp < min) min = imp + } + + representationStrategy.distanceRank(graphViewModel.vertices, max = max, min = min) + } + } + + suspend fun findCycles() { + if (graph is GraphWeighted) { + val Cycles = + graphViewModel.currentVertex?.let { (graph as GraphDirected).findCycles(it.value) } + + Cycles?.forEach { cycle -> + /*for(vertex in cycle) { + val ver = graphViewModel.vertices.find { it.value == vertex } + ver?.color = Color(0xFFFF0000) + }*/ + representationStrategy.findCycles(graphViewModel.vertices, cycle, Color(0xFFFF0000)) + delay(2000) + representationStrategy.findCycles(graphViewModel.vertices, cycle, Color(0xFF0000FF)) + /*for(vertex in cycle) { + val ver = graphViewModel.vertices.find { it.value == vertex } + ver?.color = Color(0xFF0000FF) //Change to defolt + }*/ + + } + } + + } + + fun toStartingScreen() { + currentGraph.value = null + } +} diff --git a/src/main/kotlin/viewmodel/screens/StartingScreenViewModel.kt b/src/main/kotlin/viewmodel/screens/StartingScreenViewModel.kt new file mode 100644 index 0000000..a5fa27c --- /dev/null +++ b/src/main/kotlin/viewmodel/screens/StartingScreenViewModel.kt @@ -0,0 +1,76 @@ +package viewmodel.screens + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import model.functionality.iograph.GraphType +import model.functionality.iograph.ReadWriteIntGraph +import model.graphs.Graph +import model.graphs.UndirectedWeightedGraph +import model.graphs.Vertex +import model.graphs.WeightedEdge +import kotlin.random.Random +import kotlin.system.exitProcess + +class StartingScreenViewModel( + private val currentGraph: MutableState?> +) { + val showCreateNewGraphDialog = mutableStateOf(false) + val showChooseGraphTypeDialog = mutableStateOf(false) + val showOpenExistingGraphDialog = mutableStateOf(false) + + fun openCreateDialog() { + showCreateNewGraphDialog.value = true + } + + fun openChooseDialog() { + showChooseGraphTypeDialog.value = true + closeOpenDialog() //openDialog -> chooseDialog + } + + fun openOpenDialog() { + showOpenExistingGraphDialog.value = true + } + + fun closeCreateDialog() { + showCreateNewGraphDialog.value = false + } + + fun closeChooseDialog() { + showChooseGraphTypeDialog.value = false + } + + fun closeOpenDialog() { + showOpenExistingGraphDialog.value = false + } + + fun createGraph() { + val randomGraph = UndirectedWeightedGraph() + val amount = Random.nextInt(2, 64) + val degree = Random.nextInt(1, amount * 2) + + for (vertex in 1..amount) { + randomGraph.addVertex(vertex) + } + + repeat(degree) { + val u = Vertex(Random.nextInt(amount)) + val v = Vertex(Random.nextInt(amount)) + + if (randomGraph.contains(u) && randomGraph.contains(v)) { + randomGraph.addEdge(WeightedEdge(u, v, Random.nextDouble(64.0))) + } + } + + currentGraph.value = randomGraph + } + + fun openGraph(type: GraphType) { + val graph = ReadWriteIntGraph().openGraph(type) + + currentGraph.value = graph + } + + fun closeApp() { + exitProcess(0) + } +} diff --git a/src/test/kotlin/functionalityTest/BridgeFinderTest.kt b/src/test/kotlin/functionalityTest/BridgeFinderTest.kt new file mode 100644 index 0000000..58088ea --- /dev/null +++ b/src/test/kotlin/functionalityTest/BridgeFinderTest.kt @@ -0,0 +1,182 @@ +package functionalityTest + +import model.graphs.UndirectedGraph +import model.graphs.UndirectedWeightedGraph +import model.graphs.UnweightedEdge +import model.graphs.WeightedEdge +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import kotlin.test.Test +import kotlin.test.assertEquals + + +class BridgeFinderTest { + @Nested + inner class NotWeightedGraphs { + private var graphInt = UndirectedGraph() + + @Test + @DisplayName("Every edge is a bridge in a Simple Linear Chain") + // 1 - 2 - 3 + fun findBridgesInChain() { + for (i in 1..3) { + graphInt.addVertex(i) + } + + val nodes = arrayListOf(graphInt.adjList.keys.toList()) + + graphInt.addEdge(nodes[0][0], nodes[0][1]) + graphInt.addEdge(nodes[0][1], nodes[0][2]) + + val answer = setOf( + UnweightedEdge(nodes[0][1], nodes[0][2]), + UnweightedEdge(nodes[0][0], nodes[0][1]) + ) + + assertEquals(answer, graphInt.findBridges()) + } + + @Test + @DisplayName("No bridges found in a cycle graph") + // 1---2 + // | X | + // 4---3 + fun findNoBridgesInSquareWithDiagonal() { + for (i in 1..4) { + graphInt.addVertex(i) + } + + val nodes = arrayListOf(graphInt.adjList.keys.toList()) + + graphInt.addEdge(nodes[0][0], nodes[0][1]) + graphInt.addEdge(nodes[0][1], nodes[0][2]) + graphInt.addEdge(nodes[0][2], nodes[0][3]) + graphInt.addEdge(nodes[0][3], nodes[0][0]) + graphInt.addEdge(nodes[0][0], nodes[0][2]) + graphInt.addEdge(nodes[0][1], nodes[0][3]) + + assertEquals(emptySet(), graphInt.findBridges()) + } + + @Test + @DisplayName("Find bridges in a graphs with multiple components") + // Component 1: Component 2: + // 1 5 - 6 + // / \ + // 2---3 + // \ + // 4 + fun findBridgesWithMultipleComponents() { + for (i in 1..6) { + graphInt.addVertex(i) + } + + val nodes = arrayListOf(graphInt.adjList.keys.toList()) + + graphInt.addEdge(nodes[0][0], nodes[0][1]) + graphInt.addEdge(nodes[0][1], nodes[0][2]) + graphInt.addEdge(nodes[0][2], nodes[0][0]) + graphInt.addEdge(nodes[0][2], nodes[0][3]) + graphInt.addEdge(nodes[0][4], nodes[0][5]) + + val answer = setOf( + UnweightedEdge(nodes[0][4], nodes[0][5]), + UnweightedEdge(nodes[0][2], nodes[0][3]) + ) + + assertEquals(answer, graphInt.findBridges()) + } + + @Test + @DisplayName("Find bridges in a graph with nested cycle and bridges") + //1 - 2 - 3 - 4 - 5 - 6 + //| | | + //7 - 8 - 9 10 + fun findBridgesWithNestedCycle() { + for (i in 1..10) { + graphInt.addVertex(i) + } + + val nodes = arrayListOf(graphInt.adjList.keys.toList()) + + graphInt.addEdge(nodes[0][0], nodes[0][1]) + graphInt.addEdge(nodes[0][1], nodes[0][2]) + graphInt.addEdge(nodes[0][2], nodes[0][3]) + graphInt.addEdge(nodes[0][0], nodes[0][6]) + graphInt.addEdge(nodes[0][6], nodes[0][7]) + graphInt.addEdge(nodes[0][7], nodes[0][8]) + graphInt.addEdge(nodes[0][2], nodes[0][8]) + graphInt.addEdge(nodes[0][3], nodes[0][4]) + graphInt.addEdge(nodes[0][4], nodes[0][5]) + graphInt.addEdge(nodes[0][4], nodes[0][9]) + + val answer = setOf( + UnweightedEdge(nodes[0][4], nodes[0][9]), + UnweightedEdge(nodes[0][4], nodes[0][5]), + UnweightedEdge(nodes[0][3], nodes[0][4]), + UnweightedEdge(nodes[0][2], nodes[0][3]) + ) + + assertEquals(answer, graphInt.findBridges()) + } + + @Test + @DisplayName("No bridges in an empty graph") + //⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⢀⣴⠟⠋⠉⠉⠙⢦ + //⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡾⠁⠀⠀⠀⣀⠀⠀⢻ + //⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⡞⠁ + //⠀⠀⠀ᵐᵉᵒʷ⠀⠀⠀⠀⠀⢷⡀⠀ ⠈⣧ + //⠀⠀⠀⠀♡⠀⠀⠀⠀⠀⠀⠈⠳⣄⠀ ⠙⢷⣄ + //⠀⠀⠀⠀⠀⠀⢀⣦⠀⠀⠀⠀⠀⠈⠑⠀⣀⣭⣷⣦⣤⣀ + //⠀⠀⠀⠀⠀⠀⣘⠁⠡⠀⠀⠀⠀⠀⠠⠚⠁⠀⠀⠀⠀⠹⣧ + //⠀⠀⠀⠀⠀⠀⠛⠀⠀⠐⠒⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡇ + //⠀⠀⠀⠀⡐⠁⢴⡄⠀⠀⠀⠀⠐⠈⠉⡽⠂⠀⠀⠀⠀⠀⢸⡇ + //⠀⠀⠀⠀⡇⠀⠀⣤⠀⠶⡦⠀⠀⠴⠚⠀⠀⠀⠀⠀⠀⠀⣸⠇ + //⠀⠀⠀⠀⡼⠂⠀⠒⠀⠀⠀⠀⢠⠇⠀⠀⠀⠀⠀⢀⠀⣠⠏ + //⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠘⠁⠀⠀⠀⠀⠀⢀⣎⠴⠋ + //⠀⠀⠀⠘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠋ + //⠀⠀⠀⠀⠘⠋⢠⡤⠂⠐⠀⠤⠤⠴⠚⠁⠀⠀ + // the cat ate graph + fun findNoBridgesInEmptyGraph() { + assertEquals(emptySet(), graphInt.findBridges()) + } + } + + @Nested + inner class WeightedGraphs { + private val graphInt = UndirectedWeightedGraph() + + @Test + @DisplayName("Algorithm runs on weighted graphs.") + // 1 - 2 - 3 - 4 - 5 - 6 + // | | | + // 7 - 8 - 9 10 + fun findBridgesWithNestedCycle() { + for (i in 1..10) { + graphInt.addVertex(i) + } + + val nodes = arrayListOf(graphInt.adjList.keys.toList()) + + graphInt.addEdge(nodes[0][0], nodes[0][1], 78.0) + graphInt.addEdge(nodes[0][1], nodes[0][2], 78.0) + graphInt.addEdge(nodes[0][2], nodes[0][3], 78.0) + graphInt.addEdge(nodes[0][0], nodes[0][6], 78.0) + graphInt.addEdge(nodes[0][6], nodes[0][7], 78.0) + graphInt.addEdge(nodes[0][7], nodes[0][8], 78.0) + graphInt.addEdge(nodes[0][2], nodes[0][8], 78.0) + graphInt.addEdge(nodes[0][3], nodes[0][4], 78.0) + graphInt.addEdge(nodes[0][4], nodes[0][5], 78.0) + graphInt.addEdge(nodes[0][4], nodes[0][9], 78.0) + + val answer = setOf( + WeightedEdge(nodes[0][4], nodes[0][9], 78.0), + WeightedEdge(nodes[0][4], nodes[0][5], 78.0), + WeightedEdge(nodes[0][3], nodes[0][4], 78.0), + WeightedEdge(nodes[0][2], nodes[0][3], 78.0) + ) + + assertEquals(answer, graphInt.findBridges()) + } + } +} diff --git a/src/test/kotlin/functionalityTest/CommunityDetectorTest.kt b/src/test/kotlin/functionalityTest/CommunityDetectorTest.kt new file mode 100644 index 0000000..63a06e2 --- /dev/null +++ b/src/test/kotlin/functionalityTest/CommunityDetectorTest.kt @@ -0,0 +1,378 @@ +package functionalityTest + +import model.functionality.CommunityDetector +import model.graphs.UndirectedGraph +import model.graphs.UnweightedEdge +import model.graphs.Vertex +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class CommunityDetectorTest { + + @Nested + inner class PrivateMethodsTest { + private val sampleGraph = UndirectedGraph().apply { + for (i in 1..34) { + addVertex(i) + } + + val nodes = arrayListOf(adjList.keys.toList()) + + addEdge(nodes[0][1], nodes[0][0]) + addEdge(nodes[0][2], nodes[0][0]) + addEdge(nodes[0][2], nodes[0][1]) + addEdge(nodes[0][3], nodes[0][0]) + addEdge(nodes[0][3], nodes[0][1]) + addEdge(nodes[0][3], nodes[0][2]) + addEdge(nodes[0][4], nodes[0][0]) + addEdge(nodes[0][5], nodes[0][0]) + addEdge(nodes[0][6], nodes[0][0]) + addEdge(nodes[0][6], nodes[0][4]) + addEdge(nodes[0][6], nodes[0][5]) + addEdge(nodes[0][7], nodes[0][0]) + addEdge(nodes[0][7], nodes[0][1]) + addEdge(nodes[0][7], nodes[0][2]) + addEdge(nodes[0][7], nodes[0][3]) + addEdge(nodes[0][8], nodes[0][1]) + addEdge(nodes[0][8], nodes[0][2]) + addEdge(nodes[0][9], nodes[0][2]) + addEdge(nodes[0][10], nodes[0][0]) + addEdge(nodes[0][10], nodes[0][4]) + addEdge(nodes[0][10], nodes[0][5]) + addEdge(nodes[0][11], nodes[0][0]) + addEdge(nodes[0][12], nodes[0][0]) + addEdge(nodes[0][12], nodes[0][3]) + addEdge(nodes[0][13], nodes[0][0]) + addEdge(nodes[0][13], nodes[0][1]) + addEdge(nodes[0][13], nodes[0][2]) + addEdge(nodes[0][13], nodes[0][3]) + addEdge(nodes[0][16], nodes[0][5]) + addEdge(nodes[0][16], nodes[0][6]) + addEdge(nodes[0][17], nodes[0][0]) + addEdge(nodes[0][17], nodes[0][1]) + addEdge(nodes[0][19], nodes[0][0]) + addEdge(nodes[0][19], nodes[0][1]) + addEdge(nodes[0][21], nodes[0][0]) + addEdge(nodes[0][21], nodes[0][1]) + addEdge(nodes[0][25], nodes[0][23]) + addEdge(nodes[0][25], nodes[0][24]) + addEdge(nodes[0][27], nodes[0][2]) + addEdge(nodes[0][27], nodes[0][23]) + addEdge(nodes[0][27], nodes[0][24]) + addEdge(nodes[0][28], nodes[0][2]) + addEdge(nodes[0][29], nodes[0][23]) + addEdge(nodes[0][29], nodes[0][26]) + addEdge(nodes[0][30], nodes[0][1]) + addEdge(nodes[0][30], nodes[0][8]) + addEdge(nodes[0][31], nodes[0][0]) + addEdge(nodes[0][31], nodes[0][24]) + addEdge(nodes[0][31], nodes[0][25]) + addEdge(nodes[0][31], nodes[0][28]) + addEdge(nodes[0][32], nodes[0][2]) + addEdge(nodes[0][32], nodes[0][8]) + addEdge(nodes[0][32], nodes[0][14]) + addEdge(nodes[0][32], nodes[0][15]) + addEdge(nodes[0][32], nodes[0][18]) + addEdge(nodes[0][32], nodes[0][20]) + addEdge(nodes[0][32], nodes[0][22]) + addEdge(nodes[0][32], nodes[0][23]) + addEdge(nodes[0][32], nodes[0][29]) + addEdge(nodes[0][32], nodes[0][30]) + addEdge(nodes[0][32], nodes[0][31]) + addEdge(nodes[0][33], nodes[0][8]) + addEdge(nodes[0][33], nodes[0][9]) + addEdge(nodes[0][33], nodes[0][13]) + addEdge(nodes[0][33], nodes[0][14]) + addEdge(nodes[0][33], nodes[0][15]) + addEdge(nodes[0][33], nodes[0][18]) + addEdge(nodes[0][33], nodes[0][19]) + addEdge(nodes[0][33], nodes[0][20]) + addEdge(nodes[0][33], nodes[0][22]) + addEdge(nodes[0][33], nodes[0][23]) + addEdge(nodes[0][33], nodes[0][26]) + addEdge(nodes[0][33], nodes[0][27]) + addEdge(nodes[0][33], nodes[0][28]) + addEdge(nodes[0][33], nodes[0][29]) + addEdge(nodes[0][33], nodes[0][30]) + addEdge(nodes[0][33], nodes[0][31]) + addEdge(nodes[0][33], nodes[0][32]) + } + + private val nodes = sampleGraph.adjList.keys.toList() + + @Test + @DisplayName("Each node gets assigned to its own community") + // { {node_1}, {node_2} ... {node_n} } + fun testInitPartition1() { + val expected: HashSet>> = hashSetOf() + for (i in 0..33) { + expected.add(hashSetOf(nodes[i])) + } + + assertEquals(expected, CommunityDetector(sampleGraph, 1.0, 0.001).initPartition(sampleGraph)) + } + + @Test + @DisplayName("Communities become nodes in aggregate graph") + fun aggregateGraphTest1() { + val partition: HashSet>> = hashSetOf(hashSetOf(), hashSetOf()) + for (i in 0..33) { + if (i <= 16) { + partition.first().add(nodes[i]) + } else { + if (partition.size == 1) { + partition.add(hashSetOf()) + } + + partition.last().add(nodes[i]) + } + } + + val aggregatedGraph = CommunityDetector(sampleGraph, 1.0, 0.001).aggregateGraph(sampleGraph, partition) + val expectedVertices = listOf(Vertex(partition.first()), Vertex(partition.last())) + val expectedEdges: HashSet>>> = hashSetOf() + + expectedEdges.add(UnweightedEdge(expectedVertices[0], expectedVertices[0])) + expectedEdges.add(UnweightedEdge(expectedVertices[0], expectedVertices[1])) + expectedEdges.add(UnweightedEdge(expectedVertices[1], expectedVertices[0])) + expectedEdges.add(UnweightedEdge(expectedVertices[1], expectedVertices[1])) + expectedEdges.forEach { it.copies = 20 } + expectedEdges.first().copies = 119 + expectedEdges.last().copies = 40 + + assertEquals(expectedVertices, aggregatedGraph.vertices().toList() as List<*>) + assertEquals(expectedEdges, aggregatedGraph.edges() as HashSet<*>) + } + + @Test + @DisplayName("flatVertex(vertex) will unpack nested vertices properly") + /// ___________ vertex ___________ + /// / | \ + /// ([1, 2, 3], [4, 5]) ([6], [7, 8]) ([9, 10]) + fun flatVertexTest1() { + val vertex = Vertex( + hashSetOf( + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(1), Vertex(2), Vertex(3))), + Vertex(hashSetOf(Vertex(4), Vertex(5))) + ) + ), + + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(6))), + Vertex(hashSetOf(Vertex(7), Vertex(8))) + ) + ), + + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(9), Vertex(10))) + ) + ), + ) + ) + + val expected = hashSetOf( + Vertex(1), + Vertex(2), + Vertex(3), + Vertex(4), + Vertex(5), + Vertex(6), + Vertex(7), + Vertex(8), + Vertex(9), + Vertex(10), + ) + + assertEquals(expected, CommunityDetector(sampleGraph, 1.0, 0.001).flatVertex(vertex)) + } + + @Test + @DisplayName("flatVertex(vertex) will return hashSet of given vertex if it's key is not a collection") + fun flatVertexTest2() { + val vertex = Vertex(42) + + assertEquals(hashSetOf(vertex), CommunityDetector(sampleGraph, 1.0, 0.001).flatVertex(vertex)) + } + + @Test + @DisplayName("flatCommunity() unpacks vertices properly") + fun flatCommunityTest1() { + val vertex1 = Vertex( + hashSetOf( + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(1), Vertex(2), Vertex(3))), + Vertex(hashSetOf(Vertex(4), Vertex(5))) + ) + ), + + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(6))), + Vertex(hashSetOf(Vertex(7), Vertex(8))) + ) + ), + + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(9), Vertex(10))) + ) + ), + ) + ) + + val vertex2 = Vertex( + hashSetOf( + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(11), Vertex(12), Vertex(13))), + Vertex(hashSetOf(Vertex(14), Vertex(15))) + ) + ), + + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(16))), + Vertex(hashSetOf(Vertex(17), Vertex(18))) + ) + ), + + Vertex( + hashSetOf( + Vertex(hashSetOf(Vertex(19), Vertex(20))) + ) + ), + ) + ) + + val community = hashSetOf(vertex1, vertex2) + + val expected = hashSetOf( + Vertex(1), + Vertex(2), + Vertex(3), + Vertex(4), + Vertex(5), + Vertex(6), + Vertex(7), + Vertex(8), + Vertex(9), + Vertex(10), + Vertex(11), + Vertex(12), + Vertex(13), + Vertex(14), + Vertex(15), + Vertex(16), + Vertex(17), + Vertex(18), + Vertex(19), + Vertex(20), + ) + + assertEquals(expected, CommunityDetector(sampleGraph, 1.0, 0.001).flatCommunity(community)) + } + + @Test + @DisplayName("countEdges() counts edges inside the given community properly") + fun countEdgesTest1() { + val communty = hashSetOf( + nodes[0], + nodes[1], + nodes[2], + nodes[3] + ) + + assertEquals(6, CommunityDetector(sampleGraph, 1.0, 0.001).countEdges(sampleGraph, communty, communty)) + } + + @Test + @DisplayName("countEdges() counts neighbors of the given vertex inside a specific community properly") + fun countEdgesTest2() { + val communty = hashSetOf( + nodes[0], + nodes[1], + nodes[2], + nodes[3] + ) + + val vertex = nodes[0] + + assertEquals( + 3, + CommunityDetector(sampleGraph, 1.0, 0.001).countEdges( + sampleGraph, + hashSetOf(vertex), + communty.minus(vertex) + ) + ) + } + + @Test + @DisplayName("countEdges() handles multi-edges properly") + fun countEdgesTest3() { + val testGraph = UndirectedGraph() + testGraph.addVertex(1) + testGraph.addVertex(2) + + val community = testGraph.adjList.keys.toHashSet() + + for (i in 1..50) { + testGraph.addEdge(testGraph.adjList.keys.first(), testGraph.adjList.keys.last()) + } + + assertEquals(50, CommunityDetector(testGraph, 1.0, 0.001).countEdges(testGraph, community, community)) + } + + @Test + @DisplayName("flatten() will return set of sets of nodes - communities") + fun flattenTest() { + val graph: UndirectedGraph = UndirectedGraph() + val test_set = hashSetOf( + hashSetOf(Vertex(hashSetOf(Vertex(2), Vertex(3), Vertex(4)))), + hashSetOf(Vertex(hashSetOf(Vertex(1), Vertex(5), Vertex(6)))) + ) + + val output = CommunityDetector(graph, 1.0, 1.0).flatten(test_set) + val expected = hashSetOf( + hashSetOf(Vertex(2), Vertex(3), Vertex(4)), + hashSetOf(Vertex(1), Vertex(5), Vertex(6)) + ) + + assertEquals(expected, output) + } + + @Test + @DisplayName("Maintain the structure of a partition using vertices from an aggregated graph") + fun maintainPartitionTest() { + val testGraph = UndirectedGraph>>() + testGraph.addVertex(hashSetOf(Vertex(3))) + testGraph.addVertex(hashSetOf(Vertex(7), Vertex(9))) + testGraph.addVertex(hashSetOf(Vertex(2), Vertex(4))) + testGraph.addVertex(hashSetOf(Vertex(1))) + + val partition = listOf( + hashSetOf(Vertex(2), Vertex(4)), + hashSetOf(Vertex(3), Vertex(7), Vertex(9)), + hashSetOf(Vertex(1)) + ) + + val expectedPartition = hashSetOf( + hashSetOf(Vertex(hashSetOf(Vertex(2), Vertex(4)))), + hashSetOf(Vertex(hashSetOf(Vertex(3))), Vertex(hashSetOf(Vertex(7), Vertex(9)))), + hashSetOf(Vertex(hashSetOf(Vertex(1)))) + ) + + assertEquals( + expectedPartition, + CommunityDetector(testGraph, 1.0, 1.0).maintainPartition(partition, testGraph) + ) + } + } +} diff --git a/src/test/kotlin/functionalityTest/DijkstraTest.kt b/src/test/kotlin/functionalityTest/DijkstraTest.kt new file mode 100644 index 0000000..6b45c36 --- /dev/null +++ b/src/test/kotlin/functionalityTest/DijkstraTest.kt @@ -0,0 +1,133 @@ +package functionalityTest + +import model.functionality.ShortestPathFinder +import model.graphs.UndirectedWeightedGraph +import model.graphs.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class DijkstraTest { + private val graph = UndirectedWeightedGraph() + private var nodes: List> = emptyList() + + @Test + @DisplayName("All vertices are connected to each other") + fun graphEin() { + for (i in 0..5) { + graph.addVertex(i) + } + + nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[0], nodes[1], 3.0) + graph.addEdge(nodes[0], nodes[2], 4.0) + graph.addEdge(nodes[0], nodes[3], 8.0) + graph.addEdge(nodes[0], nodes[4], 2.0) + graph.addEdge(nodes[0], nodes[5], 5.0) + + graph.addEdge(nodes[1], nodes[2], 6.0) + graph.addEdge(nodes[1], nodes[3], 10.0) + graph.addEdge(nodes[1], nodes[4], 7.0) + graph.addEdge(nodes[1], nodes[5], 9.0) + + graph.addEdge(nodes[2], nodes[3], 5.0) + graph.addEdge(nodes[2], nodes[4], 3.0) + graph.addEdge(nodes[2], nodes[5], 2.0) + + graph.addEdge(nodes[3], nodes[4], 4.0) + graph.addEdge(nodes[3], nodes[5], 6.0) + + graph.addEdge(nodes[4], nodes[5], 1.0) + + val result_ein = graph.findDistancesDijkstra(nodes[1]) + assertEquals(9.0, result_ein[nodes[3]]) + assertEquals(5.0, result_ein[nodes[4]]) + + val result_zwei = graph.findDistancesDijkstra(nodes[2]) + assertEquals(4.0, result_zwei[nodes[0]]) + assertEquals(2.0, result_zwei[nodes[5]]) + assertEquals(6.0, result_zwei[nodes[1]]) + } + + @Test + @DisplayName("Only one edge for every vertex") + fun graphZwei() { + for (i in 1..10) { + graph.addVertex(i) + } + + nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[1-1], nodes[2-1], 3.0) + graph.addEdge(nodes[3-1], nodes[4-1], 7.0) + graph.addEdge(nodes[5-1], nodes[6-1], 1.0) + graph.addEdge(nodes[7-1], nodes[8-1], 2.0) + graph.addEdge(nodes[9-1], nodes[1-1], 4.0) + + val result = graph.findDistancesDijkstra(nodes[0]) + assertEquals(Double.POSITIVE_INFINITY, result[nodes[5]]) + assertEquals(Double.POSITIVE_INFINITY, result[nodes[7]]) + assertEquals(Double.POSITIVE_INFINITY, result[nodes[3]]) + assertEquals(3.0, result[nodes[1]]) + } + + @Test + @DisplayName("Just usual graph but also checking if vertex isn't connected to the whole graph") + fun graphDrei() { + for (i in 0..7) { + graph.addVertex(i) + } + + nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[1], nodes[2], 3.0) + graph.addEdge(nodes[1], nodes[3], 6.0) + graph.addEdge(nodes[2], nodes[3], 2.0) + graph.addEdge(nodes[2], nodes[4], 1.0) + graph.addEdge(nodes[3], nodes[5], 5.0) + graph.addEdge(nodes[4], nodes[5], 4.0) + graph.addEdge(nodes[4], nodes[6], 2.0) + graph.addEdge(nodes[5], nodes[7], 3.0) + graph.addEdge(nodes[6], nodes[7], 1.0) + graph.addEdge(nodes[6], nodes[3], 7.0) + + val result_ein = graph.findDistancesDijkstra(nodes[1]) + + assertEquals(4.0, result_ein[nodes[4]]) + assertEquals(6.0, result_ein[nodes[6]]) + assertEquals(8.0, result_ein[nodes[5]]) + + val result_sieben = graph.findDistancesDijkstra(nodes[7]) + + assertEquals(4.0, result_sieben[nodes[2]]) + assertEquals(6.0, result_sieben[nodes[3]]) + assertEquals(3.0, result_sieben[nodes[4]]) + + val result_null = graph.findDistancesDijkstra(nodes[0]) + + assertEquals(Double.POSITIVE_INFINITY, result_null[nodes[4]]) + assertEquals(Double.POSITIVE_INFINITY, result_null[nodes[7]]) + assertEquals(Double.POSITIVE_INFINITY, result_null[nodes[5]]) + } + + @Test + @DisplayName("Zero edges") + fun graphVier() { + for (i in 0..10) { + graph.addVertex(i) + } + + nodes = graph.adjList.keys.toList().sortedBy { it.key } + + val result_ein = graph.findDistancesDijkstra(nodes[0]) + assertEquals(Double.POSITIVE_INFINITY, result_ein[nodes[5]]) + assertEquals(Double.POSITIVE_INFINITY, result_ein[nodes[7]]) + assertEquals(Double.POSITIVE_INFINITY, result_ein[nodes[3]]) + + val result_zwei = graph.findDistancesDijkstra(nodes[4]) + assertEquals(Double.POSITIVE_INFINITY, result_ein[nodes[2]]) + assertEquals(Double.POSITIVE_INFINITY, result_ein[nodes[9]]) + assertEquals(Double.POSITIVE_INFINITY, result_ein[nodes[10]]) + } +} diff --git a/src/test/kotlin/functionalityTest/DistRankTest.kt b/src/test/kotlin/functionalityTest/DistRankTest.kt new file mode 100644 index 0000000..f35d04b --- /dev/null +++ b/src/test/kotlin/functionalityTest/DistRankTest.kt @@ -0,0 +1,104 @@ +package functionalityTest + + +import model.functionality.DistanceRank +import model.functionality.TarjanSCC +import model.graphs.DirectedGraph +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + + + +//This is impossible to check +//So the only choice is to run it, and see in console real result, putting wrong expected +//Because it's quite hard to make correct expected, it's too big and there's no need in this + + + + +//class DistRankTest { +// private val graph = DirectedGraph() +// +// @Test +// fun test() { +// for (i in 0..39) { +// graph.addVertex(i) +// } +// +// val nodes = graph.adjList.keys.toList().sortedBy { it.key } +// +// /*graph.addEdge(nodes[1], nodes[2]) +// graph.addEdge(nodes[1], nodes[3]) +// graph.addEdge(nodes[2], nodes[3]) +// graph.addEdge(nodes[2], nodes[4]) +// graph.addEdge(nodes[3], nodes[5]) +// graph.addEdge(nodes[4], nodes[5]) +// graph.addEdge(nodes[4], nodes[6]) +// graph.addEdge(nodes[5], nodes[7]) +// graph.addEdge(nodes[6], nodes[7]) +// graph.addEdge(nodes[6], nodes[3])*/ +// +// /*graph.addEdge(nodes[0], nodes[1]) +// graph.addEdge(nodes[0], nodes[2]) +// graph.addEdge(nodes[0], nodes[3]) +// graph.addEdge(nodes[1], nodes[4]) +// graph.addEdge(nodes[2], nodes[4]) +// graph.addEdge(nodes[3], nodes[4]) +// graph.addEdge(nodes[4], nodes[0])*/ +// +// graph.addEdge(nodes[0], nodes[1]) +// graph.addEdge(nodes[0], nodes[2]) +// graph.addEdge(nodes[0], nodes[3]) +// graph.addEdge(nodes[1], nodes[4]) +// graph.addEdge(nodes[1], nodes[5]) +// graph.addEdge(nodes[2], nodes[5]) +// graph.addEdge(nodes[2], nodes[6]) +// graph.addEdge(nodes[3], nodes[6]) +// graph.addEdge(nodes[3], nodes[7]) +// graph.addEdge(nodes[4], nodes[8]) +// graph.addEdge(nodes[4], nodes[9]) +// graph.addEdge(nodes[5], nodes[8]) +// graph.addEdge(nodes[5], nodes[10]) +// graph.addEdge(nodes[6], nodes[9]) +// graph.addEdge(nodes[6], nodes[10]) +// graph.addEdge(nodes[7], nodes[11]) +// graph.addEdge(nodes[8], nodes[12]) +// graph.addEdge(nodes[9], nodes[12]) +// graph.addEdge(nodes[10], nodes[13]) +// graph.addEdge(nodes[11], nodes[14]) +// graph.addEdge(nodes[12], nodes[15]) +// graph.addEdge(nodes[13], nodes[16]) +// graph.addEdge(nodes[14], nodes[17]) +// graph.addEdge(nodes[15], nodes[18]) +// graph.addEdge(nodes[16], nodes[19]) +// graph.addEdge(nodes[17], nodes[19]) +// graph.addEdge(nodes[18], nodes[19]) +// graph.addEdge(nodes[19], nodes[20]) +// graph.addEdge(nodes[19], nodes[21]) +// graph.addEdge(nodes[19], nodes[22]) +// graph.addEdge(nodes[20], nodes[23]) +// graph.addEdge(nodes[20], nodes[24]) +// graph.addEdge(nodes[21], nodes[24]) +// graph.addEdge(nodes[21], nodes[25]) +// graph.addEdge(nodes[22], nodes[26]) +// graph.addEdge(nodes[22], nodes[27]) +// graph.addEdge(nodes[23], nodes[28]) +// graph.addEdge(nodes[24], nodes[29]) +// graph.addEdge(nodes[24], nodes[30]) +// graph.addEdge(nodes[25], nodes[31]) +// graph.addEdge(nodes[26], nodes[32]) +// graph.addEdge(nodes[27], nodes[33]) +// graph.addEdge(nodes[28], nodes[34]) +// graph.addEdge(nodes[29], nodes[35]) +// graph.addEdge(nodes[30], nodes[36]) +// graph.addEdge(nodes[31], nodes[37]) +// graph.addEdge(nodes[32], nodes[38]) +// graph.addEdge(nodes[33], nodes[39]) +// graph.addEdge(nodes[39], nodes[0]) +// +// +// val result = DistanceRank(graph).rank() +// assertEquals(1, result) +// //assertEquals(1, TarjanSCC().findSCCs(graph)) +// } +//} \ No newline at end of file diff --git a/src/test/kotlin/functionalityTest/JohnsonAlgTest.kt b/src/test/kotlin/functionalityTest/JohnsonAlgTest.kt new file mode 100644 index 0000000..bb43f24 --- /dev/null +++ b/src/test/kotlin/functionalityTest/JohnsonAlgTest.kt @@ -0,0 +1,147 @@ +package functionalityTest + +import model.functionality.JohnsonAlg +import model.graphs.DirectedGraph +import model.graphs.Vertex +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class JohnsonAlgTest { + private val graph = DirectedGraph() + + @Test + fun findCyclesEin() { //Simple case(Graph 1) + for (i in 1..13) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[1-1], nodes[2-1]) + graph.addEdge(nodes[2-1], nodes[3-1]) + graph.addEdge(nodes[3-1], nodes[1-1]) + graph.addEdge(nodes[4-1], nodes[5-1]) + graph.addEdge(nodes[5-1], nodes[6-1]) + graph.addEdge(nodes[6-1], nodes[4-1]) + graph.addEdge(nodes[7-1], nodes[8-1]) + graph.addEdge(nodes[8-1], nodes[9-1]) + graph.addEdge(nodes[9-1], nodes[7-1]) + graph.addEdge(nodes[10-1], nodes[11-1]) + graph.addEdge(nodes[11-1], nodes[12-1]) + graph.addEdge(nodes[12-1], nodes[13-1]) + graph.addEdge(nodes[13-1], nodes[10-1]) + + val resultEin = JohnsonAlg(graph).findCycles(nodes[11+1]) + val expectedResultEin = setOf(listOf(nodes[12], nodes[9], nodes[10], nodes[11])) + assertEquals(expectedResultEin, resultEin) + + val resultZwei = JohnsonAlg(graph).findCycles(nodes[4+1]) + val expectedResultZwei = setOf(listOf(nodes[5], nodes[3], nodes[4])) + assertEquals(expectedResultZwei, resultZwei) + } + + @Test + fun findCyclesZwei() { + for (i in 1..13) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[1-1], nodes[2-1]) + graph.addEdge(nodes[2-1], nodes[3-1]) + graph.addEdge(nodes[3-1], nodes[4-1]) + graph.addEdge(nodes[4-1], nodes[5-1]) + graph.addEdge(nodes[5-1], nodes[6-1]) + graph.addEdge(nodes[6-1], nodes[3-1]) + graph.addEdge(nodes[6-1], nodes[7-1]) + graph.addEdge(nodes[7-1], nodes[8-1]) + graph.addEdge(nodes[8-1], nodes[9-1]) + graph.addEdge(nodes[9-1], nodes[7-1]) + graph.addEdge(nodes[10-1], nodes[11-1]) + graph.addEdge(nodes[11-1], nodes[12-1]) + graph.addEdge(nodes[12-1], nodes[13-1]) + graph.addEdge(nodes[13-1], nodes[10-1]) + graph.addEdge(nodes[13-1], nodes[11-1]) + + val result_ein = JohnsonAlg(graph).findCycles(nodes[3]) + val expectedResult_ein = setOf(listOf(nodes[3], nodes[4], nodes[5], nodes[2])) + assertEquals(expectedResult_ein, result_ein) + + val result_zwei = JohnsonAlg(graph).findCycles(nodes[7]) + val expectedResult_zwei = setOf(listOf(nodes[7], nodes[8], nodes[6])) + assertEquals(expectedResult_zwei, result_zwei) + + val result_drei = JohnsonAlg(graph).findCycles(nodes[12]) + val expectedResult_drei = setOf(listOf(nodes[12], nodes[9], nodes[10], nodes[11]), listOf(nodes[12], nodes[10], nodes[11])) + assertEquals(expectedResult_drei, result_drei) + } + + @Test + fun findCyclesDrei() { + for (i in 1..9) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[8-1], nodes[9-1]) + graph.addEdge(nodes[9-1], nodes[8-1]) + graph.addEdge(nodes[1-1], nodes[2-1]) + graph.addEdge(nodes[2-1], nodes[7-1]) + graph.addEdge(nodes[1-1], nodes[8-1]) + graph.addEdge(nodes[2-1], nodes[9-1]) + graph.addEdge(nodes[2-1], nodes[3-1]) + graph.addEdge(nodes[3-1], nodes[2-1]) + graph.addEdge(nodes[3-1], nodes[1-1]) + graph.addEdge(nodes[3-1], nodes[4-1]) + graph.addEdge(nodes[3-1], nodes[6-1]) + graph.addEdge(nodes[4-1], nodes[5-1]) + graph.addEdge(nodes[5-1], nodes[2-1]) + graph.addEdge(nodes[1-1], nodes[5-1]) + graph.addEdge(nodes[6-1], nodes[4-1]) + + val result = JohnsonAlg(graph).findCycles(nodes[0]) + val first = listOf(nodes[0], nodes[1], nodes[2]) + val second = listOf(nodes[0], nodes[4], nodes[1], nodes[2]) + val expectedResult = setOf(first, second) + assertEquals(expectedResult, result) + } + + @Test + fun VeryMuchCicles() { + for (i in 1..7) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[1-1], nodes[3-1]) + graph.addEdge(nodes[3-1], nodes[2-1]) + graph.addEdge(nodes[3-1], nodes[6-1]) + graph.addEdge(nodes[6-1], nodes[2-1]) + graph.addEdge(nodes[6-1], nodes[4-1]) + graph.addEdge(nodes[7-1], nodes[4-1]) + graph.addEdge(nodes[7-1], nodes[5-1]) + graph.addEdge(nodes[5-1], nodes[4-1]) + graph.addEdge(nodes[4-1], nodes[2-1]) + graph.addEdge(nodes[2-1], nodes[1-1]) + graph.addEdge(nodes[6-1], nodes[7-1]) + + val result = JohnsonAlg(graph).findCycles(nodes[0]) + val first = listOf(nodes[0], nodes[2], nodes[1]) + val second = listOf(nodes[0], nodes[2], nodes[5], nodes[1]) + val third = listOf(nodes[0], nodes[2], nodes[5], nodes[3], nodes[1]) + val fourth = listOf(nodes[0], nodes[2], nodes[5], nodes[6], nodes[3], nodes[1]) + val fifth = listOf(nodes[0], nodes[2], nodes[5], nodes[6], nodes[4], nodes[3], nodes[1]) + val expectedResult = setOf(first, second, third, fourth, fifth) + assertEquals(expectedResult, result) + } + + @Test + fun NoCycles() { + for (i in 1..7) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + + val result = JohnsonAlg(graph).findCycles(nodes[0]) + assertEquals(setOf>(), result) + } +} diff --git a/src/test/kotlin/functionalityTest/JsonConverterTest.kt b/src/test/kotlin/functionalityTest/JsonConverterTest.kt new file mode 100644 index 0000000..1efb9d2 --- /dev/null +++ b/src/test/kotlin/functionalityTest/JsonConverterTest.kt @@ -0,0 +1,149 @@ +package functionalityTest + +import model.functionality.iograph.ReadWriteIntGraph +import model.graphs.DirectedGraph +import model.graphs.DirectedWeightedGraph +import model.graphs.UndirectedGraph +import model.graphs.UndirectedWeightedGraph +import model.graphs.UnweightedEdge +import model.graphs.Vertex +import model.graphs.WeightedEdge + +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.test.assertEquals + +class JsonConverterTest { + companion object { + @JvmStatic + @BeforeAll + fun createTestDirectory() { + val directoryPath = Paths.get("./testGraphs") + if (!Files.exists(directoryPath)) { + Files.createDirectory(directoryPath) + } + } + + + //COMMENT THIS FUNCTION IF YOU WANT testGraphs DIRECTORY (all test graphs saved here) + @JvmStatic + @AfterAll + fun deleteTestDirectory() { + val directoryPath = Paths.get("./testGraphs") + if (Files.exists(directoryPath)) { + directoryPath.toFile().deleteRecursively() + } + } + } + + @Test + fun jsonUndirectedReadWriteTest() { + val vertices = Array(5) { Vertex(it) } + val edges = arrayOf( + UnweightedEdge(Vertex(0), Vertex(1)), + UnweightedEdge(vertices[1], vertices[2]), + UnweightedEdge(vertices[2], vertices[3]), + UnweightedEdge(vertices[3], vertices[4]), + UnweightedEdge(vertices[0], vertices[3]), + UnweightedEdge(vertices[0], vertices[4]), + ) + + val graph = UndirectedGraph() + graph.addVertices(*vertices) + graph.addEdges(*edges) + + val otherGraph = UndirectedGraph() + otherGraph.addVertices(*vertices) + otherGraph.addEdges(*edges) + + val file = File("./testGraphs/graphU.json") + + ReadWriteIntGraph().writeUGraph(file, graph) + val graphRead = ReadWriteIntGraph().readUGraph(file) + //using toString, because without them test won't pass (though graph are really identical) + assertEquals(graph.vertices().toString(), graphRead.vertices().toString()) + assertEquals(graph.edges().toString(), graphRead.edges().toString()) + } + + @Test + fun jsonDirectedReadWriteTest() { + val vertices = Array(6) { Vertex(it) } + val edges = arrayOf( + UnweightedEdge(Vertex(0), Vertex(1)), + UnweightedEdge(vertices[1], vertices[2]), + UnweightedEdge(vertices[2], vertices[4]), + UnweightedEdge(vertices[4], vertices[2]), + UnweightedEdge(vertices[0], vertices[5]), + UnweightedEdge(vertices[0], vertices[4]), + ) + + val graph = DirectedGraph() + graph.addVertices(*vertices) + graph.addEdges(*edges) + for (vertex in graph) println(vertex) + + val file = File("./testGraphs/graphD.json") + + ReadWriteIntGraph().writeDGraph(file, graph) + + val graphRead = ReadWriteIntGraph().readDGraph(file) + //using toString, because without them test won't pass + assertEquals(graph.vertices().toString(), graphRead.vertices().toString()) + assertEquals(graph.edges().toString(), graphRead.edges().toString()) + } + + @Test + fun jsonUndirectedWeightedReadWriteTest() { + val vertices = Array(10) { Vertex(it) } + val edges = arrayOf( + WeightedEdge(Vertex(0), Vertex(1), 42.0), + WeightedEdge(vertices[1], vertices[2], 7461.0), + WeightedEdge(vertices[2], vertices[3], 808.0), + WeightedEdge(vertices[3], vertices[4], 101.0), + WeightedEdge(vertices[0], vertices[3], 104.6), + WeightedEdge(vertices[0], vertices[4], 1976.0), + WeightedEdge(vertices[8], vertices[9], 0.0) + ) + + val graph = UndirectedWeightedGraph() + graph.addVertices(*vertices) + graph.addEdges(*edges) + + val file = File("./testGraphs/graphUW.json") + + ReadWriteIntGraph().writeUWGraph(file, graph) + + val graphRead = ReadWriteIntGraph().readUWGraph(file) + //using toString, because without them this test won't pass + assertEquals(graph.vertices().toString(), graphRead.vertices().toString()) + assertEquals(graph.edges().toString(), graphRead.edges().toString()) + } + + @Test + fun jsonDirectedWeightedReadWriteTest() { + val vertices = Array(4) { Vertex(it) } + val edges = arrayOf( + WeightedEdge(Vertex(0), Vertex(1), 1999.0), + WeightedEdge(vertices[2], vertices[3], 2000.0), + WeightedEdge(vertices[0], vertices[3], 52.0), + WeightedEdge(vertices[2], vertices[1], 7.0), + ) + + val graph = DirectedWeightedGraph() + graph.addVertices(*vertices) + graph.addEdges(*edges) + + val file = File("./testGraphs/graphDW.json") + + ReadWriteIntGraph().writeDWGraph(file, graph) + + val graphRead = ReadWriteIntGraph().readDWGraph(file) + //using toString, because without them this test won't pass + assertEquals(graph.vertices().toString(), graphRead.vertices().toString()) + assertEquals(graph.edges().toString(), graphRead.edges().toString()) + } +} diff --git a/src/test/kotlin/functionalityTest/MinSpanTreeFinderTest.kt b/src/test/kotlin/functionalityTest/MinSpanTreeFinderTest.kt new file mode 100644 index 0000000..76ecbd6 --- /dev/null +++ b/src/test/kotlin/functionalityTest/MinSpanTreeFinderTest.kt @@ -0,0 +1,97 @@ +package functionalityTest + +import model.graphs.UndirectedWeightedGraph +import model.graphs.Vertex +import model.graphs.WeightedEdge +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test +import kotlin.test.assertEquals + +class MinSpanTreeFinderTest { + private lateinit var graphInt: UndirectedWeightedGraph + private lateinit var expectedEdges: MutableSet> + + @BeforeEach + fun setup() { + graphInt = UndirectedWeightedGraph() + expectedEdges = mutableSetOf() + } + + @DisplayName("Impossible to find spanning tree (graph is not connected).") + @Test + fun mstTest1() { + val vertices = Array(6) { Vertex(it) } + val edges = arrayOf( + WeightedEdge(vertices[0], vertices[1], 1.0), + WeightedEdge(vertices[0], vertices[2], 2.0), + WeightedEdge(vertices[0], vertices[3], 3.0), + WeightedEdge(vertices[4], vertices[5], 4.0), + ) + + graphInt.addVertices(*vertices) + graphInt.addEdges(*edges) + + assertEquals(emptySet(), graphInt.findMinSpanTree()) + } + + @DisplayName("Spanning tree equals to initial graph.") + @Test + fun mstTest2() { + val vertices = Array(5) { Vertex(it) } + val edges = arrayOf( + WeightedEdge(vertices[0], vertices[1], 10.0), + WeightedEdge(vertices[1], vertices[2], 12.0), + WeightedEdge(vertices[2], vertices[3], 21.0), + WeightedEdge(vertices[3], vertices[4], 23.0), + ) + + graphInt.addVertices(*vertices) + graphInt.addEdges(*edges) + + for (edge in edges) { + expectedEdges.add(edge) + } + val actualEdges = graphInt.findMinSpanTree() + + for (edge in expectedEdges) { + assertTrue(actualEdges!!.contains(edge) || actualEdges.contains(edge.reverse())) + } + } + + @DisplayName("Find minimal spanning tree in shamrock.") + @Test + fun mstTest3() { + val vertices = Array(7) { Vertex(it) } + val edges = arrayOf( + WeightedEdge(vertices[0], vertices[1], 3.0), + WeightedEdge(vertices[0], vertices[2], 2.0), + WeightedEdge(vertices[0], vertices[3], 1.0), + WeightedEdge(vertices[0], vertices[4], 3.0), + WeightedEdge(vertices[0], vertices[5], 1.0), + WeightedEdge(vertices[0], vertices[6], 2.0), + WeightedEdge(vertices[1], vertices[2], 1.0), + WeightedEdge(vertices[3], vertices[4], 2.0), + WeightedEdge(vertices[5], vertices[6], 3.0), + ) + + graphInt.addVertices(*vertices) + graphInt.addEdges(*edges) + + expectedEdges = mutableSetOf( + WeightedEdge(vertices[0], vertices[2], 2.0), + WeightedEdge(vertices[0], vertices[3], 1.0), + WeightedEdge(vertices[0], vertices[5], 1.0), + WeightedEdge(vertices[0], vertices[6], 2.0), + WeightedEdge(vertices[1], vertices[2], 1.0), + WeightedEdge(vertices[3], vertices[4], 2.0), + ) + + val actualEdges = graphInt.findMinSpanTree() + + for (edge in expectedEdges) { + assertTrue(actualEdges!!.contains(edge) || actualEdges.contains(edge.reverse())) + } + } +} diff --git a/src/test/kotlin/functionalityTest/ShortestPathFinderTest.kt b/src/test/kotlin/functionalityTest/ShortestPathFinderTest.kt new file mode 100644 index 0000000..72a9633 --- /dev/null +++ b/src/test/kotlin/functionalityTest/ShortestPathFinderTest.kt @@ -0,0 +1,321 @@ +package functionalityTest + +import model.graphs.DirectedWeightedGraph +import model.graphs.UndirectedWeightedGraph +import model.graphs.Vertex +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.Double.Companion.NEGATIVE_INFINITY +import kotlin.Double.Companion.POSITIVE_INFINITY +import kotlin.test.assertEquals + + +class ShortestPathFinderTest { + @Nested + inner class DisconnectedPartsTest { + private val graph = UndirectedWeightedGraph() + private var nodes: List> = emptyList() + + private fun setup(end: Int) { + for (i in 0..end) { + graph.addVertex(i) + } + + nodes = graph.adjList.keys.toList().sortedBy { it.key } + } + + @Test + @DisplayName("Disconnected negative self-loop does not affect the rest of the graph.") + // 0 -- 1 (weight 1) + // 1 -- 2 (weight 2) + // 0 -- 2 (weight 10) + // 3 -- 3 (weight -1) + fun disconnectedSelfLoopCheck() { + setup(3) + + graph.addEdge(nodes[0], nodes[1], 1.0) + graph.addEdge(nodes[1], nodes[2], 2.0) + graph.addEdge(nodes[0], nodes[2], 10.0) + graph.addEdge(nodes[3], nodes[3], -1.0) + + val answer = mapOf( + nodes[0] to 0.0, + nodes[1] to 1.0, + nodes[2] to 3.0, + ) + + val actualOutput = graph.findDistancesBellman(nodes[0]).minus(nodes[3]) + + assertEquals(answer, actualOutput) + } + + @Test + @DisplayName( + "Nodes from disconnected components of a graph" + + " have an infinite distance to each other." + ) + // 0 -- - (no edges) + // 1 -- - (no edges) + fun disconnectedNodesCheck() { + setup(1) + + val answer = mapOf( + nodes[0] to POSITIVE_INFINITY, + nodes[1] to POSITIVE_INFINITY, + ) + + val actualOutputA = graph.findDistancesBellman(nodes[0]).minus(nodes[0]) + val actualOutputB = graph.findDistancesBellman(nodes[1]).minus(nodes[1]) + + assertEquals(answer, actualOutputA.plus(actualOutputB)) + } + } + + @Nested + inner class DirectedGraphTest { + private val graph = DirectedWeightedGraph() + private var nodes: List> = emptyList() + + private fun setup(end: Int) { + for (i in 0..end) { + graph.addVertex(i) + } + + nodes = graph.adjList.keys.toList().sortedBy { it.key } + } + + @Test + @DisplayName("Directed graph with negative weights but with no negative cycles.") + // 0 -> 1 (weight 1) + // 1 -> 2 (weight -1) + // 2 -> 3 (weight -1) + // 3 -> 0 (weight 2) + fun noFalseCycleCheck() { + setup(3) + + graph.addEdge(nodes[0], nodes[1], 5.0) + graph.addEdge(nodes[1], nodes[2], -5.0) + graph.addEdge(nodes[2], nodes[3], -5.0) + graph.addEdge(nodes[3], nodes[0], 10.0) + + val answer = mapOf( + nodes[0] to 0.0, + nodes[1] to 5.0, + nodes[2] to 0.0, + nodes[3] to -5.0 + ) + + val actualAnswer = graph.findDistancesBellman(nodes[0]) + + for (i in 0..3) { + assertEquals(answer[nodes[i]], actualAnswer[nodes[i]]) + } + } + + @DisplayName("Directed graph with a negative self-loop that affect entire graph.") + // 0 -> 1 (weight 50) + // 0 -> 2 (weight 5000) + // 1 -> 2 (weight 500) + // 1 -> 0 (weight 2) + // 1 -> 1 (weight -1) + // 2 -> 0 (weight 5000) + fun detectSelfLoopCheck1() { + setup(2) + + graph.addEdge(nodes[0], nodes[1], 50.0) + graph.addEdge(nodes[0], nodes[2], 5000.0) + graph.addEdge(nodes[1], nodes[2], 500.0) + graph.addEdge(nodes[1], nodes[1], -1.0) + graph.addEdge(nodes[1], nodes[0], 2.0) + graph.addEdge(nodes[2], nodes[0], 5000.0) + + val answer = mapOf( + nodes[0] to NEGATIVE_INFINITY, + nodes[1] to NEGATIVE_INFINITY, + nodes[2] to NEGATIVE_INFINITY, + ) + + val actualAnswer = graph.findDistancesBellman(nodes[0]) + + for (i in 0..2) { + assertEquals(answer[nodes[i]], actualAnswer[nodes[i]]) + } + } + + @Test + @DisplayName("Directed graph with a negative self-loop that doesn't affect entire graph.") + // 0 -> 1 (weight 50) + // 0 -> 2 (weight 500) + // 0 -> 3 (weight 5500) + // 1 -> 1 (weight -1) + // 2 -> 3 (weight 55) + fun detectSelfLoopCheck2() { + setup(3) + + graph.addEdge(nodes[0], nodes[1], 50.0) + graph.addEdge(nodes[0], nodes[2], 500.0) + graph.addEdge(nodes[0], nodes[3], 5500.0) + graph.addEdge(nodes[1], nodes[1], -1.0) + graph.addEdge(nodes[2], nodes[3], 55.0) + + val answer = mapOf( + nodes[0] to 0.0, + nodes[1] to NEGATIVE_INFINITY, + nodes[2] to 500.0, + nodes[3] to 555.0 + ) + + val actualAnswer = graph.findDistancesBellman(nodes[0]) + + for (i in 0..3) { + assertEquals(answer[nodes[i]], actualAnswer[nodes[i]]) + } + } + + @Test + @DisplayName( + "Find the shortest distance correctly " + + "in a directed graph without negative weights." + ) + // 0 -> 2 (weight 9) + // 0 -> 6 (weight 14) + // 0 -> 1 (weight 15) + // 1 -> 5 (weight 20) + // 1 -> 7 (weight 44) + // 2 -> 3 (weight 24) + // 3 -> 5 (weight 2) + // 3 -> 7 (weight 19) + // 4 -> 3 (weight 6) + // 4 -> 7 (weight 6) + // 5 -> 4 (weight 11) + // 5 -> 7 (weight 16) + // 6 -> 3 (weight 18) + // 6 -> 5 (weight 30) + // 6 -> 1 (weight 5) + fun correctDisanceCheck() { + setup(7) + + graph.addEdge(nodes[0], nodes[2], 9.0) + graph.addEdge(nodes[0], nodes[6], 14.0) + graph.addEdge(nodes[0], nodes[1], 15.0) + graph.addEdge(nodes[1], nodes[5], 20.0) + graph.addEdge(nodes[1], nodes[7], 44.0) + graph.addEdge(nodes[2], nodes[3], 24.0) + graph.addEdge(nodes[3], nodes[5], 2.0) + graph.addEdge(nodes[3], nodes[7], 19.0) + graph.addEdge(nodes[4], nodes[3], 6.0) + graph.addEdge(nodes[4], nodes[7], 6.0) + graph.addEdge(nodes[5], nodes[4], 11.0) + graph.addEdge(nodes[5], nodes[7], 16.0) + graph.addEdge(nodes[6], nodes[3], 18.0) + graph.addEdge(nodes[6], nodes[5], 30.0) + graph.addEdge(nodes[6], nodes[1], 5.0) + + val answer = mapOf( + nodes[0] to 0.0, + nodes[1] to 15.0, + nodes[2] to 9.0, + nodes[3] to 32.0, + nodes[4] to 45.0, + nodes[5] to 34.0, + nodes[6] to 14.0, + nodes[7] to 50.0 + ) + + val actualAnswer = graph.findDistancesBellman(nodes[0]) + + for (i in 0..7) { + assertEquals(answer[nodes[i]], actualAnswer[nodes[i]]) + } + } + } + + @Nested + inner class UndirectedGraphTest { + private val graph = UndirectedWeightedGraph() + private var nodes: List> = emptyList() + + private fun setup(end: Int) { + for (i in 0..end) { + graph.addVertex(i) + } + + nodes = graph.adjList.keys.toList().sortedBy { it.key } + } + + @Test + @DisplayName("Undirected graph with negative weight has a negative cycle.") + // 0 -- 1 (weight 1) + // 0 -- 4 (weight -1) + // 1 -- 2 (weight 1) + // 2 -- 3 (weight 1) + // 3 -- 0 (weight 2) + fun findNegativeCycleCheck() { + setup(4) + + graph.addEdge(nodes[0], nodes[1], 1.0) + graph.addEdge(nodes[0], nodes[4], -1.0) + graph.addEdge(nodes[1], nodes[2], 1.0) + graph.addEdge(nodes[2], nodes[3], 1.0) + graph.addEdge(nodes[3], nodes[0], 2.0) + + val answer = mapOf( + nodes[0] to NEGATIVE_INFINITY, + nodes[1] to NEGATIVE_INFINITY, + nodes[2] to NEGATIVE_INFINITY, + nodes[3] to NEGATIVE_INFINITY, + nodes[4] to NEGATIVE_INFINITY + ) + + val actualAnswer = graph.findDistancesBellman(nodes[0]) + + for (i in 0..3) { + assertEquals(answer[nodes[i]], actualAnswer[nodes[i]]) + } + } + + @Test + @DisplayName( + "Find the shortest distance correctly " + + "in an undirected graph without negative weights." + ) + // 0 -> 1 (weight 2) + // 0 -> 3 (weight 8) + // 1 -> 3 (weight 5) + // 1 -> 4 (weight 6) + // 2 -> 4 (weight 9) + // 2 -> 5 (weight 3) + // 3 -> 5 (weight 2) + // 3 -> 4 (weight 3) + // 4 -> 5 (weight 1) + fun correctDistanceCheck() { + setup(5) + + graph.addEdge(nodes[0], nodes[1], 2.0) + graph.addEdge(nodes[0], nodes[3], 8.0) + graph.addEdge(nodes[1], nodes[3], 5.0) + graph.addEdge(nodes[1], nodes[4], 6.0) + graph.addEdge(nodes[2], nodes[4], 9.0) + graph.addEdge(nodes[2], nodes[5], 3.0) + graph.addEdge(nodes[3], nodes[5], 2.0) + graph.addEdge(nodes[3], nodes[4], 3.0) + graph.addEdge(nodes[4], nodes[5], 1.0) + + val answer = mapOf( + nodes[0] to 0.0, + nodes[1] to 2.0, + nodes[2] to 12.0, + nodes[3] to 7.0, + nodes[4] to 8.0, + nodes[5] to 9.0, + ) + + val actualAnswer = graph.findDistancesBellman(nodes[0]) + + for (i in 0..5) { + assertEquals(answer[nodes[i]], actualAnswer[nodes[i]]) + } + } + } +} diff --git a/src/test/kotlin/functionalityTest/StrConCompFinderTest.kt b/src/test/kotlin/functionalityTest/StrConCompFinderTest.kt new file mode 100644 index 0000000..3eb6f21 --- /dev/null +++ b/src/test/kotlin/functionalityTest/StrConCompFinderTest.kt @@ -0,0 +1,83 @@ +package functionalityTest + +import model.graphs.DirectedGraph +import model.graphs.UnweightedEdge +import model.graphs.Vertex +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test +import kotlin.test.assertEquals + +class StrConCompFinderTest { + private lateinit var graphInt: DirectedGraph + private lateinit var expectedSCC: MutableSet>> + + @BeforeEach + fun clear() { + graphInt = DirectedGraph() + expectedSCC = mutableSetOf() + } + + @Test + @DisplayName("Max-edged graph.") + fun sccTest1() { + val vertices = Array(6) { Vertex(it) } + + graphInt.addVertices(*vertices) + + for (digit1 in 0..5) { + for (digit2 in 0..5) { + if (digit1 != digit2) { + graphInt.addEdge(Vertex(digit1), Vertex(digit2)) + } + } + } + + val component = vertices.toSet() + expectedSCC.add(component) + + assertEquals(expectedSCC, graphInt.findSCC()) + } + + @Test + @DisplayName("Zero-edged graph.") + fun sccTest2() { + val vertices = Array(6) { Vertex(it) } + + graphInt.addVertices(*vertices) + + for (vertex in vertices) { + val component = setOf(vertex) + expectedSCC.add(component) + } + + assertEquals(expectedSCC, graphInt.findSCC()) + } + + @Test + @DisplayName("3 strong connected components without edges between them.") + fun sccTest3() { + val vertices = Array(8) { Vertex(it) } + val edges = arrayOf( + UnweightedEdge(vertices[0], vertices[1]), + UnweightedEdge(vertices[1], vertices[0]), + UnweightedEdge(vertices[2], vertices[3]), + UnweightedEdge(vertices[3], vertices[4]), + UnweightedEdge(vertices[4], vertices[2]), + UnweightedEdge(vertices[5], vertices[6]), + UnweightedEdge(vertices[6], vertices[7]), + UnweightedEdge(vertices[7], vertices[6]), + UnweightedEdge(vertices[7], vertices[5]), + ) + + graphInt.addVertices(*vertices) + graphInt.addEdges(*edges) + expectedSCC = mutableSetOf( + setOf(Vertex(0), Vertex(1)), + setOf(Vertex(2), Vertex(3), Vertex(4)), + setOf(Vertex(5), Vertex(6), Vertex(7)), + ) + + assertEquals(expectedSCC, graphInt.findSCC()) + } +} diff --git a/src/test/kotlin/functionalityTest/TarjanSCCTest.kt b/src/test/kotlin/functionalityTest/TarjanSCCTest.kt new file mode 100644 index 0000000..d716873 --- /dev/null +++ b/src/test/kotlin/functionalityTest/TarjanSCCTest.kt @@ -0,0 +1,138 @@ +package functionalityTest + +import model.functionality.TarjanSCC +import model.graphs.DirectedGraph +import model.graphs.Edge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TarjanSCCTest { + private val graph = DirectedGraph() + + @Test + fun findSCC_Ein() { + for (i in 0..15) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + graph.addEdge(nodes[1], nodes[2]) + graph.addEdge(nodes[2], nodes[3]) + graph.addEdge(nodes[3], nodes[1]) + graph.addEdge(nodes[4], nodes[5]) + graph.addEdge(nodes[5], nodes[6]) + graph.addEdge(nodes[6], nodes[4]) + graph.addEdge(nodes[7], nodes[8]) + graph.addEdge(nodes[8], nodes[9]) + graph.addEdge(nodes[9], nodes[7]) + graph.addEdge(nodes[10], nodes[11]) + graph.addEdge(nodes[11], nodes[12]) + graph.addEdge(nodes[12], nodes[13]) + graph.addEdge(nodes[13], nodes[10]) + + val resultEin = graph.Tarjan(nodes[10]).toSet() + //val resultEin = TarjanSCC>().findSCC(nodes[10], graph).toSet() + val expectedResultEin = setOf(nodes[11], nodes[10], nodes[12], nodes[13]) + assertEquals(expectedResultEin, resultEin) + + val resultZwei = graph.Tarjan(nodes[7]).toSet() + //val resultZwei = TarjanSCC().findSCC(nodes[7], graph).toSet() + val expectedResultZwei = setOf(nodes[7], nodes[8], nodes[9]) + assertEquals(expectedResultZwei, resultZwei) + + val resultDrei = graph.Tarjan(nodes[4]).toSet() + //val resultDrei = TarjanSCC().findSCC(nodes[4], graph).toSet() + val expectedResultDrei = setOf(nodes[5], nodes[6], nodes[4]) + assertEquals(expectedResultDrei, resultDrei) + } + + @Test + fun findSCC_Zwei() { + for (i in 1..15) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + graph.addEdge(nodes[1], nodes[2]) + graph.addEdge(nodes[2], nodes[3]) + graph.addEdge(nodes[3], nodes[4]) + graph.addEdge(nodes[4], nodes[5]) + graph.addEdge(nodes[5], nodes[6]) + graph.addEdge(nodes[6], nodes[3]) + graph.addEdge(nodes[6], nodes[7]) + graph.addEdge(nodes[7], nodes[8]) + graph.addEdge(nodes[8], nodes[9]) + graph.addEdge(nodes[9], nodes[7]) + graph.addEdge(nodes[10], nodes[11]) + graph.addEdge(nodes[11], nodes[12]) + graph.addEdge(nodes[12], nodes[13]) + graph.addEdge(nodes[13], nodes[14]) + graph.addEdge(nodes[14], nodes[10]) + graph.addEdge(nodes[4], nodes[1]) + + val resultEin = graph.Tarjan(nodes[3]).toSet() + //val resultEin = TarjanSCC().findSCC(nodes[3], graph).toSet() + val expectedResultEin = setOf(nodes[6], nodes[1], nodes[2], nodes[3], nodes[4], nodes[5]) + assertEquals(expectedResultEin, resultEin) + + val resultZwei = graph.Tarjan(nodes[7]).toSet() + //val resultZwei = TarjanSCC().findSCC(nodes[7], graph).toSet() + val expectedResultZwei = setOf(nodes[9], nodes[7], nodes[8]) + assertEquals(expectedResultZwei, resultZwei) + } + + + //THIS METHOD EXISTS IN ANOTHER METHOD AND TWICE +// IMPLEMENT THE SAME IS CRINGE SO THERE'S NO THIS TEST ANYMORE + + /*@Test + fun findSCC_Drei() { + for (i in 1..14) { + graph.addVertex(i) + } + val nodes = graph.adjList.keys.toList().sortedBy { it.key } + + graph.addEdge(nodes[1], nodes[2]) + graph.addEdge(nodes[2], nodes[3]) + graph.addEdge(nodes[3], nodes[4]) + graph.addEdge(nodes[4], nodes[2]) + graph.addEdge(nodes[3], nodes[5]) + graph.addEdge(nodes[5], nodes[6]) + graph.addEdge(nodes[6], nodes[7]) + graph.addEdge(nodes[7], nodes[5]) + graph.addEdge(nodes[6], nodes[8]) + graph.addEdge(nodes[8], nodes[9]) + graph.addEdge(nodes[9], nodes[10]) + graph.addEdge(nodes[10], nodes[6]) + graph.addEdge(nodes[10], nodes[11]) + graph.addEdge(nodes[11], nodes[12]) + graph.addEdge(nodes[12], nodes[13]) + graph.addEdge(nodes[13], nodes[11]) + + val resultEin = graph.Tarjan(nodes[12]).toSet() + //val resultEin = TarjanSCC().findSCC(nodes[12], graph).toSet() + val expectedResultEin = setOf(nodes[13], nodes[11], nodes[12]) + assertEquals(expectedResultEin, resultEin) + + val resultZwei = graph.Tarjan(nodes[0]).toSet() + //val resultZwei = TarjanSCC().findSCC(nodes[0], graph).toSet() + val expectedResultZwei = setOf(nodes[0]) + assertEquals(expectedResultZwei, resultZwei) + + val resultDrei = graph.Tarjan(nodes[5]).toSet() + //val resultDrei = TarjanSCC().findSCC(nodes[5], graph).toSet() + val expectedResultDrei = setOf(nodes[10], nodes[5], nodes[6], nodes[7], nodes[8], nodes[9]) + assertEquals(expectedResultDrei, resultDrei) + + val resultViel = graph.Tarjan(nodes[3]).toSet() + //val resultViel = TarjanSCC().findSCC(nodes[3], graph).toSet() + val expectedResultViel = setOf(nodes[4], nodes[2], nodes[3]) + assertEquals(expectedResultViel, resultViel) + + val resultFunf = graph.Tarjan(nodes[1]).toSet() + //val resultFunf = TarjanSCC().findSCC(nodes[1], graph).toSet() + + val commonResult = setOf(resultFunf, expectedResultViel, expectedResultDrei, expectedResultEin, expectedResultZwei) + //val commonSCCs = graph.Tarjan(nodes[10]).toSet() + val commonSCCs = TarjanSCC().findSCCs(graph).toSet() + assertEquals(commonResult, commonSCCs) + }*/ +} diff --git a/src/test/kotlin/graphsTest/GraphTest.kt b/src/test/kotlin/graphsTest/GraphTest.kt new file mode 100644 index 0000000..36e5b17 --- /dev/null +++ b/src/test/kotlin/graphsTest/GraphTest.kt @@ -0,0 +1,117 @@ +package graphsTest + +import model.graphs.UndirectedGraph +import model.graphs.UnweightedEdge +import model.graphs.Vertex +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertFails +import kotlin.test.assertTrue + +class GraphTest { + private var graph = UndirectedGraph() + + @Nested + inner class AddVertexTest { + @Test + @DisplayName("New vertex in an empty graph") + fun emptyGraph() { + val v1 = graph.addVertex(1) + + val answer = HashMap, HashSet>>() + answer[v1] = HashSet() + + assertEquals(answer, graph.adjList) + } + + @Test + @DisplayName("New vertex in a non-empty graph") + fun addVertex() { + val v1 = graph.addVertex(1) + val v2 = graph.addVertex(2) + + val answer = HashMap, HashSet>>() + answer[v1] = HashSet() + answer[v2] = HashSet() + + assertEquals(answer, graph.adjList) + } + + @Test + @DisplayName("Return existing node if it has the same key as given key") + fun noDoubles() { + val v1 = graph.addVertex(1) + val testVertex = graph.addVertex(1) + + val answer = HashMap, HashSet>>() + answer[v1] = HashSet() + + assertEquals(answer, graph.adjList) + assertEquals(v1, testVertex) + } + } + + @Nested + inner class AddEdgesTest { + private val v1 = Vertex(1) + private val v2 = Vertex(2) + + @BeforeEach + fun init() { + graph.adjList[v1] = HashSet() + graph.adjList[v2] = HashSet() + } + + @Test + @DisplayName("Edge can't be added if at least one of the nodes does not exist") + fun edgeException() { + val graphString = UndirectedGraph() + val vertex = Vertex("exists") + + graphString.adjList[vertex] = HashSet() + + assertThrows(IllegalArgumentException::class.java) { + graphString.addEdge( + Vertex("doesn't exist"), + Vertex("doesn't exist") + ) + } + assertThrows(IllegalArgumentException::class.java) { + graphString.addEdge(vertex, Vertex("doesn't exist")) + } + assertThrows(IllegalArgumentException::class.java) { + graphString.addEdge(Vertex("doesn't exist"), vertex) + } + assertDoesNotThrow { + graphString.addEdge(vertex, vertex) + } + } + + @Test + @DisplayName("When add an edge between two nodes, v1 points to v2, and v2 points to v1") + fun edgeAdd() { + graph.addEdge(v1, v2) + + val edge1 = UnweightedEdge(v1, v2) + val edge2 = UnweightedEdge(v2, v1) + assertTrue { + graph.adjList[v1]!!.contains(edge1) + && graph.adjList[v2]!!.contains(edge2) + } + } + + @Test + @DisplayName("Don't create an edge if edge already exists; throws exception") + fun edgeAlreadyExists() { + graph.adjList[v1]?.add(UnweightedEdge(v1, v2)) + graph.adjList[v2]?.add(UnweightedEdge(v1, v2)) + + assertFails { graph.addEdge(v1, v2) } + } + } +}