From 87e804060be6c367fe596afded2372a5b450701c Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:47:41 +0000 Subject: [PATCH 001/172] Setting up GitHub Classroom Feedback From 12051eb5592256d9f7b761245125256389d98630 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:43:17 +0300 Subject: [PATCH 002/172] Create LICENSE --- LICENSE | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c93f45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. From 4d62b1790a1686eddee9eb985f3a56d97fdc5144 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 6 May 2024 23:18:23 +0300 Subject: [PATCH 003/172] init repo --- .gitignore | 7 + README.md | 14 + build.gradle.kts | 8 + composeApp/build.gradle.kts | 97 +++++ .../src/androidMain/AndroidManifest.xml | 23 + .../androidMain/kotlin/Platform.android.kt | 7 + .../org/example/project/MainActivity.kt | 24 ++ .../drawable-v24/ic_launcher_foreground.xml | 30 ++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../src/androidMain/res/values/strings.xml | 3 + .../drawable/compose-multiplatform.xml | 36 ++ composeApp/src/commonMain/kotlin/App.kt | 37 ++ composeApp/src/commonMain/kotlin/Greeting.kt | 7 + composeApp/src/commonMain/kotlin/Platform.kt | 5 + .../src/desktopMain/kotlin/Platform.jvm.kt | 5 + composeApp/src/desktopMain/kotlin/main.kt | 11 + .../src/iosMain/kotlin/MainViewController.kt | 3 + composeApp/src/iosMain/kotlin/Platform.ios.kt | 7 + gradle.properties | 12 + gradle/libs.versions.toml | 36 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 249 +++++++++++ gradlew.bat | 92 ++++ iosApp/Configuration/Config.xcconfig | 3 + iosApp/iosApp.xcodeproj/project.pbxproj | 398 ++++++++++++++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 14 + .../AppIcon.appiconset/app-icon-1024.png | Bin 0 -> 67285 bytes iosApp/iosApp/Assets.xcassets/Contents.json | 6 + iosApp/iosApp/ContentView.swift | 21 + iosApp/iosApp/Info.plist | 50 +++ .../Preview Assets.xcassets/Contents.json | 6 + iosApp/iosApp/iOSApp.swift | 10 + lib/build.gradle.kts | 41 ++ .../androidMain/kotlin/fibiprops.android.kt | 0 .../androidUnitTest/kotlin/AndroidFibiTest.kt | 11 + lib/src/commonMain/kotlin/Graph.kt | 5 + lib/src/commonTest/kotlin/GraphTest.kt | 10 + lib/src/iosMain/kotlin/graph.ios.kt | 0 lib/src/iosTest/kotlin/IosGraphTest.kt | 10 + lib/src/jvmMain/kotlin/graph.jvm.kt | 0 lib/src/jvmTest/kotlin/JvmGraphTest.kt | 10 + lib/src/linuxX64Main/kotlin/graph.linuxX64.kt | 0 lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt | 9 + local.properties | 8 + settings.gradle.kts | 32 ++ 59 files changed, 1555 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.gradle.kts create mode 100644 composeApp/build.gradle.kts create mode 100644 composeApp/src/androidMain/AndroidManifest.xml create mode 100644 composeApp/src/androidMain/kotlin/Platform.android.kt create mode 100644 composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt create mode 100644 composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 composeApp/src/androidMain/res/drawable/ic_launcher_background.xml create mode 100644 composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png create mode 100644 composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png create mode 100644 composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png create mode 100644 composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 composeApp/src/androidMain/res/values/strings.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml create mode 100644 composeApp/src/commonMain/kotlin/App.kt create mode 100644 composeApp/src/commonMain/kotlin/Greeting.kt create mode 100644 composeApp/src/commonMain/kotlin/Platform.kt create mode 100644 composeApp/src/desktopMain/kotlin/Platform.jvm.kt create mode 100644 composeApp/src/desktopMain/kotlin/main.kt create mode 100644 composeApp/src/iosMain/kotlin/MainViewController.kt create mode 100644 composeApp/src/iosMain/kotlin/Platform.ios.kt create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 iosApp/Configuration/Config.xcconfig create mode 100644 iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png create mode 100644 iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 iosApp/iosApp/ContentView.swift create mode 100644 iosApp/iosApp/Info.plist create mode 100644 iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 iosApp/iosApp/iOSApp.swift create mode 100644 lib/build.gradle.kts create mode 100644 lib/src/androidMain/kotlin/fibiprops.android.kt create mode 100644 lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt create mode 100644 lib/src/commonMain/kotlin/Graph.kt create mode 100644 lib/src/commonTest/kotlin/GraphTest.kt create mode 100644 lib/src/iosMain/kotlin/graph.ios.kt create mode 100644 lib/src/iosTest/kotlin/IosGraphTest.kt create mode 100644 lib/src/jvmMain/kotlin/graph.jvm.kt create mode 100644 lib/src/jvmTest/kotlin/JvmGraphTest.kt create mode 100644 lib/src/linuxX64Main/kotlin/graph.linuxX64.kt create mode 100644 lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt create mode 100644 local.properties create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bb1be5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +/.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef5ec7b --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +This is a Kotlin Multiplatform project targeting Android, iOS, Desktop. + +* `/composeApp` is for code that will be shared across your Compose Multiplatform applications. + It contains several subfolders: + - `commonMain` is for code that’s common for all targets. + - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. + For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, + `iosMain` would be the right folder for such calls. + +* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, + you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. + + +Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..52cf9fa --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.jetbrainsCompose) apply false + alias(libs.plugins.kotlinMultiplatform) apply false +} \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts new file mode 100644 index 0000000..4c0f3f0 --- /dev/null +++ b/composeApp/build.gradle.kts @@ -0,0 +1,97 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsCompose) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "11" + } + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + + sourceSets { + val desktopMain by getting + + androidMain.dependencies { + implementation(libs.compose.ui.tooling.preview) + implementation(libs.androidx.activity.compose) + } + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(project(":lib")) + } + desktopMain.dependencies { + implementation(compose.desktop.currentOs) + } + } +} + +android { + namespace = "org.example.project" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].res.srcDirs("src/androidMain/res") + sourceSets["main"].resources.srcDirs("src/commonMain/resources") + + defaultConfig { + applicationId = "org.example.project" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + dependencies { + debugImplementation(libs.compose.ui.tooling) + } +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "org.example.project" + packageVersion = "1.0.0" + } + } +} diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..c5db0b1 --- /dev/null +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/Platform.android.kt b/composeApp/src/androidMain/kotlin/Platform.android.kt new file mode 100644 index 0000000..4f3ea05 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/Platform.android.kt @@ -0,0 +1,7 @@ +import android.os.Build + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt new file mode 100644 index 0000000..02d1e4e --- /dev/null +++ b/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt @@ -0,0 +1,24 @@ +package org.example.project + +import App +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + App() + } + } +} + +@Preview +@Composable +fun AppAndroidPreview() { + App() +} \ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..e93e11a --- /dev/null +++ b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a571e60098c92c2baca8a5df62f2929cbff01b52 GIT binary patch literal 3593 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 P00000NkvXXu0mjfP=x?Y literal 0 HcmV?d00001 diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..61da551c5594a1f9d26193983d2cd69189014603 GIT binary patch literal 5339 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 literal 0 HcmV?d00001 diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b216f2d313cc673d8b8c4da591c174ebed52795c GIT binary patch literal 11873 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 literal 0 HcmV?d00001 diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml new file mode 100644 index 0000000..3385b16 --- /dev/null +++ b/composeApp/src/androidMain/res/values/strings.xml @@ -0,0 +1,3 @@ + + KotlinProject + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml new file mode 100644 index 0000000..c0bcfb2 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000..da7c2af --- /dev/null +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -0,0 +1,37 @@ +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.ui.tooling.preview.Preview + +import graphvisualizer.composeapp.generated.resources.Res +import graphvisualizer.composeapp.generated.resources.compose_multiplatform + +@OptIn(ExperimentalResourceApi::class) +@Composable +@Preview +fun App() { + MaterialTheme { + var showContent by remember { mutableStateOf(false) } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = { showContent = !showContent }) { + Text("Click me!") + } + AnimatedVisibility(showContent) { + val greeting = remember { Greeting().greet() } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Image(painterResource(Res.drawable.compose_multiplatform), null) + Text("Compose: $greeting") + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/Greeting.kt b/composeApp/src/commonMain/kotlin/Greeting.kt new file mode 100644 index 0000000..887d835 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/Greeting.kt @@ -0,0 +1,7 @@ +class Greeting { + private val platform = getPlatform() + + fun greet(): String { + return "Hello, ${platform.name}!" + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/Platform.kt b/composeApp/src/commonMain/kotlin/Platform.kt new file mode 100644 index 0000000..87ca3ff --- /dev/null +++ b/composeApp/src/commonMain/kotlin/Platform.kt @@ -0,0 +1,5 @@ +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/Platform.jvm.kt b/composeApp/src/desktopMain/kotlin/Platform.jvm.kt new file mode 100644 index 0000000..f5e7e49 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/Platform.jvm.kt @@ -0,0 +1,5 @@ +class JVMPlatform: Platform { + override val name: String = "Java ${System.getProperty("java.version")}" +} + +actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt new file mode 100644 index 0000000..780ab4f --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/main.kt @@ -0,0 +1,11 @@ +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Graph Visualizer", + ) { + App() + } +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/MainViewController.kt b/composeApp/src/iosMain/kotlin/MainViewController.kt new file mode 100644 index 0000000..fa143d4 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/MainViewController.kt @@ -0,0 +1,3 @@ +import androidx.compose.ui.window.ComposeUIViewController + +fun MainViewController() = ComposeUIViewController { App() } \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/Platform.ios.kt b/composeApp/src/iosMain/kotlin/Platform.ios.kt new file mode 100644 index 0000000..5cef987 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/Platform.ios.kt @@ -0,0 +1,7 @@ +import platform.UIKit.UIDevice + +class IOSPlatform: Platform { + override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion +} + +actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5db355f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +kotlin.code.style=official + +#Gradle +org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" + +#Android +android.nonTransitiveRClass=true +android.useAndroidX=true + +#MPP +kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..4db653e --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,36 @@ +[versions] +agp = "8.2.0" +android-compileSdk = "34" +android-minSdk = "24" +android-targetSdk = "34" +androidx-activityCompose = "1.9.0" +androidx-appcompat = "1.6.1" +androidx-constraintlayout = "2.1.4" +androidx-core-ktx = "1.13.0" +androidx-espresso-core = "3.5.1" +androidx-material = "1.11.0" +androidx-test-junit = "1.1.5" +compose = "1.6.6" +compose-plugin = "1.6.2" +junit = "4.13.2" +kotlin = "1.9.23" + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } +androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } +androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7f93135c49b765f8051ef9d0a6055ff8e46073d8 GIT binary patch literal 63721 zcmb5Wb9gP!wgnp7wrv|bwr$&XvSZt}Z6`anZSUAlc9NHKf9JdJ;NJVr`=eI(_pMp0 zy1VAAG3FfAOI`{X1O)&90s;U4K;XLp008~hCjbEC_fbYfS%6kTR+JtXK>nW$ZR+`W ze|#J8f4A@M|F5BpfUJb5h>|j$jOe}0oE!`Zf6fM>CR?!y@zU(cL8NsKk`a z6tx5mAkdjD;J=LcJ;;Aw8p!v#ouk>mUDZF@ zK>yvw%+bKu+T{Nk@LZ;zkYy0HBKw06_IWcMHo*0HKpTsEFZhn5qCHH9j z)|XpN&{`!0a>Vl+PmdQc)Yg4A(AG-z!+@Q#eHr&g<9D?7E)_aEB?s_rx>UE9TUq|? z;(ggJt>9l?C|zoO@5)tu?EV0x_7T17q4fF-q3{yZ^ipUbKcRZ4Qftd!xO(#UGhb2y>?*@{xq%`(-`2T^vc=#< zx!+@4pRdk&*1ht2OWk^Z5IAQ0YTAXLkL{(D*$gENaD)7A%^XXrCchN&z2x+*>o2FwPFjWpeaL=!tzv#JOW#( z$B)Nel<+$bkH1KZv3&-}=SiG~w2sbDbAWarg%5>YbC|}*d9hBjBkR(@tyM0T)FO$# zPtRXukGPnOd)~z=?avu+4Co@wF}1T)-uh5jI<1$HLtyDrVak{gw`mcH@Q-@wg{v^c zRzu}hMKFHV<8w}o*yg6p@Sq%=gkd~;`_VGTS?L@yVu`xuGy+dH6YOwcP6ZE`_0rK% zAx5!FjDuss`FQ3eF|mhrWkjux(Pny^k$u_)dyCSEbAsecHsq#8B3n3kDU(zW5yE|( zgc>sFQywFj5}U*qtF9Y(bi*;>B7WJykcAXF86@)z|0-Vm@jt!EPoLA6>r)?@DIobIZ5Sx zsc@OC{b|3%vaMbyeM|O^UxEYlEMHK4r)V-{r)_yz`w1*xV0|lh-LQOP`OP`Pk1aW( z8DSlGN>Ts|n*xj+%If~+E_BxK)~5T#w6Q1WEKt{!Xtbd`J;`2a>8boRo;7u2M&iOop4qcy<)z023=oghSFV zST;?S;ye+dRQe>ygiJ6HCv4;~3DHtJ({fWeE~$H@mKn@Oh6Z(_sO>01JwH5oA4nvK zr5Sr^g+LC zLt(i&ecdmqsIJGNOSUyUpglvhhrY8lGkzO=0USEKNL%8zHshS>Qziu|`eyWP^5xL4 zRP122_dCJl>hZc~?58w~>`P_s18VoU|7(|Eit0-lZRgLTZKNq5{k zE?V=`7=R&ro(X%LTS*f+#H-mGo_j3dm@F_krAYegDLk6UV{`UKE;{YSsn$ z(yz{v1@p|p!0>g04!eRSrSVb>MQYPr8_MA|MpoGzqyd*$@4j|)cD_%^Hrd>SorF>@ zBX+V<@vEB5PRLGR(uP9&U&5=(HVc?6B58NJT_igiAH*q~Wb`dDZpJSKfy5#Aag4IX zj~uv74EQ_Q_1qaXWI!7Vf@ZrdUhZFE;L&P_Xr8l@GMkhc#=plV0+g(ki>+7fO%?Jb zl+bTy7q{w^pTb{>(Xf2q1BVdq?#f=!geqssXp z4pMu*q;iiHmA*IjOj4`4S&|8@gSw*^{|PT}Aw~}ZXU`6=vZB=GGeMm}V6W46|pU&58~P+?LUs%n@J}CSrICkeng6YJ^M? zS(W?K4nOtoBe4tvBXs@@`i?4G$S2W&;$z8VBSM;Mn9 zxcaEiQ9=vS|bIJ>*tf9AH~m&U%2+Dim<)E=}KORp+cZ^!@wI`h1NVBXu{@%hB2Cq(dXx_aQ9x3mr*fwL5!ZryQqi|KFJuzvP zK1)nrKZ7U+B{1ZmJub?4)Ln^J6k!i0t~VO#=q1{?T)%OV?MN}k5M{}vjyZu#M0_*u z8jwZKJ#Df~1jcLXZL7bnCEhB6IzQZ-GcoQJ!16I*39iazoVGugcKA{lhiHg4Ta2fD zk1Utyc5%QzZ$s3;p0N+N8VX{sd!~l*Ta3|t>lhI&G`sr6L~G5Lul`>m z{!^INm?J|&7X=;{XveF!(b*=?9NAp4y&r&N3(GKcW4rS(Ejk|Lzs1PrxPI_owB-`H zg3(Rruh^&)`TKA6+_!n>RdI6pw>Vt1_j&+bKIaMTYLiqhZ#y_=J8`TK{Jd<7l9&sY z^^`hmi7^14s16B6)1O;vJWOF$=$B5ONW;;2&|pUvJlmeUS&F;DbSHCrEb0QBDR|my zIs+pE0Y^`qJTyH-_mP=)Y+u^LHcuZhsM3+P||?+W#V!_6E-8boP#R-*na4!o-Q1 zVthtYhK{mDhF(&7Okzo9dTi03X(AE{8cH$JIg%MEQca`S zy@8{Fjft~~BdzWC(di#X{ny;!yYGK9b@=b|zcKZ{vv4D8i+`ilOPl;PJl{!&5-0!w z^fOl#|}vVg%=n)@_e1BrP)`A zKPgs`O0EO}Y2KWLuo`iGaKu1k#YR6BMySxQf2V++Wo{6EHmK>A~Q5o73yM z-RbxC7Qdh0Cz!nG+7BRZE>~FLI-?&W_rJUl-8FDIaXoNBL)@1hwKa^wOr1($*5h~T zF;%f^%<$p8Y_yu(JEg=c_O!aZ#)Gjh$n(hfJAp$C2he555W5zdrBqjFmo|VY+el;o z=*D_w|GXG|p0**hQ7~9-n|y5k%B}TAF0iarDM!q-jYbR^us(>&y;n^2l0C%@2B}KM zyeRT9)oMt97Agvc4sEKUEy%MpXr2vz*lb zh*L}}iG>-pqDRw7ud{=FvTD?}xjD)w{`KzjNom-$jS^;iw0+7nXSnt1R@G|VqoRhE%12nm+PH?9`(4rM0kfrZzIK9JU=^$YNyLvAIoxl#Q)xxDz!^0@zZ zSCs$nfcxK_vRYM34O<1}QHZ|hp4`ioX3x8(UV(FU$J@o%tw3t4k1QPmlEpZa2IujG&(roX_q*%e`Hq|);0;@k z0z=fZiFckp#JzW0p+2A+D$PC~IsakhJJkG(c;CqAgFfU0Z`u$PzG~-9I1oPHrCw&)@s^Dc~^)#HPW0Ra}J^=|h7Fs*<8|b13ZzG6MP*Q1dkoZ6&A^!}|hbjM{2HpqlSXv_UUg1U4gn z3Q)2VjU^ti1myodv+tjhSZp%D978m~p& z43uZUrraHs80Mq&vcetqfQpQP?m!CFj)44t8Z}k`E798wxg&~aCm+DBoI+nKq}&j^ zlPY3W$)K;KtEajks1`G?-@me7C>{PiiBu+41#yU_c(dITaqE?IQ(DBu+c^Ux!>pCj zLC|HJGU*v+!it1(;3e`6igkH(VA)-S+k(*yqxMgUah3$@C zz`7hEM47xr>j8^g`%*f=6S5n>z%Bt_Fg{Tvmr+MIsCx=0gsu_sF`q2hlkEmisz#Fy zj_0;zUWr;Gz}$BS%Y`meb(=$d%@Crs(OoJ|}m#<7=-A~PQbyN$x%2iXP2@e*nO0b7AwfH8cCUa*Wfu@b)D_>I*%uE4O3 z(lfnB`-Xf*LfC)E}e?%X2kK7DItK6Tf<+M^mX0Ijf_!IP>7c8IZX%8_#0060P{QMuV^B9i<^E`_Qf0pv9(P%_s8D`qvDE9LK9u-jB}J2S`(mCO&XHTS04Z5Ez*vl^T%!^$~EH8M-UdwhegL>3IQ*)(MtuH2Xt1p!fS4o~*rR?WLxlA!sjc2(O znjJn~wQ!Fp9s2e^IWP1C<4%sFF}T4omr}7+4asciyo3DntTgWIzhQpQirM$9{EbQd z3jz9vS@{aOqTQHI|l#aUV@2Q^Wko4T0T04Me4!2nsdrA8QY1%fnAYb~d2GDz@lAtfcHq(P7 zaMBAGo}+NcE-K*@9y;Vt3*(aCaMKXBB*BJcD_Qnxpt75r?GeAQ}*|>pYJE=uZb73 zC>sv)18)q#EGrTG6io*}JLuB_jP3AU1Uiu$D7r|2_zlIGb9 zjhst#ni)Y`$)!fc#reM*$~iaYoz~_Cy7J3ZTiPm)E?%`fbk`3Tu-F#`{i!l5pNEn5 zO-Tw-=TojYhzT{J=?SZj=Z8#|eoF>434b-DXiUsignxXNaR3 zm_}4iWU$gt2Mw5NvZ5(VpF`?X*f2UZDs1TEa1oZCif?Jdgr{>O~7}-$|BZ7I(IKW`{f;@|IZFX*R8&iT= zoWstN8&R;}@2Ka%d3vrLtR|O??ben;k8QbS-WB0VgiCz;<$pBmIZdN!aalyCSEm)crpS9dcD^Y@XT1a3+zpi-`D}e#HV<} z$Y(G&o~PvL-xSVD5D?JqF3?B9rxGWeb=oEGJ3vRp5xfBPlngh1O$yI95EL+T8{GC@ z98i1H9KhZGFl|;`)_=QpM6H?eDPpw~^(aFQWwyXZ8_EEE4#@QeT_URray*mEOGsGc z6|sdXtq!hVZo=d#+9^@lm&L5|q&-GDCyUx#YQiccq;spOBe3V+VKdjJA=IL=Zn%P} zNk=_8u}VhzFf{UYZV0`lUwcD&)9AFx0@Fc6LD9A6Rd1=ga>Mi0)_QxM2ddCVRmZ0d z+J=uXc(?5JLX3=)e)Jm$HS2yF`44IKhwRnm2*669_J=2LlwuF5$1tAo@ROSU@-y+;Foy2IEl2^V1N;fk~YR z?&EP8#t&m0B=?aJeuz~lHjAzRBX>&x=A;gIvb>MD{XEV zV%l-+9N-)i;YH%nKP?>f`=?#`>B(`*t`aiPLoQM(a6(qs4p5KFjDBN?8JGrf3z8>= zi7sD)c)Nm~x{e<^jy4nTx${P~cwz_*a>%0_;ULou3kHCAD7EYkw@l$8TN#LO9jC( z1BeFW`k+bu5e8Ns^a8dPcjEVHM;r6UX+cN=Uy7HU)j-myRU0wHd$A1fNI~`4;I~`zC)3ul#8#^rXVSO*m}Ag>c%_;nj=Nv$rCZ z*~L@C@OZg%Q^m)lc-kcX&a*a5`y&DaRxh6O*dfhLfF+fU5wKs(1v*!TkZidw*)YBP za@r`3+^IHRFeO%!ai%rxy;R;;V^Fr=OJlpBX;(b*3+SIw}7= zIq$*Thr(Zft-RlY)D3e8V;BmD&HOfX+E$H#Y@B3?UL5L~_fA-@*IB-!gItK7PIgG9 zgWuGZK_nuZjHVT_Fv(XxtU%)58;W39vzTI2n&)&4Dmq7&JX6G>XFaAR{7_3QB6zsT z?$L8c*WdN~nZGiscY%5KljQARN;`w$gho=p006z;n(qIQ*Zu<``TMO3n0{ARL@gYh zoRwS*|Niw~cR!?hE{m*y@F`1)vx-JRfqET=dJ5_(076st(=lFfjtKHoYg`k3oNmo_ zNbQEw8&sO5jAYmkD|Zaz_yUb0rC})U!rCHOl}JhbYIDLzLvrZVw0~JO`d*6f;X&?V=#T@ND*cv^I;`sFeq4 z##H5;gpZTb^0Hz@3C*~u0AqqNZ-r%rN3KD~%Gw`0XsIq$(^MEb<~H(2*5G^<2(*aI z%7}WB+TRlMIrEK#s0 z93xn*Ohb=kWFc)BNHG4I(~RPn-R8#0lqyBBz5OM6o5|>x9LK@%HaM}}Y5goCQRt2C z{j*2TtT4ne!Z}vh89mjwiSXG=%DURar~=kGNNaO_+Nkb+tRi~Rkf!7a$*QlavziD( z83s4GmQ^Wf*0Bd04f#0HX@ua_d8 z23~z*53ePD6@xwZ(vdl0DLc=>cPIOPOdca&MyR^jhhKrdQO?_jJh`xV3GKz&2lvP8 zEOwW6L*ufvK;TN{=S&R@pzV^U=QNk^Ec}5H z+2~JvEVA{`uMAr)?Kf|aW>33`)UL@bnfIUQc~L;TsTQ6>r-<^rB8uoNOJ>HWgqMI8 zSW}pZmp_;z_2O5_RD|fGyTxaxk53Hg_3Khc<8AUzV|ZeK{fp|Ne933=1&_^Dbv5^u zB9n=*)k*tjHDRJ@$bp9mrh}qFn*s}npMl5BMDC%Hs0M0g-hW~P*3CNG06G!MOPEQ_ zi}Qs-6M8aMt;sL$vlmVBR^+Ry<64jrm1EI1%#j?c?4b*7>)a{aDw#TfTYKq+SjEFA z(aJ&z_0?0JB83D-i3Vh+o|XV4UP+YJ$9Boid2^M2en@APw&wx7vU~t$r2V`F|7Qfo z>WKgI@eNBZ-+Og<{u2ZiG%>YvH2L3fNpV9J;WLJoBZda)01Rn;o@){01{7E#ke(7U zHK>S#qZ(N=aoae*4X!0A{)nu0R_sKpi1{)u>GVjC+b5Jyl6#AoQ-1_3UDovNSo`T> z?c-@7XX*2GMy?k?{g)7?Sv;SJkmxYPJPs!&QqB12ejq`Lee^-cDveVWL^CTUldb(G zjDGe(O4P=S{4fF=#~oAu>LG>wrU^z_?3yt24FOx>}{^lCGh8?vtvY$^hbZ)9I0E3r3NOlb9I?F-Yc=r$*~l`4N^xzlV~N zl~#oc>U)Yjl0BxV>O*Kr@lKT{Z09OXt2GlvE38nfs+DD7exl|&vT;)>VFXJVZp9Np zDK}aO;R3~ag$X*|hRVY3OPax|PG`@_ESc8E!mHRByJbZQRS38V2F__7MW~sgh!a>98Q2%lUNFO=^xU52|?D=IK#QjwBky-C>zOWlsiiM&1n z;!&1((Xn1$9K}xabq~222gYvx3hnZPg}VMF_GV~5ocE=-v>V=T&RsLBo&`)DOyIj* zLV{h)JU_y*7SdRtDajP_Y+rBkNN*1_TXiKwHH2&p51d(#zv~s#HwbNy?<+(=9WBvo zw2hkk2Dj%kTFhY+$T+W-b7@qD!bkfN#Z2ng@Pd=i3-i?xYfs5Z*1hO?kd7Sp^9`;Y zM2jeGg<-nJD1er@Pc_cSY7wo5dzQX44=%6rn}P_SRbpzsA{6B+!$3B0#;}qwO37G^ zL(V_5JK`XT?OHVk|{_$vQ|oNEpab*BO4F zUTNQ7RUhnRsU`TK#~`)$icsvKh~(pl=3p6m98@k3P#~upd=k*u20SNcb{l^1rUa)>qO997)pYRWMncC8A&&MHlbW?7i^7M`+B$hH~Y|J zd>FYOGQ;j>Zc2e7R{KK7)0>>nn_jYJy&o@sK!4G>-rLKM8Hv)f;hi1D2fAc$+six2 zyVZ@wZ6x|fJ!4KrpCJY=!Mq0;)X)OoS~{Lkh6u8J`eK%u0WtKh6B>GW_)PVc zl}-k`p09qwGtZ@VbYJC!>29V?Dr>>vk?)o(x?!z*9DJ||9qG-&G~#kXxbw{KKYy}J zQKa-dPt~M~E}V?PhW0R26xdA%1T*%ra6SguGu50YHngOTIv)@N|YttEXo#OZfgtP7;H?EeZZxo<}3YlYxtBq znJ!WFR^tmGf0Py}N?kZ(#=VtpC@%xJkDmfcCoBTxq zr_|5gP?u1@vJZbxPZ|G0AW4=tpb84gM2DpJU||(b8kMOV1S3|(yuwZJ&rIiFW(U;5 zUtAW`O6F6Zy+eZ1EDuP~AAHlSY-+A_eI5Gx)%*uro5tljy}kCZU*_d7)oJ>oQSZ3* zneTn`{gnNC&uJd)0aMBzAg021?YJ~b(fmkwZAd696a=0NzBAqBN54KuNDwa*no(^O z6p05bioXUR^uXjpTol*ppHp%1v9e)vkoUAUJyBx3lw0UO39b0?^{}yb!$yca(@DUn zCquRF?t=Zb9`Ed3AI6|L{eX~ijVH`VzSMheKoP7LSSf4g>md>`yi!TkoG5P>Ofp+n z(v~rW+(5L96L{vBb^g51B=(o)?%%xhvT*A5btOpw(TKh^g^4c zw>0%X!_0`{iN%RbVk+A^f{w-4-SSf*fu@FhruNL##F~sF24O~u zyYF<3el2b$$wZ_|uW#@Ak+VAGk#e|kS8nL1g>2B-SNMjMp^8;-FfeofY2fphFHO!{ z*!o4oTb{4e;S<|JEs<1_hPsmAlVNk?_5-Fp5KKU&d#FiNW~Y+pVFk@Cua1I{T+1|+ zHx6rFMor)7L)krbilqsWwy@T+g3DiH5MyVf8Wy}XbEaoFIDr~y;@r&I>FMW{ z?Q+(IgyebZ)-i4jNoXQhq4Muy9Fv+OxU;9_Jmn+<`mEC#%2Q_2bpcgzcinygNI!&^ z=V$)o2&Yz04~+&pPWWn`rrWxJ&}8khR)6B(--!9Q zubo}h+1T)>a@c)H^i``@<^j?|r4*{;tQf78(xn0g39IoZw0(CwY1f<%F>kEaJ zp9u|IeMY5mRdAlw*+gSN^5$Q)ShM<~E=(c8QM+T-Qk)FyKz#Sw0EJ*edYcuOtO#~Cx^(M7w5 z3)rl#L)rF|(Vun2LkFr!rg8Q@=r>9p>(t3Gf_auiJ2Xx9HmxYTa|=MH_SUlYL`mz9 zTTS$`%;D-|Jt}AP1&k7PcnfFNTH0A-*FmxstjBDiZX?}%u%Yq94$fUT&z6od+(Uk> zuqsld#G(b$G8tus=M!N#oPd|PVFX)?M?tCD0tS%2IGTfh}3YA3f&UM)W$_GNV8 zQo+a(ml2Km4o6O%gKTCSDNq+#zCTIQ1*`TIJh~k6Gp;htHBFnne))rlFdGqwC6dx2+La1&Mnko*352k0y z+tQcwndQlX`nc6nb$A9?<-o|r*%aWXV#=6PQic0Ok_D;q>wbv&j7cKc!w4~KF#-{6 z(S%6Za)WpGIWf7jZ3svNG5OLs0>vCL9{V7cgO%zevIVMH{WgP*^D9ws&OqA{yr|m| zKD4*07dGXshJHd#e%x%J+qmS^lS|0Bp?{drv;{@{l9ArPO&?Q5=?OO9=}h$oVe#3b z3Yofj&Cb}WC$PxmRRS)H%&$1-)z7jELS}!u!zQ?A^Y{Tv4QVt*vd@uj-^t2fYRzQj zfxGR>-q|o$3sGn^#VzZ!QQx?h9`njeJry}@x?|k0-GTTA4y3t2E`3DZ!A~D?GiJup z)8%PK2^9OVRlP(24P^4_<|D=H^7}WlWu#LgsdHzB%cPy|f8dD3|A^mh4WXxhLTVu_ z@abE{6Saz|Y{rXYPd4$tfPYo}ef(oQWZ=4Bct-=_9`#Qgp4ma$n$`tOwq#&E18$B; z@Bp)bn3&rEi0>fWWZ@7k5WazfoX`SCO4jQWwVuo+$PmSZn^Hz?O(-tW@*DGxuf)V1 zO_xm&;NVCaHD4dqt(-MlszI3F-p?0!-e$fbiCeuaw66h^TTDLWuaV<@C-`=Xe5WL) zwooG7h>4&*)p3pKMS3O!4>-4jQUN}iAMQ)2*70?hP~)TzzR?-f@?Aqy$$1Iy8VGG$ zMM?8;j!pUX7QQD$gRc_#+=raAS577ga-w?jd`vCiN5lu)dEUkkUPl9!?{$IJNxQys z*E4e$eF&n&+AMRQR2gcaFEjAy*r)G!s(P6D&TfoApMFC_*Ftx0|D0@E-=B7tezU@d zZ{hGiN;YLIoSeRS;9o%dEua4b%4R3;$SugDjP$x;Z!M!@QibuSBb)HY!3zJ7M;^jw zlx6AD50FD&p3JyP*>o+t9YWW8(7P2t!VQQ21pHJOcG_SXQD;(5aX#M6x##5H_Re>6lPyDCjxr*R(+HE%c&QN+b^tbT zXBJk?p)zhJj#I?&Y2n&~XiytG9!1ox;bw5Rbj~)7c(MFBb4>IiRATdhg zmiEFlj@S_hwYYI(ki{}&<;_7(Z0Qkfq>am z&LtL=2qc7rWguk3BtE4zL41@#S;NN*-jWw|7Kx7H7~_%7fPt;TIX}Ubo>;Rmj94V> zNB1=;-9AR7s`Pxn}t_6^3ahlq53e&!Lh85uG zec0vJY_6e`tg7LgfrJ3k!DjR)Bi#L@DHIrZ`sK=<5O0Ip!fxGf*OgGSpP@Hbbe&$9 z;ZI}8lEoC2_7;%L2=w?tb%1oL0V+=Z`7b=P&lNGY;yVBazXRYu;+cQDKvm*7NCxu&i;zub zAJh#11%?w>E2rf2e~C4+rAb-&$^vsdACs7 z@|Ra!OfVM(ke{vyiqh7puf&Yp6cd6{DptUteYfIRWG3pI+5< zBVBI_xkBAc<(pcb$!Y%dTW(b;B;2pOI-(QCsLv@U-D1XJ z(Gk8Q3l7Ws46Aktuj>|s{$6zA&xCPuXL-kB`CgYMs}4IeyG*P51IDwW?8UNQd+$i~ zlxOPtSi5L|gJcF@DwmJA5Ju8HEJ>o{{upwIpb!f{2(vLNBw`7xMbvcw<^{Fj@E~1( z?w`iIMieunS#>nXlmUcSMU+D3rX28f?s7z;X=se6bo8;5vM|O^(D6{A9*ChnGH!RG zP##3>LDC3jZPE4PH32AxrqPk|yIIrq~`aL-=}`okhNu9aT%q z1b)7iJ)CN=V#Ly84N_r7U^SH2FGdE5FpTO2 z630TF$P>GNMu8`rOytb(lB2};`;P4YNwW1<5d3Q~AX#P0aX}R2b2)`rgkp#zTxcGj zAV^cvFbhP|JgWrq_e`~exr~sIR$6p5V?o4Wym3kQ3HA+;Pr$bQ0(PmADVO%MKL!^q z?zAM8j1l4jrq|5X+V!8S*2Wl@=7*pPgciTVK6kS1Ge zMsd_u6DFK$jTnvVtE;qa+8(1sGBu~n&F%dh(&c(Zs4Fc#A=gG^^%^AyH}1^?|8quj zl@Z47h$){PlELJgYZCIHHL= z{U8O>Tw4x3<1{?$8>k-P<}1y9DmAZP_;(3Y*{Sk^H^A=_iSJ@+s5ktgwTXz_2$~W9>VVZsfwCm@s0sQ zeB50_yu@uS+e7QoPvdCwDz{prjo(AFwR%C?z`EL{1`|coJHQTk^nX=tvs1<0arUOJ z!^`*x&&BvTYmemyZ)2p~{%eYX=JVR?DYr(rNgqRMA5E1PR1Iw=prk=L2ldy3r3Vg@27IZx43+ywyzr-X*p*d@tZV+!U#~$-q=8c zgdSuh#r?b4GhEGNai)ayHQpk>5(%j5c@C1K3(W1pb~HeHpaqijJZa-e6vq_8t-^M^ zBJxq|MqZc?pjXPIH}70a5vt!IUh;l}<>VX<-Qcv^u@5(@@M2CHSe_hD$VG-eiV^V( zj7*9T0?di?P$FaD6oo?)<)QT>Npf6Og!GO^GmPV(Km0!=+dE&bk#SNI+C9RGQ|{~O*VC+tXK3!n`5 zHfl6>lwf_aEVV3`0T!aHNZLsj$paS$=LL(?b!Czaa5bbSuZ6#$_@LK<(7yrrl+80| z{tOFd=|ta2Z`^ssozD9BINn45NxUeCQis?-BKmU*Kt=FY-NJ+)8S1ecuFtN-M?&42 zl2$G>u!iNhAk*HoJ^4v^9#ORYp5t^wDj6|lx~5w45#E5wVqI1JQ~9l?nPp1YINf++ zMAdSif~_ETv@Er(EFBI^@L4BULFW>)NI+ejHFP*T}UhWNN`I)RRS8za? z*@`1>9ZB}An%aT5K=_2iQmfE;GcBVHLF!$`I99o5GO`O%O_zLr9AG18>&^HkG(;=V z%}c!OBQ~?MX(9h~tajX{=x)+!cbM7$YzTlmsPOdp2L-?GoW`@{lY9U3f;OUo*BwRB z8A+nv(br0-SH#VxGy#ZrgnGD(=@;HME;yd46EgWJ`EL%oXc&lFpc@Y}^>G(W>h_v_ zlN!`idhX+OjL+~T?19sroAFVGfa5tX-D49w$1g2g_-T|EpHL6}K_aX4$K=LTvwtlF zL*z}j{f+Uoe7{-px3_5iKPA<_7W=>Izkk)!l9ez2w%vi(?Y;i8AxRNLSOGDzNoqoI zP!1uAl}r=_871(G?y`i&)-7{u=%nxk7CZ_Qh#!|ITec zwQn`33GTUM`;D2POWnkqngqJhJRlM>CTONzTG}>^Q0wUunQyn|TAiHzyX2_%ATx%P z%7gW)%4rA9^)M<_%k@`Y?RbC<29sWU&5;@|9thf2#zf8z12$hRcZ!CSb>kUp=4N#y zl3hE#y6>kkA8VY2`W`g5Ip?2qC_BY$>R`iGQLhz2-S>x(RuWv)SPaGdl^)gGw7tjR zH@;jwk!jIaCgSg_*9iF|a);sRUTq30(8I(obh^|}S~}P4U^BIGYqcz;MPpC~Y@k_m zaw4WG1_vz2GdCAX!$_a%GHK**@IrHSkGoN>)e}>yzUTm52on`hYot7cB=oA-h1u|R ztH$11t?54Qg2L+i33FPFKKRm1aOjKST{l1*(nps`>sv%VqeVMWjl5+Gh+9);hIP8? zA@$?}Sc z3qIRpba+y5yf{R6G(u8Z^vkg0Fu&D-7?1s=QZU`Ub{-!Y`I?AGf1VNuc^L3v>)>i# z{DV9W$)>34wnzAXUiV^ZpYKw>UElrN_5Xj6{r_3| z$X5PK`e5$7>~9Dj7gK5ash(dvs`vwfk}&RD`>04;j62zoXESkFBklYaKm5seyiX(P zqQ-;XxlV*yg?Dhlx%xt!b0N3GHp@(p$A;8|%# zZ5m2KL|{on4nr>2_s9Yh=r5ScQ0;aMF)G$-9-Ca6%wA`Pa)i?NGFA|#Yi?{X-4ZO_ z^}%7%vkzvUHa$-^Y#aA+aiR5sa%S|Ebyn`EV<3Pc?ax_f>@sBZF1S;7y$CXd5t5=WGsTKBk8$OfH4v|0?0I=Yp}7c=WBSCg!{0n)XmiU;lfx)**zZaYqmDJelxk$)nZyx5`x$6R|fz(;u zEje5Dtm|a%zK!!tk3{i9$I2b{vXNFy%Bf{50X!x{98+BsDr_u9i>G5%*sqEX|06J0 z^IY{UcEbj6LDwuMh7cH`H@9sVt1l1#8kEQ(LyT@&+K}(ReE`ux8gb0r6L_#bDUo^P z3Ka2lRo52Hdtl_%+pwVs14=q`{d^L58PsU@AMf(hENumaxM{7iAT5sYmWh@hQCO^ zK&}ijo=`VqZ#a3vE?`7QW0ZREL17ZvDfdqKGD?0D4fg{7v%|Yj&_jcKJAB)>=*RS* zto8p6@k%;&^ZF>hvXm&$PCuEp{uqw3VPG$9VMdW5$w-fy2CNNT>E;>ejBgy-m_6`& z97L1p{%srn@O_JQgFpa_#f(_)eb#YS>o>q3(*uB;uZb605(iqM$=NK{nHY=+X2*G) zO3-_Xh%aG}fHWe*==58zBwp%&`mge<8uq8;xIxOd=P%9EK!34^E9sk|(Zq1QSz-JVeP12Fp)-`F|KY$LPwUE?rku zY@OJ)Z9A!ojfzfeyJ9;zv2EM7ZQB)AR5xGa-tMn^bl)FmoIiVyJ@!~@%{}qXXD&Ns zPnfe5U+&ohKefILu_1mPfLGuapX@btta5C#gPB2cjk5m4T}Nfi+Vfka!Yd(L?-c~5 z#ZK4VeQEXNPc4r$K00Fg>g#_W!YZ)cJ?JTS<&68_$#cZT-ME`}tcwqg3#``3M3UPvn+pi}(VNNx6y zFIMVb6OwYU(2`at$gHba*qrMVUl8xk5z-z~fb@Q3Y_+aXuEKH}L+>eW__!IAd@V}L zkw#s%H0v2k5-=vh$^vPCuAi22Luu3uKTf6fPo?*nvj$9(u)4$6tvF-%IM+3pt*cgs z_?wW}J7VAA{_~!?))?s6{M=KPpVhg4fNuU*|3THp@_(q!b*hdl{fjRVFWtu^1dV(f z6iOux9hi&+UK=|%M*~|aqFK{Urfl!TA}UWY#`w(0P!KMe1Si{8|o))Gy6d7;!JQYhgMYmXl?3FfOM2nQGN@~Ap6(G z3+d_5y@=nkpKAhRqf{qQ~k7Z$v&l&@m7Ppt#FSNzKPZM z8LhihcE6i=<(#87E|Wr~HKvVWhkll4iSK$^mUHaxgy8*K$_Zj;zJ`L$naPj+^3zTi z-3NTaaKnD5FPY-~?Tq6QHnmDDRxu0mh0D|zD~Y=vv_qig5r-cIbCpxlju&8Sya)@{ zsmv6XUSi)@(?PvItkiZEeN*)AE~I_?#+Ja-r8$(XiXei2d@Hi7Rx8+rZZb?ZLa{;@*EHeRQ-YDadz~M*YCM4&F-r;E#M+@CSJMJ0oU|PQ^ z=E!HBJDMQ2TN*Y(Ag(ynAL8%^v;=~q?s4plA_hig&5Z0x_^Oab!T)@6kRN$)qEJ6E zNuQjg|G7iwU(N8pI@_6==0CL;lRh1dQF#wePhmu@hADFd3B5KIH#dx(2A zp~K&;Xw}F_N6CU~0)QpQk7s$a+LcTOj1%=WXI(U=Dv!6 z{#<#-)2+gCyyv=Jw?Ab#PVkxPDeH|sAxyG`|Ys}A$PW4TdBv%zDz z^?lwrxWR<%Vzc8Sgt|?FL6ej_*e&rhqJZ3Y>k=X(^dytycR;XDU16}Pc9Vn0>_@H+ zQ;a`GSMEG64=JRAOg%~L)x*w{2re6DVprNp+FcNra4VdNjiaF0M^*>CdPkt(m150rCue?FVdL0nFL$V%5y6N z%eLr5%YN7D06k5ji5*p4v$UMM)G??Q%RB27IvH7vYr_^3>1D-M66#MN8tWGw>WED} z5AhlsanO=STFYFs)Il_0i)l)f<8qn|$DW7ZXhf5xI;m+7M5-%P63XFQrG9>DMqHc} zsgNU9nR`b}E^mL5=@7<1_R~j@q_2U^3h|+`7YH-?C=vme1C3m`Fe0HC>pjt6f_XMh zy~-i-8R46QNYneL4t@)<0VU7({aUO?aH`z4V2+kxgH5pYD5)wCh75JqQY)jIPN=U6 z+qi8cGiOtXG2tXm;_CfpH9ESCz#i5B(42}rBJJF$jh<1sbpj^8&L;gzGHb8M{of+} zzF^8VgML2O9nxBW7AvdEt90vp+#kZxWf@A)o9f9}vKJy9NDBjBW zSt=Hcs=YWCwnfY1UYx*+msp{g!w0HC<_SM!VL1(I2PE?CS}r(eh?{I)mQixmo5^p# zV?2R!R@3GV6hwTCrfHiK#3Orj>I!GS2kYhk1S;aFBD_}u2v;0HYFq}Iz1Z(I4oca4 zxquja8$+8JW_EagDHf$a1OTk5S97umGSDaj)gH=fLs9>_=XvVj^Xj9a#gLdk=&3tl zfmK9MNnIX9v{?%xdw7568 zNrZ|roYs(vC4pHB5RJ8>)^*OuyNC>x7ad)tB_}3SgQ96+-JT^Qi<`xi=)_=$Skwv~ zdqeT9Pa`LYvCAn&rMa2aCDV(TMI#PA5g#RtV|CWpgDYRA^|55LLN^uNh*gOU>Z=a06qJ;$C9z8;n-Pq=qZnc1zUwJ@t)L;&NN+E5m zRkQ(SeM8=l-aoAKGKD>!@?mWTW&~)uF2PYUJ;tB^my`r9n|Ly~0c%diYzqs9W#FTjy?h&X3TnH zXqA{QI82sdjPO->f=^K^f>N`+B`q9&rN0bOXO79S&a9XX8zund(kW7O76f4dcWhIu zER`XSMSFbSL>b;Rp#`CuGJ&p$s~G|76){d?xSA5wVg##_O0DrmyEYppyBr%fyWbbv zp`K84JwRNP$d-pJ!Qk|(RMr?*!wi1if-9G#0p>>1QXKXWFy)eB3ai)l3601q8!9JC zvU#ZWWDNKq9g6fYs?JQ)Q4C_cgTy3FhgKb8s&m)DdmL5zhNK#8wWg!J*7G7Qhe9VU zha?^AQTDpYcuN!B+#1dE*X{<#!M%zfUQbj=zLE{dW0XeQ7-oIsGY6RbkP2re@Q{}r_$iiH0xU%iN*ST`A)-EH6eaZB$GA#v)cLi z*MpA(3bYk$oBDKAzu^kJoSUsDd|856DApz={3u8sbQV@JnRkp2nC|)m;#T=DvIL-O zI4vh;g7824l}*`_p@MT4+d`JZ2%6NQh=N9bmgJ#q!hK@_<`HQq3}Z8Ij>3%~<*= zcv=!oT#5xmeGI92lqm9sGVE%#X$ls;St|F#u!?5Y7syhx6q#MVRa&lBmmn%$C0QzU z);*ldgwwCmzM3uglr}!Z2G+?& zf%Dpo&mD%2ZcNFiN-Z0f;c_Q;A%f@>26f?{d1kxIJD}LxsQkB47SAdwinfMILZdN3 zfj^HmTzS3Ku5BxY>ANutS8WPQ-G>v4^_Qndy==P3pDm+Xc?>rUHl-4+^%Sp5atOja z2oP}ftw-rqnb}+khR3CrRg^ibi6?QYk1*i^;kQGirQ=uB9Sd1NTfT-Rbv;hqnY4neE5H1YUrjS2m+2&@uXiAo- zrKUX|Ohg7(6F(AoP~tj;NZlV#xsfo-5reuQHB$&EIAhyZk;bL;k9ouDmJNBAun;H& zn;Of1z_Qj`x&M;5X;{s~iGzBQTY^kv-k{ksbE*Dl%Qf%N@hQCfY~iUw!=F-*$cpf2 z3wix|aLBV0b;W@z^%7S{>9Z^T^fLOI68_;l@+Qzaxo`nAI8emTV@rRhEKZ z?*z_{oGdI~R*#<2{bkz$G~^Qef}$*4OYTgtL$e9q!FY7EqxJ2`zk6SQc}M(k(_MaV zSLJnTXw&@djco1~a(vhBl^&w=$fa9{Sru>7g8SHahv$&Bl(D@(Zwxo_3r=;VH|uc5 zi1Ny)J!<(KN-EcQ(xlw%PNwK8U>4$9nVOhj(y0l9X^vP1TA>r_7WtSExIOsz`nDOP zs}d>Vxb2Vo2e5x8p(n~Y5ggAyvib>d)6?)|E@{FIz?G3PVGLf7-;BxaP;c?7ddH$z zA+{~k^V=bZuXafOv!RPsE1GrR3J2TH9uB=Z67gok+u`V#}BR86hB1xl}H4v`F+mRfr zYhortD%@IGfh!JB(NUNSDh+qDz?4ztEgCz&bIG-Wg7w-ua4ChgQR_c+z8dT3<1?uX z*G(DKy_LTl*Ea!%v!RhpCXW1WJO6F`bgS-SB;Xw9#! z<*K}=#wVu9$`Yo|e!z-CPYH!nj7s9dEPr-E`DXUBu0n!xX~&|%#G=BeM?X@shQQMf zMvr2!y7p_gD5-!Lnm|a@z8Of^EKboZsTMk%5VsJEm>VsJ4W7Kv{<|#4f-qDE$D-W>gWT%z-!qXnDHhOvLk=?^a1*|0j z{pW{M0{#1VcR5;F!!fIlLVNh_Gj zbnW(_j?0c2q$EHIi@fSMR{OUKBcLr{Y&$hrM8XhPByyZaXy|dd&{hYQRJ9@Fn%h3p7*VQolBIV@Eq`=y%5BU~3RPa^$a?ixp^cCg z+}Q*X+CW9~TL29@OOng(#OAOd!)e$d%sr}^KBJ-?-X&|4HTmtemxmp?cT3uA?md4% zT8yZ0U;6Rg6JHy3fJae{6TMGS?ZUX6+gGTT{Q{)SI85$5FD{g-eR%O0KMpWPY`4@O zx!hen1*8^E(*}{m^V_?}(b5k3hYo=T+$&M32+B`}81~KKZhY;2H{7O-M@vbCzuX0n zW-&HXeyr1%I3$@ns-V1~Lb@wIpkmx|8I~ob1Of7i6BTNysEwI}=!nU%q7(V_^+d*G z7G;07m(CRTJup!`cdYi93r^+LY+`M*>aMuHJm(A8_O8C#A*$!Xvddgpjx5)?_EB*q zgE8o5O>e~9IiSC@WtZpF{4Bj2J5eZ>uUzY%TgWF7wdDE!fSQIAWCP)V{;HsU3ap?4 znRsiiDbtN7i9hapO;(|Ew>Ip2TZSvK9Z^N21%J?OiA_&eP1{(Pu_=%JjKy|HOardq ze?zK^K zA%sjF64*Wufad%H<) z^|t>e*h+Z1#l=5wHexzt9HNDNXgM=-OPWKd^5p!~%SIl>Fo&7BvNpbf8{NXmH)o{r zO=aBJ;meX1^{O%q;kqdw*5k!Y7%t_30 zy{nGRVc&5qt?dBwLs+^Sfp;f`YVMSB#C>z^a9@fpZ!xb|b-JEz1LBX7ci)V@W+kvQ89KWA0T~Lj$aCcfW#nD5bt&Y_< z-q{4ZXDqVg?|0o)j1%l0^_it0WF*LCn-+)c!2y5yS7aZIN$>0LqNnkujV*YVes(v$ zY@_-!Q;!ZyJ}Bg|G-~w@or&u0RO?vlt5*9~yeoPV_UWrO2J54b4#{D(D>jF(R88u2 zo#B^@iF_%S>{iXSol8jpmsZuJ?+;epg>k=$d`?GSegAVp3n$`GVDvK${N*#L_1`44 z{w0fL{2%)0|E+qgZtjX}itZz^KJt4Y;*8uSK}Ft38+3>j|K(PxIXXR-t4VopXo#9# zt|F{LWr-?34y`$nLBVV_*UEgA6AUI65dYIbqpNq9cl&uLJ0~L}<=ESlOm?Y-S@L*d z<7vt}`)TW#f%Rp$Q}6@3=j$7Tze@_uZO@aMn<|si{?S}~maII`VTjs&?}jQ4_cut9$)PEqMukwoXobzaKx^MV z2fQwl+;LSZ$qy%Tys0oo^K=jOw$!YwCv^ei4NBVauL)tN%=wz9M{uf{IB(BxK|lT*pFkmNK_1tV`nb%jH=a0~VNq2RCKY(rG7jz!-D^k)Ec)yS%17pE#o6&eY+ z^qN(hQT$}5F(=4lgNQhlxj?nB4N6ntUY6(?+R#B?W3hY_a*)hnr4PA|vJ<6p`K3Z5Hy z{{8(|ux~NLUW=!?9Qe&WXMTAkQnLXg(g=I@(VG3{HE13OaUT|DljyWXPs2FE@?`iU z4GQlM&Q=T<4&v@Fe<+TuXiZQT3G~vZ&^POfmI1K2h6t4eD}Gk5XFGpbj1n_g*{qmD6Xy z`6Vv|lLZtLmrnv*{Q%xxtcWVj3K4M%$bdBk_a&ar{{GWyu#ljM;dII;*jP;QH z#+^o-A4np{@|Mz+LphTD0`FTyxYq#wY)*&Ls5o{0z9yg2K+K7ZN>j1>N&;r+Z`vI| zDzG1LJZ+sE?m?>x{5LJx^)g&pGEpY=fQ-4}{x=ru;}FL$inHemOg%|R*ZXPodU}Kh zFEd5#+8rGq$Y<_?k-}r5zgQ3jRV=ooHiF|@z_#D4pKVEmn5CGV(9VKCyG|sT9nc=U zEoT67R`C->KY8Wp-fEcjjFm^;Cg(ls|*ABVHq8clBE(;~K^b+S>6uj70g? z&{XQ5U&!Z$SO7zfP+y^8XBbiu*Cv-yJG|l-oe*!s5$@Lh_KpxYL2sx`B|V=dETN>5K+C+CU~a_3cI8{vbu$TNVdGf15*>D zz@f{zIlorkY>TRh7mKuAlN9A0>N>SV`X)+bEHms=mfYTMWt_AJtz_h+JMmrgH?mZt zm=lfdF`t^J*XLg7v+iS)XZROygK=CS@CvUaJo&w2W!Wb@aa?~Drtf`JV^cCMjngVZ zv&xaIBEo8EYWuML+vxCpjjY^s1-ahXJzAV6hTw%ZIy!FjI}aJ+{rE&u#>rs)vzuxz z+$5z=7W?zH2>Eb32dvgHYZtCAf!=OLY-pb4>Ae79rd68E2LkVPj-|jFeyqtBCCwiW zkB@kO_(3wFq)7qwV}bA=zD!*@UhT`geq}ITo%@O(Z5Y80nEX~;0-8kO{oB6|(4fQh z);73T!>3@{ZobPwRv*W?7m0Ml9GmJBCJd&6E?hdj9lV= z4flNfsc(J*DyPv?RCOx!MSvk(M952PJ-G|JeVxWVjN~SNS6n-_Ge3Q;TGE;EQvZg86%wZ`MB zSMQua(i*R8a75!6$QRO^(o7sGoomb+Y{OMy;m~Oa`;P9Yqo>?bJAhqXxLr7_3g_n>f#UVtxG!^F#1+y@os6x(sg z^28bsQ@8rw%Gxk-stAEPRbv^}5sLe=VMbkc@Jjimqjvmd!3E7+QnL>|(^3!R} zD-l1l7*Amu@j+PWLGHXXaFG0Ct2Q=}5YNUxEQHCAU7gA$sSC<5OGylNnQUa>>l%sM zyu}z6i&({U@x^hln**o6r2s-(C-L50tQvz|zHTqW!ir?w&V23tuYEDJVV#5pE|OJu z7^R!A$iM$YCe?8n67l*J-okwfZ+ZTkGvZ)tVPfR;|3gyFjF)8V zyXXN=!*bpyRg9#~Bg1+UDYCt0 ztp4&?t1X0q>uz;ann$OrZs{5*r`(oNvw=$7O#rD|Wuv*wIi)4b zGtq4%BX+kkagv3F9Id6~-c+1&?zny%w5j&nk9SQfo0k4LhdSU_kWGW7axkfpgR`8* z!?UTG*Zi_baA1^0eda8S|@&F z{)Rad0kiLjB|=}XFJhD(S3ssKlveFFmkN{Vl^_nb!o5M!RC=m)V&v2%e?ZoRC@h3> zJ(?pvToFd`*Zc@HFPL#=otWKwtuuQ_dT-Hr{S%pQX<6dqVJ8;f(o)4~VM_kEQkMR+ zs1SCVi~k>M`u1u2xc}>#D!V&6nOOh-E$O&SzYrjJdZpaDv1!R-QGA141WjQe2s0J~ zQ;AXG)F+K#K8_5HVqRoRM%^EduqOnS(j2)|ctA6Q^=|s_WJYU;Z%5bHp08HPL`YF2 zR)Ad1z{zh`=sDs^&V}J z%$Z$!jd7BY5AkT?j`eqMs%!Gm@T8)4w3GYEX~IwgE~`d|@T{WYHkudy(47brgHXx& zBL1yFG6!!!VOSmDxBpefy2{L_u5yTwja&HA!mYA#wg#bc-m%~8aRR|~AvMnind@zs zy>wkShe5&*un^zvSOdlVu%kHsEo>@puMQ`b1}(|)l~E{5)f7gC=E$fP(FC2=F<^|A zxeIm?{EE!3sO!Gr7e{w)Dx(uU#3WrFZ>ibmKSQ1tY?*-Nh1TDHLe+k*;{Rp!Bmd_m zb#^kh`Y*8l|9Cz2e{;RL%_lg{#^Ar+NH|3z*Zye>!alpt{z;4dFAw^^H!6ING*EFc z_yqhr8d!;%nHX9AKhFQZBGrSzfzYCi%C!(Q5*~hX>)0N`vbhZ@N|i;_972WSx*>LH z87?en(;2_`{_JHF`Sv6Wlps;dCcj+8IJ8ca6`DsOQCMb3n# z3)_w%FuJ3>fjeOOtWyq)ag|PmgQbC-s}KRHG~enBcIwqIiGW8R8jFeBNY9|YswRY5 zjGUxdGgUD26wOpwM#8a!Nuqg68*dG@VM~SbOroL_On0N6QdT9?)NeB3@0FCC?Z|E0 z6TPZj(AsPtwCw>*{eDEE}Gby>0q{*lI+g2e&(YQrsY&uGM{O~}(oM@YWmb*F zA0^rr5~UD^qmNljq$F#ARXRZ1igP`MQx4aS6*MS;Ot(1L5jF2NJ;de!NujUYg$dr# z=TEL_zTj2@>ZZN(NYCeVX2==~=aT)R30gETO{G&GM4XN<+!&W&(WcDP%oL8PyIVUC zs5AvMgh6qr-2?^unB@mXK*Dbil^y-GTC+>&N5HkzXtozVf93m~xOUHn8`HpX=$_v2 z61H;Z1qK9o;>->tb8y%#4H)765W4E>TQ1o0PFj)uTOPEvv&}%(_mG0ISmyhnQV33Z$#&yd{ zc{>8V8XK$3u8}04CmAQ#I@XvtmB*s4t8va?-IY4@CN>;)mLb_4!&P3XSw4pA_NzDb zORn!blT-aHk1%Jpi>T~oGLuh{DB)JIGZ9KOsciWs2N7mM1JWM+lna4vkDL?Q)z_Ct z`!mi0jtr+4*L&N7jk&LodVO#6?_qRGVaucqVB8*us6i3BTa^^EI0x%EREQSXV@f!lak6Wf1cNZ8>*artIJ(ADO*=<-an`3zB4d*oO*8D1K!f z*A@P1bZCNtU=p!742MrAj%&5v%Xp_dSX@4YCw%F|%Dk=u|1BOmo)HsVz)nD5USa zR~??e61sO(;PR)iaxK{M%QM_rIua9C^4ppVS$qCT9j2%?*em?`4Z;4@>I(c%M&#cH z>4}*;ej<4cKkbCAjjDsyKS8rIm90O)Jjgyxj5^venBx&7B!xLmzxW3jhj7sR(^3Fz z84EY|p1NauwXUr;FfZjdaAfh%ivyp+^!jBjJuAaKa!yCq=?T_)R!>16?{~p)FQ3LDoMyG%hL#pR!f@P%*;#90rs_y z@9}@r1BmM-SJ#DeuqCQk=J?ixDSwL*wh|G#us;dd{H}3*-Y7Tv5m=bQJMcH+_S`zVtf;!0kt*(zwJ zs+kedTm!A}cMiM!qv(c$o5K%}Yd0|nOd0iLjus&;s0Acvoi-PFrWm?+q9f^FslxGi z6ywB`QpL$rJzWDg(4)C4+!2cLE}UPCTBLa*_=c#*$b2PWrRN46$y~yST3a2$7hEH= zNjux+wna^AzQ=KEa_5#9Ph=G1{S0#hh1L3hQ`@HrVnCx{!fw_a0N5xV(iPdKZ-HOM za)LdgK}1ww*C_>V7hbQnTzjURJL`S%`6nTHcgS+dB6b_;PY1FsrdE8(2K6FN>37!62j_cBlui{jO^$dPkGHV>pXvW0EiOA zqW`YaSUBWg_v^Y5tPJfWLcLpsA8T zG)!x>pKMpt!lv3&KV!-um= zKCir6`bEL_LCFx4Z5bAFXW$g3Cq`?Q%)3q0r852XI*Der*JNuKUZ`C{cCuu8R8nkt z%pnF>R$uY8L+D!V{s^9>IC+bmt<05h**>49R*#vpM*4i0qRB2uPbg8{{s#9yC;Z18 zD7|4m<9qneQ84uX|J&f-g8a|nFKFt34@Bt{CU`v(SYbbn95Q67*)_Esl_;v291s=9 z+#2F2apZU4Tq=x+?V}CjwD(P=U~d<=mfEFuyPB`Ey82V9G#Sk8H_Ob_RnP3s?)S_3 zr%}Pb?;lt_)Nf>@zX~D~TBr;-LS<1I##8z`;0ZCvI_QbXNh8Iv)$LS=*gHr;}dgb=w5$3k2la1keIm|=7<-JD>)U%=Avl0Vj@+&vxn zt-)`vJxJr88D&!}2^{GPXc^nmRf#}nb$4MMkBA21GzB`-Or`-3lq^O^svO7Vs~FdM zv`NvzyG+0T!P8l_&8gH|pzE{N(gv_tgDU7SWeiI-iHC#0Ai%Ixn4&nt{5y3(GQs)i z&uA;~_0shP$0Wh0VooIeyC|lak__#KVJfxa7*mYmZ22@(<^W}FdKjd*U1CqSjNKW% z*z$5$=t^+;Ui=MoDW~A7;)Mj%ibX1_p4gu>RC}Z_pl`U*{_z@+HN?AF{_W z?M_X@o%w8fgFIJ$fIzBeK=v#*`mtY$HC3tqw7q^GCT!P$I%=2N4FY7j9nG8aIm$c9 zeKTxVKN!UJ{#W)zxW|Q^K!3s;(*7Gbn;e@pQBCDS(I|Y0euK#dSQ_W^)sv5pa%<^o zyu}3d?Lx`)3-n5Sy9r#`I{+t6x%I%G(iewGbvor&I^{lhu-!#}*Q3^itvY(^UWXgvthH52zLy&T+B)Pw;5>4D6>74 zO_EBS)>l!zLTVkX@NDqyN2cXTwsUVao7$HcqV2%t$YzdAC&T)dwzExa3*kt9d(}al zA~M}=%2NVNUjZiO7c>04YH)sRelXJYpWSn^aC$|Ji|E13a^-v2MB!Nc*b+=KY7MCm zqIteKfNkONq}uM;PB?vvgQvfKLPMB8u5+Am=d#>g+o&Ysb>dX9EC8q?D$pJH!MTAqa=DS5$cb+;hEvjwVfF{4;M{5U&^_+r zvZdu_rildI!*|*A$TzJ&apQWV@p{!W`=?t(o0{?9y&vM)V)ycGSlI3`;ps(vf2PUq zX745#`cmT*ra7XECC0gKkpu2eyhFEUb?;4@X7weEnLjXj_F~?OzL1U1L0|s6M+kIhmi%`n5vvDALMagi4`wMc=JV{XiO+^ z?s9i7;GgrRW{Mx)d7rj)?(;|b-`iBNPqdwtt%32se@?w4<^KU&585_kZ=`Wy^oLu9 z?DQAh5z%q;UkP48jgMFHTf#mj?#z|=w= z(q6~17Vn}P)J3M?O)x))%a5+>TFW3No~TgP;f}K$#icBh;rSS+R|}l鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b82aa23 --- /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.7-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/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig new file mode 100644 index 0000000..26af985 --- /dev/null +++ b/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,3 @@ +TEAM_ID= +BUNDLE_ID=org.example.project.KotlinProject +APP_NAME=KotlinProject \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4de11f9 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,398 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; + 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 7555FF7B242A565900829871 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B92378962B6B1156000C7307 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058557D7273AAEEB004C7B11 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 42799AB246E5F90AF97AA0EF /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 7555FF72242A565900829871 = { + isa = PBXGroup; + children = ( + AB1DB47929225F7C00F7AF9C /* Configuration */, + 7555FF7D242A565900829871 /* iosApp */, + 7555FF7C242A565900829871 /* Products */, + 42799AB246E5F90AF97AA0EF /* Frameworks */, + ); + sourceTree = ""; + }; + 7555FF7C242A565900829871 /* Products */ = { + isa = PBXGroup; + children = ( + 7555FF7B242A565900829871 /* KotlinProject.app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 058557BA273AAA24004C7B11 /* Assets.xcassets */, + 7555FF82242A565900829871 /* ContentView.swift */, + 7555FF8C242A565B00829871 /* Info.plist */, + 2152FB032600AC8F00CF470E /* iOSApp.swift */, + 058557D7273AAEEB004C7B11 /* Preview Content */, + ); + path = iosApp; + sourceTree = ""; + }; + AB1DB47929225F7C00F7AF9C /* Configuration */ = { + isa = PBXGroup; + children = ( + AB3632DC29227652001CCB65 /* Config.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7555FF7A242A565900829871 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, + 7555FF77242A565900829871 /* Sources */, + B92378962B6B1156000C7307 /* Frameworks */, + 7555FF79242A565900829871 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = 7555FF7B242A565900829871 /* KotlinProject.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7555FF73242A565900829871 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = orgName; + TargetAttributes = { + 7555FF7A242A565900829871 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7555FF72242A565900829871; + packageReferences = ( + ); + productRefGroup = 7555FF7C242A565900829871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7555FF7A242A565900829871 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7555FF79242A565900829871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + 7555FF83242A565900829871 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7555FFA3242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7555FFA4242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7555FFA6242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + composeApp, + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7555FFA7242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + composeApp, + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA3242A565B00829871 /* Debug */, + 7555FFA4242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA6242A565B00829871 /* Debug */, + 7555FFA7242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7555FF73242A565900829871 /* Project object */; +} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..ee7e3ca --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..8edf56e --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "app-icon-1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..53fc536fb9ac5c1dbb27c7e1da13db3760070a11 GIT binary patch literal 67285 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 BXNCX( literal 0 HcmV?d00001 diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..4aa7c53 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000..3cd5c32 --- /dev/null +++ b/iosApp/iosApp/ContentView.swift @@ -0,0 +1,21 @@ +import UIKit +import SwiftUI +import ComposeApp + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + MainViewControllerKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ContentView: View { + var body: some View { + ComposeView() + .ignoresSafeArea(.keyboard) // Compose has own keyboard handler + } +} + + + diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 0000000..412e378 --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..4aa7c53 --- /dev/null +++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..0648e86 --- /dev/null +++ b/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 0000000..412d48d --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) +} + +kotlin { + jvm() + androidTarget { + publishLibraryVariants("release") + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + iosX64() + iosArm64() + iosSimulatorArm64() + linuxX64() + + sourceSets { + val commonMain by getting { + dependencies { + //put your multiplatform dependencies here + } + } + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + } + } + } +} + +android { + namespace = "org.jetbrains.kotlinx.multiplatform.library.template" + compileSdk = libs.versions.android.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } +} diff --git a/lib/src/androidMain/kotlin/fibiprops.android.kt b/lib/src/androidMain/kotlin/fibiprops.android.kt new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt b/lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt new file mode 100644 index 0000000..7f79c37 --- /dev/null +++ b/lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt @@ -0,0 +1,11 @@ +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AndroidGraphTest { + + @Test + fun `graph test example`() { + assertTrue(true) + } +} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/Graph.kt b/lib/src/commonMain/kotlin/Graph.kt new file mode 100644 index 0000000..638fa26 --- /dev/null +++ b/lib/src/commonMain/kotlin/Graph.kt @@ -0,0 +1,5 @@ +package lib.graph + +class graph(){ + +} diff --git a/lib/src/commonTest/kotlin/GraphTest.kt b/lib/src/commonTest/kotlin/GraphTest.kt new file mode 100644 index 0000000..5db2ab4 --- /dev/null +++ b/lib/src/commonTest/kotlin/GraphTest.kt @@ -0,0 +1,10 @@ +import kotlin.test.Test +import kotlin.test.assertTrue + +class GraphTest { + + @Test + fun `graph test example`() { + assertTrue(true) + } +} \ No newline at end of file diff --git a/lib/src/iosMain/kotlin/graph.ios.kt b/lib/src/iosMain/kotlin/graph.ios.kt new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/iosTest/kotlin/IosGraphTest.kt b/lib/src/iosTest/kotlin/IosGraphTest.kt new file mode 100644 index 0000000..54a4be2 --- /dev/null +++ b/lib/src/iosTest/kotlin/IosGraphTest.kt @@ -0,0 +1,10 @@ +import kotlin.test.Test +import kotlin.test.assertTrue + +class IosGraphTest { + + @Test + fun `graph test example`() { + assertTrue(true) + } +} \ No newline at end of file diff --git a/lib/src/jvmMain/kotlin/graph.jvm.kt b/lib/src/jvmMain/kotlin/graph.jvm.kt new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/jvmTest/kotlin/JvmGraphTest.kt b/lib/src/jvmTest/kotlin/JvmGraphTest.kt new file mode 100644 index 0000000..9071a1e --- /dev/null +++ b/lib/src/jvmTest/kotlin/JvmGraphTest.kt @@ -0,0 +1,10 @@ +import kotlin.test.Test +import kotlin.test.assertTrue + +class JvmGraphTest { + + @Test + fun `graph test example`() { + assertTrue(true) + } +} \ No newline at end of file diff --git a/lib/src/linuxX64Main/kotlin/graph.linuxX64.kt b/lib/src/linuxX64Main/kotlin/graph.linuxX64.kt new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt b/lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt new file mode 100644 index 0000000..5dca9e5 --- /dev/null +++ b/lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt @@ -0,0 +1,9 @@ +import kotlin.test.Test +import kotlin.test.assertTrue + +class LinuxGraphTest { + @Test + fun `graph test example`() { + assertTrue(true) + } +} diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..bcf2a89 --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. + +sdk.dir= diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..1873b02 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,32 @@ +rootProject.name = "GraphVisualizer" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +pluginManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} + +include(":composeApp") +include(":lib") \ No newline at end of file From 6634a7491454302d789891d0aae4ea4646c12d2c Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 6 May 2024 23:23:08 +0300 Subject: [PATCH 004/172] remove readme from project template --- README.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index ef5ec7b..0000000 --- a/README.md +++ /dev/null @@ -1,14 +0,0 @@ -This is a Kotlin Multiplatform project targeting Android, iOS, Desktop. - -* `/composeApp` is for code that will be shared across your Compose Multiplatform applications. - It contains several subfolders: - - `commonMain` is for code that’s common for all targets. - - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. - For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, - `iosMain` would be the right folder for such calls. - -* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, - you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. - - -Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… \ No newline at end of file From addfd20e976174534b49b05ff34546a183893d8e Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 6 May 2024 23:24:39 +0300 Subject: [PATCH 005/172] remove local.properties file --- local.properties | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 local.properties diff --git a/local.properties b/local.properties deleted file mode 100644 index bcf2a89..0000000 --- a/local.properties +++ /dev/null @@ -1,8 +0,0 @@ -## This file must *NOT* be checked into Version Control Systems, -# as it contains information specific to your local configuration. -# -# Location of the SDK. This is only used by Gradle. -# For customization when using a Version Control System, please read the -# header note. - -sdk.dir= From 89cf2b4a311f7480d1ca592e49a47db13b9f5abe Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 6 May 2024 23:37:05 +0300 Subject: [PATCH 006/172] update jdk version to 21 --- composeApp/build.gradle.kts | 6 +++--- lib/build.gradle.kts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 4c0f3f0..7d7c0c0 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -10,7 +10,7 @@ kotlin { androidTarget { compilations.all { kotlinOptions { - jvmTarget = "11" + jvmTarget = "21" } } } @@ -76,8 +76,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } dependencies { debugImplementation(libs.compose.ui.tooling) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 412d48d..b7c34de 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -9,7 +9,7 @@ kotlin { publishLibraryVariants("release") compilations.all { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "21" } } } From fd7ba5e7cbd81b8fb19b9c2f1f44160a53d5ce5b Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 7 May 2024 03:10:54 +0300 Subject: [PATCH 007/172] add navigation compose lib --- composeApp/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 7d7c0c0..9d74850 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -43,6 +43,7 @@ kotlin { implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(project(":lib")) + implementation("androidx.navigation:navigation-compose:2.6.0") } desktopMain.dependencies { implementation(compose.desktop.currentOs) From 909e8be61677f11b444cc92b37a29dd6ce079edd Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 8 May 2024 03:23:53 +0300 Subject: [PATCH 008/172] build: change .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0bb1be5..b109c83 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ # Ignore Gradle build output directory build +local.properties +.kotlin/ /.idea/ \ No newline at end of file From 82f1b6bbeb61d11d191c9244ee75c757e30a143e Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 8 May 2024 03:27:55 +0300 Subject: [PATCH 009/172] build: add compose libs, update versions --- build.gradle.kts | 3 ++- composeApp/build.gradle.kts | 5 +++-- gradle.properties | 4 +++- gradle/libs.versions.toml | 19 ++++++++++++------- lib/build.gradle.kts | 4 ++++ 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 52cf9fa..07a9127 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { // in each subproject's classloader alias(libs.plugins.androidApplication) apply false alias(libs.plugins.androidLibrary) apply false - alias(libs.plugins.jetbrainsCompose) apply false alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false } \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 9d74850..37177fd 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -3,7 +3,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidApplication) - alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) } kotlin { @@ -43,7 +44,7 @@ kotlin { implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(project(":lib")) - implementation("androidx.navigation:navigation-compose:2.6.0") + implementation(libs.androidx.navigation.compose) } desktopMain.dependencies { implementation(compose.desktop.currentOs) diff --git a/gradle.properties b/gradle.properties index 5db355f..c1a4334 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,4 +9,6 @@ android.useAndroidX=true #MPP kotlin.mpp.androidSourceSetLayoutVersion=2 -kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file +kotlin.mpp.enableCInteropCommonization=true + +kotlin.native.ignoreDisabledTargets=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4db653e..7c10930 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,10 +10,12 @@ androidx-core-ktx = "1.13.0" androidx-espresso-core = "3.5.1" androidx-material = "1.11.0" androidx-test-junit = "1.1.5" -compose = "1.6.6" -compose-plugin = "1.6.2" +compose-android = "1.6.6" junit = "4.13.2" -kotlin = "1.9.23" +kotlin = "2.0.0-RC2" +androidx-navigation = "2.7.0-alpha04" +androidx-lifecycle = "2.8.0-rc01" +compose-multiplatform = "1.6.10-rc01" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -25,12 +27,15 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } -compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-android" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose-android" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } -jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } -kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index b7c34de..c660428 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -38,4 +38,8 @@ android { defaultConfig { minSdk = libs.versions.android.minSdk.get().toInt() } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_11 + } } From 2289d60b244800e36934f6c8318e9a0ce7653f86 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 8 May 2024 03:29:58 +0300 Subject: [PATCH 010/172] ref: remove compose files from template build --- composeApp/src/commonMain/kotlin/Greeting.kt | 7 ------- composeApp/src/commonMain/kotlin/Platform.kt | 5 ----- composeApp/src/desktopMain/kotlin/Platform.jvm.kt | 5 ----- 3 files changed, 17 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/Greeting.kt delete mode 100644 composeApp/src/commonMain/kotlin/Platform.kt delete mode 100644 composeApp/src/desktopMain/kotlin/Platform.jvm.kt diff --git a/composeApp/src/commonMain/kotlin/Greeting.kt b/composeApp/src/commonMain/kotlin/Greeting.kt deleted file mode 100644 index 887d835..0000000 --- a/composeApp/src/commonMain/kotlin/Greeting.kt +++ /dev/null @@ -1,7 +0,0 @@ -class Greeting { - private val platform = getPlatform() - - fun greet(): String { - return "Hello, ${platform.name}!" - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/Platform.kt b/composeApp/src/commonMain/kotlin/Platform.kt deleted file mode 100644 index 87ca3ff..0000000 --- a/composeApp/src/commonMain/kotlin/Platform.kt +++ /dev/null @@ -1,5 +0,0 @@ -interface Platform { - val name: String -} - -expect fun getPlatform(): Platform \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/Platform.jvm.kt b/composeApp/src/desktopMain/kotlin/Platform.jvm.kt deleted file mode 100644 index f5e7e49..0000000 --- a/composeApp/src/desktopMain/kotlin/Platform.jvm.kt +++ /dev/null @@ -1,5 +0,0 @@ -class JVMPlatform: Platform { - override val name: String = "Java ${System.getProperty("java.version")}" -} - -actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file From 4029c299eb487e1e9dd59603665d0dcb824998b3 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 8 May 2024 03:30:48 +0300 Subject: [PATCH 011/172] feat: add styles --- composeApp/src/commonMain/kotlin/styling.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/styling.kt diff --git a/composeApp/src/commonMain/kotlin/styling.kt b/composeApp/src/commonMain/kotlin/styling.kt new file mode 100644 index 0000000..939b8fd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/styling.kt @@ -0,0 +1,9 @@ +package visualizer.styling + +import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.TextStyle + + +val defaultStyle = TextStyle(fontSize = 28.sp) + +val bigStyle = TextStyle(fontSize = 50.sp) \ No newline at end of file From fad65792ad176a8724e1e32cdaf8ee8fad6148a1 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 8 May 2024 03:36:31 +0300 Subject: [PATCH 012/172] feat: add app navigation, basic list creating and search --- composeApp/src/commonMain/kotlin/App.kt | 34 +--- .../src/commonMain/kotlin/BounceClick.kt | 41 +++++ .../src/commonMain/kotlin/Navigation.kt | 26 +++ composeApp/src/commonMain/kotlin/Screens.kt | 153 ++++++++++++++++++ composeApp/src/desktopMain/kotlin/main.kt | 20 ++- 5 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/BounceClick.kt create mode 100644 composeApp/src/commonMain/kotlin/Navigation.kt create mode 100644 composeApp/src/commonMain/kotlin/Screens.kt diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index da7c2af..95fd55f 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -1,37 +1,9 @@ -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Button import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import org.jetbrains.compose.resources.ExperimentalResourceApi -import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.ui.tooling.preview.Preview -import graphvisualizer.composeapp.generated.resources.Res -import graphvisualizer.composeapp.generated.resources.compose_multiplatform - -@OptIn(ExperimentalResourceApi::class) @Composable -@Preview -fun App() { - MaterialTheme { - var showContent by remember { mutableStateOf(false) } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - val greeting = remember { Greeting().greet() } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("Compose: $greeting") - } - } - } +fun App(){ + MaterialTheme(){ + Navigation() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/BounceClick.kt b/composeApp/src/commonMain/kotlin/BounceClick.kt new file mode 100644 index 0000000..596fa54 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/BounceClick.kt @@ -0,0 +1,41 @@ +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput + +enum class ButtonState { Pressed, Idle } +fun Modifier.bounceClick() = composed { + var buttonState by remember { mutableStateOf(ButtonState.Idle) } + val scale by animateFloatAsState(if (buttonState == ButtonState.Pressed) 0.70f else 1f) + + this + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { } + ) + .pointerInput(buttonState) { + awaitPointerEventScope { + buttonState = if (buttonState == ButtonState.Pressed) { + waitForUpOrCancellation() + ButtonState.Idle + } else { + awaitFirstDown(false) + ButtonState.Pressed + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/Navigation.kt b/composeApp/src/commonMain/kotlin/Navigation.kt new file mode 100644 index 0000000..8a8d9cb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/Navigation.kt @@ -0,0 +1,26 @@ +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import mainscreen.* + +@Composable +fun Navigation() { + val navController = rememberNavController() + NavHost(navController = navController, + startDestination = Screen.MainScreen.route, + modifier = Modifier.padding(16.dp)) { + composable(route = Screen.MainScreen.route){ + MainScreen(navController = navController) + } + composable(route = Screen.GraphScreen.route){ + GraphScreen(navController = navController) + } + composable(route = Screen.SettingsScreen.route){ + SettingsScreen(navController = navController) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/Screens.kt b/composeApp/src/commonMain/kotlin/Screens.kt new file mode 100644 index 0000000..8be3c11 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/Screens.kt @@ -0,0 +1,153 @@ +package mainscreen + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import bounceClick +import visualizer.styling.bigStyle +import visualizer.styling.defaultStyle + +sealed class Screen(val route: String){ + object MainScreen: Screen("main_screen") + object GraphScreen: Screen("graph_screen") + object SettingsScreen: Screen("settings_screen") +} + +@Composable +fun MainScreen(navController: NavController){ + val graphs = remember { mutableStateListOf() } + var search by remember { mutableStateOf("") } + Column(modifier = Modifier.fillMaxSize()) { + Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { + TextField( + value = search, + textStyle = bigStyle, + placeholder = { Text(text = "Search a graph", style = bigStyle) }, + onValueChange = { search = it }, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .border( + width = 4.dp, + color = MaterialTheme.colors.primary, + shape = RoundedCornerShape(10.dp) + ), + shape = RoundedCornerShape(10.dp), + trailingIcon = { + Icon( + Icons.Filled.Search, contentDescription = "SearchIcon", modifier = Modifier + .size(100.dp) + .padding(10.dp) + ) + }, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + + Spacer(modifier = Modifier.width(40.dp)) + + // add graph + IconButton( + onClick = { graphs.add(search) }, + modifier = Modifier + .padding(horizontal = 10.dp) + .size(100.dp) + .clip(shape = RoundedCornerShape(25.dp)) + .clickable { } + .border( + width = 5.dp, + color = MaterialTheme.colors.primary, + shape = RoundedCornerShape(25.dp) + ) + .bounceClick(), + ) { + Icon( + Icons.Filled.Add, + contentDescription = "Add graph", + modifier = Modifier.size(100.dp) + ) + } + + // to settings + IconButton( + onClick = { navController.navigate(Screen.SettingsScreen.route) }, + modifier = Modifier + .padding(horizontal = 20.dp) + .size(100.dp) + .clip(shape = RoundedCornerShape(25.dp)) + .clickable { } + .border( + width = 5.dp, + color = MaterialTheme.colors.primary, + shape = RoundedCornerShape(25.dp) + ) + .bounceClick(), + + ) { + Icon( + Icons.Filled.Settings, + contentDescription = "Settings", + modifier = Modifier.size(100.dp) + ) + } + } + + LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(graphs) { graph -> + if ( !graph.startsWith(search) ) return@items + Row(modifier = Modifier.padding(30.dp)) { + Text( + graph, + style = bigStyle, + modifier = Modifier + .fillMaxWidth() + .clip(shape = RoundedCornerShape(45.dp)) + .border( + width = 5.dp, + color = MaterialTheme.colors.primary, + shape = RoundedCornerShape(45.dp) + ) + .padding(vertical = 16.dp, horizontal = 30.dp) + ) + } + } + } + } +} + +@Composable +fun GraphScreen(navController: NavController){ + Text("Здесь будут графы", style= defaultStyle) +} + +@Composable +fun SettingsScreen(navController: NavController){ + Column{ + Text(text = "Тут наверно будут настройки", fontSize = 28.sp) + Button(onClick = {navController.navigate(Screen.MainScreen.route)}, + modifier = Modifier + .padding(16.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick()) { + Text("Назад", style = defaultStyle) + } + } + +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt index 780ab4f..6e54362 100644 --- a/composeApp/src/desktopMain/kotlin/main.kt +++ b/composeApp/src/desktopMain/kotlin/main.kt @@ -1,11 +1,25 @@ -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.runtime.mutableStateListOf +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.window.* +import java.awt.Dimension fun main() = application { + val state = WindowState( + width = 1920.dp, + height = 1080.dp, + position = WindowPosition(alignment = Alignment.Center), + ) Window( + state = state, onCloseRequest = ::exitApplication, title = "Graph Visualizer", ) { - App() + window.minimumSize = Dimension(100,100) + App() } } \ No newline at end of file From be49d412a02602e93aefdd0d3bf5a38526de7e23 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Thu, 9 May 2024 10:19:56 +0300 Subject: [PATCH 013/172] feat: template for find cycle in graph the algorithm will only work for undirected graphs; oriented ones will be converted to undirected --- lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt | 5 +++++ lib/src/commonMain/kotlin/cycleFind/findCycle.kt | 1 + 2 files changed, 6 insertions(+) create mode 100644 lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt create mode 100644 lib/src/commonMain/kotlin/cycleFind/findCycle.kt diff --git a/lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt b/lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt new file mode 100644 index 0000000..dc98b54 --- /dev/null +++ b/lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt @@ -0,0 +1,5 @@ +package cycleFind + +class LoadingGraph { + //TODO("make it possible to use cycle search algorithms through this algorithm") +} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/cycleFind/findCycle.kt b/lib/src/commonMain/kotlin/cycleFind/findCycle.kt new file mode 100644 index 0000000..b339358 --- /dev/null +++ b/lib/src/commonMain/kotlin/cycleFind/findCycle.kt @@ -0,0 +1 @@ +package cycleFind \ No newline at end of file From 6762025381f50a2e706a04472e45197ccea406b3 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Thu, 9 May 2024 10:22:20 +0300 Subject: [PATCH 014/172] change: add macOS, Linux and Gradle in `.gitignore` --- .gitignore | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b109c83..858e8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,55 @@ build local.properties .kotlin/ -/.idea/ \ No newline at end of file +/.idea/ +kls_database.db +# ---- 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 + +# ---- 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 From fc2556420b2fa0572421ea24f778cb53fbb0d145 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 9 May 2024 18:22:13 +0300 Subject: [PATCH 015/172] build: remove ios and linux native support --- composeApp/build.gradle.kts | 11 - .../androidMain/kotlin/Platform.android.kt | 7 - .../org/example/project/MainActivity.kt | 8 - .../src/iosMain/kotlin/MainViewController.kt | 3 - composeApp/src/iosMain/kotlin/Platform.ios.kt | 7 - iosApp/Configuration/Config.xcconfig | 3 - iosApp/iosApp.xcodeproj/project.pbxproj | 398 ------------------ .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 14 - .../AppIcon.appiconset/app-icon-1024.png | Bin 67285 -> 0 bytes iosApp/iosApp/Assets.xcassets/Contents.json | 6 - iosApp/iosApp/ContentView.swift | 21 - iosApp/iosApp/Info.plist | 50 --- .../Preview Assets.xcassets/Contents.json | 6 - iosApp/iosApp/iOSApp.swift | 10 - lib/build.gradle.kts | 6 +- lib/src/commonMain/kotlin/Graph.kt | 3 +- lib/src/iosMain/kotlin/graph.ios.kt | 0 lib/src/iosTest/kotlin/IosGraphTest.kt | 10 - lib/src/linuxX64Main/kotlin/graph.linuxX64.kt | 0 lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt | 9 - 21 files changed, 3 insertions(+), 580 deletions(-) delete mode 100644 composeApp/src/androidMain/kotlin/Platform.android.kt delete mode 100644 composeApp/src/iosMain/kotlin/MainViewController.kt delete mode 100644 composeApp/src/iosMain/kotlin/Platform.ios.kt delete mode 100644 iosApp/Configuration/Config.xcconfig delete mode 100644 iosApp/iosApp.xcodeproj/project.pbxproj delete mode 100644 iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png delete mode 100644 iosApp/iosApp/Assets.xcassets/Contents.json delete mode 100644 iosApp/iosApp/ContentView.swift delete mode 100644 iosApp/iosApp/Info.plist delete mode 100644 iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 iosApp/iosApp/iOSApp.swift delete mode 100644 lib/src/iosMain/kotlin/graph.ios.kt delete mode 100644 lib/src/iosTest/kotlin/IosGraphTest.kt delete mode 100644 lib/src/linuxX64Main/kotlin/graph.linuxX64.kt delete mode 100644 lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 37177fd..6087af4 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -18,17 +18,6 @@ kotlin { jvm("desktop") - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { iosTarget -> - iosTarget.binaries.framework { - baseName = "ComposeApp" - isStatic = true - } - } - sourceSets { val desktopMain by getting diff --git a/composeApp/src/androidMain/kotlin/Platform.android.kt b/composeApp/src/androidMain/kotlin/Platform.android.kt deleted file mode 100644 index 4f3ea05..0000000 --- a/composeApp/src/androidMain/kotlin/Platform.android.kt +++ /dev/null @@ -1,7 +0,0 @@ -import android.os.Build - -class AndroidPlatform : Platform { - override val name: String = "Android ${Build.VERSION.SDK_INT}" -} - -actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt index 02d1e4e..90d5c40 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt @@ -4,8 +4,6 @@ import App import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -15,10 +13,4 @@ class MainActivity : ComponentActivity() { App() } } -} - -@Preview -@Composable -fun AppAndroidPreview() { - App() } \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/MainViewController.kt b/composeApp/src/iosMain/kotlin/MainViewController.kt deleted file mode 100644 index fa143d4..0000000 --- a/composeApp/src/iosMain/kotlin/MainViewController.kt +++ /dev/null @@ -1,3 +0,0 @@ -import androidx.compose.ui.window.ComposeUIViewController - -fun MainViewController() = ComposeUIViewController { App() } \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/Platform.ios.kt b/composeApp/src/iosMain/kotlin/Platform.ios.kt deleted file mode 100644 index 5cef987..0000000 --- a/composeApp/src/iosMain/kotlin/Platform.ios.kt +++ /dev/null @@ -1,7 +0,0 @@ -import platform.UIKit.UIDevice - -class IOSPlatform: Platform { - override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion -} - -actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig deleted file mode 100644 index 26af985..0000000 --- a/iosApp/Configuration/Config.xcconfig +++ /dev/null @@ -1,3 +0,0 @@ -TEAM_ID= -BUNDLE_ID=org.example.project.KotlinProject -APP_NAME=KotlinProject \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj deleted file mode 100644 index 4de11f9..0000000 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,398 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; - 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; - 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; - 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; - 7555FF7B242A565900829871 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B92378962B6B1156000C7307 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 058557D7273AAEEB004C7B11 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 42799AB246E5F90AF97AA0EF /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 7555FF72242A565900829871 = { - isa = PBXGroup; - children = ( - AB1DB47929225F7C00F7AF9C /* Configuration */, - 7555FF7D242A565900829871 /* iosApp */, - 7555FF7C242A565900829871 /* Products */, - 42799AB246E5F90AF97AA0EF /* Frameworks */, - ); - sourceTree = ""; - }; - 7555FF7C242A565900829871 /* Products */ = { - isa = PBXGroup; - children = ( - 7555FF7B242A565900829871 /* KotlinProject.app */, - ); - name = Products; - sourceTree = ""; - }; - 7555FF7D242A565900829871 /* iosApp */ = { - isa = PBXGroup; - children = ( - 058557BA273AAA24004C7B11 /* Assets.xcassets */, - 7555FF82242A565900829871 /* ContentView.swift */, - 7555FF8C242A565B00829871 /* Info.plist */, - 2152FB032600AC8F00CF470E /* iOSApp.swift */, - 058557D7273AAEEB004C7B11 /* Preview Content */, - ); - path = iosApp; - sourceTree = ""; - }; - AB1DB47929225F7C00F7AF9C /* Configuration */ = { - isa = PBXGroup; - children = ( - AB3632DC29227652001CCB65 /* Config.xcconfig */, - ); - path = Configuration; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 7555FF7A242A565900829871 /* iosApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; - buildPhases = ( - F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, - 7555FF77242A565900829871 /* Sources */, - B92378962B6B1156000C7307 /* Frameworks */, - 7555FF79242A565900829871 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = iosApp; - packageProductDependencies = ( - ); - productName = iosApp; - productReference = 7555FF7B242A565900829871 /* KotlinProject.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 7555FF73242A565900829871 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1130; - ORGANIZATIONNAME = orgName; - TargetAttributes = { - 7555FF7A242A565900829871 = { - CreatedOnToolsVersion = 11.3.1; - }; - }; - }; - buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; - compatibilityVersion = "Xcode 12.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 7555FF72242A565900829871; - packageReferences = ( - ); - productRefGroup = 7555FF7C242A565900829871 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 7555FF7A242A565900829871 /* iosApp */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 7555FF79242A565900829871 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, - 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Compile Kotlin Framework"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 7555FF77242A565900829871 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, - 7555FF83242A565900829871 /* ContentView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 7555FFA3242A565B00829871 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 7555FFA4242A565B00829871 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 7555FFA6242A565B00829871 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", - ); - INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - composeApp, - ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - PRODUCT_NAME = "${APP_NAME}"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 7555FFA7242A565B00829871 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", - ); - INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - composeApp, - ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - PRODUCT_NAME = "${APP_NAME}"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7555FFA3242A565B00829871 /* Debug */, - 7555FFA4242A565B00829871 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7555FFA6242A565B00829871 /* Debug */, - 7555FFA7242A565B00829871 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 7555FF73242A565900829871 /* Project object */; -} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index ee7e3ca..0000000 --- a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 8edf56e..0000000 --- a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "images" : [ - { - "filename" : "app-icon-1024.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png deleted file mode 100644 index 53fc536fb9ac5c1dbb27c7e1da13db3760070a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67285 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 BXNCX( diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c53..0000000 --- a/iosApp/iosApp/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift deleted file mode 100644 index 3cd5c32..0000000 --- a/iosApp/iosApp/ContentView.swift +++ /dev/null @@ -1,21 +0,0 @@ -import UIKit -import SwiftUI -import ComposeApp - -struct ComposeView: UIViewControllerRepresentable { - func makeUIViewController(context: Context) -> UIViewController { - MainViewControllerKt.MainViewController() - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} -} - -struct ContentView: View { - var body: some View { - ComposeView() - .ignoresSafeArea(.keyboard) // Compose has own keyboard handler - } -} - - - diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist deleted file mode 100644 index 412e378..0000000 --- a/iosApp/iosApp/Info.plist +++ /dev/null @@ -1,50 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - - UILaunchScreen - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c53..0000000 --- a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift deleted file mode 100644 index 0648e86..0000000 --- a/iosApp/iosApp/iOSApp.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -@main -struct iOSApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index c660428..578f45b 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -13,10 +13,6 @@ kotlin { } } } - iosX64() - iosArm64() - iosSimulatorArm64() - linuxX64() sourceSets { val commonMain by getting { @@ -40,6 +36,6 @@ android { } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_21 } } diff --git a/lib/src/commonMain/kotlin/Graph.kt b/lib/src/commonMain/kotlin/Graph.kt index 638fa26..0a0f908 100644 --- a/lib/src/commonMain/kotlin/Graph.kt +++ b/lib/src/commonMain/kotlin/Graph.kt @@ -1,5 +1,6 @@ package lib.graph +import java.util.* class graph(){ - + val x : Queue = LinkedList() } diff --git a/lib/src/iosMain/kotlin/graph.ios.kt b/lib/src/iosMain/kotlin/graph.ios.kt deleted file mode 100644 index e69de29..0000000 diff --git a/lib/src/iosTest/kotlin/IosGraphTest.kt b/lib/src/iosTest/kotlin/IosGraphTest.kt deleted file mode 100644 index 54a4be2..0000000 --- a/lib/src/iosTest/kotlin/IosGraphTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -import kotlin.test.Test -import kotlin.test.assertTrue - -class IosGraphTest { - - @Test - fun `graph test example`() { - assertTrue(true) - } -} \ No newline at end of file diff --git a/lib/src/linuxX64Main/kotlin/graph.linuxX64.kt b/lib/src/linuxX64Main/kotlin/graph.linuxX64.kt deleted file mode 100644 index e69de29..0000000 diff --git a/lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt b/lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt deleted file mode 100644 index 5dca9e5..0000000 --- a/lib/src/linuxX64Test/kotlin/LinuxGraphTest.kt +++ /dev/null @@ -1,9 +0,0 @@ -import kotlin.test.Test -import kotlin.test.assertTrue - -class LinuxGraphTest { - @Test - fun `graph test example`() { - assertTrue(true) - } -} From a9bd893df54f1fb5b6eae37fe89348a8280bd14f Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 9 May 2024 18:24:19 +0300 Subject: [PATCH 016/172] ref: refactor .gitignore code --- .gitignore | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 858e8dd..d3b056d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,15 @@ .gradle # Ignore Gradle build output directory -build +**/build/ +!src/**/build/ + local.properties .kotlin/ /.idea/ kls_database.db + # ---- macOS ---- # General .DS_Store @@ -17,7 +20,6 @@ kls_database.db # Icon must end with two \r Icon - # Thumbnails ._* @@ -37,9 +39,6 @@ Network Trash Folder Temporary Items .apdisk -# ---- Gradle ---- -**/build/ -!src/**/build/ # Ignore Gradle GUI config gradle-app.setting From 3079da0d9686a7e322fa8437a1beef26ec9453ad Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 16:04:36 +0300 Subject: [PATCH 017/172] feat: template for CI - standart linter - coverage tests - gradle test --- .github/.keep | 0 .github/coverage.yml | 40 ++++++++++++++++++ .github/gradle-test.yml | 22 ++++++++++ .github/megalinter.yml | 89 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) delete mode 100644 .github/.keep create mode 100644 .github/coverage.yml create mode 100644 .github/gradle-test.yml create mode 100644 .github/megalinter.yml diff --git a/.github/.keep b/.github/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/coverage.yml b/.github/coverage.yml new file mode 100644 index 0000000..ebd9b83 --- /dev/null +++ b/.github/coverage.yml @@ -0,0 +1,40 @@ +name: Coverage Test + +on: + push: +jobs: + Coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "oracle" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + + - name: Run Test Coverage + run: ./gradlew jacocoTestReport + + - name: Jacoco Test Coverage Report + uses: cicirello/jacoco-badge-generator@v2.8.0 + with: + generate-branches-badge: true + jacoco-csv-file: lib/build/jacoco/report.csv + + - name: Add coverage to PR + id: jacoco + uses: madrapps/jacoco-report@v1.6.1 + with: + paths: | + ${{ github.workspace }}/**/build/reports/jacoco/prodNormalDebugCoverage/prodNormalDebugCoverage.xml, + ${{ github.workspace }}/**/build/reports/jacoco/**/debugCoverage.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 40 + update-comment: true diff --git a/.github/gradle-test.yml b/.github/gradle-test.yml new file mode 100644 index 0000000..47555de --- /dev/null +++ b/.github/gradle-test.yml @@ -0,0 +1,22 @@ +name: Build & Test + +on: + push: +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: "oracle" + java-version: '21' + + - name: Clean, Build and Test + run: ./gradlew clean test \ No newline at end of file diff --git a/.github/megalinter.yml b/.github/megalinter.yml new file mode 100644 index 0000000..8edc2dd --- /dev/null +++ b/.github/megalinter.yml @@ -0,0 +1,89 @@ +--- +# MegaLinter GitHub Action configuration file +# More info at https://megalinter.io +name: MegaLinter + +on: + # Trigger mega-linter at every push. Action will also be visible from Pull Requests to main + push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) + +env: # Comment env block if you don't want to apply fixes + # Apply linter fixes configuration + APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) + APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all) + APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request) + ENABLE_LINTERS: KOTLIN_KTLINT +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + megalinter: + name: MegaLinter + runs-on: ubuntu-latest + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR + # Remove the ones you do not need + contents: write + issues: write + pull-requests: write + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances + + # MegaLinter + - name: MegaLinter + id: ml + # You can override MegaLinter flavor used to have faster performances + # More info at https://megalinter.io/flavors/ + uses: oxsecurity/megalinter@v7 + env: + # All available variables are described in documentation + # https://megalinter.io/configuration/ + VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY + # DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks + + # Upload MegaLinter artifacts + - name: Archive production artifacts + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: MegaLinter reports + path: | + megalinter-reports + mega-linter.log + + # Create pull request if applicable (for now works only on PR from same repository, not from forks) + - name: Create Pull Request with applied fixes + id: cpr + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + commit-message: "[MegaLinter] Apply linters automatic fixes" + title: "[MegaLinter] Apply linters automatic fixes" + labels: bot + - name: Create PR output + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + run: | + echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" + + # Push new commit if applicable (for now works only on PR from same repository, not from forks) + - name: Prepare commit + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + run: sudo chown -Rc $UID .git/ + - name: Commit and push applied linter fixes + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + uses: stefanzweifel/git-auto-commit-action@v4 + with: + branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} + commit_message: "[MegaLinter] Apply linters fixes" + commit_user_name: megalinter-bot + commit_user_email: nicolas.vuillamy@ox.security From 81efa9422cff2fe37aab747ca386b0d9d5b08717 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 16:24:02 +0300 Subject: [PATCH 018/172] change: add setup android sdk --- .github/coverage.yml | 3 +++ .github/gradle-test.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/coverage.yml b/.github/coverage.yml index ebd9b83..acecbec 100644 --- a/.github/coverage.yml +++ b/.github/coverage.yml @@ -16,6 +16,9 @@ jobs: java-version: "21" distribution: "oracle" + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + - name: Setup Gradle uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 diff --git a/.github/gradle-test.yml b/.github/gradle-test.yml index 47555de..465c2f0 100644 --- a/.github/gradle-test.yml +++ b/.github/gradle-test.yml @@ -18,5 +18,8 @@ jobs: distribution: "oracle" java-version: '21' + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + - name: Clean, Build and Test run: ./gradlew clean test \ No newline at end of file From cb99cb5533dc43760ee90d271c2ec4c84fe99412 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 16:25:57 +0300 Subject: [PATCH 019/172] change: moved everything to the `workflows` folder --- .github/{ => workflows}/coverage.yml | 0 .github/{ => workflows}/gradle-test.yml | 0 .github/{ => workflows}/megalinter.yml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/coverage.yml (100%) rename .github/{ => workflows}/gradle-test.yml (100%) rename .github/{ => workflows}/megalinter.yml (100%) diff --git a/.github/coverage.yml b/.github/workflows/coverage.yml similarity index 100% rename from .github/coverage.yml rename to .github/workflows/coverage.yml diff --git a/.github/gradle-test.yml b/.github/workflows/gradle-test.yml similarity index 100% rename from .github/gradle-test.yml rename to .github/workflows/gradle-test.yml diff --git a/.github/megalinter.yml b/.github/workflows/megalinter.yml similarity index 100% rename from .github/megalinter.yml rename to .github/workflows/megalinter.yml From 54f197f772dfcc8ecc6e71a7e3e078fa54bbb0e4 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 16:59:03 +0300 Subject: [PATCH 020/172] change: entered with jacoco on kotlinx-cover change: test coverage is start for every push --- .github/workflows/coverage.yml | 54 +++++++++++++--------------------- lib/build.gradle.kts | 3 ++ 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index acecbec..d235958 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,43 +1,31 @@ -name: Coverage Test +name: Measure coverage on: - push: + push + jobs: - Coverage: + build: runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 - + - uses: actions/checkout@v4 - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: "21" - distribution: "oracle" - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 - - - name: Run Test Coverage - run: ./gradlew jacocoTestReport - - - name: Jacoco Test Coverage Report - uses: cicirello/jacoco-badge-generator@v2.8.0 - with: - generate-branches-badge: true - jacoco-csv-file: lib/build/jacoco/report.csv - - - name: Add coverage to PR - id: jacoco - uses: madrapps/jacoco-report@v1.6.1 + distribution: 'oracle' + java-version: '21' + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Generate kover coverage report + run: ./gradlew koverXmlReport + + - name: Add coverage report to PR + id: kover + uses: mi-kas/kover-report@v1 with: - paths: | - ${{ github.workspace }}/**/build/reports/jacoco/prodNormalDebugCoverage/prodNormalDebugCoverage.xml, - ${{ github.workspace }}/**/build/reports/jacoco/**/debugCoverage.xml - token: ${{ secrets.GITHUB_TOKEN }} - min-coverage-overall: 40 + path: | + ${{ github.workspace }}/lib/build/reports/kover/report.xml + title: Code Coverage update-comment: true + min-coverage-overall: 70 + min-coverage-changed-files: 70 + coverage-counter-type: LINE \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 578f45b..9871bbc 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,6 +1,9 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType + plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) + id("org.jetbrains.kotlinx.kover") version "0.7.6" } kotlin { From af59ea81b7673f5ed2b4974c849101eadd5d4727 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 17:08:34 +0300 Subject: [PATCH 021/172] feat: add `.editorconfig` for megalinter --- .editorconfig | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6bc1e84 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.{kt,kts}] +ktlint_code_style = intellij_idea +max_line_length = 120 From 3546b8c4246c4f2508ca0489942fcfbccd3552e8 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 18:20:48 +0300 Subject: [PATCH 022/172] feat: add pre-commit hooks --- .pre-commit-config.yaml | 11 +++++++++++ build.gradle.kts | 4 +--- gradle.properties | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a56e591 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: name-tests-test + - id: detect-private-key + - id: check-merge-conflict diff --git a/build.gradle.kts b/build.gradle.kts index 07a9127..58a2e4a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,7 @@ plugins { - // this is necessary to avoid the plugins to be loaded multiple times - // in each subproject's classloader alias(libs.plugins.androidApplication) apply false alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index c1a4334..f963a88 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ android.useAndroidX=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.enableCInteropCommonization=true -kotlin.native.ignoreDisabledTargets=true \ No newline at end of file +kotlin.native.ignoreDisabledTargets=true From bd98c7217b704b9df91bfaaa7b75f4c6001899c9 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 19:18:20 +0300 Subject: [PATCH 023/172] feat: setup mergeable bot Now need a description and 2 approvals for the pull request --- .github/mergeable.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/mergeable.yml diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..f00fe6f --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,15 @@ +version: 2 +mergeable: + + - when: pull_request.*, pull_request_review.* + validate: + - do: description + no_empty: + enabled: true + message: "Description should not be empty." + + - do: approvals + min: + count: 1 + required: + assignees: true From 417e19d986ece86d51abf9e12c18821a36745ec5 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 19:19:27 +0300 Subject: [PATCH 024/172] feat: init readme with codefactor badge --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5d0307 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +[![CodeFactor](https://www.codefactor.io/repository/github/spbu-coding-2023/graphs-graphs-8/badge)](https://www.codefactor.io/repository/github/spbu-coding-2023/graphs-graphs-8) From aa5ea3a807b985c6de037aea6b022339dfb45324 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Fri, 10 May 2024 19:49:47 +0300 Subject: [PATCH 025/172] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 5994a8f8a82cbe20bcbd692bbce822b29c001b6d Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 22:26:52 +0300 Subject: [PATCH 026/172] change: small bug fix --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d235958..67dd58f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,9 +23,9 @@ jobs: uses: mi-kas/kover-report@v1 with: path: | - ${{ github.workspace }}/lib/build/reports/kover/report.xml + ${{ github.workspace }}/project1/build/reports/kover/report.xml title: Code Coverage update-comment: true min-coverage-overall: 70 min-coverage-changed-files: 70 - coverage-counter-type: LINE \ No newline at end of file + coverage-counter-type: LINE From b886413fe0be2ee106a6f8d926fd35010b6b4b34 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 22:34:13 +0300 Subject: [PATCH 027/172] bug fix --- .github/workflows/coverage.yml | 2 +- README.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 67dd58f..c9044da 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,7 +23,7 @@ jobs: uses: mi-kas/kover-report@v1 with: path: | - ${{ github.workspace }}/project1/build/reports/kover/report.xml + lib/build/reports/kover/report.xml title: Code Coverage update-comment: true min-coverage-overall: 70 diff --git a/README.md b/README.md index c5d0307..ebed141 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ [![CodeFactor](https://www.codefactor.io/repository/github/spbu-coding-2023/graphs-graphs-8/badge)](https://www.codefactor.io/repository/github/spbu-coding-2023/graphs-graphs-8) +[![Build & Test](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/gradle-test.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/gradle-test.yml) +[![Measure coverage](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/coverage.yml) +[![MegaLinter](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/megalinter.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/megalinter.yml) +[![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](http://www.wtfpl.net/about/) From c2ee949fbf3466e1dc8ad86c3e8020773cd6b93b Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Fri, 10 May 2024 22:48:48 +0300 Subject: [PATCH 028/172] change: update mergeable.yml --- .github/mergeable.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/mergeable.yml b/.github/mergeable.yml index f00fe6f..5699908 100644 --- a/.github/mergeable.yml +++ b/.github/mergeable.yml @@ -7,9 +7,3 @@ mergeable: no_empty: enabled: true message: "Description should not be empty." - - - do: approvals - min: - count: 1 - required: - assignees: true From 7bcd1c58590e19f2e951e28a10af8353c2ff0844 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Fri, 10 May 2024 22:59:50 +0300 Subject: [PATCH 029/172] feat: delete coverage.yml because the rights to the repository are limited --- .github/workflows/coverage.yml | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index c9044da..0000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Measure coverage - -on: - push - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - distribution: 'oracle' - java-version: '21' - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v3 - - name: Generate kover coverage report - run: ./gradlew koverXmlReport - - - name: Add coverage report to PR - id: kover - uses: mi-kas/kover-report@v1 - with: - path: | - lib/build/reports/kover/report.xml - title: Code Coverage - update-comment: true - min-coverage-overall: 70 - min-coverage-changed-files: 70 - coverage-counter-type: LINE From 805ce71476b37e7501a6b7a22c8f0c0c8355c505 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 13 May 2024 03:50:49 +0300 Subject: [PATCH 030/172] Feat: implemented graph screen and button for adding vertices --- .../kotlin/{ => view}/BounceClick.kt | 2 + .../commonMain/kotlin/{ => view}/Screens.kt | 104 ++++++++++++++---- .../commonMain/kotlin/{ => view}/styling.kt | 2 +- .../commonMain/kotlin/view/views/GraphView.kt | 4 + .../kotlin/view/views/VertexView.kt | 39 +++++++ .../kotlin/viewmodel/GraphViewModel.kt | 4 + .../kotlin/viewmodel/VertexViewModel.kt | 4 + 7 files changed, 136 insertions(+), 23 deletions(-) rename composeApp/src/commonMain/kotlin/{ => view}/BounceClick.kt (99%) rename composeApp/src/commonMain/kotlin/{ => view}/Screens.kt (60%) rename composeApp/src/commonMain/kotlin/{ => view}/styling.kt (86%) create mode 100644 composeApp/src/commonMain/kotlin/view/views/GraphView.kt create mode 100644 composeApp/src/commonMain/kotlin/view/views/VertexView.kt create mode 100644 composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/BounceClick.kt b/composeApp/src/commonMain/kotlin/view/BounceClick.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/BounceClick.kt rename to composeApp/src/commonMain/kotlin/view/BounceClick.kt index 596fa54..4ea8223 100644 --- a/composeApp/src/commonMain/kotlin/BounceClick.kt +++ b/composeApp/src/commonMain/kotlin/view/BounceClick.kt @@ -1,3 +1,5 @@ +package view + import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitFirstDown diff --git a/composeApp/src/commonMain/kotlin/Screens.kt b/composeApp/src/commonMain/kotlin/view/Screens.kt similarity index 60% rename from composeApp/src/commonMain/kotlin/Screens.kt rename to composeApp/src/commonMain/kotlin/view/Screens.kt index 8be3c11..ae53ee9 100644 --- a/composeApp/src/commonMain/kotlin/Screens.kt +++ b/composeApp/src/commonMain/kotlin/view/Screens.kt @@ -1,5 +1,6 @@ -package mainscreen +package view +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,10 +18,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import androidx.navigation.NavController -import bounceClick -import visualizer.styling.bigStyle -import visualizer.styling.defaultStyle +import androidx.lifecycle.ViewModel +import lib.graph.Graph +import model.GraphViewModel +import ui.VertexView +import ui.bigStyle +import ui.bounceClick +import ui.defaultStyle sealed class Screen(val route: String){ object MainScreen: Screen("main_screen") @@ -28,16 +34,36 @@ sealed class Screen(val route: String){ object SettingsScreen: Screen("settings_screen") } +class MainScreenViewModel : ViewModel(){ + val graphs = mutableStateOf(arrayListOf()) + val searchText = mutableStateOf("") + + fun addGraph(graph: Graph){ + graphs.value.add(graph) + } + fun removeGraph(index : Int){ + if (index in graphs.value.indices){ + graphs.value.removeAt(index) + } + else throw IllegalArgumentException("graph index out of range") + } + + fun changeSearchText(text: String){ + searchText.value = text + } +} + @Composable fun MainScreen(navController: NavController){ val graphs = remember { mutableStateListOf() } var search by remember { mutableStateOf("") } - Column(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { + // Search tab TextField( value = search, textStyle = bigStyle, - placeholder = { Text(text = "Search a graph", style = bigStyle) }, + placeholder = { Text(text = "Enter graph name", style = bigStyle) }, onValueChange = { search = it }, modifier = Modifier .weight(1f) @@ -45,9 +71,9 @@ fun MainScreen(navController: NavController){ .border( width = 4.dp, color = MaterialTheme.colors.primary, - shape = RoundedCornerShape(10.dp) + shape = RoundedCornerShape(30.dp) ), - shape = RoundedCornerShape(10.dp), + shape = RoundedCornerShape(30.dp), trailingIcon = { Icon( Icons.Filled.Search, contentDescription = "SearchIcon", modifier = Modifier @@ -71,9 +97,10 @@ fun MainScreen(navController: NavController){ .size(100.dp) .clip(shape = RoundedCornerShape(25.dp)) .clickable { } + .background(MaterialTheme.colors.primary) .border( width = 5.dp, - color = MaterialTheme.colors.primary, + color = Color.Black, shape = RoundedCornerShape(25.dp) ) .bounceClick(), @@ -93,9 +120,10 @@ fun MainScreen(navController: NavController){ .size(100.dp) .clip(shape = RoundedCornerShape(25.dp)) .clickable { } + .background(MaterialTheme.colors.primary) .border( width = 5.dp, - color = MaterialTheme.colors.primary, + color = Color.Black, shape = RoundedCornerShape(25.dp) ) .bounceClick(), @@ -112,29 +140,61 @@ fun MainScreen(navController: NavController){ LazyColumn(modifier = Modifier.fillMaxWidth()) { items(graphs) { graph -> if ( !graph.startsWith(search) ) return@items - Row(modifier = Modifier.padding(30.dp)) { - Text( - graph, - style = bigStyle, + Button( + onClick = {navController.navigate(Screen.GraphScreen.route)}, modifier = Modifier + .padding(20.dp) .fillMaxWidth() + .height(100.dp) .clip(shape = RoundedCornerShape(45.dp)) + .border( - width = 5.dp, - color = MaterialTheme.colors.primary, + width = 5.dp, + color = Color.Black, shape = RoundedCornerShape(45.dp) - ) - .padding(vertical = 16.dp, horizontal = 30.dp) - ) - } + ) + .background(MaterialTheme.colors.primary) + ) { + Text(text = graph, style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp))) + } } } } } @Composable -fun GraphScreen(navController: NavController){ - Text("Здесь будут графы", style= defaultStyle) +fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = GraphViewModel()) { + val graph = remember { mutableListOf(4) } + Button( + onClick = { navController.navigate(Screen.MainScreen.route) }, + modifier = Modifier + .offset (x = 16.dp, y = 16.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .background(MaterialTheme.colors.primary) + .size(120.dp, 70.dp) + .zIndex(1f) + ) { + Text("Home", style = defaultStyle) + } + Button( + onClick = { graph.add(2) }, + modifier = Modifier + .offset(x = 156.dp, y = 16.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .background(MaterialTheme.colors.primary) + .size(120.dp, 70.dp) + .zIndex(1f) + ) { + Text("Add", style = defaultStyle) + } + + Box( modifier = Modifier.fillMaxSize()){ + for (vertex in graph){ + VertexView(vertex) + } + } } @Composable diff --git a/composeApp/src/commonMain/kotlin/styling.kt b/composeApp/src/commonMain/kotlin/view/styling.kt similarity index 86% rename from composeApp/src/commonMain/kotlin/styling.kt rename to composeApp/src/commonMain/kotlin/view/styling.kt index 939b8fd..1cb8246 100644 --- a/composeApp/src/commonMain/kotlin/styling.kt +++ b/composeApp/src/commonMain/kotlin/view/styling.kt @@ -1,4 +1,4 @@ -package visualizer.styling +package view import androidx.compose.ui.unit.sp import androidx.compose.ui.text.TextStyle diff --git a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt new file mode 100644 index 0000000..26c7e03 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt @@ -0,0 +1,4 @@ +package view.views + +class GraphView { +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt new file mode 100644 index 0000000..4c66c77 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt @@ -0,0 +1,39 @@ +package view.views + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +@Composable +fun VertexView(index : Int) { + val index = index + var offsetX by remember { mutableStateOf(0f) } + var offsetY by remember { mutableStateOf(0f) } + Box(modifier = Modifier + .offset {IntOffset(offsetX.roundToInt(), offsetY.roundToInt())} + .clip(shape = CircleShape) + .size(100.dp) + .background(Color.DarkGray) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + offsetX += dragAmount.x + offsetY += dragAmount.y + } + } + ){ + Text(text = "$index") + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt new file mode 100644 index 0000000..2165aad --- /dev/null +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -0,0 +1,4 @@ +package viewmodel + +class GraphViewModel { +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt new file mode 100644 index 0000000..fd048f2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt @@ -0,0 +1,4 @@ +package viewmodel + +class VertexViewModel { +} \ No newline at end of file From 0feb61b8a145f04221c1c4e280dbf1fba4d94ef8 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 13 May 2024 05:32:49 +0300 Subject: [PATCH 031/172] Feat: change styling --- .../src/commonMain/kotlin/Navigation.kt | 11 ++- .../src/commonMain/kotlin/view/Screens.kt | 86 +++++++++++-------- .../src/commonMain/kotlin/view/styling.kt | 8 +- .../commonMain/kotlin/view/views/GraphView.kt | 10 ++- .../kotlin/view/views/VertexView.kt | 30 ++++--- .../kotlin/viewmodel/GraphViewModel.kt | 16 +++- .../kotlin/viewmodel/VertexViewModel.kt | 5 +- lib/src/commonMain/kotlin/Graph.kt | 19 +++- 8 files changed, 128 insertions(+), 57 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/Navigation.kt b/composeApp/src/commonMain/kotlin/Navigation.kt index 8a8d9cb..e339792 100644 --- a/composeApp/src/commonMain/kotlin/Navigation.kt +++ b/composeApp/src/commonMain/kotlin/Navigation.kt @@ -1,18 +1,17 @@ -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import mainscreen.* +import view.GraphScreen +import view.MainScreen +import view.Screen +import view.SettingsScreen @Composable fun Navigation() { val navController = rememberNavController() NavHost(navController = navController, - startDestination = Screen.MainScreen.route, - modifier = Modifier.padding(16.dp)) { + startDestination = Screen.MainScreen.route) { composable(route = Screen.MainScreen.route){ MainScreen(navController = navController) } diff --git a/composeApp/src/commonMain/kotlin/view/Screens.kt b/composeApp/src/commonMain/kotlin/view/Screens.kt index ae53ee9..d9e0241 100644 --- a/composeApp/src/commonMain/kotlin/view/Screens.kt +++ b/composeApp/src/commonMain/kotlin/view/Screens.kt @@ -6,10 +6,12 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.* @@ -22,11 +24,8 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavController import androidx.lifecycle.ViewModel import lib.graph.Graph -import model.GraphViewModel -import ui.VertexView -import ui.bigStyle -import ui.bounceClick -import ui.defaultStyle +import view.views.GraphView +import viewmodel.GraphViewModel sealed class Screen(val route: String){ object MainScreen: Screen("main_screen") @@ -57,7 +56,7 @@ class MainScreenViewModel : ViewModel(){ fun MainScreen(navController: NavController){ val graphs = remember { mutableStateListOf() } var search by remember { mutableStateOf("") } - Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { // Search tab TextField( @@ -70,10 +69,10 @@ fun MainScreen(navController: NavController){ .fillMaxHeight() .border( width = 4.dp, - color = MaterialTheme.colors.primary, - shape = RoundedCornerShape(30.dp) + color = DefaultColors.primary, + shape = RoundedCornerShape(45.dp) ), - shape = RoundedCornerShape(30.dp), + shape = RoundedCornerShape(45.dp), trailingIcon = { Icon( Icons.Filled.Search, contentDescription = "SearchIcon", modifier = Modifier @@ -95,13 +94,13 @@ fun MainScreen(navController: NavController){ modifier = Modifier .padding(horizontal = 10.dp) .size(100.dp) - .clip(shape = RoundedCornerShape(25.dp)) + .clip(shape = RoundedCornerShape(45.dp)) .clickable { } - .background(MaterialTheme.colors.primary) + .background(DefaultColors.primary) .border( width = 5.dp, color = Color.Black, - shape = RoundedCornerShape(25.dp) + shape = RoundedCornerShape(45.dp) ) .bounceClick(), ) { @@ -116,15 +115,15 @@ fun MainScreen(navController: NavController){ IconButton( onClick = { navController.navigate(Screen.SettingsScreen.route) }, modifier = Modifier - .padding(horizontal = 20.dp) + .padding(horizontal = 10.dp) .size(100.dp) - .clip(shape = RoundedCornerShape(25.dp)) + .clip(shape = RoundedCornerShape(45.dp)) .clickable { } - .background(MaterialTheme.colors.primary) + .background(DefaultColors.primary) .border( width = 5.dp, color = Color.Black, - shape = RoundedCornerShape(25.dp) + shape = RoundedCornerShape(45.dp) ) .bounceClick(), @@ -137,26 +136,46 @@ fun MainScreen(navController: NavController){ } } + Spacer(modifier = Modifier.height(30.dp)) + LazyColumn(modifier = Modifier.fillMaxWidth()) { - items(graphs) { graph -> - if ( !graph.startsWith(search) ) return@items + itemsIndexed(graphs) { index, graph -> + if (!graph.startsWith(search)) return@itemsIndexed + Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( - onClick = {navController.navigate(Screen.GraphScreen.route)}, + onClick = { navController.navigate(Screen.GraphScreen.route) }, modifier = Modifier - .padding(20.dp) .fillMaxWidth() .height(100.dp) + .weight(1f) .clip(shape = RoundedCornerShape(45.dp)) - .border( width = 5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp) - ) - .background(MaterialTheme.colors.primary) + ), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { Text(text = graph, style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp))) } + + Spacer(modifier = Modifier.width(10.dp)) + + IconButton( + onClick = { graphs.removeAt(index) }, + modifier = Modifier + .padding(horizontal = 10.dp) + .size(100.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .background(Color(0xe8,0x08,0x3e)) + .border(5.dp , color = Color.Black, shape = RoundedCornerShape(45.dp)) + .bounceClick(), + ){ + Icon(Icons.Filled.Delete, contentDescription = "Remove graph", modifier = Modifier + .padding(5.dp) + .fillMaxSize()) + } + } } } } @@ -164,37 +183,35 @@ fun MainScreen(navController: NavController){ @Composable fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = GraphViewModel()) { - val graph = remember { mutableListOf(4) } Button( onClick = { navController.navigate(Screen.MainScreen.route) }, modifier = Modifier .offset (x = 16.dp, y = 16.dp) .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .background(MaterialTheme.colors.primary) + .background(DefaultColors.primary) .size(120.dp, 70.dp) .zIndex(1f) ) { Text("Home", style = defaultStyle) } Button( - onClick = { graph.add(2) }, + onClick = { graphViewModel.addVertex()}, modifier = Modifier .offset(x = 156.dp, y = 16.dp) .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .background(MaterialTheme.colors.primary) + .background(DefaultColors.primary) .size(120.dp, 70.dp) - .zIndex(1f) + .zIndex(1f), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { Text("Add", style = defaultStyle) } - Box( modifier = Modifier.fillMaxSize()){ - for (vertex in graph){ - VertexView(vertex) - } - } + Box(modifier = Modifier.fillMaxSize()){ + GraphView(graphViewModel) + } } @Composable @@ -205,7 +222,8 @@ fun SettingsScreen(navController: NavController){ modifier = Modifier .padding(16.dp) .border(width = 3.dp, color = Color.Black) - .bounceClick()) { + .bounceClick(), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.background)) { Text("Назад", style = defaultStyle) } } diff --git a/composeApp/src/commonMain/kotlin/view/styling.kt b/composeApp/src/commonMain/kotlin/view/styling.kt index 1cb8246..250c55f 100644 --- a/composeApp/src/commonMain/kotlin/view/styling.kt +++ b/composeApp/src/commonMain/kotlin/view/styling.kt @@ -1,9 +1,15 @@ package view +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp import androidx.compose.ui.text.TextStyle val defaultStyle = TextStyle(fontSize = 28.sp) -val bigStyle = TextStyle(fontSize = 50.sp) \ No newline at end of file +val bigStyle = TextStyle(fontSize = 50.sp) + +object DefaultColors{ + val primary = Color(0xff,0xf1,0x4a) + val background = Color.White +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt index 26c7e03..7e43e94 100644 --- a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt +++ b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt @@ -1,4 +1,12 @@ package view.views -class GraphView { +import androidx.compose.runtime.Composable +import viewmodel.GraphViewModel +import viewmodel.VertexViewModel + +@Composable +fun GraphView(graphViewModel : GraphViewModel) { + graphViewModel.vertices.forEach{ vertexvm -> + VertexView(vertexvm) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt index 4c66c77..11a4141 100644 --- a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt +++ b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt @@ -1,31 +1,37 @@ package view.views import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.DefaultColors +import viewmodel.VertexViewModel import kotlin.math.roundToInt @Composable -fun VertexView(index : Int) { - val index = index - var offsetX by remember { mutableStateOf(0f) } - var offsetY by remember { mutableStateOf(0f) } +fun VertexView(vertexViewModel: VertexViewModel) { + val index = vertexViewModel.index + var offsetX by remember { mutableStateOf(1000f) } + var offsetY by remember { mutableStateOf(540f) } Box(modifier = Modifier .offset {IntOffset(offsetX.roundToInt(), offsetY.roundToInt())} .clip(shape = CircleShape) - .size(100.dp) - .background(Color.DarkGray) + .size(120.dp) + .background(DefaultColors.background) + .border(5.dp, Color.Black, CircleShape) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() @@ -34,6 +40,10 @@ fun VertexView(index : Int) { } } ){ - Text(text = "$index") + Text(text = "$index", + fontSize = 40.sp, + modifier = Modifier + .fillMaxSize() + .wrapContentSize(),) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index 2165aad..6a93cfe 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -1,4 +1,18 @@ package viewmodel -class GraphViewModel { +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import lib.graph.Graph + +class GraphViewModel(graph : Graph = Graph()): ViewModel() { + val vertices = mutableStateListOf() + init { + for (vertex in graph.vertices) { + vertices.add(VertexViewModel(vertex)) + } + } + + fun addVertex() = vertices.add(VertexViewModel(vertices.size)) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt index fd048f2..ec95919 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt @@ -1,4 +1,7 @@ package viewmodel -class VertexViewModel { +import androidx.lifecycle.ViewModel + +class VertexViewModel(index: Int): ViewModel() { + val index : Int = index } \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/Graph.kt b/lib/src/commonMain/kotlin/Graph.kt index 0a0f908..888b078 100644 --- a/lib/src/commonMain/kotlin/Graph.kt +++ b/lib/src/commonMain/kotlin/Graph.kt @@ -1,6 +1,19 @@ package lib.graph -import java.util.* -class graph(){ - val x : Queue = LinkedList() + +class Graph(){ + val vertices = arrayListOf() + var size = vertices.size + private set + + fun addVertex(){ + vertices.add(vertices.size) + size+=1 + } + + fun forEach(action : (Int) -> Unit ) = vertices.forEach(action) + + operator fun iterator() : Iterator { + return vertices.iterator() + } } From b04806fb03f3bc37126297f5a8fca09f1c787e44 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 13 May 2024 06:34:36 +0300 Subject: [PATCH 032/172] Feat: add MainScreenViewModel -list of graphs is saved when you back in MainScreen -restructure files in view package --- .../src/commonMain/kotlin/Navigation.kt | 12 +- .../kotlin/view/screens/GraphScreen.kt | 56 +++++++++ .../{Screens.kt => screens/MainScreen.kt} | 112 +++--------------- .../kotlin/view/screens/SealedScreens.kt | 7 ++ .../kotlin/view/screens/SettingsScreen.kt | 33 ++++++ .../kotlin/view/views/VertexView.kt | 2 +- .../kotlin/viewmodel/GraphViewModel.kt | 3 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 14 +++ 8 files changed, 139 insertions(+), 100 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt rename composeApp/src/commonMain/kotlin/view/{Screens.kt => screens/MainScreen.kt} (61%) create mode 100644 composeApp/src/commonMain/kotlin/view/screens/SealedScreens.kt create mode 100644 composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/Navigation.kt b/composeApp/src/commonMain/kotlin/Navigation.kt index e339792..4c441ef 100644 --- a/composeApp/src/commonMain/kotlin/Navigation.kt +++ b/composeApp/src/commonMain/kotlin/Navigation.kt @@ -2,18 +2,20 @@ import androidx.compose.runtime.Composable import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import view.GraphScreen -import view.MainScreen -import view.Screen -import view.SettingsScreen +import view.screens.GraphScreen +import view.screens.MainScreen +import view.screens.Screen +import view.screens.SettingsScreen +import viewmodel.MainScreenViewModel @Composable fun Navigation() { val navController = rememberNavController() + val mainScreenViewModel = MainScreenViewModel() NavHost(navController = navController, startDestination = Screen.MainScreen.route) { composable(route = Screen.MainScreen.route){ - MainScreen(navController = navController) + MainScreen(navController = navController, mainScreenViewModel) } composable(route = Screen.GraphScreen.route){ GraphScreen(navController = navController) diff --git a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt new file mode 100644 index 0000000..a5260a1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt @@ -0,0 +1,56 @@ +package view.screens + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import view.DefaultColors +import view.defaultStyle +import view.views.GraphView +import viewmodel.GraphViewModel + +@Composable +fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = GraphViewModel("")) { + + // To MainScreen + Button( + onClick = { navController.popBackStack() }, + modifier = Modifier + .offset (x = 16.dp, y = 16.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(120.dp, 70.dp) + .zIndex(1f), + colors = ButtonDefaults.buttonColors(DefaultColors.primary) + ) { + Text("Home", style = defaultStyle) + } + Button( + onClick = { graphViewModel.addVertex()}, + modifier = Modifier + .offset(x = 156.dp, y = 16.dp) + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(120.dp, 70.dp) + .zIndex(1f), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text("Add", style = defaultStyle) + } + + Box(modifier = Modifier.fillMaxSize()){ + GraphView(graphViewModel) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/Screens.kt b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt similarity index 61% rename from composeApp/src/commonMain/kotlin/view/Screens.kt rename to composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt index d9e0241..a7c4317 100644 --- a/composeApp/src/commonMain/kotlin/view/Screens.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt @@ -1,11 +1,10 @@ -package view +package view.screens import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -19,43 +18,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex import androidx.navigation.NavController -import androidx.lifecycle.ViewModel -import lib.graph.Graph -import view.views.GraphView -import viewmodel.GraphViewModel - -sealed class Screen(val route: String){ - object MainScreen: Screen("main_screen") - object GraphScreen: Screen("graph_screen") - object SettingsScreen: Screen("settings_screen") -} - -class MainScreenViewModel : ViewModel(){ - val graphs = mutableStateOf(arrayListOf()) - val searchText = mutableStateOf("") - - fun addGraph(graph: Graph){ - graphs.value.add(graph) - } - fun removeGraph(index : Int){ - if (index in graphs.value.indices){ - graphs.value.removeAt(index) - } - else throw IllegalArgumentException("graph index out of range") - } - - fun changeSearchText(text: String){ - searchText.value = text - } -} +import view.DefaultColors +import view.bigStyle +import view.bounceClick +import viewmodel.MainScreenViewModel @Composable -fun MainScreen(navController: NavController){ - val graphs = remember { mutableStateListOf() } - var search by remember { mutableStateOf("") } +fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel = MainScreenViewModel()){ + var search by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { // Search tab @@ -88,9 +59,9 @@ fun MainScreen(navController: NavController){ Spacer(modifier = Modifier.width(40.dp)) - // add graph + // Add graph IconButton( - onClick = { graphs.add(search) }, + onClick = { mainScreenViewModel.addGraph(search) }, modifier = Modifier .padding(horizontal = 10.dp) .size(100.dp) @@ -111,7 +82,7 @@ fun MainScreen(navController: NavController){ ) } - // to settings + // To settings IconButton( onClick = { navController.navigate(Screen.SettingsScreen.route) }, modifier = Modifier @@ -139,8 +110,10 @@ fun MainScreen(navController: NavController){ Spacer(modifier = Modifier.height(30.dp)) LazyColumn(modifier = Modifier.fillMaxWidth()) { - itemsIndexed(graphs) { index, graph -> - if (!graph.startsWith(search)) return@itemsIndexed + itemsIndexed(mainScreenViewModel.graphs) { index, graph -> + if (!graph.name.startsWith(search)) return@itemsIndexed + + // To GraphScreen Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( onClick = { navController.navigate(Screen.GraphScreen.route) }, @@ -156,13 +129,14 @@ fun MainScreen(navController: NavController){ ), colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { - Text(text = graph, style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp))) + Text(text = graph.name, style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp))) } Spacer(modifier = Modifier.width(10.dp)) + // Remove Graph IconButton( - onClick = { graphs.removeAt(index) }, + onClick = { mainScreenViewModel.graphs.removeAt(index) }, modifier = Modifier .padding(horizontal = 10.dp) .size(100.dp) @@ -171,7 +145,8 @@ fun MainScreen(navController: NavController){ .border(5.dp , color = Color.Black, shape = RoundedCornerShape(45.dp)) .bounceClick(), ){ - Icon(Icons.Filled.Delete, contentDescription = "Remove graph", modifier = Modifier + Icon( + Icons.Filled.Delete, contentDescription = "Remove graph", modifier = Modifier .padding(5.dp) .fillMaxSize()) } @@ -179,53 +154,4 @@ fun MainScreen(navController: NavController){ } } } -} - -@Composable -fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = GraphViewModel()) { - Button( - onClick = { navController.navigate(Screen.MainScreen.route) }, - modifier = Modifier - .offset (x = 16.dp, y = 16.dp) - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .background(DefaultColors.primary) - .size(120.dp, 70.dp) - .zIndex(1f) - ) { - Text("Home", style = defaultStyle) - } - Button( - onClick = { graphViewModel.addVertex()}, - modifier = Modifier - .offset(x = 156.dp, y = 16.dp) - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .background(DefaultColors.primary) - .size(120.dp, 70.dp) - .zIndex(1f), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text("Add", style = defaultStyle) - } - - Box(modifier = Modifier.fillMaxSize()){ - GraphView(graphViewModel) - } -} - -@Composable -fun SettingsScreen(navController: NavController){ - Column{ - Text(text = "Тут наверно будут настройки", fontSize = 28.sp) - Button(onClick = {navController.navigate(Screen.MainScreen.route)}, - modifier = Modifier - .padding(16.dp) - .border(width = 3.dp, color = Color.Black) - .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.background)) { - Text("Назад", style = defaultStyle) - } - } - } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/screens/SealedScreens.kt b/composeApp/src/commonMain/kotlin/view/screens/SealedScreens.kt new file mode 100644 index 0000000..4e44e6b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/screens/SealedScreens.kt @@ -0,0 +1,7 @@ +package view.screens + +sealed class Screen(val route: String){ + object MainScreen: Screen("main_screen") + object GraphScreen: Screen("graph_screen") + object SettingsScreen: Screen("settings_screen") +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt new file mode 100644 index 0000000..ba1a409 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt @@ -0,0 +1,33 @@ +package view.screens + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import view.DefaultColors +import view.bounceClick +import view.defaultStyle + +@Composable +fun SettingsScreen(navController: NavController){ + Column{ + Text(text = "Тут наверно будут настройки", fontSize = 28.sp) + Button(onClick = {navController.navigate(Screen.MainScreen.route)}, + modifier = Modifier + .padding(16.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick(), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { + Text("Назад", style = defaultStyle) + } + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt index 11a4141..d819d5b 100644 --- a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt +++ b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt @@ -30,7 +30,7 @@ fun VertexView(vertexViewModel: VertexViewModel) { .offset {IntOffset(offsetX.roundToInt(), offsetY.roundToInt())} .clip(shape = CircleShape) .size(120.dp) - .background(DefaultColors.background) + .background(DefaultColors.primary) .border(5.dp, Color.Black, CircleShape) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index 6a93cfe..267dd67 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -6,7 +6,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import lib.graph.Graph -class GraphViewModel(graph : Graph = Graph()): ViewModel() { +class GraphViewModel(name : String, graph : Graph = Graph()): ViewModel() { + val name by mutableStateOf(name) val vertices = mutableStateListOf() init { for (vertex in graph.vertices) { diff --git a/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt new file mode 100644 index 0000000..e453267 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt @@ -0,0 +1,14 @@ +package viewmodel + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel + +class MainScreenViewModel: ViewModel() { + val graphs = mutableStateListOf() + + fun addGraph(name : String) { + graphs.add(GraphViewModel(name)) + } +} \ No newline at end of file From e20b35ce209066093e17d99cfd61f7ce16f687f0 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 13 May 2024 16:04:56 -0400 Subject: [PATCH 033/172] fix: dependancy bug in some AGP & Greadle versions fix --- composeApp/build.gradle.kts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 6087af4..85a20de 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask +import com.android.build.gradle.internal.lint.LintModelWriterTask import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { @@ -72,6 +74,13 @@ android { } dependencies { debugImplementation(libs.compose.ui.tooling) + tasks.withType{ + dependsOn("generateResourceAccessorsForAndroidUnitTest") + } + tasks.withType{ + dependsOn("generateResourceAccessorsForAndroidUnitTest") + } + } } From 9646dbd59e358e2f23e4ae33c93ec6624439ebf4 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> Date: Tue, 14 May 2024 06:06:29 +0300 Subject: [PATCH 034/172] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ebed141..ce33104 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,8 @@ [![Measure coverage](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/coverage.yml) [![MegaLinter](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/megalinter.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/megalinter.yml) [![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](http://www.wtfpl.net/about/) + +You can run it by +``` +gradle desktopRun -DmainClass=MainKt --quiet +``` From 81d2acc218ed2efa526abbdde4b8b024f935739a Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 14 May 2024 08:53:29 +0300 Subject: [PATCH 035/172] Feat: implemented saving graph into MainScreenViewModel -navigate to GraphScreen now passing the graphId --- composeApp/src/commonMain/kotlin/Navigation.kt | 13 +++++++++++-- .../commonMain/kotlin/view/screens/GraphScreen.kt | 10 +++++++--- .../commonMain/kotlin/view/screens/MainScreen.kt | 4 ++-- .../kotlin/view/screens/SettingsScreen.kt | 2 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 4 ++++ 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/Navigation.kt b/composeApp/src/commonMain/kotlin/Navigation.kt index 4c441ef..4f2dfa2 100644 --- a/composeApp/src/commonMain/kotlin/Navigation.kt +++ b/composeApp/src/commonMain/kotlin/Navigation.kt @@ -1,7 +1,9 @@ import androidx.compose.runtime.Composable +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import view.screens.GraphScreen import view.screens.MainScreen import view.screens.Screen @@ -17,8 +19,15 @@ fun Navigation() { composable(route = Screen.MainScreen.route){ MainScreen(navController = navController, mainScreenViewModel) } - composable(route = Screen.GraphScreen.route){ - GraphScreen(navController = navController) + composable( + route = "${Screen.GraphScreen.route}/{graphId}", + arguments = listOf(navArgument("graphId") { type = NavType.IntType }) + ){ navBackStackEntry -> + val graphId = navBackStackEntry.arguments?.getInt("graphId") + graphId?.let{ + GraphScreen(navController, mainScreenViewModel, graphId) + } + } composable(route = Screen.SettingsScreen.route){ SettingsScreen(navController = navController) diff --git a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt index a5260a1..682df7e 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt @@ -10,6 +10,8 @@ import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -20,9 +22,11 @@ import view.DefaultColors import view.defaultStyle import view.views.GraphView import viewmodel.GraphViewModel +import viewmodel.MainScreenViewModel @Composable -fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = GraphViewModel("")) { +fun GraphScreen(navController: NavController, mainScreenViewModel : MainScreenViewModel, graphId : Int) { + val graphModel by mutableStateOf(mainScreenViewModel.getGraph(graphId)) // To MainScreen Button( @@ -38,7 +42,7 @@ fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = Text("Home", style = defaultStyle) } Button( - onClick = { graphViewModel.addVertex()}, + onClick = { graphModel.addVertex()}, modifier = Modifier .offset(x = 156.dp, y = 16.dp) .clip(shape = RoundedCornerShape(45.dp)) @@ -51,6 +55,6 @@ fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = } Box(modifier = Modifier.fillMaxSize()){ - GraphView(graphViewModel) + GraphView(graphModel) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt index a7c4317..df62b19 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt @@ -25,7 +25,7 @@ import view.bounceClick import viewmodel.MainScreenViewModel @Composable -fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel = MainScreenViewModel()){ +fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel){ var search by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { @@ -116,7 +116,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView // To GraphScreen Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( - onClick = { navController.navigate(Screen.GraphScreen.route) }, + onClick = { navController.navigate("${Screen.GraphScreen.route}/$index") }, modifier = Modifier .fillMaxWidth() .height(100.dp) diff --git a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt index ba1a409..52b44fb 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt @@ -20,7 +20,7 @@ import view.defaultStyle fun SettingsScreen(navController: NavController){ Column{ Text(text = "Тут наверно будут настройки", fontSize = 28.sp) - Button(onClick = {navController.navigate(Screen.MainScreen.route)}, + Button(onClick = {navController.popBackStack()}, modifier = Modifier .padding(16.dp) .border(width = 3.dp, color = Color.Black) diff --git a/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt index e453267..600d84a 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt @@ -11,4 +11,8 @@ class MainScreenViewModel: ViewModel() { fun addGraph(name : String) { graphs.add(GraphViewModel(name)) } + + fun getGraph(graphId: Int): GraphViewModel { + return graphs[graphId] + } } \ No newline at end of file From eeb0e433aad9063852a47d333391f5754dab9611 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 14 May 2024 11:08:55 +0300 Subject: [PATCH 036/172] Feat: Implemented graph classes and edges view in gui --- .../commonMain/kotlin/view/views/GraphView.kt | 2 +- .../kotlin/view/views/VertexView.kt | 34 +++++++++----- .../kotlin/viewmodel/GraphViewModel.kt | 7 +-- .../kotlin/viewmodel/VertexViewModel.kt | 12 ++++- lib/src/commonMain/kotlin/Graph.kt | 19 -------- lib/src/commonMain/kotlin/Graphs.kt | 44 +++++++++++++++++++ 6 files changed, 82 insertions(+), 36 deletions(-) delete mode 100644 lib/src/commonMain/kotlin/Graph.kt create mode 100644 lib/src/commonMain/kotlin/Graphs.kt diff --git a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt index 7e43e94..24e3843 100644 --- a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt +++ b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt @@ -7,6 +7,6 @@ import viewmodel.VertexViewModel @Composable fun GraphView(graphViewModel : GraphViewModel) { graphViewModel.vertices.forEach{ vertexvm -> - VertexView(vertexvm) + VertexView(vertexvm, graphViewModel) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt index d819d5b..3bee29d 100644 --- a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt +++ b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt @@ -1,33 +1,33 @@ package view.views +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import view.DefaultColors +import viewmodel.GraphViewModel import viewmodel.VertexViewModel import kotlin.math.roundToInt @Composable -fun VertexView(vertexViewModel: VertexViewModel) { - val index = vertexViewModel.index - var offsetX by remember { mutableStateOf(1000f) } - var offsetY by remember { mutableStateOf(540f) } +fun VertexView(vertexVM: VertexViewModel, graphViewModel: GraphViewModel) { + val number = vertexVM.number + Box(modifier = Modifier - .offset {IntOffset(offsetX.roundToInt(), offsetY.roundToInt())} + .offset {IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt())} .clip(shape = CircleShape) .size(120.dp) .background(DefaultColors.primary) @@ -35,15 +35,27 @@ fun VertexView(vertexViewModel: VertexViewModel) { .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() - offsetX += dragAmount.x - offsetY += dragAmount.y + vertexVM.offsetX += dragAmount.x + vertexVM.offsetY += dragAmount.y } } ){ - Text(text = "$index", + Text(text = "$number", fontSize = 40.sp, modifier = Modifier .fillMaxSize() .wrapContentSize(),) } + + vertexVM.edges.forEach{ otherNumber -> + val otherX = (graphViewModel.vertices.find{ vertexVM -> vertexVM.number == otherNumber})!!.offsetX + val otherY = (graphViewModel.vertices.find{vertexVM -> vertexVM.number == otherNumber})!!.offsetY + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)){ + drawLine( + start = Offset(vertexVM.offsetX + vertexVM.vertexSize/2, vertexVM.offsetY + vertexVM.vertexSize/2), + end = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2), + strokeWidth = 10f, + color = Color.Black) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index 267dd67..a6e6904 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -5,13 +5,14 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import lib.graph.Graph +import lib.graph.UndirectedGraph -class GraphViewModel(name : String, graph : Graph = Graph()): ViewModel() { +class GraphViewModel(name : String, graph : Graph = UndirectedGraph()): ViewModel() { val name by mutableStateOf(name) val vertices = mutableStateListOf() init { - for (vertex in graph.vertices) { - vertices.add(VertexViewModel(vertex)) + for (vertex in graph){ + vertices.add(VertexViewModel(vertex.key,vertex.value)) } } diff --git a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt index ec95919..46391d8 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt @@ -1,7 +1,15 @@ package viewmodel +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel -class VertexViewModel(index: Int): ViewModel() { - val index : Int = index +class VertexViewModel(number: Int, edges: MutableList = mutableListOf()): ViewModel() { + val number : Int = number + val edges = edges + var offsetX by mutableStateOf(1000f) + var offsetY by mutableStateOf(540f) + val vertexSize = 120f } \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/Graph.kt b/lib/src/commonMain/kotlin/Graph.kt deleted file mode 100644 index 888b078..0000000 --- a/lib/src/commonMain/kotlin/Graph.kt +++ /dev/null @@ -1,19 +0,0 @@ -package lib.graph - - -class Graph(){ - val vertices = arrayListOf() - var size = vertices.size - private set - - fun addVertex(){ - vertices.add(vertices.size) - size+=1 - } - - fun forEach(action : (Int) -> Unit ) = vertices.forEach(action) - - operator fun iterator() : Iterator { - return vertices.iterator() - } -} diff --git a/lib/src/commonMain/kotlin/Graphs.kt b/lib/src/commonMain/kotlin/Graphs.kt new file mode 100644 index 0000000..fea5018 --- /dev/null +++ b/lib/src/commonMain/kotlin/Graphs.kt @@ -0,0 +1,44 @@ +package lib.graph + + +abstract class Graph(){ + protected val graph = mutableMapOf>() + var size = graph.size + private set + + init{ + graph[2] = mutableListOf(1,3,4,5,6,7) + graph[4] = mutableListOf(4,5) + graph[5] = mutableListOf(4,2) + graph[6] = mutableListOf(1,2,4,5) + graph[1] = mutableListOf(2,4,5,6) + graph[3] = mutableListOf(1,2,4,5,6) + graph[7] = mutableListOf(1,2,3,4,5,6) + size = 7 + } + + fun addVertex(number: Int){ + graph.putIfAbsent(number, mutableListOf()) + } + + abstract fun addEdge(from: Int, to: Int) + + fun forEach(action : (MutableList) -> Unit ) { + graph.forEach { number, list -> action(list) } + } + + operator fun iterator() = graph.entries.iterator() +} + +class DirectedGraph: Graph(){ + override fun addEdge(from: Int, to: Int) { + graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} + } +} + +class UndirectedGraph: Graph(){ + override fun addEdge(from: Int, to: Int) { + graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} + graph[to]?.add(from) ?: {graph[to] = mutableListOf(from)} + } +} From 6c5e30d4c08fca00171072fa156dbe35249f7e4c Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 14 May 2024 13:07:21 +0300 Subject: [PATCH 037/172] Feat: add edgesFrom method in Graph class --- .../src/commonMain/kotlin/view/screens/GraphScreen.kt | 2 ++ .../src/commonMain/kotlin/viewmodel/GraphViewModel.kt | 6 ++++-- .../commonMain/kotlin/viewmodel/MainScreenViewModel.kt | 2 -- lib/src/commonMain/kotlin/Graphs.kt | 9 +++++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt index 682df7e..4ea0dae 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt @@ -41,6 +41,8 @@ fun GraphScreen(navController: NavController, mainScreenViewModel : MainScreenVi ) { Text("Home", style = defaultStyle) } + + // Add vertex Button( onClick = { graphModel.addVertex()}, modifier = Modifier diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index a6e6904..1674129 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -3,6 +3,7 @@ package viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable import androidx.lifecycle.ViewModel import lib.graph.Graph import lib.graph.UndirectedGraph @@ -15,6 +16,7 @@ class GraphViewModel(name : String, graph : Graph = UndirectedGraph()): ViewMode vertices.add(VertexViewModel(vertex.key,vertex.value)) } } - - fun addVertex() = vertices.add(VertexViewModel(vertices.size)) + fun addVertex(){ + vertices.add(VertexViewModel(vertices.size)) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt index 600d84a..d5a509b 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt @@ -1,8 +1,6 @@ package viewmodel -import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember import androidx.lifecycle.ViewModel class MainScreenViewModel: ViewModel() { diff --git a/lib/src/commonMain/kotlin/Graphs.kt b/lib/src/commonMain/kotlin/Graphs.kt index fea5018..64a44d4 100644 --- a/lib/src/commonMain/kotlin/Graphs.kt +++ b/lib/src/commonMain/kotlin/Graphs.kt @@ -6,14 +6,15 @@ abstract class Graph(){ var size = graph.size private set + // temporary init{ - graph[2] = mutableListOf(1,3,4,5,6,7) + graph[2] = mutableListOf(1,3,4,5,6,0) graph[4] = mutableListOf(4,5) graph[5] = mutableListOf(4,2) graph[6] = mutableListOf(1,2,4,5) graph[1] = mutableListOf(2,4,5,6) graph[3] = mutableListOf(1,2,4,5,6) - graph[7] = mutableListOf(1,2,3,4,5,6) + graph[0] = mutableListOf(1,2,3,4,5,6) size = 7 } @@ -23,6 +24,10 @@ abstract class Graph(){ abstract fun addEdge(from: Int, to: Int) + fun edgesFrom(from: Int): MutableList{ + return graph[from]?: mutableListOf() + } + fun forEach(action : (MutableList) -> Unit ) { graph.forEach { number, list -> action(list) } } From 383fda8ef2f666a3c849933dcd308a93b97aa91c Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> Date: Tue, 14 May 2024 13:21:32 +0300 Subject: [PATCH 038/172] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce33104..aab5314 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,5 @@ You can run it by ``` -gradle desktopRun -DmainClass=MainKt --quiet +./gradlew run ``` From 37fe7440831151f21b2950c0c6e0be2612eea5d5 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 14 May 2024 13:40:42 +0300 Subject: [PATCH 039/172] Feat: set base graph classes --- .../kotlin/viewmodel/GraphViewModel.kt | 5 ++--- .../kotlin/algos/FordBellman/FordBellman.kt | 1 + .../{ => algos}/cycleFind/LoadingGraph.kt | 2 +- .../kotlin/algos/cycleFind/findCycle.kt | 1 + .../commonMain/kotlin/cycleFind/findCycle.kt | 1 - lib/src/commonMain/kotlin/graph/Digraph.kt | 7 +++++++ lib/src/commonMain/kotlin/graph/Graph.kt | 8 ++++++++ .../{Graphs.kt => graph/GraphAbstract.kt} | 18 ++---------------- .../commonMain/kotlin/graph/WeightedDigraph.kt | 4 ++++ .../commonMain/kotlin/graph/WeightedGraph.kt | 6 ++++++ 10 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt rename lib/src/commonMain/kotlin/{ => algos}/cycleFind/LoadingGraph.kt (81%) create mode 100644 lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt delete mode 100644 lib/src/commonMain/kotlin/cycleFind/findCycle.kt create mode 100644 lib/src/commonMain/kotlin/graph/Digraph.kt create mode 100644 lib/src/commonMain/kotlin/graph/Graph.kt rename lib/src/commonMain/kotlin/{Graphs.kt => graph/GraphAbstract.kt} (69%) create mode 100644 lib/src/commonMain/kotlin/graph/WeightedDigraph.kt create mode 100644 lib/src/commonMain/kotlin/graph/WeightedGraph.kt diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index 1674129..9daf3ca 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -3,10 +3,9 @@ package viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable import androidx.lifecycle.ViewModel -import lib.graph.Graph -import lib.graph.UndirectedGraph +import lib.graph.Graph.Graph +import lib.graph.Graph.UndirectedGraph class GraphViewModel(name : String, graph : Graph = UndirectedGraph()): ViewModel() { val name by mutableStateOf(name) diff --git a/lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt b/lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt new file mode 100644 index 0000000..29f6ef3 --- /dev/null +++ b/lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt @@ -0,0 +1 @@ +package algos.FordBellman diff --git a/lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt b/lib/src/commonMain/kotlin/algos/cycleFind/LoadingGraph.kt similarity index 81% rename from lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt rename to lib/src/commonMain/kotlin/algos/cycleFind/LoadingGraph.kt index dc98b54..ace3f7e 100644 --- a/lib/src/commonMain/kotlin/cycleFind/LoadingGraph.kt +++ b/lib/src/commonMain/kotlin/algos/cycleFind/LoadingGraph.kt @@ -1,4 +1,4 @@ -package cycleFind +package algos.cycleFind class LoadingGraph { //TODO("make it possible to use cycle search algorithms through this algorithm") diff --git a/lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt b/lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt new file mode 100644 index 0000000..44e42ec --- /dev/null +++ b/lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt @@ -0,0 +1 @@ +package algos.cycleFind \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/cycleFind/findCycle.kt b/lib/src/commonMain/kotlin/cycleFind/findCycle.kt deleted file mode 100644 index b339358..0000000 --- a/lib/src/commonMain/kotlin/cycleFind/findCycle.kt +++ /dev/null @@ -1 +0,0 @@ -package cycleFind \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/graph/Digraph.kt b/lib/src/commonMain/kotlin/graph/Digraph.kt new file mode 100644 index 0000000..d6a97c6 --- /dev/null +++ b/lib/src/commonMain/kotlin/graph/Digraph.kt @@ -0,0 +1,7 @@ +package lib.graph + +class Digraph: GraphAbstract(){ + override fun addEdge(from: Int, to: Int) { + graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} + } +} diff --git a/lib/src/commonMain/kotlin/graph/Graph.kt b/lib/src/commonMain/kotlin/graph/Graph.kt new file mode 100644 index 0000000..5410fb8 --- /dev/null +++ b/lib/src/commonMain/kotlin/graph/Graph.kt @@ -0,0 +1,8 @@ +package lib.graph + +class Graph: GraphAbstract(){ + override fun addEdge(from: Int, to: Int) { + graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} + graph[to]?.add(from) ?: {graph[to] = mutableListOf(from)} + } +} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/Graphs.kt b/lib/src/commonMain/kotlin/graph/GraphAbstract.kt similarity index 69% rename from lib/src/commonMain/kotlin/Graphs.kt rename to lib/src/commonMain/kotlin/graph/GraphAbstract.kt index 64a44d4..bbb3a7f 100644 --- a/lib/src/commonMain/kotlin/Graphs.kt +++ b/lib/src/commonMain/kotlin/graph/GraphAbstract.kt @@ -1,7 +1,6 @@ package lib.graph - -abstract class Graph(){ +abstract class GraphAbstract(){ protected val graph = mutableMapOf>() var size = graph.size private set @@ -33,17 +32,4 @@ abstract class Graph(){ } operator fun iterator() = graph.entries.iterator() -} - -class DirectedGraph: Graph(){ - override fun addEdge(from: Int, to: Int) { - graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} - } -} - -class UndirectedGraph: Graph(){ - override fun addEdge(from: Int, to: Int) { - graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} - graph[to]?.add(from) ?: {graph[to] = mutableListOf(from)} - } -} +} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/graph/WeightedDigraph.kt b/lib/src/commonMain/kotlin/graph/WeightedDigraph.kt new file mode 100644 index 0000000..81e44f3 --- /dev/null +++ b/lib/src/commonMain/kotlin/graph/WeightedDigraph.kt @@ -0,0 +1,4 @@ +package graph + +class WeightedDigraph { +} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/graph/WeightedGraph.kt b/lib/src/commonMain/kotlin/graph/WeightedGraph.kt new file mode 100644 index 0000000..5029513 --- /dev/null +++ b/lib/src/commonMain/kotlin/graph/WeightedGraph.kt @@ -0,0 +1,6 @@ +package graph + +import lib.graph.GraphAbstract + +//class WeightedGraph: GraphAbstract { +//} \ No newline at end of file From 3e7c4c46eead5a9eb740e5777d37216ba1dcfb0b Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 14 May 2024 13:03:24 -0400 Subject: [PATCH 040/172] add: settings & cn-en-ru lang support --- .../kotlin/view/screens/SettingsScreen.kt | 56 ++++++++++++++++++- composeApp/src/settings.json | 1 + 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 composeApp/src/settings.json diff --git a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt index ba1a409..0fa095b 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt @@ -12,14 +12,43 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import view.DefaultColors import view.bounceClick import view.defaultStyle +import java.io.File + + +@Serializable +class SettingsJSON(var language: String) +enum class SettingType{ + LANGUAGE +} + +fun resetSettings(){ + File("src/settings.json").writeText(Json.encodeToString(SettingsJSON("en-EN"))) +} + +fun makeSetting(name: SettingType, value: String){ + try{ + val data = Json.decodeFromString(File("src/settings.json").readText()) + when (name){ + SettingType.LANGUAGE -> data.language = value; + } + File("src/settings.json").writeText(Json.encodeToString(data)) + } + catch(exception: Exception){ + resetSettings() + return + } +} @Composable fun SettingsScreen(navController: NavController){ Column{ - Text(text = "Тут наверно будут настройки", fontSize = 28.sp) + Text(text = "我没错", fontSize = 28.sp) Button(onClick = {navController.navigate(Screen.MainScreen.route)}, modifier = Modifier .padding(16.dp) @@ -28,6 +57,29 @@ fun SettingsScreen(navController: NavController){ colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { Text("Назад", style = defaultStyle) } + Button(onClick = {makeSetting(SettingType.LANGUAGE, "cn-CN")}, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick(), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { + Text("Chinese", style = defaultStyle) + } + Button(onClick = {makeSetting(SettingType.LANGUAGE, "ru-RU")}, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick(), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { + Text("Russian", style = defaultStyle) + } + Button(onClick = {makeSetting(SettingType.LANGUAGE, "en-US")}, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick(), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { + Text("English", style = defaultStyle) + } } - } \ No newline at end of file diff --git a/composeApp/src/settings.json b/composeApp/src/settings.json new file mode 100644 index 0000000..c38add4 --- /dev/null +++ b/composeApp/src/settings.json @@ -0,0 +1 @@ +{"language":"cn-CN"} \ No newline at end of file From 8d6a5413ce141c50703945a9dd2e5379485ed3aa Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 14 May 2024 14:10:09 -0400 Subject: [PATCH 041/172] add: Russian and Chinese localisation --- composeApp/build.gradle.kts | 2 ++ .../src/commonMain/kotlin/Localisation.kt | 30 +++++++++++++++++++ .../kotlin/view/screens/GraphScreen.kt | 11 +++---- .../kotlin/view/screens/MainScreen.kt | 3 +- .../kotlin/view/screens/SettingsScreen.kt | 26 ++++++++-------- composeApp/src/localisation/cn-CN.json | 0 composeApp/src/localisation/en-US.json | 2 ++ composeApp/src/localisation/ru-RU.json | 2 ++ 8 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/Localisation.kt create mode 100644 composeApp/src/localisation/cn-CN.json create mode 100644 composeApp/src/localisation/en-US.json create mode 100644 composeApp/src/localisation/ru-RU.json diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 85a20de..95e34f6 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" } kotlin { @@ -35,6 +36,7 @@ kotlin { implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(project(":lib")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation(libs.androidx.navigation.compose) } desktopMain.dependencies { diff --git a/composeApp/src/commonMain/kotlin/Localisation.kt b/composeApp/src/commonMain/kotlin/Localisation.kt new file mode 100644 index 0000000..0708d4b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/Localisation.kt @@ -0,0 +1,30 @@ +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import view.screens.SettingsJSON +import java.io.File + +@Serializable +class TranslationPair(val code: String, val localisation: String) + +@Serializable +class TranslationList(val transList: List) + +fun localisation(text: String): String{ + try { + + println("HEREEEE") + val language = Json.decodeFromString(File("src/settings.json").readText()).language + val data = Json.decodeFromString(File("src/localisation/$language.json").readText()) + for (wordPair in data.transList) { + if (wordPair.code == text){ + return wordPair.localisation + } + } + println("HEREEEE") + return text + } + catch (ex: Exception){ + println(ex) + return text + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt index a5260a1..c76a47d 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController +import localisation import view.DefaultColors import view.defaultStyle import view.views.GraphView @@ -31,23 +32,23 @@ fun GraphScreen(navController: NavController, graphViewModel : GraphViewModel = .offset (x = 16.dp, y = 16.dp) .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(120.dp, 70.dp) + .size(240.dp, 80.dp) .zIndex(1f), colors = ButtonDefaults.buttonColors(DefaultColors.primary) ) { - Text("Home", style = defaultStyle) + Text(localisation("home"), style = defaultStyle) } Button( onClick = { graphViewModel.addVertex()}, modifier = Modifier - .offset(x = 156.dp, y = 16.dp) + .offset(x = 266.dp, y = 16.dp) .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(120.dp, 70.dp) + .size(240.dp, 80.dp) .zIndex(1f), colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { - Text("Add", style = defaultStyle) + Text(localisation("add"), style = defaultStyle) } Box(modifier = Modifier.fillMaxSize()){ diff --git a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt index a7c4317..4d349cb 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import localisation import view.DefaultColors import view.bigStyle import view.bounceClick @@ -33,7 +34,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView TextField( value = search, textStyle = bigStyle, - placeholder = { Text(text = "Enter graph name", style = bigStyle) }, + placeholder = { Text(text = localisation("enter_graph_name"), style = bigStyle) }, onValueChange = { search = it }, modifier = Modifier .weight(1f) diff --git a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt index 0fa095b..471d129 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt @@ -15,6 +15,7 @@ import androidx.navigation.NavController import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import localisation import view.DefaultColors import view.bounceClick import view.defaultStyle @@ -28,7 +29,7 @@ enum class SettingType{ } fun resetSettings(){ - File("src/settings.json").writeText(Json.encodeToString(SettingsJSON("en-EN"))) + File("src/settings.json").writeText(Json.encodeToString(SettingsJSON("en-US"))) } fun makeSetting(name: SettingType, value: String){ @@ -48,22 +49,14 @@ fun makeSetting(name: SettingType, value: String){ @Composable fun SettingsScreen(navController: NavController){ Column{ - Text(text = "我没错", fontSize = 28.sp) - Button(onClick = {navController.navigate(Screen.MainScreen.route)}, - modifier = Modifier - .padding(16.dp) - .border(width = 3.dp, color = Color.Black) - .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { - Text("Назад", style = defaultStyle) - } + Text(text = localisation("settings"), fontSize = 28.sp, modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp)) Button(onClick = {makeSetting(SettingType.LANGUAGE, "cn-CN")}, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) .border(width = 3.dp, color = Color.Black) .bounceClick(), colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { - Text("Chinese", style = defaultStyle) + Text("汉语", style = defaultStyle) } Button(onClick = {makeSetting(SettingType.LANGUAGE, "ru-RU")}, modifier = Modifier @@ -71,7 +64,7 @@ fun SettingsScreen(navController: NavController){ .border(width = 3.dp, color = Color.Black) .bounceClick(), colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { - Text("Russian", style = defaultStyle) + Text("Русский", style = defaultStyle) } Button(onClick = {makeSetting(SettingType.LANGUAGE, "en-US")}, modifier = Modifier @@ -81,5 +74,14 @@ fun SettingsScreen(navController: NavController){ colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { Text("English", style = defaultStyle) } + Button(onClick = {navController.navigate(Screen.MainScreen.route)}, + modifier = Modifier + .padding(16.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick(), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.error)) { + Text(localisation("back"), style = defaultStyle, color = Color.White) + } + } } \ No newline at end of file diff --git a/composeApp/src/localisation/cn-CN.json b/composeApp/src/localisation/cn-CN.json new file mode 100644 index 0000000..e69de29 diff --git a/composeApp/src/localisation/en-US.json b/composeApp/src/localisation/en-US.json new file mode 100644 index 0000000..03a8c93 --- /dev/null +++ b/composeApp/src/localisation/en-US.json @@ -0,0 +1,2 @@ + +cn-CN.json \ No newline at end of file diff --git a/composeApp/src/localisation/ru-RU.json b/composeApp/src/localisation/ru-RU.json new file mode 100644 index 0000000..03a8c93 --- /dev/null +++ b/composeApp/src/localisation/ru-RU.json @@ -0,0 +1,2 @@ + +cn-CN.json \ No newline at end of file From a5d62a6c34214eb8e8d5b968e6074c469e524bf7 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 14 May 2024 14:34:18 -0400 Subject: [PATCH 042/172] add: translation error report --- composeApp/src/commonMain/kotlin/Localisation.kt | 6 ++---- composeApp/src/commonMain/kotlin/view/styling.kt | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/Localisation.kt b/composeApp/src/commonMain/kotlin/Localisation.kt index 0708d4b..2288bc0 100644 --- a/composeApp/src/commonMain/kotlin/Localisation.kt +++ b/composeApp/src/commonMain/kotlin/Localisation.kt @@ -1,3 +1,4 @@ + import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import view.screens.SettingsJSON @@ -11,8 +12,6 @@ class TranslationList(val transList: List) fun localisation(text: String): String{ try { - - println("HEREEEE") val language = Json.decodeFromString(File("src/settings.json").readText()).language val data = Json.decodeFromString(File("src/localisation/$language.json").readText()) for (wordPair in data.transList) { @@ -20,11 +19,10 @@ fun localisation(text: String): String{ return wordPair.localisation } } - println("HEREEEE") return text } catch (ex: Exception){ - println(ex) + println("Localisation Error with code $text") return text } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/styling.kt b/composeApp/src/commonMain/kotlin/view/styling.kt index 250c55f..9bfdcae 100644 --- a/composeApp/src/commonMain/kotlin/view/styling.kt +++ b/composeApp/src/commonMain/kotlin/view/styling.kt @@ -1,8 +1,8 @@ package view import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.sp import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp val defaultStyle = TextStyle(fontSize = 28.sp) @@ -11,5 +11,6 @@ val bigStyle = TextStyle(fontSize = 50.sp) object DefaultColors{ val primary = Color(0xff,0xf1,0x4a) + val error = Color.Red val background = Color.White } \ No newline at end of file From ee4e783e61e46aeb28519a3fb138b174a6fd16d4 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 14 May 2024 14:38:02 -0400 Subject: [PATCH 043/172] add: cn/en/ru translating dictionaries --- .../kotlin/view/screens/SettingsScreen.kt | 1 - composeApp/src/localisation/cn-CN.json | 24 +++++++++++++++++ composeApp/src/localisation/en-US.json | 26 +++++++++++++++++-- composeApp/src/localisation/ru-RU.json | 26 +++++++++++++++++-- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt index 471d129..bd28597 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt @@ -82,6 +82,5 @@ fun SettingsScreen(navController: NavController){ colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.error)) { Text(localisation("back"), style = defaultStyle, color = Color.White) } - } } \ No newline at end of file diff --git a/composeApp/src/localisation/cn-CN.json b/composeApp/src/localisation/cn-CN.json index e69de29..9a3e889 100644 --- a/composeApp/src/localisation/cn-CN.json +++ b/composeApp/src/localisation/cn-CN.json @@ -0,0 +1,24 @@ +{ + "transList": [ + { + "code": "settings", + "localisation": "设置" + }, + { + "code": "back", + "localisation": "回去" + }, + { + "code": "enter_graph_name", + "localisation": "输入图表名称" + }, + { + "code": "home", + "localisation": "返回主屏幕" + }, + { + "code": "add", + "localisation": "添加" + } + ] +} \ No newline at end of file diff --git a/composeApp/src/localisation/en-US.json b/composeApp/src/localisation/en-US.json index 03a8c93..9c4940f 100644 --- a/composeApp/src/localisation/en-US.json +++ b/composeApp/src/localisation/en-US.json @@ -1,2 +1,24 @@ - -cn-CN.json \ No newline at end of file +{ + "transList": [ + { + "code": "settings", + "localisation": "Settings" + }, + { + "code": "back", + "localisation": "Back" + }, + { + "code": "enter_graph_name", + "localisation": "Enter graph name" + }, + { + "code": "home", + "localisation": "Home" + }, + { + "code": "add", + "localisation": "Add" + } + ] +} \ No newline at end of file diff --git a/composeApp/src/localisation/ru-RU.json b/composeApp/src/localisation/ru-RU.json index 03a8c93..efb9ad8 100644 --- a/composeApp/src/localisation/ru-RU.json +++ b/composeApp/src/localisation/ru-RU.json @@ -1,2 +1,24 @@ - -cn-CN.json \ No newline at end of file +{ + "transList": [ + { + "code": "settings", + "localisation": "Настройки" + }, + { + "code": "back", + "localisation": "Назад" + }, + { + "code": "enter_graph_name", + "localisation": "Введите название графа" + }, + { + "code": "home", + "localisation": "На главную" + }, + { + "code": "add", + "localisation": "Добавить" + } + ] +} \ No newline at end of file From 079b161a35cad82e3abfd821ddceeca2443895a5 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 15 May 2024 04:01:49 +0300 Subject: [PATCH 044/172] Feat: add highlighting of the selected language --- .../src/commonMain/kotlin/Localisation.kt | 7 +++- .../kotlin/view/screens/SettingsScreen.kt | 32 +++++++++++++------ .../src/commonMain/kotlin/view/styling.kt | 1 + composeApp/src/settings.json | 2 +- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/Localisation.kt b/composeApp/src/commonMain/kotlin/Localisation.kt index 2288bc0..363bc67 100644 --- a/composeApp/src/commonMain/kotlin/Localisation.kt +++ b/composeApp/src/commonMain/kotlin/Localisation.kt @@ -12,7 +12,7 @@ class TranslationList(val transList: List) fun localisation(text: String): String{ try { - val language = Json.decodeFromString(File("src/settings.json").readText()).language + val language = getLocalisation() val data = Json.decodeFromString(File("src/localisation/$language.json").readText()) for (wordPair in data.transList) { if (wordPair.code == text){ @@ -25,4 +25,9 @@ fun localisation(text: String): String{ println("Localisation Error with code $text") return text } +} + +fun getLocalisation(): String{ + val language = Json.decodeFromString(File("src/settings.json").readText()).language + return language } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt index bd28597..f45d902 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt @@ -6,12 +6,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import getLocalisation import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -48,30 +49,41 @@ fun makeSetting(name: SettingType, value: String){ @Composable fun SettingsScreen(navController: NavController){ + var language by mutableStateOf(getLocalisation()) Column{ Text(text = localisation("settings"), fontSize = 28.sp, modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp)) - Button(onClick = {makeSetting(SettingType.LANGUAGE, "cn-CN")}, + Button(onClick = { + makeSetting(SettingType.LANGUAGE, "cn-CN") + language = "cn-CN" + }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = 3.dp, color = Color.Black) + .border(width = if (language == "cn-CN") 5.dp else 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { + colors = ButtonDefaults.buttonColors(backgroundColor = + if (language =="cn-CN") DefaultColors.primarySelected else DefaultColors.primary)) { Text("汉语", style = defaultStyle) } - Button(onClick = {makeSetting(SettingType.LANGUAGE, "ru-RU")}, + Button(onClick = { + makeSetting(SettingType.LANGUAGE, "ru-RU") + language = "ru-RU" }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = 3.dp, color = Color.Black) + .border(width = if (language == "ru-RU") 5.dp else 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { + colors = ButtonDefaults.buttonColors(backgroundColor = + if (language =="ru-RU") DefaultColors.primarySelected else DefaultColors.primary)) { Text("Русский", style = defaultStyle) } - Button(onClick = {makeSetting(SettingType.LANGUAGE, "en-US")}, + Button(onClick = { + makeSetting(SettingType.LANGUAGE, "en-US") + language = "en-US"}, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = 3.dp, color = Color.Black) + .border(width = if (language == "en-US") 5.dp else 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary)) { + colors = ButtonDefaults.buttonColors(backgroundColor = + if (language =="en-US") DefaultColors.primarySelected else DefaultColors.primary)) { Text("English", style = defaultStyle) } Button(onClick = {navController.navigate(Screen.MainScreen.route)}, diff --git a/composeApp/src/commonMain/kotlin/view/styling.kt b/composeApp/src/commonMain/kotlin/view/styling.kt index 9bfdcae..621ba04 100644 --- a/composeApp/src/commonMain/kotlin/view/styling.kt +++ b/composeApp/src/commonMain/kotlin/view/styling.kt @@ -11,6 +11,7 @@ val bigStyle = TextStyle(fontSize = 50.sp) object DefaultColors{ val primary = Color(0xff,0xf1,0x4a) + val primarySelected = Color(0xcf,0xc0,0x07) val error = Color.Red val background = Color.White } \ No newline at end of file diff --git a/composeApp/src/settings.json b/composeApp/src/settings.json index c38add4..ffac3fa 100644 --- a/composeApp/src/settings.json +++ b/composeApp/src/settings.json @@ -1 +1 @@ -{"language":"cn-CN"} \ No newline at end of file +{"language":"ru-RU"} \ No newline at end of file From 2f247051ab10b46c8af80239960cd26def07ad36 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 15 May 2024 04:23:29 +0300 Subject: [PATCH 045/172] CI: change mergeable workflow -remove not empty description requirement -add pull request min count approvals --- .github/mergeable.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/mergeable.yml b/.github/mergeable.yml index 5699908..bc259fe 100644 --- a/.github/mergeable.yml +++ b/.github/mergeable.yml @@ -1,9 +1,9 @@ version: 2 mergeable: - - when: pull_request.*, pull_request_review.* validate: - - do: description - no_empty: - enabled: true - message: "Description should not be empty." + - do: approvals + min: + count: 1 + required: + assignees: true From 434b96acbcda6dbefccd4d888a20c82d3f9e7ffa Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 15 May 2024 04:39:36 +0300 Subject: [PATCH 046/172] CI: temporary disable megalinter --- .github/workflows/megalinter.yml | 178 +++++++++++++++---------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/.github/workflows/megalinter.yml b/.github/workflows/megalinter.yml index 8edc2dd..bbe4a75 100644 --- a/.github/workflows/megalinter.yml +++ b/.github/workflows/megalinter.yml @@ -1,89 +1,89 @@ ---- -# MegaLinter GitHub Action configuration file -# More info at https://megalinter.io -name: MegaLinter - -on: - # Trigger mega-linter at every push. Action will also be visible from Pull Requests to main - push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) - -env: # Comment env block if you don't want to apply fixes - # Apply linter fixes configuration - APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) - APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all) - APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request) - ENABLE_LINTERS: KOTLIN_KTLINT -concurrency: - group: ${{ github.ref }}-${{ github.workflow }} - cancel-in-progress: true - -jobs: - megalinter: - name: MegaLinter - runs-on: ubuntu-latest - permissions: - # Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR - # Remove the ones you do not need - contents: write - issues: write - pull-requests: write - steps: - # Git Checkout - - name: Checkout Code - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances - - # MegaLinter - - name: MegaLinter - id: ml - # You can override MegaLinter flavor used to have faster performances - # More info at https://megalinter.io/flavors/ - uses: oxsecurity/megalinter@v7 - env: - # All available variables are described in documentation - # https://megalinter.io/configuration/ - VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY - # DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks - - # Upload MegaLinter artifacts - - name: Archive production artifacts - if: success() || failure() - uses: actions/upload-artifact@v4 - with: - name: MegaLinter reports - path: | - megalinter-reports - mega-linter.log - - # Create pull request if applicable (for now works only on PR from same repository, not from forks) - - name: Create Pull Request with applied fixes - id: cpr - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - uses: peter-evans/create-pull-request@v6 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - commit-message: "[MegaLinter] Apply linters automatic fixes" - title: "[MegaLinter] Apply linters automatic fixes" - labels: bot - - name: Create PR output - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - run: | - echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" - echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" - - # Push new commit if applicable (for now works only on PR from same repository, not from forks) - - name: Prepare commit - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - run: sudo chown -Rc $UID .git/ - - name: Commit and push applied linter fixes - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} - commit_message: "[MegaLinter] Apply linters fixes" - commit_user_name: megalinter-bot - commit_user_email: nicolas.vuillamy@ox.security +#--- +## MegaLinter GitHub Action configuration file +## More info at https://megalinter.io +#name: MegaLinter +# +#on: +# # Trigger mega-linter at every push. Action will also be visible from Pull Requests to main +# push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) +# +#env: # Comment env block if you don't want to apply fixes +# # Apply linter fixes configuration +# APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) +# APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all) +# APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request) +# ENABLE_LINTERS: KOTLIN_KTLINT +#concurrency: +# group: ${{ github.ref }}-${{ github.workflow }} +# cancel-in-progress: true +# +#jobs: +# megalinter: +# name: MegaLinter +# runs-on: ubuntu-latest +# permissions: +# # Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR +# # Remove the ones you do not need +# contents: write +# issues: write +# pull-requests: write +# steps: +# # Git Checkout +# - name: Checkout Code +# uses: actions/checkout@v3 +# with: +# token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} +# fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances +# +# # MegaLinter +# - name: MegaLinter +# id: ml +# # You can override MegaLinter flavor used to have faster performances +# # More info at https://megalinter.io/flavors/ +# uses: oxsecurity/megalinter@v7 +# env: +# # All available variables are described in documentation +# # https://megalinter.io/configuration/ +# VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY +# # DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks +# +# # Upload MegaLinter artifacts +# - name: Archive production artifacts +# if: success() || failure() +# uses: actions/upload-artifact@v4 +# with: +# name: MegaLinter reports +# path: | +# megalinter-reports +# mega-linter.log +# +# # Create pull request if applicable (for now works only on PR from same repository, not from forks) +# - name: Create Pull Request with applied fixes +# id: cpr +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# uses: peter-evans/create-pull-request@v6 +# with: +# token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} +# commit-message: "[MegaLinter] Apply linters automatic fixes" +# title: "[MegaLinter] Apply linters automatic fixes" +# labels: bot +# - name: Create PR output +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# run: | +# echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" +# echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" +# +# # Push new commit if applicable (for now works only on PR from same repository, not from forks) +# - name: Prepare commit +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# run: sudo chown -Rc $UID .git/ +# - name: Commit and push applied linter fixes +# if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') +# uses: stefanzweifel/git-auto-commit-action@v4 +# with: +# branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} +# commit_message: "[MegaLinter] Apply linters fixes" +# commit_user_name: megalinter-bot +# commit_user_email: nicolas.vuillamy@ox.security From 6bf73dd131f4ede69cb61b4ef55689491d6e9e28 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 15 May 2024 05:59:49 +0300 Subject: [PATCH 047/172] CI: add kotlinx-kover workflow --- .github/workflows/coverage.yml | 36 ++++++++++++++++++ .github/workflows/gradle-test.yml | 2 +- build.gradle.kts | 6 +++ composeApp/build.gradle.kts | 37 +++++++++++++++++++ .../kotlin/viewmodel/GraphViewModel.kt | 5 +-- lib/build.gradle.kts | 32 +++++++++++++++- lib/src/commonMain/kotlin/graph/Digraph.kt | 2 +- lib/src/commonMain/kotlin/graph/Graph.kt | 2 +- .../commonMain/kotlin/graph/GraphAbstract.kt | 2 +- .../commonMain/kotlin/graph/WeightedGraph.kt | 2 +- 10 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..adf9d60 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,36 @@ +name: Measure coverage + +on: + pull_request: + +jobs: + build: + permissions: + id-token: write + contents: read + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '21' + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Generate kover coverage report + run: ./gradlew koverXmlReport + + - name: Add coverage report to PR + id: kover + uses: mi-kas/kover-report@v1 + with: + path: | + ${{ github.workspace }}/build/reports/kover/report.xml + title: Code Coverage + update-comment: true + min-coverage-overall: 0 + min-coverage-changed-files: 0 + coverage-counter-type: LINE \ No newline at end of file diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 465c2f0..30326fa 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -15,7 +15,7 @@ jobs: - name: Set up JDK 21 uses: actions/setup-java@v4 with: - distribution: "oracle" + distribution: 'adopt' java-version: '21' - name: Setup Android SDK diff --git a/build.gradle.kts b/build.gradle.kts index 58a2e4a..5fa5fcc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,4 +4,10 @@ plugins { alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false + id("org.jetbrains.kotlinx.kover") version "0.7.6" +} + +dependencies{ + kover(project(":composeApp")) + kover(project(":lib")) } diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 95e34f6..8d3132a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" + id("org.jetbrains.kotlinx.kover") version "0.7.6" } kotlin { @@ -97,3 +98,39 @@ compose.desktop { } } } + +koverReport { + filters { + // filters for all reports + excludes{ +// packages("viewmodel.*") + } + } + + verify { + // verification rules for all reports + } + + defaults { + mergeWith("release") + xml { /* default XML report config */ } + html { /* default HTML report config */ } + verify { /* default verification config */ } + log { /* default logging config */ } + } + + androidReports("release") { + filters { + // override report filters for all reports for `release` build variant + // all filters specified by the level above cease to work + excludes{ + classes("viewmodel.*") + } + } + + xml { /* XML report config for `release` build variant */ } + html { /* HTML report config for `release` build variant */ } + verify { /* verification config for `release` build variant */ } + log { /* logging config for `release` build variant */ } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index 9daf3ca..2b62a05 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -4,10 +4,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import lib.graph.Graph.Graph -import lib.graph.Graph.UndirectedGraph +import graph.Graph -class GraphViewModel(name : String, graph : Graph = UndirectedGraph()): ViewModel() { +class GraphViewModel(name : String, graph : Graph = Graph()): ViewModel() { val name by mutableStateOf(name) val vertices = mutableStateListOf() init { diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 9871bbc..896fb94 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType - plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) @@ -42,3 +40,33 @@ android { targetCompatibility = JavaVersion.VERSION_21 } } + +koverReport { + filters { + // filters for all reports + } + + verify { + // verification rules for all reports + } + + defaults { + mergeWith("release") + xml { /* default XML report config */ } + html { /* default HTML report config */ } + verify { /* default verification config */ } + log { /* default logging config */ } + } + + androidReports("release") { + filters { + // override report filters for all reports for `release` build variant + // all filters specified by the level above cease to work + } + + xml { /* XML report config for `release` build variant */ } + html { /* HTML report config for `release` build variant */ } + verify { /* verification config for `release` build variant */ } + log { /* logging config for `release` build variant */ } + } +} diff --git a/lib/src/commonMain/kotlin/graph/Digraph.kt b/lib/src/commonMain/kotlin/graph/Digraph.kt index d6a97c6..d953b24 100644 --- a/lib/src/commonMain/kotlin/graph/Digraph.kt +++ b/lib/src/commonMain/kotlin/graph/Digraph.kt @@ -1,4 +1,4 @@ -package lib.graph +package graph class Digraph: GraphAbstract(){ override fun addEdge(from: Int, to: Int) { diff --git a/lib/src/commonMain/kotlin/graph/Graph.kt b/lib/src/commonMain/kotlin/graph/Graph.kt index 5410fb8..114f280 100644 --- a/lib/src/commonMain/kotlin/graph/Graph.kt +++ b/lib/src/commonMain/kotlin/graph/Graph.kt @@ -1,4 +1,4 @@ -package lib.graph +package graph class Graph: GraphAbstract(){ override fun addEdge(from: Int, to: Int) { diff --git a/lib/src/commonMain/kotlin/graph/GraphAbstract.kt b/lib/src/commonMain/kotlin/graph/GraphAbstract.kt index bbb3a7f..cbc9fe6 100644 --- a/lib/src/commonMain/kotlin/graph/GraphAbstract.kt +++ b/lib/src/commonMain/kotlin/graph/GraphAbstract.kt @@ -1,4 +1,4 @@ -package lib.graph +package graph abstract class GraphAbstract(){ protected val graph = mutableMapOf>() diff --git a/lib/src/commonMain/kotlin/graph/WeightedGraph.kt b/lib/src/commonMain/kotlin/graph/WeightedGraph.kt index 5029513..c79c622 100644 --- a/lib/src/commonMain/kotlin/graph/WeightedGraph.kt +++ b/lib/src/commonMain/kotlin/graph/WeightedGraph.kt @@ -1,6 +1,6 @@ package graph -import lib.graph.GraphAbstract +import graph.GraphAbstract //class WeightedGraph: GraphAbstract { //} \ No newline at end of file From 4d228c3cae1f1e3e1eb7cee996ce2f08480ae766 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 15 May 2024 20:38:24 +0300 Subject: [PATCH 048/172] Feat: implemented adding edges --- .../kotlin/view/screens/GraphScreen.kt | 134 +++++++++++++----- .../commonMain/kotlin/view/views/GraphView.kt | 4 +- .../kotlin/view/views/VertexView.kt | 7 +- .../kotlin/viewmodel/GraphViewModel.kt | 28 ++-- .../kotlin/viewmodel/VertexViewModel.kt | 4 +- composeApp/src/localisation/cn-CN.json | 10 +- composeApp/src/localisation/en-US.json | 10 +- composeApp/src/localisation/ru-RU.json | 10 +- composeApp/src/settings.json | 2 +- .../commonMain/kotlin/graph/GraphAbstract.kt | 30 ++-- 10 files changed, 174 insertions(+), 65 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt index ab404d9..37aabed 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt @@ -1,21 +1,21 @@ package view.screens +import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.material.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation @@ -26,38 +26,108 @@ import viewmodel.GraphViewModel import viewmodel.MainScreenViewModel @Composable -fun GraphScreen(navController: NavController, mainScreenViewModel : MainScreenViewModel, graphId : Int) { +fun GraphScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel, graphId: Int) { val graphModel by mutableStateOf(mainScreenViewModel.getGraph(graphId)) - // To MainScreen - Button( - onClick = { navController.popBackStack() }, - modifier = Modifier - .offset (x = 16.dp, y = 16.dp) - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp) - .zIndex(1f), - colors = ButtonDefaults.buttonColors(DefaultColors.primary) - ) { - Text(localisation("home"), style = defaultStyle) + Box(modifier = Modifier.fillMaxSize()) { + GraphView(graphModel) } - // Add vertex + Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { + // To MainScreen + Button( + onClick = { navController.popBackStack() }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(DefaultColors.primary) + ) { + Text(localisation("home"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Add vertex + Button( + onClick = { graphModel.addVertex() }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_vertex"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + + Button( + onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("open_edge"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(10.dp)) + + if (isOpenedEdgeMenu) { + Column( + modifier = Modifier + .background(Color(0xffeeeeee)) + .border(3.dp, color = Color.Black) + .padding(10.dp) + + ) { + AddEdgeMenu(graphModel) + } + + } + } +} + +@Composable +fun AddEdgeMenu(graphModel: GraphViewModel) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + Row { + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(10.dp)) + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }) + } + + Spacer(modifier = Modifier.height(16.dp)) Button( - onClick = { graphModel.addVertex()}, - modifier = Modifier - .offset(x = 266.dp, y = 16.dp) + onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphModel.addEdge(sourceInt, destinationInt) + } + }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp) - .zIndex(1f), + .size(240.dp, 80.dp), colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { - Text(localisation("add"), style = defaultStyle) - } - - Box(modifier = Modifier.fillMaxSize()){ - GraphView(graphModel) + Text(localisation("add_edge"), style = defaultStyle) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt index 24e3843..0cc38d9 100644 --- a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt +++ b/composeApp/src/commonMain/kotlin/view/views/GraphView.kt @@ -6,7 +6,7 @@ import viewmodel.VertexViewModel @Composable fun GraphView(graphViewModel : GraphViewModel) { - graphViewModel.vertices.forEach{ vertexvm -> - VertexView(vertexvm, graphViewModel) + for (vertexVM in graphViewModel.graphView.values){ + VertexView(vertexVM, graphViewModel) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt index 3bee29d..28abc87 100644 --- a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt +++ b/composeApp/src/commonMain/kotlin/view/views/VertexView.kt @@ -23,7 +23,7 @@ import viewmodel.VertexViewModel import kotlin.math.roundToInt @Composable -fun VertexView(vertexVM: VertexViewModel, graphViewModel: GraphViewModel) { +fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel) { val number = vertexVM.number Box(modifier = Modifier @@ -48,8 +48,9 @@ fun VertexView(vertexVM: VertexViewModel, graphViewModel: GraphViewModel) { } vertexVM.edges.forEach{ otherNumber -> - val otherX = (graphViewModel.vertices.find{ vertexVM -> vertexVM.number == otherNumber})!!.offsetX - val otherY = (graphViewModel.vertices.find{vertexVM -> vertexVM.number == otherNumber})!!.offsetY + val otherVM = graphVM.graphView[otherNumber]!! + val otherX = otherVM.offsetX + val otherY = otherVM.offsetY Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)){ drawLine( start = Offset(vertexVM.offsetX + vertexVM.vertexSize/2, vertexVM.offsetY + vertexVM.vertexSize/2), diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index 2b62a05..1926d90 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -1,20 +1,30 @@ package viewmodel -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import graph.Graph -class GraphViewModel(name : String, graph : Graph = Graph()): ViewModel() { +class GraphViewModel(name: String, graph: Graph = Graph()) : ViewModel() { val name by mutableStateOf(name) - val vertices = mutableStateListOf() + val graphView = mutableStateMapOf() + val graphModel = graph.getGraphProp() + init { - for (vertex in graph){ - vertices.add(VertexViewModel(vertex.key,vertex.value)) + for (vertex in graphModel.entries) { + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } } - fun addVertex(){ - vertices.add(VertexViewModel(vertices.size)) + + fun addVertex() { + graphView[graphView.size] = VertexViewModel(graphView.size) + } + + fun addEdge(source: Int, destination: Int) { + if (graphView[source] == null) { + throw IllegalArgumentException("Can't create edge from non-existent vertex") + } + val EdgesCopy = graphView[source]?.edges?.toMutableList()!! + EdgesCopy.add(destination) + graphView[source]?.edges = EdgesCopy } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt index 46391d8..36f11a6 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt @@ -6,9 +6,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel -class VertexViewModel(number: Int, edges: MutableList = mutableListOf()): ViewModel() { +class VertexViewModel(number: Int, _edges: MutableList = mutableListOf()): ViewModel() { val number : Int = number - val edges = edges + var edges by mutableStateOf(_edges) var offsetX by mutableStateOf(1000f) var offsetY by mutableStateOf(540f) val vertexSize = 120f diff --git a/composeApp/src/localisation/cn-CN.json b/composeApp/src/localisation/cn-CN.json index 9a3e889..48c1447 100644 --- a/composeApp/src/localisation/cn-CN.json +++ b/composeApp/src/localisation/cn-CN.json @@ -17,8 +17,16 @@ "localisation": "返回主屏幕" }, { - "code": "add", + "code": "add_vertex", "localisation": "添加" + }, + { + "code": "open_edge", + "localisation":"TODO" + }, + { + "code": "add_edge", + "localisation": "TODO" } ] } \ No newline at end of file diff --git a/composeApp/src/localisation/en-US.json b/composeApp/src/localisation/en-US.json index 9c4940f..dbf4351 100644 --- a/composeApp/src/localisation/en-US.json +++ b/composeApp/src/localisation/en-US.json @@ -17,7 +17,15 @@ "localisation": "Home" }, { - "code": "add", + "code": "add_vertex", + "localisation": "Add vertex" + }, + { + "code": "open_edge", + "localisation":"Add edge" + }, + { + "code": "add_edge", "localisation": "Add" } ] diff --git a/composeApp/src/localisation/ru-RU.json b/composeApp/src/localisation/ru-RU.json index efb9ad8..a94742b 100644 --- a/composeApp/src/localisation/ru-RU.json +++ b/composeApp/src/localisation/ru-RU.json @@ -17,7 +17,15 @@ "localisation": "На главную" }, { - "code": "add", + "code": "add_vertex", + "localisation": "Добавить вершину" + }, + { + "code": "add_edge", + "localisation": "Добавить ребро" + }, + { + "code": "open_edge", "localisation": "Добавить" } ] diff --git a/composeApp/src/settings.json b/composeApp/src/settings.json index ffac3fa..6e3de82 100644 --- a/composeApp/src/settings.json +++ b/composeApp/src/settings.json @@ -1 +1 @@ -{"language":"ru-RU"} \ No newline at end of file +{"language":"en-US"} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/graph/GraphAbstract.kt b/lib/src/commonMain/kotlin/graph/GraphAbstract.kt index cbc9fe6..aa9931e 100644 --- a/lib/src/commonMain/kotlin/graph/GraphAbstract.kt +++ b/lib/src/commonMain/kotlin/graph/GraphAbstract.kt @@ -1,33 +1,37 @@ package graph -abstract class GraphAbstract(){ +abstract class GraphAbstract() { protected val graph = mutableMapOf>() var size = graph.size private set // temporary - init{ - graph[2] = mutableListOf(1,3,4,5,6,0) - graph[4] = mutableListOf(4,5) - graph[5] = mutableListOf(4,2) - graph[6] = mutableListOf(1,2,4,5) - graph[1] = mutableListOf(2,4,5,6) - graph[3] = mutableListOf(1,2,4,5,6) - graph[0] = mutableListOf(1,2,3,4,5,6) + init { + graph[2] = mutableListOf(1, 3, 4, 5, 6, 0) + graph[4] = mutableListOf(4, 5) + graph[5] = mutableListOf(4, 2) + graph[6] = mutableListOf(1, 2, 4, 5) + graph[1] = mutableListOf(2, 4, 5, 6) + graph[3] = mutableListOf(1, 2, 4, 5, 6) + graph[0] = mutableListOf(1, 2, 3, 4, 5, 6) size = 7 } - fun addVertex(number: Int){ + fun addVertex(number: Int) { graph.putIfAbsent(number, mutableListOf()) } abstract fun addEdge(from: Int, to: Int) - fun edgesFrom(from: Int): MutableList{ - return graph[from]?: mutableListOf() + fun getGraphProp(): MutableMap> { + return graph } - fun forEach(action : (MutableList) -> Unit ) { + fun edgesFrom(from: Int): MutableList { + return graph[from] ?: mutableListOf() + } + + fun forEach(action: (MutableList) -> Unit) { graph.forEach { number, list -> action(list) } } From 316293a994a0314c0467ddc955dac0a90ddec609 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 15 May 2024 20:41:39 +0300 Subject: [PATCH 049/172] CI: exclude main branch from mergeable workflow --- .github/mergeable.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/mergeable.yml b/.github/mergeable.yml index bc259fe..b0d4430 100644 --- a/.github/mergeable.yml +++ b/.github/mergeable.yml @@ -1,6 +1,8 @@ version: 2 mergeable: - when: pull_request.*, pull_request_review.* + branches-ignore: + -"main" validate: - do: approvals min: From a164c54a80c2df4e7e4c0a8b3b3c9b068005dcef Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 15 May 2024 15:52:32 -0400 Subject: [PATCH 050/172] add: localisation resources storaging --- composeApp/src/{ => commonMain/resources}/localisation/cn-CN.json | 0 composeApp/src/{ => commonMain/resources}/localisation/en-US.json | 0 composeApp/src/{ => commonMain/resources}/localisation/ru-RU.json | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename composeApp/src/{ => commonMain/resources}/localisation/cn-CN.json (100%) rename composeApp/src/{ => commonMain/resources}/localisation/en-US.json (100%) rename composeApp/src/{ => commonMain/resources}/localisation/ru-RU.json (100%) diff --git a/composeApp/src/localisation/cn-CN.json b/composeApp/src/commonMain/resources/localisation/cn-CN.json similarity index 100% rename from composeApp/src/localisation/cn-CN.json rename to composeApp/src/commonMain/resources/localisation/cn-CN.json diff --git a/composeApp/src/localisation/en-US.json b/composeApp/src/commonMain/resources/localisation/en-US.json similarity index 100% rename from composeApp/src/localisation/en-US.json rename to composeApp/src/commonMain/resources/localisation/en-US.json diff --git a/composeApp/src/localisation/ru-RU.json b/composeApp/src/commonMain/resources/localisation/ru-RU.json similarity index 100% rename from composeApp/src/localisation/ru-RU.json rename to composeApp/src/commonMain/resources/localisation/ru-RU.json From db3d2ab2228159deaf22bfe7eab88784ac0cd1ce Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 15 May 2024 15:53:49 -0400 Subject: [PATCH 051/172] fix: reading from rresources bug fix --- composeApp/src/commonMain/kotlin/Localisation.kt | 2 +- composeApp/src/settings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/Localisation.kt b/composeApp/src/commonMain/kotlin/Localisation.kt index 363bc67..270f933 100644 --- a/composeApp/src/commonMain/kotlin/Localisation.kt +++ b/composeApp/src/commonMain/kotlin/Localisation.kt @@ -13,7 +13,7 @@ class TranslationList(val transList: List) fun localisation(text: String): String{ try { val language = getLocalisation() - val data = Json.decodeFromString(File("src/localisation/$language.json").readText()) + val data = Json.decodeFromString(object {}.javaClass.getResourceAsStream("localisation/$language.json")?.bufferedReader()!!.readText()) for (wordPair in data.transList) { if (wordPair.code == text){ return wordPair.localisation diff --git a/composeApp/src/settings.json b/composeApp/src/settings.json index 6e3de82..ffac3fa 100644 --- a/composeApp/src/settings.json +++ b/composeApp/src/settings.json @@ -1 +1 @@ -{"language":"en-US"} \ No newline at end of file +{"language":"ru-RU"} \ No newline at end of file From 648bf214c8ad7998786c3f63af7e2e8853f49d1c Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 15 May 2024 16:42:34 -0400 Subject: [PATCH 052/172] add: localisation errors logger --- .../src/commonMain/kotlin/Localisation.kt | 16 +++++--- composeApp/src/logs/localisationError.log | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 composeApp/src/logs/localisationError.log diff --git a/composeApp/src/commonMain/kotlin/Localisation.kt b/composeApp/src/commonMain/kotlin/Localisation.kt index 270f933..7e507af 100644 --- a/composeApp/src/commonMain/kotlin/Localisation.kt +++ b/composeApp/src/commonMain/kotlin/Localisation.kt @@ -1,8 +1,8 @@ - import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import view.screens.SettingsJSON import java.io.File +import java.time.LocalDateTime @Serializable class TranslationPair(val code: String, val localisation: String) @@ -11,8 +11,8 @@ class TranslationPair(val code: String, val localisation: String) class TranslationList(val transList: List) fun localisation(text: String): String{ + val language = getLocalisation() try { - val language = getLocalisation() val data = Json.decodeFromString(object {}.javaClass.getResourceAsStream("localisation/$language.json")?.bufferedReader()!!.readText()) for (wordPair in data.transList) { if (wordPair.code == text){ @@ -22,12 +22,18 @@ fun localisation(text: String): String{ return text } catch (ex: Exception){ - println("Localisation Error with code $text") + File("src/logs/localisationError.log").appendText("LOCALISATION ERROR ${LocalDateTime.now()} -- Key $text is not found in $language\nEXCEPTION IS $ex\n") return text } } fun getLocalisation(): String{ - val language = Json.decodeFromString(File("src/settings.json").readText()).language - return language + try { + val language = Json.decodeFromString(File("src/settings.json").readText()).language + return language + } + catch(ex: Exception){ + File("src/logs/localisationError.log").appendText("FILE OF LOC NOT FOUND ${LocalDateTime.now()} -- EXCEPTION IS $ex\n") + return "en-US" + } } \ No newline at end of file diff --git a/composeApp/src/logs/localisationError.log b/composeApp/src/logs/localisationError.log new file mode 100644 index 0000000..37fba40 --- /dev/null +++ b/composeApp/src/logs/localisationError.log @@ -0,0 +1,41 @@ +[Текст песни «+7(952)812»] + +[Интро] +Это второй +А +That's a Krishtall +Ау-у, YEEI, а + +[Припев] +52 (Алло) +Да здравствует Санкт-Петербург (А), и это город наш (YEEI) +Я каждый свой новый куплет валю как никогда (YEEI, а) +Альбом, он чисто мой, никому его не продам (Он мой) +Не думаю о том (YEEI), как хорошо было вчера (А-а; мне пох) + +[Куплет] +Меняю города (А) +Представляю район — у меня есть репертуар (YEEI, 2-3) +Никогда не просил, но всегда где-то доставал (Где?) +Чем больше денег (А), тем больше мне нравится Москва (А) +Но в Питере душа (YEEI), в Питере семья (YEEI) +В Питере братва (А, а), там знают наши имена (52) ++7(952)8-1-2 (Алло) +Это второй альбом (А), вторая глава (Второй) +Не думал, не гадал, всё, что я делал, — рэповал (Всегда) +Андеграунд — это не броуки в протёртых штанах (Пошёл на хуй) +Нужно прожить мою жизнь, чтоб так же, как я, слагать (Ага) +Нужно мой рэп услышать (YEEI), чтоб точно его понять + +[Припев] +52 (Алло) +Да здравствует Санкт-Петербург (А), и это город наш (YEEI) +Я каждый свой новый куплет валю как никогда (YEEI, а) +Альбом, он чисто мой, никому его не продам (Он мой) +Не думаю о том (YEEI), как хорошо было вчера (Ага) + +[Аутро] +Да здравствует 52 +Да здравствует Петербург, да здравствует 52 +Да здравствует Петербург, да здравствует 52 (Ау; YEEI, а) +Да здравствует 52 (Ау), YEEI, long live (Это второй) \ No newline at end of file From 5187d20f9ce1edd93beed87d4d183ae41bca0ebc Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> Date: Thu, 16 May 2024 06:23:37 +0300 Subject: [PATCH 053/172] Update LICENSE --- LICENSE | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/LICENSE b/LICENSE index 5c93f45..951ff97 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,21 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 +MIT License - Copyright (C) 2004 Sam Hocevar +Copyright (c) 2024 spbu-coding-2023 - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. +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: - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - 0. You just DO WHAT THE FUCK YOU WANT TO. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 002cd1bbd7dbb1ba1bad3dccd62786ee1f9c93d3 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 16 May 2024 06:54:17 +0300 Subject: [PATCH 054/172] Fix: fix crash when add edge from non-existent vertex --- .../src/commonMain/kotlin/view/screens/GraphScreen.kt | 8 +------- .../src/commonMain/kotlin/viewmodel/GraphViewModel.kt | 8 ++++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt index 37aabed..e3fffe1 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt @@ -4,18 +4,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text -import androidx.compose.material.TextField +import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt index 1926d90..e914310 100644 --- a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt @@ -21,10 +21,10 @@ class GraphViewModel(name: String, graph: Graph = Graph()) : ViewModel() { fun addEdge(source: Int, destination: Int) { if (graphView[source] == null) { - throw IllegalArgumentException("Can't create edge from non-existent vertex") + return } - val EdgesCopy = graphView[source]?.edges?.toMutableList()!! - EdgesCopy.add(destination) - graphView[source]?.edges = EdgesCopy + val edgesCopy = graphView[source]?.edges?.toMutableList()!! + edgesCopy.add(destination) + graphView[source]?.edges = edgesCopy } } \ No newline at end of file From 08a37345bf082d7f4d7dea1899b8194f2ad9810f Mon Sep 17 00:00:00 2001 From: Aleksei Dmitrievstev <93659834+admitrievtsev@users.noreply.github.com> Date: Thu, 16 May 2024 15:18:38 +0300 Subject: [PATCH 055/172] fix: ser English(US) as default app language Co-authored-by: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> --- composeApp/src/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/settings.json b/composeApp/src/settings.json index ffac3fa..6e3de82 100644 --- a/composeApp/src/settings.json +++ b/composeApp/src/settings.json @@ -1 +1 @@ -{"language":"ru-RU"} \ No newline at end of file +{"language":"en-US"} \ No newline at end of file From 1d63572a67ddca622361858f8e1b29101b0048d9 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Thu, 16 May 2024 14:04:24 -0400 Subject: [PATCH 056/172] feat: new add menue --- .../kotlin/view/screens/MainScreen.kt | 75 ++++++++++++++++++- .../src/commonMain/kotlin/view/styling.kt | 2 + 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt index 0b4bd89..14d6274 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt @@ -14,20 +14,28 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState import androidx.navigation.NavController import localisation import view.DefaultColors import view.bigStyle import view.bounceClick +import view.defaultStyle import viewmodel.MainScreenViewModel @Composable fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel){ var search by remember { mutableStateOf("") } + var graphName by remember { mutableStateOf("") } + val dialogState = remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { // Search tab @@ -62,7 +70,9 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView // Add graph IconButton( - onClick = { mainScreenViewModel.addGraph(search) }, + onClick = { + dialogState.value = true + }, modifier = Modifier .padding(horizontal = 10.dp) .size(100.dp) @@ -108,6 +118,69 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView } } + DialogWindow(visible = dialogState.value, title = "New Graph",onCloseRequest = { dialogState.value = false }, state = rememberDialogState(size = DpSize(960.dp, 640.dp))) { + Text(text = localisation("enter_new_graph_name"), modifier = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), style = defaultStyle) + TextField( + value = graphName, + textStyle = bigStyle, + placeholder = { Text(text = localisation("write_name"), style = bigStyle) }, + onValueChange = { graphName = it }, + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 70.dp) + .size(800.dp, 90.dp) + .weight(1f) + .border( + width = 4.dp, + color = Color.Cyan, + shape = RoundedCornerShape(25.dp) + ), + shape = RoundedCornerShape(25.dp), + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + Button(modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(horizontal = 20.dp, vertical = 180.dp) + .border( + width = 2.dp, + color = Color.Black, + shape = RoundedCornerShape(25.dp) + ) + .width(300.dp) + .height(60.dp), + shape = RoundedCornerShape(25.dp), + colors = if(graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.simpleGreen) else ButtonDefaults.buttonColors(backgroundColor = DefaultColors.darkGreen), + onClick = { + if(graphName != ""){ + mainScreenViewModel.addGraph(graphName) + dialogState.value = false + } + }, + ) { + Text(text= localisation("add"), color = if(graphName != "") Color.White else Color.Black, fontSize = 28.sp) + } + Button(modifier = Modifier + + .padding(horizontal = 20.dp, vertical = 260.dp) + .border( + width = 2.dp, + color = Color.Black, + shape = RoundedCornerShape(25.dp) + ) + .width(300.dp) + .height(60.dp), + shape = RoundedCornerShape(25.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red), + onClick = { + dialogState.value = false + }, + ) { + Text(text= localisation("back"), color = Color.White, fontSize = 28.sp) + } + } + Spacer(modifier = Modifier.height(30.dp)) LazyColumn(modifier = Modifier.fillMaxWidth()) { diff --git a/composeApp/src/commonMain/kotlin/view/styling.kt b/composeApp/src/commonMain/kotlin/view/styling.kt index 621ba04..4018c6a 100644 --- a/composeApp/src/commonMain/kotlin/view/styling.kt +++ b/composeApp/src/commonMain/kotlin/view/styling.kt @@ -13,5 +13,7 @@ object DefaultColors{ val primary = Color(0xff,0xf1,0x4a) val primarySelected = Color(0xcf,0xc0,0x07) val error = Color.Red + val darkGreen = Color(0x00,0x64,0x00) + val simpleGreen = Color(0x00,0xe4,0x00) val background = Color.White } \ No newline at end of file From 18c9325a7c4650ed8e9e8b61f1076942fd0fef71 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Thu, 16 May 2024 14:14:57 -0400 Subject: [PATCH 057/172] upd: Ru/En localisation dictionary update --- .../src/commonMain/resources/localisation/cn-CN.json | 12 ++++++++++++ .../src/commonMain/resources/localisation/en-US.json | 12 ++++++++++++ .../src/commonMain/resources/localisation/ru-RU.json | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/composeApp/src/commonMain/resources/localisation/cn-CN.json b/composeApp/src/commonMain/resources/localisation/cn-CN.json index 48c1447..71b46c5 100644 --- a/composeApp/src/commonMain/resources/localisation/cn-CN.json +++ b/composeApp/src/commonMain/resources/localisation/cn-CN.json @@ -27,6 +27,18 @@ { "code": "add_edge", "localisation": "TODO" + }, + { + "code": "add", + "localisation": "TODO" + }, + { + "code": "write_name", + "localisation": "TODO" + }, + { + "code": "enter_new_graph_name", + "localisation": "TODO" } ] } \ No newline at end of file diff --git a/composeApp/src/commonMain/resources/localisation/en-US.json b/composeApp/src/commonMain/resources/localisation/en-US.json index dbf4351..ea8d952 100644 --- a/composeApp/src/commonMain/resources/localisation/en-US.json +++ b/composeApp/src/commonMain/resources/localisation/en-US.json @@ -27,6 +27,18 @@ { "code": "add_edge", "localisation": "Add" + }, + { + "code": "add", + "localisation": "Add" + }, + { + "code": "write_name", + "localisation": "Write name" + }, + { + "code": "enter_new_graph_name", + "localisation": "Create new graph" } ] } \ No newline at end of file diff --git a/composeApp/src/commonMain/resources/localisation/ru-RU.json b/composeApp/src/commonMain/resources/localisation/ru-RU.json index a94742b..5311dc7 100644 --- a/composeApp/src/commonMain/resources/localisation/ru-RU.json +++ b/composeApp/src/commonMain/resources/localisation/ru-RU.json @@ -27,6 +27,18 @@ { "code": "open_edge", "localisation": "Добавить" + }, + { + "code": "add", + "localisation": "Добавить" + }, + { + "code": "write_name", + "localisation": "Введите название" + }, + { + "code": "enter_new_graph_name", + "localisation": "Создание нового графа" } ] } \ No newline at end of file From eadadb9771482b41e534f6b7c6c9a88982c89179 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 17 May 2024 23:07:52 +0300 Subject: [PATCH 058/172] Migrate project from multiplatform to desktop --- .editorconfig | 5 - .gitignore | 88 ++++----- LICENSE | 21 --- README.md | 10 -- build.gradle.kts | 45 ++++- composeApp/build.gradle.kts | 136 -------------- .../src/androidMain/AndroidManifest.xml | 23 --- .../org/example/project/MainActivity.kt | 16 -- .../drawable-v24/ic_launcher_foreground.xml | 30 ---- .../res/drawable/ic_launcher_background.xml | 170 ------------------ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../res/mipmap-hdpi/ic_launcher.png | Bin 3593 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5339 -> 0 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 2636 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3388 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 4926 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7472 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 7909 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 11873 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 10652 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 16570 -> 0 bytes .../src/androidMain/res/values/strings.xml | 3 - .../drawable/compose-multiplatform.xml | 36 ---- composeApp/src/commonMain/kotlin/App.kt | 9 - composeApp/src/logs/localisationError.log | 41 ----- gradle.properties | 16 +- gradle/libs.versions.toml | 41 ----- gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 41 ++--- gradlew.bat | 15 +- lib/build.gradle.kts | 72 -------- .../androidMain/kotlin/fibiprops.android.kt | 0 .../androidUnitTest/kotlin/AndroidFibiTest.kt | 11 -- .../kotlin/algos/FordBellman/FordBellman.kt | 1 - .../kotlin/algos/cycleFind/LoadingGraph.kt | 5 - .../kotlin/algos/cycleFind/findCycle.kt | 1 - lib/src/commonTest/kotlin/GraphTest.kt | 10 -- lib/src/jvmMain/kotlin/graph.jvm.kt | 0 lib/src/jvmTest/kotlin/JvmGraphTest.kt | 10 -- settings.gradle.kts | 31 +--- .../kotlin/main.kt => src/main/kotlin/Main.kt | 23 ++- .../main}/kotlin/Navigation.kt | 0 .../main/kotlin/localisation}/Localisation.kt | 10 +- src/main/kotlin/model/algos/FordBellman.kt | 4 + .../main/kotlin/model}/graph/Digraph.kt | 0 .../main/kotlin/model}/graph/Graph.kt | 0 .../main/kotlin/model}/graph/GraphAbstract.kt | 0 .../kotlin/model}/graph/WeightedDigraph.kt | 0 .../main/kotlin/model}/graph/WeightedGraph.kt | 0 .../src => src/main/kotlin}/settings.json | 0 .../main}/kotlin/view/BounceClick.kt | 0 .../main}/kotlin/view/screens/GraphScreen.kt | 2 +- .../main}/kotlin/view/screens/MainScreen.kt | 2 +- .../kotlin/view/screens/SealedScreens.kt | 0 .../kotlin/view/screens/SettingsScreen.kt | 11 +- .../main}/kotlin/view/styling.kt | 0 .../main}/kotlin/view/views/GraphView.kt | 0 .../main}/kotlin/view/views/VertexView.kt | 0 .../main}/kotlin/viewmodel/GraphViewModel.kt | 0 .../kotlin/viewmodel/MainScreenViewModel.kt | 0 .../main}/kotlin/viewmodel/VertexViewModel.kt | 0 .../main}/resources/localisation/cn-CN.json | 0 .../main}/resources/localisation/en-US.json | 0 .../main}/resources/localisation/ru-RU.json | 0 66 files changed, 139 insertions(+), 815 deletions(-) delete mode 100644 .editorconfig delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 composeApp/build.gradle.kts delete mode 100644 composeApp/src/androidMain/AndroidManifest.xml delete mode 100644 composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt delete mode 100644 composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 composeApp/src/androidMain/res/drawable/ic_launcher_background.xml delete mode 100644 composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png delete mode 100644 composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png delete mode 100644 composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 composeApp/src/androidMain/res/values/strings.xml delete mode 100644 composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml delete mode 100644 composeApp/src/commonMain/kotlin/App.kt delete mode 100644 composeApp/src/logs/localisationError.log delete mode 100644 gradle/libs.versions.toml delete mode 100644 lib/build.gradle.kts delete mode 100644 lib/src/androidMain/kotlin/fibiprops.android.kt delete mode 100644 lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt delete mode 100644 lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt delete mode 100644 lib/src/commonMain/kotlin/algos/cycleFind/LoadingGraph.kt delete mode 100644 lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt delete mode 100644 lib/src/commonTest/kotlin/GraphTest.kt delete mode 100644 lib/src/jvmMain/kotlin/graph.jvm.kt delete mode 100644 lib/src/jvmTest/kotlin/JvmGraphTest.kt rename composeApp/src/desktopMain/kotlin/main.kt => src/main/kotlin/Main.kt (55%) rename {composeApp/src/commonMain => src/main}/kotlin/Navigation.kt (100%) rename {composeApp/src/commonMain/kotlin => src/main/kotlin/localisation}/Localisation.kt (68%) create mode 100644 src/main/kotlin/model/algos/FordBellman.kt rename {lib/src/commonMain/kotlin => src/main/kotlin/model}/graph/Digraph.kt (100%) rename {lib/src/commonMain/kotlin => src/main/kotlin/model}/graph/Graph.kt (100%) rename {lib/src/commonMain/kotlin => src/main/kotlin/model}/graph/GraphAbstract.kt (100%) rename {lib/src/commonMain/kotlin => src/main/kotlin/model}/graph/WeightedDigraph.kt (100%) rename {lib/src/commonMain/kotlin => src/main/kotlin/model}/graph/WeightedGraph.kt (100%) rename {composeApp/src => src/main/kotlin}/settings.json (100%) rename {composeApp/src/commonMain => src/main}/kotlin/view/BounceClick.kt (100%) rename {composeApp/src/commonMain => src/main}/kotlin/view/screens/GraphScreen.kt (99%) rename {composeApp/src/commonMain => src/main}/kotlin/view/screens/MainScreen.kt (99%) rename {composeApp/src/commonMain => src/main}/kotlin/view/screens/SealedScreens.kt (100%) rename {composeApp/src/commonMain => src/main}/kotlin/view/screens/SettingsScreen.kt (90%) rename {composeApp/src/commonMain => src/main}/kotlin/view/styling.kt (100%) rename {composeApp/src/commonMain => src/main}/kotlin/view/views/GraphView.kt (100%) rename {composeApp/src/commonMain => src/main}/kotlin/view/views/VertexView.kt (100%) rename {composeApp/src/commonMain => src/main}/kotlin/viewmodel/GraphViewModel.kt (100%) rename {composeApp/src/commonMain => src/main}/kotlin/viewmodel/MainScreenViewModel.kt (100%) rename {composeApp/src/commonMain => src/main}/kotlin/viewmodel/VertexViewModel.kt (100%) rename {composeApp/src/commonMain => src/main}/resources/localisation/cn-CN.json (100%) rename {composeApp/src/commonMain => src/main}/resources/localisation/en-US.json (100%) rename {composeApp/src/commonMain => src/main}/resources/localisation/ru-RU.json (100%) diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 6bc1e84..0000000 --- a/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -root = true - -[*.{kt,kts}] -ktlint_code_style = intellij_idea -max_line_length = 120 diff --git a/.gitignore b/.gitignore index d3b056d..61765ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,59 +1,49 @@ -# Ignore Gradle project-specific cache directory .gradle - -# Ignore Gradle build output directory -**/build/ -!src/**/build/ - -local.properties - -.kotlin/ -/.idea/ -kls_database.db - -# ---- macOS ---- -# General +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +**/localisationError.log + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### .DS_Store .AppleDouble .LSOverride -# 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 - - -# 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 +.apdisk \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 951ff97..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 spbu-coding-2023 - -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 deleted file mode 100644 index aab5314..0000000 --- a/README.md +++ /dev/null @@ -1,10 +0,0 @@ -[![CodeFactor](https://www.codefactor.io/repository/github/spbu-coding-2023/graphs-graphs-8/badge)](https://www.codefactor.io/repository/github/spbu-coding-2023/graphs-graphs-8) -[![Build & Test](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/gradle-test.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/gradle-test.yml) -[![Measure coverage](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/coverage.yml) -[![MegaLinter](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/megalinter.yml/badge.svg?branch=main)](https://github.com/spbu-coding-2023/graphs-graphs-8/actions/workflows/megalinter.yml) -[![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](http://www.wtfpl.net/about/) - -You can run it by -``` -./gradlew run -``` diff --git a/build.gradle.kts b/build.gradle.kts index 5fa5fcc..f9d2e79 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,13 +1,40 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + plugins { - alias(libs.plugins.androidApplication) apply false - alias(libs.plugins.androidLibrary) apply false - alias(libs.plugins.kotlinMultiplatform) apply false - alias(libs.plugins.composeMultiplatform) apply false - alias(libs.plugins.composeCompiler) apply false - id("org.jetbrains.kotlinx.kover") version "0.7.6" + kotlin("jvm") version "1.9.22" + id("org.jetbrains.compose") + id("org.jetbrains.kotlinx.kover") version "0.8.0" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" +} + +group = "visualizer" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } -dependencies{ - kover(project(":composeApp")) - kover(project(":lib")) +dependencies { + // Note, if you develop a library, you should use compose.desktop.common. + // compose.desktop.currentOs should be used in launcher-sourceSet + // (in a separate module for demo project and in testMain). + // With compose.desktop.common you will also lose @Preview functionality + implementation(compose.desktop.currentOs) + val nav_version = "2.8.0-alpha02" + implementation("org.jetbrains.androidx.navigation:navigation-compose:$nav_version") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "GraphVisualizer" + packageVersion = "1.0.0" + } + } } diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts deleted file mode 100644 index 8d3132a..0000000 --- a/composeApp/build.gradle.kts +++ /dev/null @@ -1,136 +0,0 @@ -import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask -import com.android.build.gradle.internal.lint.LintModelWriterTask -import org.jetbrains.compose.desktop.application.dsl.TargetFormat - -plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidApplication) - alias(libs.plugins.composeMultiplatform) - alias(libs.plugins.composeCompiler) - id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" - id("org.jetbrains.kotlinx.kover") version "0.7.6" -} - -kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "21" - } - } - } - - jvm("desktop") - - sourceSets { - val desktopMain by getting - - androidMain.dependencies { - implementation(libs.compose.ui.tooling.preview) - implementation(libs.androidx.activity.compose) - } - commonMain.dependencies { - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(project(":lib")) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") - implementation(libs.androidx.navigation.compose) - } - desktopMain.dependencies { - implementation(compose.desktop.currentOs) - } - } -} - -android { - namespace = "org.example.project" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - sourceSets["main"].res.srcDirs("src/androidMain/res") - sourceSets["main"].resources.srcDirs("src/commonMain/resources") - - defaultConfig { - applicationId = "org.example.project" - minSdk = libs.versions.android.minSdk.get().toInt() - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } - buildTypes { - getByName("release") { - isMinifyEnabled = false - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - dependencies { - debugImplementation(libs.compose.ui.tooling) - tasks.withType{ - dependsOn("generateResourceAccessorsForAndroidUnitTest") - } - tasks.withType{ - dependsOn("generateResourceAccessorsForAndroidUnitTest") - } - - } -} - -compose.desktop { - application { - mainClass = "MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "org.example.project" - packageVersion = "1.0.0" - } - } -} - -koverReport { - filters { - // filters for all reports - excludes{ -// packages("viewmodel.*") - } - } - - verify { - // verification rules for all reports - } - - defaults { - mergeWith("release") - xml { /* default XML report config */ } - html { /* default HTML report config */ } - verify { /* default verification config */ } - log { /* default logging config */ } - } - - androidReports("release") { - filters { - // override report filters for all reports for `release` build variant - // all filters specified by the level above cease to work - excludes{ - classes("viewmodel.*") - } - } - - xml { /* XML report config for `release` build variant */ } - html { /* HTML report config for `release` build variant */ } - verify { /* verification config for `release` build variant */ } - log { /* logging config for `release` build variant */ } - } -} \ No newline at end of file diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml deleted file mode 100644 index c5db0b1..0000000 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt deleted file mode 100644 index 90d5c40..0000000 --- a/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.example.project - -import App -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - App() - } - } -} \ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml deleted file mode 100644 index e93e11a..0000000 --- a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cf..0000000 --- a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cf..0000000 --- a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a571e60098c92c2baca8a5df62f2929cbff01b52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3593 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 P00000NkvXXu0mjfP=x?Y diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 61da551c5594a1f9d26193983d2cd69189014603..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5339 zcmV<16eR13P)Id|UZ0P}EI-1@)I=X~DGdw1?T_xsK{_uTvL8wG`@xdHSL zi(gOK!kzzrvteWHAo2y%6u%c~FYnJ<{N`T=3@w2g$1Fm|W?3HbvT3QGvT;S=yZYsV z;Ux5#j?uZ!)cIU&lDjT_%=}{Tn4nc%?;kSe8vq_&%eGAXoY=)gfJHN3HRxZ>B(Z_MschsoM6AUCjPu&A03`pU`P@H& z-Hldo)2LhkOv(g+79zsWLK6F$uY^-8!$ow=uuO2jh2SxRvH;PPs;xr%>aSRNI!<*k zq54?efxFGi!}O%x@0qhGX;;FAnHp6DCoZk~0VY&zmNZ7(K!PJ_APP1drc`bP>0_;h z&Qm$bcWJm(}i`WLgp2 zB!Saf;inDgfjrc$$+TEt@mPcR1IsBF%ve$XBbby0fpkyuOahYhptv_F4TPl^cFuY% z?j|wKCAHsATwcEiKD!!=-Rcj*rL{kREWvXSay1%O)$IkoG9;U>9D$AX2iq+}=c!zK zW#~F|y=6S-m(=bSuBh7sp;w||;ji02=~j1>n56y%KZ-d`CU}*Vr4Kbx#$l%nQktf zay7|dPxqqVP#g?4KFBTpC4g94a7d(I?Axdoz50FWHg^b+VQIjj*168V!-BZvwln~A zbKH-RtH}*WGN*#QmN8LoJ=px$01}Vc?i>8J3A9hHnIyNX`EfxD=_YXVIKs{VT3Ndn zW>tOBQlZBH$fP_7=2U+P&b2>w91zzwom{tMxdOJt%p6O<(sru*9vm-yM{=LrGg*A; zdzO^ZUi!GSIH4T8kpm@-mto`OgS_RuFCT{W^#^#*lhAo8$9JBR$l9jsaNtH3yDncj z9=-2VI~SII2{y5Q#*d6e5)(5m5qxJ>5ez6o)AC@Dmht5wuo5#@bKJK+ClNCgSImHK z-n$L4f1hQ)kyUO%%{MT;DuTBj5;{-iWSt||N^Q6Z*Y7p3>zTDvk2$AzYh73y(Ykaq z-S$a`7~Y)6@=WksXsXwxd#=vLpuN{KnDUhFcejffqj+47gj>yxu;Skx*L=&ijF8^lE3`V9ohnj~S&~kFu#to{@S-dohp8hv1H|3H&ftNS7f~Utf0s z-0Ba3@0BRndhI0axt07RCPdAk(OH`c?f>Mvkw)i#6?2gwcRS#Z7G zd>2F_5wA3$3sv9!1Cnl?gV3unFu8II%&++xD(_x{jN2uw{;mRg;AZ(A*EBq*^_OPS zqW3b$^)#DVy#pT1?REno`cCElZvG#G)QHy99*{=~0lSF3y@HHeTsgFs+5^r|WbX5XGTV4F1VJhg!y=hf7Reuqp}5 zpjo-u)jNf=s&|4cp{$jH>RjCOm6?Yz;^2*JxF>3UtZ*dKh{2k!N7v=kX)dSt9Dcop zb81lcyzm@k@zO&sTre7HI`lsiOGC;R*6af7$}J)ahO)%EGMpu4HrV~jI&WLG9e&21 zsJmTC9+#u*QYRowFVdIvCjDi%>vNHH^;Vcw_<5!BNaa2c12vZv4G*(@+qhJ4jaHo2}dFnxWlf-cFM)5Co`@Hf~jXV|1r?XR4QTQ0IB`3a47oVt z|6g6V5B_<=meX43`m1qB(K;T<3&^(kvxbr0HY3{r`e4_B5m;#>1JsFb9^)44eq||r zPuL7M8yn#EKX0t_p#Y8CWhr{I@fJ*t_J%S09bnu6C)j^6u}gryx)1{z z$5(=Sv@^^~4S~O!WMB72Qv<9l`<`YFI~IeALT?Y=U_MF;khm8cvUXB`qZ0oP2Wc83 z#osChA)h-mVaA)Z1=J9Z_Mv4EQKU`0Hs=d~uWLHHTj8F9fi!(vsQuh;Y9yGaXi_p3%9HylQ<{^u|E!Jpr zY4t0U3I+e|NG9!Y>09{qPVF-dsPK9j%*YIZDH(y_R=OYc-^rUv&#w9c?Be_n6N?s8 z9^Am}C9TAD-W?gNlC}N*&tK0ppev0xU{3z$pqt_X^K-X=L7_MAVAb%vKN#(G4ki|| z2CFZAwC7VR2B_UZ-$Otf>JRYdBF~DDeyfUhfnJI$1Eib25%kY`Kj__9fTqtCfnZSN z3+h2LXA+B+vx;J0>)HR4aYLq;ZoMM!gxQvBC!T3I5(z4a1ie%O6wUzYWD+DFsT?SP zO_=Fqx?LS;{=o=h(dLy0j@WC~g~8Fxg5;QT4XloWxSBkOtLCIeEb%q@kX~C136}~W z{!;!!sV!(Bsr5yWTz3}Y>+pMBAtcndmE_Askap!)NVt3&60XRQ-_JnO?`I+V+IdLC z&xu#1<7WJTkCaZW%6ugjd1<_`8UKkBlY z0Le3HPfsN^POO44|8)?{0Y@fde{uqwC=bv&v>e7pE@q z8(`eg?mj^_Z1R%;MZ&a)J+NoLmJOajThV#;*a*1Wppyfh8O(*koU0dg@3+iTmx-3%pq!1D#A~P}?85fI(%ICB387Z+3225a;)w{qpIRI>qdBW1z zFqn4S2W*aeflag*Oo{OpORNt}IpG6SPx^vWVi?R%2m#ypO<Q@c_!eeohr+BJl-$n%^@rJc zVJrtCu`dV*&tLa~{pqb>e+K0&?Y9Z-i?)H~Pa86@&HYs@Enk**Wmz8;Un@HUbREg- z1@g`)8lLw9tyAk@>Tz$-j&g3}R?-3alM`NG7VFx^t)v68d7=kcC;PQ=D@iaWF-&oT zIoY3qPO3`_w|WqasawzTfQ4rwKtIO=-3r|-&;7n`p(ki!T?3by%%?VMEYXl}}eR0u~8-*>a7egC@(77 z0ebnKpj+S})JAty@v{!0HV(4Wd!;iAU3(}SjHJgO!_=c!#v7LSv(=#;ee_JLNvT1y zx^k;{AC~8|mjp6EsR6ujDCRIgc?gIH4#gY;w46o7Xh8+u&ARAjs=MYV(Zd|>5l<)I zq!ydq8;WngK2|GjL#6ng2SIa3pUo2_YEbJuhcaZ!bJ|M+3DA@@K^wP{&U1`1Ji$Jn z0J+J8Lovr7-wPaycQhMdw>~yi0A+MG*48?Xw#eSAWmkVP<>noS@arM=%bUAyX2#;LLWhoZSwe7Dd3P#rU~6 zqIuD8I~kmb8|JQ~HVif#{YH1fk!(F*8$FmR9;Ul?nv-6Z`z>y~#uj9EWSuk(aOv(_ zC;72FM|Kh@4$2eKFze0?lxaBoWI4n7 zst!_O^F5Dg>)A*91N!HK_XgOEvq9IWqHJ6I-g`jDUdcqLQ*%Qw&++2TkjbScru)Lw ztRP-E6myJoykY(s9EfsBAmuqag`OgEwJ`@5SG{TRkuB*wP^|l7e+#rlT(7;8E-aa$zBqnCzNuow4YP46D)HB_>({al(7k>W(V`ap_pTmi-6FrbZPj2 z88Rh-TKHSlukBAMzM`m2y7tw3yq41@CcU9CjNT?5i1N{h&C`OkQeFP0?wq|hUnXc? zTqECW;WlOAY<92p@IexgCuZV676I|WAuBP?^S(d-?6zjTLNCzCaRc>Z&VQ?TTWv<& z=w;r4oUTv&Ut@YGXbkApYlt!}dK{r-q%vvrUWXX!HRzc*`{#wqP@y5u%w&sYz~Yxm zWac@OGI5lj6Cx81rX3=h&oL?Rg#|_1(N)*MhhNNzRZ<^HFYu1&rQEAO>G(9@NN+Fp z`CuUV_F$TGd)LWu(YS+4(mpNPE;7FuBzC=uKoNVag0Q4#2BgKdwz1Fjw1=bRbtuz;rX1c3LE7MhE zk>xL(o*OD8C}=S>MarOPAw;#K&R0K-m=)Q7nkG$G(2|v5z2ENr&a+@OeA^33Ix2lR zwf~Hn)lLp7ENta?tmUvR#BG(^XESLpd z4eagIqL$Z>+GQU%++~u_tHb-5aTYVIm$GtyB^4z~{+^5f5_*9Ky1hSQ7WFPIKcaxy z=iRrAK6D)Kq!YFv%y|FGsF^4IbEc;RmRV)`Uzwa6c*D9N_!fy(j^M_GIFBpi53en= z*uO5v;_H=B8h$gwROT5uQ5~GMP@RLxYL!Q_LG|Pfr5(4%amYp?ni6?hSP#J z>irZI7001yQKOYK-kbQA?r=*I`b@|0oFR%gg(T*i>$J5J1p#4~U6HrAJQS4rYPAy^-!I;eb$Kms1miPp znxu9z(fBqhs4PKV3X42eMfL^am?*ly8X6;V=hyFCxI1@I!=f1d!=3rfz31$AzVkch zp7VX*?j1Mo)#oMtMB>2sS>>u9y+{y;Q4?1|^+Uo-lgUx>5e@WdRZozbvM0%m8E+E& zjRkKC_X0v6qoZ;DkLX5cPgn9y9K?woG4pg)e7W~$bKAG=@-t=M@-yXF2!W6TfI}+35(&+V>#9m}{q7V15swrfqgQl1VStksa9&pOgHMKd~-Qm-SCZ z?FUZ`Kxmd(TGg-o^jTfLhHOaM(jG_+>6}EL#`zf3T%@UpzZWCQyq%NjGwgI>rUEX| zm}93Sne<{E*^&M5Imr+C<9#y@UWRncZce-7vTxrjO={uAC4C?NeF@U!V|2oB?0Q~j2J#&otpvOoP5rT|)SY+M_K^CyIeK-7B zjf!=V=Iu~0vSJ;{q!;VRj_ileNq)#5-4h2NV-^Bh)V)r5OaDA#0B)bInH**;>{;Bg zn;dcx?eBrGsACsab$$pz7O=MSV=QdnVW)fN`UhCnvByqFGU>%SvLpN9bCMtONB6`b zvV)CnE$*G+NC5N%Ue+FPdKJK{0KSI+q^yaogge_O~^OwkSt)o zr543qrFOb^JO7R4*Wb6(kxY6)j$+t-rwpH1svnt?{E$C>9ODpmeJ2*R?r^+`ef2p# zlrfnhgOeLFL7*j%&-RckV14I*Q1i7O^Vt$9=;oPWE-_fv=$bgLLmaw&*vbgESe-U?cKQ`Rhht-`Q@p}56 zi0!jf@^&vp4}`GVK7X$j`L|BtbZ-+nzU@L!e;>Xb=m*DfxIgd!-Thzl`eQv>6y83K zYWCE~?u7>sWggs&4EMj{$vO%ePj+NKrUB4StS}VxP>qI}w{fB7A`l|^9rj-kWJ0*P z7$4oKVA<^(6?p+L-Pr9lOM&}fOMOO2E^!4Aj>2KV> z3x9pi^ACWQ!M$wB6qD+--bTRD7_2y#%Lnsa0rd5MgB4YU2rg6NX5U@A?{-};fmdtV zvo`T}_W*5J=KHtpOM+#!z4uGp>a#dhLSOx_8y)vMp}hv zV{)|CM+=&F?WH|fqAf&(vH0m$p^-{x`|Z-_LS8_={s`t&svx_V1ZivP*!RHBo26*H ztsjB`x-K&sy9|T4Loh;j*No=7CN$nP+R$P#LuYA6lf^WMZWEfj&A8HY9ZfxE8@3sa zA-F0P(y9b_)Fs06TI$#aAZbxz`mt4T`sD9Cd_LO*=L7%1w9i&z+Cg?b^e*JbHpBDy z1~zUroKLKQ^XF?JJ+&FLOXJ{DvK})^H(utKf2o;qYp>99fOoC!*nX zf{{A04z8cChwG{Jke5co?`#6xN;ks&>?WSPrzRR96{(n69u1E#V&HK;7M@jc2&v70 zye1i*wd^TeOys1EO87QsjP37%NPRH^PA6c&aU}wd#lr7+Ec{Qz!T)4DB1%*UEm0z{ zG!cPkk`Qz*8R42VM3t)%tWmP8s}RhHhn!Ex-)ah>s7{BXCIcZCG7)-Fjpf>6L^R|g ztRV;U8nd~1O}SX8%^mw6^^z+p1ePSQ%&)@qBMe7Z^JU|GG8&STth7$9h0E!6eA#%N ziH2`k0%n}s2-mVreA!Uu6|CN=Y}_kj;9eEWmyMz>gKy%Q7ugf5PvAVXNs!eh_Bv%Q z9Q)H~WLpv3OE%ibQ_Xvyis5TsAWtTDC$|6)+J+R z9qR*aBIj`_8FCiDAD>46d|zBi!;G^VZ4K*vIu_EBEp`nnD`RD*Ng5kG1;*Ip5>ppd2QR+CX|Xu zO*%p~sR-1hAh2ACpo*;sugpMHbq?mRnx|zlxHcUjLk+878CPht5OOISA&uEsp=0yu z3J|KxL-^%9F8pdfA})=hi31GT-B0`9sQ1+jp5*MZczBkvENfyQDUX3qMKXff4l6w$ z&u>y*)rqXGlMzv$!x}c3)qDzHHu44~BAWBz*TjB1H>X0TQ*qvx)8OAgfA0QeGDaV-zCDn$*;%0^z10RJkbUBl8kA6B2mmkl*6)jX9=XmbuDuYzYY>jRyV zlU&{k?*>)x)WXG6pBRAf(!go^;@|jQQ{VM7KHCe9fL1ll}^JDk+PzN|`LJh_}kmCs^m#WLmwd60NdohMFX+tTx#?Uz=t1 zsZ;gJ>y=jdh2(D61FMh!!sRV0pYe{qseFy$w-dZ3`%GNms+bt+%wy8fRSd^;PKt>^ zgLoroiVYLzIw>a2bymE=u7rs^MD`1u6%(YBeTfTka`;^_4V)4=j#Q|q*LzL~C5KRdRgR$D<-wqU{rxAoiE9G_nq^fd;fFZx%V+( zz=Qq)42*!CPde(h*x_ei!)?Zrdj~wOKN-lL5ERP>b$3m0PBz57LG|+FTE*)q_#JiK zjwLqG)?)=8V9NSeQ2m;@f%Vy&XVh;zHr>3z5M)~YQ;>O0BNg%;b$AWO;8?upkq3fH z-%f>}Hx3ClXV2mrRuu}2swN`9H>e=Ylmj8AZ2FxmsKaaQZ@dTZMH{oOWj@oLkB9eX z0v>JC0@V^EYM!+CrOb zPS6#8Soy(COrAc)$=#sP5`k%CHc0@CdtFKk&!AvfKq00z5M*549vCaA!)xsU<2~eF zw1KwT^eI~O(Vg!H22W;ag}YJN$~vEB&S}Nj>kPEN0dQ9UZM9DV`Y@!dc;FzoH~Jbf zHsP#O2RP$|0yt|AEdXMR(u&w-^}e-foBwbS+-k7ohcCCyzPJS<>o+iw=Jm|<`VD}x z@Y3fn_u?nO{$^#~#m^w>;-_8osKaZW^=JcavA@v=`ud<@3oNSt_jUqd;O`59lRQ4g z^p9sZY=%(N8b)YJXMBz6z{^ZhIs=-nAdgDqYkfi)}sxy#nquN^!Y*k zX7D*@T^rba+ewpl>#@T}~!e z6KGF##@dBCZWrY9Y1E{wVP$yS0U!p7rB)7;G@>QlQi+Wy_{x^SVdk}U)9Tj&kyiY~ z3Nf?cW3cMlCHcy3*m1KGBI?)M=&{<&ZTO_ic+}xFu8ve2*m+Y6(#yNLj7Oj7o5d2| zunwktpP_g9dg-%WR)LKu;C%Y50COe~Vf;y(fHIeqGZGZAzgby&=_}CRy$Xwe_|is? z6=eni)_FYY@ETVqy1WAn#KzJ~Uv?RfKG8S(8!`Fm)4@xV7-hQ(oYFM;yrPihKD(4X zQ)n$@UdspdFXzCIL#6&wD9Drrnx;Bx18wz~1Nx2!D1N$DON!WBpxD_5gwILEoBTRu zQ+uD%X8<|m`H)RPNC}-h46DfR9FSbz3IDlK2KyRyP}yXl*Y`A5!xz^}=(Q;%2ppSn z?Eq9X>8XuglbG8(8I|CEM%LuEYw?)&hZ|d#{7x&P1fW}Jl0{OdSC@EY7hJo4>kk9(ENBaDa($pr^v%^Fw$S=) zn0hMRG%P;w`St+Dte<&1AeqX!a_|U+21kp%s_eCMhQ@_*7pGKw57~atX z<<1)sXvnzPR{)rBST?ziZ{2Nzs;lSWPV?PeaWtZ-2V?7J&a* zRpZ<1-yPK+fc>^PZ}umE)T?>W%(U1zU9I~T#%+tDpUtf;eS*g^YtHTl$Gj!5=G>kx z*Ho8svF7&~z*}k4#&qPsmJf#c*Jk|GTL8Ys3|cNb1KLrmhADXx`q|Qt0C3E9lNzR~ zQy{lN)8+cP+ZVy}gdBYIX*~uYJf-~kjl|Fq?Ews1$a_A#ZcVRAthl-ter@SWllv{r zaQ#kWzh<91)7S6bg8SW+-=^l@Kz!ya2tA$AV-knfq?%rw`pyg7e(tG=vss#+%IJFy zn;`GjiHDxJJ;|<18VJ!SVb0kN^gO9^84amWXbI-Q+(vGYk5=}1PZSC=X2Iz@7av&w zH8+jmU783%<#KR6nMiWN_CY2%82dHBY)7$MTZw^!f|w;30PVjy?F0sZv(VW5>mv)` z#@*W>)FhJtQoyN91g@u&+FBfJCC;aS>sRwuB4(RbVqDe?2hwNU?yi{=k|Yi&m4VOR z81S}Ac%Brd9FTxdo(Oyo#DQ;qJopwQKzN}X!Vb$ocvuX6hb7>5gh){$gsaK+w3t+o zVriQkONM}wWC$-?1@Bjoc3C5bKms_hf=Fcw@XN#yRG|PTjR>5|V^8cg+X;-3!2B z&jR4@i-yU0AHn$ji-;_S@duW``1~cnKNJg|hvUHU&@y6YIZQZAGAz2Og{Ah45AaZaeOfHOp zfFp#{MN;4&5dptQM1k|w@!(HZA*_t>x?b%<)zVce=*$jPeTgotF4)_))Lg;=8`0tAYk9{%Vxt~a0 zEO_O|!qkIO2stDL??dt6T^J8OhZDf3NKER!oX|)KzUo8}s*^x?ObWshDFLs7cgr)t zPa^|=lC%gsK&ybT>NJ>LlLLV|6$Bk$)f#*v6?_Wg4MRu0G`!o5y)~jgkKOj67|&ub zVS3us^Ull3vM18nN7^{#E(C{tizsb8^2zcS#8BEe7A&QdLGd^e2i`{$C~YPl{fJQJ zBT5@VNdowlB~#ismBqGEh6ukh5vCkhfm2ny#aSn|OsWvUsO<1$#Mtfm5GSIS3FmZu z9jk;HvcZEaxx?NL@Z<9qgGWIu@DIk=fJe@I6p;YbVjJ+tc|oZd{K@Qd!6WAd+9U|k ztpew&gcg@-G1%uWI6<)egYLw3Mm*WusoYZ|5`#ls&Pea$@d^o`wWl2!=EOt-0)bN@ z3F~n%mL@D0JSMEiQ9>!T#0ESjtVfvy0tj`u;7P)Qpo#=go!UxfA0`}Id4JeKegtB3 z+%nIuKSzs0$9^_PMtu{p~z>_4uPqCy+ zwZWtfAf=NF-dP(D9>=9j=*cvTQ@IF6uAZKbnEE_g?AYnkC3?jpZ_)LX$SE zDi!#IGJ+~82&$zNe85Q+6RFDphfkw+AQpQG=u#o1 zCXMhuy%ig|$ePs<@=e?Ug5jTtrAOZP@q*(iA|sr>U9{cp`(&WU8oj*W;MJypP%9@1 z8&7G&O<1oI3HX*Jb*VO3+XJhW;G~VSV8SBjkv0xn=ito0ffxib!Jt3%mWEAgBEv_2 zJTu+(gyf#}HIOCDnB77Guyi>aHDrNrmCOpfBVoNr#q!liyHp#msw7KbwE}@#u-Z&4 zj=ncCb6N)ad?4^PbQ&|}Psqd9=JVfmEL^U`)d(m24=}H`w5>?Tn@4&wr_ZE`$W2%; zGW){vWD0yzxro&DIL5gmzQtRYYzeMWp$;5&FVMX_+j%DCJn{LvY13O`kC8=S5O@+W zdi2^EDS@TQdf~ZLu&xLdo7b$ha>nVnn3+(rl9^B%!}wH48NbS8W+DOZM1mu9X{$CQ z`MvW+`jN^|1+o1W`k=o4AOD76t-(mCm+byN*ug$yhIrzEWhFeFjI;%An`T}yWasFSq8TBU(BUsr`Els9~96gNDMC0z9>h&OoeUa6h1 zHEPG(itwbDg!X~t-ceQ?Pg9$+$MZiE7|gR)AeeZg?f&+h<4~93{1<%2`l8@>)ZsPj zm=~@0*gf)p_ULX!5X6|BvOih#gk2r{|A)U=){M0000mR-|nJ ziD!nlM5WpyKdG{c3k2M;jXYyyVo*^yGIoo3`~=S|F7P^2q1SWS$X&WX;`m|lvakY#7qwtaxT_5#?fq+k)xD_wHQ zyOv!iWuFs&s&k8$>66s&pN$6(OHEJH8Iv+e1ce=IQ2k}QWOKrE(R&G&rrwRul5JO? z9Uk8YLMp2>9IqF#Te_G{OqvQMdu+CapwA4T<&Q@QcIv*Lg9wCU@r|C(t0{!0uNy}p2{-c$-u10k!W;Vg~%I&@z+#7Zi7r~hD8!> zpn1}&ANh%cY`4tCA32CA8i#xOs?h4F_7zdAHMab<*W)CuwR|(~gd5`m3bQqKX^YNG z+~{>s$Jk%6cClss$H84jVN#H-lJD2DGwI}SA zu}tz|ZwBc|Pw=EGw^kh`Vk_xMX|KfNCGdbgab3{y-S*BeH0I5?Fmdh355OcbEk&^| zvJH}xPR|SFnmgsUkXAZ4wj<1U04=0TZjaXuYB~;x?~Ljrb98Ioa7$W@Q2QHJmAU3m zqlJ2~r0VR++WqVw;&dIr@dIHqjUh+ASQh@B(NS@~cD1|dsV_-;UPjE8^RNw3E?oOx zSawJ0BrAl>2pdY6WexcT5X1q?^`Am81jG3nOs~fmQ$LhX9bynlAH4$-4lBA9QiYq@ z87)AMgAz(4!fMjm9M<0w0a6v{tIV^NELObpXP3`b)U*@x89Tb^oO+db`gC@e(i|b` ze67ZZ)BB~r(*Qpqoo`Z}T1l_aj#u&OY)!Dzm}f9df7x`HDRr$b;S`>(2aRx?w^7$t zp_L2SLwiLhm-FJ$ZHb+HJ7c0JKl0+sH@!SL|IheR2Of?`TP?pRa8i{~W;*EZeiU;! z5qg1lRW#x}?|K&Fq6|x^H3Q09CRZ14A}?5rOE%fsHgbZ;pRpI;nrtX##M(YnKkkk3 z+~&?#V1fxYR?-#{_;rMDS7${>_1W~iW^pf+R{8V$q~hG zUj~ld*aJ{`0%9kHw*9lEZDL0H32F{V&21_p^|9KQOZ%(tH&iu#-3N2M1Oqu=%QMi) z3a!@quYHxs5mE$*16Q&)2UBmDU*nJw+cVC%T6}3p3y>DMkb|)L)lti?c%_LG1@z1Y z`O0Nc)Qe2`t(A=Nx@S-67lfIMT>Z~C1iCb;(6G!=-@6n{h*4Lbzb@xt6wbJ=GtlqPq%4|UJ~huHD1cmeY)$p=}87X%EjT<#QNXdk!a+04QLozV|jq@$tbmh zpao9vHJHhQpjvywl(1?PE{BS zfR{NBD8e6C^$``kE!T9P9nZe@25vZLg&y^Ao*qb^nTes4#=LOmYXkDsiTF=zn}0jrbE{YJ2QDvE0x2)7y(Ha}6$KtxlNp z;n(;S{ex!!X?=Ij-kdhogzEktXGnH|JzUO_edSyAXRv4nLYTwEfl#KVS+7%bqIYCP z&ur^~ZSZtANr8eUyQne{v(gw++&~%2)9p(*3iM+2oFo6$4_%fmG}($R8Zaq{=*v4` zV!nyJ@5vIXQ1m?j1P)8`sLf>nrc_UlatmZ=)H+st(SRps zxN#&CRCYp(79mnAy*pBRv1>hmJjf?BH^u0slOl&xgTlsm$Om)hVJd^1pw4p?10fzlXzO(| zbC^>xs!xnAKfHePWTo%hPXFv8`7IYqX4gT` zQp(=7i+KlBm-}5**KPuCw9u!rR)J;9#3s|m!}eO2EEDB?Pkw-lW*+C<{DR2Le5qD; zzW@8)0)O3mN~otlX@tuhMxW;eIGuX+$rh3RWDgY7H8H4MMK0V0;bN9|!@w63^l3&5 z&0)q+q@6rD=7qQk$KedGU)PVDaA-g0fo}fn9X~WTc}y8_Lj%CE2dVh@8NOLV10^oF zQI_gsGrQl%rRNcT`SgZzAFOvvC4dF?AeqWY?4l@*#U3O*MGdG^xOm5JV%3;SOATnC z?9tAd{*w^|RtEk`S%@DO?b=lWR>)||^HL+is%@`JzWz^pKeH;4-@qzLS8dlpcx49nHQ47}Z2YEuTDZEA(kW3fYY_p}B6cIFk zMbt8vgs1oug8 zCnR@us&d9lEL~oxDKzSww@MWCZXwy07+^2K-AXe{GvG?+83e%j7Yl=f%Wb4B)huao zbP=@84F{aNVYG1Qhajw~Y1qVPFM1Qkkb`Yy&!y;yTE(C{18v*gn>iwt74810m`a_j zaeX94mEQ@K&M}<#Z@w(hKC*E2WHWD)aW;8Ua;S+nTxrjgc~uYuVX9eNx@n2>nQ}l) z;B1~Sl1qH^^=wCgv3{;zvR7E`t1eGiP7&c2d+p1;-4J!)xm3Fy$-)_obcQRPY%u7? z7XZstD$nFs>PYE%Mk7Z{QrB2riY@bl%aA*O>%{wOH%T-++P~>LC$UivlwLe&{{}*+ zkbH2ug77!!3m_rRpBFHht_jt>Us4q($OqsvHD3?|8t7vwAtJ;_*cvb{S`NuWeEIon zjsj(8M}cyEYQ>V-6XE1Hk4Wp-sts3$%7Mpv9*9VOz!5|H}i>_1X} zG`$FAG#B1$-wY#f-mxdT>FlkZLKBH?LVAFB!E}EpL75H{6wBvM^fdB%R?-j~0d|zFTA*n!Sbq@R7I$sS)Sf>=TgS> z7DkZ`m`^wC_Q@rUNntv|0Ijbf9@edvA$M)+#jMo`0r?s#41#UZ0l`5jQ8RIPkWYkL zLuSnjlMf=nsvrXsbLOTQ^D;=vJ4mu6B%p$6II+3u_iquF#Dv=&_{Ne5M{*;lK;68G zCcB|s+9?b}BBHf%?-TpXD^VR_P2J5myX1qdO&uW~Rc4(W7+B=mt#w&%j7)yuSIH`t zvogKN-ARwD5bj&d;OK|`hx40`q@@8|QhsDpp0fOFB|4a zU1aM=Yf<2ymK zU)xMo{8RuIn0NEhLK+-->qo3hthYqL6fpI~8=Tz!8VDrj z@vG(yaO``ZSJL~M*f_nb>_GJJSMJoZ*88oEkhy(K3iaPYXuH$dX>EnPP{xi--@Dwg z8bG_SeeY6%=g@5Mxo0Doc1WM#-}0nC;rzZU_NEIRnJ6u}J@fBxdZ$f@l{?MD&mg$S z$EPCM$0zZwcWT`FU8Ej^5NG;)p+aG`xn!?$Ve)&}j!{ORq1@*_ZMk}L0Xz(ns0%wv z9I$7!d>;Njr6K{E7`|9mr3TLh#}wtivvU+hRX$+hNoyYhzm|q6NXEYB#;z=!b~YVO zWr0qjXwDrkt-=^PD4HVWGMq`hmTMQky0!3gBy|fkG9WF~kSkw-QzO(sS=AbRuW`op ziGH!+lMV1j#rCixt9)sG6m~TjhW8@qc&IPD{BVWND zE}dlIZ@O6{V18XdiKR=l<6aTB2BC&kpPu^4(Q%5cZf_ImMCN6)=Q;MHw2-oy@2Dq? zBq7jYByn6Ri}-6uueQEcae}Jfz;iW9-@@@%gT6?;;VkD{|RNoav#$0VNE zk286ieB7O8wkeB~4|tO=-Xbmsf3}F4F>ZOgHfk8otsKVsWsAHTSaa8kixa6o-Ri^V z0)MR_rp^PW%$7L2Smf5N&hU;cW4ZGprO>fj*|YxR`_GR&s^#MgsOp7EmAx&@#MrCd zyIaPnnh;UNM5d{7{h@D7*U-~T?d!MX93o|1b~=jXSLmU?qT;fW${(B>2Xkjm*GkNF z&(^d3J)=9>N78NIp1Mp3lsdWVqBKFPu2q<(dE3}t|E*)2wDb9~gCECHE8@~_#Vp&a zzNrs!hW)H{u=fDT_Q!n=TZu}6ReD;sxxz$>nGv(gZ_n! z;P!3tj(sx=w_Y;NUw>m_{`wMv#{|y_Ub1-3epZZSuq+;f$KpBgTzJmvqStkVy|*s` zM7`DU*~KB<%nCwg%`Dow)2uKggWyjBFe?a#HD!ljS;;<_ksr(p*2VkiF?cKmbFM4& z+~gW~t?C^C>-4Ya@sh;rW(KqwmFF{kRIbk7OSAYiGH)Iyv5bNP|Oc%MLy< zDcH#LMkFZP`;8>w)lnA#s)G}RUX#6^Nq!Juov?0LN3Ooo=BM}OB}u$qk$-#rTyG!J zz^B;bZA%Yeqp7)&MS6V+P+bhH1J-3#$pLOeJjJ?Vou#$qz3BDm>Tz#J<@(Mhjmi_7 z8q(lZr3ZwQ^MZI2T3-Tiz`9_a=p2(RHcfeYc|LQ*E-<#K!H)(uQpJDA=KFRbjX2B^ z&zTu)AojKfCjgEB92Km2qTgZNNgJ>&+}zM$13Jk`OFz$h66yIRv;j;b%OxA!kOh!{ z1{j|kP)<-m0P^5adYGmR6qVz!tav}nFAU{f9?Rk} ze9L29uueS6V%y4%^VWky!J*^{34#uP%Shnt-=fStZCuKJPTch<3hYY{mD`mb1U}gD z;1amsISPEsZ@hON{O+FOT^`HgF?`EoU9e7k%VS$ZA4Y;>{(+=v#|7=)>72lM05p@C z>l=nWe@*F6%}wTW_isUE?vmQiY5L0f4cw@DRj`za4Q*f%)GmDJtIs&F-fRK z#NPcxd%r}G^+5pcb1ym{XeK%xC0sR@;7vKbU-!1>EH1YrnO^uHfJADW@S}T!n4&P7 zc}f`t+=Mbb%~5q!j!zDo6REPy_d$TF%cs;7rMc#P5jv-1ohN1X;6}Qco?h(4E396b z4+2#CKG#R6ds{#z6a%OdN=cDO+ zSNB6MEo%}RaJJt#Gr--XAP7wIH;5+ZZ2)PQo*xVzWyfefMOK;W*m*w^p1gSu_uu>h zmc{>5SRT!TdC?x;=f|>)nNxh;7v+D^x?r97o*&zaZN|3CDnob^8UMBp3@$qO)o3md zu(=HNBi60;vb}Ce^L*-Rf^16;LfF%5AQFk-*C#1pnB(`(O^{J;AVfd=jn?7JlPk1N zN;5&(m7HlLIAnIWozOv&TVA$b`?}jSX@0-5CgFueyP^26hw$jlpESk$t_46d^+Na; zt;52?UCQ%KC5*W6*q3Cp?s=7P%Tt+DPc!2v}}i**qIC%@o(7vVLT3(}tFgF&|M zI}>0c>HRsc?$T>x9k4FS7C;;wXL`bj2-{x>r%e<`$LtW96eZ|N6fBkHdMe8e9h>71 z*IyJ9BFd>3qMz*}Q-B4em(D8KN+&tDJ4a#donv&-1wASc@;`otn{v(aL*ToDoiYV5 zB=y`)yqpwu`(ic6}Qm@e#8oiZY&!zPc7LgOB-9MjYT=b_D(` ze+ii{%jnV|euhHe_X~@5!KQm*kor6iN?$*M-(Nq0r{yoG>3B(iBqH!V;xRF2cV0h+ zlD{57+_Nky>Vm>hFwR{szV>&8JE4q}!E55Rl^%%6FhhpF+RjIA)sIx$CNIVNX>6Lg zaT}lBuM7e3_{e9s=wygJb86lu8Y3X-&j?BQd0l{lCH|QMn~9LPf_3_7I{iHSkLzLr z>q`J`6zKit2@}Fy|A*Yl_J+6_die0BGjcblzAFJZn~m-u`s1&Juj@>@Ea18E8h9-9e6FgCSLoU z2tdrxSLy4X4%s$$2y)D=AxjltOtQzj$4T$B*UK9XSQo5Qy$HZe z#G>h$n?UQtDj(_dK&5~B(d^q>_Slylf<;B&3l|etP7%=cLwC@kcn|O?zp~^9$ar4Z zAjp>#0b>!Y8=p2{Td~d9c0T177w-|;7X1h&7u*jLj+?#}4@iW_%}jsWbP;ceBR;nf z{cc6TU1;d;;a(g?WtSH3g{v=$K-fTtmju=c>xOky)DCPbwi(;bha)oK3$2Uxf^nqB zWx{dGx6=~Ln?{`s)mu-<^uLP1jJ*6$ZA_49{uYRNmP!3~Q3DhJfpx<=PRrk{G!w+- zg^*LjSm&E<)w_3~dx#`GAujvb%Xey*3E2Vp$`%0A3>W^mMqR*$NSu#p8Y-d!qre1ZX_q2lFqDa{`|zQvh`D?!A8c-U)zpmgSn(T7Xo+Q#HYqVQ+at zVgYu~8)Tdt_)J*>U=HTWivop>Eq!($Hm4t@$a_+MaY6ReQrLX+I0WB13HM(l_h{dwhwH(AFj~dEdJvjn4WQmK?fF57#_2Q z`!Aj-o%}n`AA#;!TNrj~8O4IQAo%^oWBKlB`D+L%IS=|-$`e4%)mRI;mMTF1t#j0s zWrA?I4l|RAh>0(|0YeX(GXfkWIJ6j|ORp(ifUuHOG5NzzF9WS}t04J)ro!XOUOa@U z8S6kV(@QBPsJFxT5i$kn=lAs&6SCJSWfI2BCLdxl?&W~qFDu04BW^y-SGoXc53u0{a z!>e(x%iqAyS&{JdSr0Hhw-!RK{t7~&@?(W^a?V|u=V0b#KZ;)pV(5w(pJQ)7Ee4Y~ zFVISIq9dW!ZfLAaQKzZH)R60{`5-0`Ym7mH(Jj9^2V%HdRg+W$5?=JjT_}Eb4_=km zV>+6gyX5(O3SkWb!oNr-alXDEMn>9#R*DN4Wck!gfLtFMh#5pW-fY#gQ&+lqw@ONy zT?Zy;JMG5$@VcfVa53e5b2}9w>0u_AL<_(q#uH4h1cL9KlQm977+r9|R73~LwV+BW z0vZ_#3~@-bo}Ll7w=T&z`_e=3_|5ZwoB)qr{Q;Iq!7wv!9n6U*0%ZOIO9`n8IV#*O zPR30*<#3pA+=g;peQ};$Bxp&7i3d$bGk1yCI34X&_A_0d{ig}={LL${z4kpZLw2AQ zWe*la48wGRcw$zNj;=7hy%9$2HOCFREu}8Vupc(p_}O~SOm?NHrVBEdKRNg)u0duy z>z*wY!v4ZblzgqIHBBdM zwONuJo3l>5!2VA}#JvpAk9Gp>%asCX#H_)c&@x8?wSNZ>e}818zFaQg}6 zSRiAIqS^}MkIA3*Qxd#FYqKlDBsU1MpOwMA=a1#$(Tk@v-9X>JkcB5=Jbd{FJb3xE z^0Sxn@sO0oNt1hjUm9Lj;=!w@@c7lUDxXP1_Mc^76u%a6<&bHj*TJnsQthpiRE^nw6PFLEI6UO0mlQNdslxe-hwyukDlL8LcKuZ}1m z2A6%nGIk5t#P5I^(Y`Pvh9K6j3e4jC8N?&j!Gfes;F`9V)_rDDH6#bXtmHtLmBK(L z#sRcr7y%68T*Ty4#5;mchMQOfZex~qnk$U(pSv8n?I~E$T=v#PCOBx(<15YndN&2d ze9TaFFG%mUCk#Kol1VK{q!$o_e=?_-dE5hZk1U75KU=`yBMgT8VhKZzT2KvUgQrwzLXK* zj3Y1dho4&k#uwdSIvFi|$VZHhbcTg-8+nmW1&AdAq;0DdK!SYC86mV$glw;JG(Q6m zE^|HZmU?bLUEJ5Nt?DAh0-M@6_mMgk#SEWlv~vreo9-J>gbkxvCUivl?D zB3~@PC2wBjkGy0HqoZ6{0Th!@C)_wG0whQXkmLlK$xan`%c@q2GpM;wwnk3n+JA9k zjxj?mKklsBM=QRwJ(1X8j(7@Uc4nPq1mHtHnw_uDdBB9TPQ1uRvtt}y zRRDS9W3R6+fIRZ)WEA2V^&$s{?i(7)@x~~$ozM=Z z;F2S?^&HUbjE-V3CB_SuC2oV!(JnA1+7-sc5X2(fh}-E7W8&RmEF!^!!YEMyb{XHp zjSDAkC}7=!&-p&oMY~RxonOa?0<;nxVG+%|>ZhXYamS*PHZK z7VU?5(Sb1Y)LIJruwa;f#usLt7QpN?o(#@nY~PZh-l53~)tkK|Eq3EKAx3 zUTFtlVd5rONIas2$(vwN@@80+vIQ2UZh^&!v|w1A9t`H`Az+!l4FYcc0?RUXfiwG+IuR%c^6*fQvoh{fLW9eFY*y+b`~XW=0!dgAVER^3G&hAYot1h(C;U0 zdeG6J&uHYZr(w_LwYgcoQAgdr_-Oa;gAXkZ!W)m3ai=_v1oXM}j<4cHJ{5ojXcNO+ zc#)42?&L@mz?T>KIN^?oaf3xko8^-);qB-o5&?+$F-Uf=LO%9>;<$)Ll5>9UXSyA^ z>)5wrn;Q52N|#6-=YkH+y0jml5$BL8EiS0d?r59BA7EUJJ0V>$`Dk`9DxMhT%8PvL z^;Ce%e!R%XUXKDSPTHcd=X0KpZlVh;y-EZ~@eq@b&`xm{YNfis-~)?uns!qiMi*cB z`2IXb!6$0|rq(*wJ%D>uSzYfBn3T1i5uM5FmvUz(s^v(cz>XpV^FEjhuDRRBK!N-e39pNTqvQTt@3N`1sOeXo_%+ zQyF*2pgE!M99i{WEmBK^gMY%mT9;b zjc)nocBlX`{=9QLW8*x)90ibLb|k$W-DFp=zP^hHu$Cb|)wP_OoYY(%V4+ zmfhF|W70e*`6I$@q0ic>n~@uqqk4IsbR(7S-CL-%YK8k+`VBg;_%PmpY?L1;vMWBQ zln1xsNI(**dpnrdF($zk-`tK#G!YYXgTKTXNCprXN1WS2!lezd|XGF3$3y z3mzKhZ5V{vfEkHuO(Hx%;k$yT|(53 zW`PSTv5pj&)zpc1qPZQb^zAgjq9A@gdO8$j!o?m>k;*_n&Anp9?L9)ncsEer_Dv+= zVi4to;ileyVWSB*AE-2KI%MH_{{-AYY+rUrXj^iiLKzS5wk`e1yO+%PI0@y zHg-EKh~5ATV_1-2Zc*GuF&4*fVvw*I)}-tP_tbr0PJDawWCj*wlC>aq9$}e=`JAm3 zR_WWoHe)x2SaRkivJ0uehhS#Uv zmu`xPd(~R4YbWxzXVaEVhc7tmpE&-8FEvLvCn)3b_2aVq!61?JxQnY{Zlpg#E+b+dpCZAPrj#+O zxjZA3rWP=|r64}OL24xo)7HXhV)I952t?TP&GtE_G;PsT136&1_^3Wjk2DduNx2un z&>@E{!nui=J|98Oh9$la?Zb_*nsIArVr>$MZu#bRro?)|?Dzo1xgB=W#gww;mF+TZ zKDwHmw}Upn|JJ!^c5s_{FNsO_o&UlTUa(oKUY+q5hVWPD2KWE|yCYa}=1D8elVt1q z)I=0vZu&-=Uf`SCnG)v>vl9Y%CDw4l#eBXcF+H-#M?atOc2>a`>*<7xj~wXDw!PWk zL4Fkx*dd4`VPL<&85>5%*uO!y5+i1M$9**+YWmp9Mftnn>(q5H;u62y4iz9VkQe!g z@yVW*0!Sv-Fugz`Tnw^?o?QN>kIN)a>m6*1yT@$Q41QeS6jBUEAT4p}uU>yOW;!?(a@uBXKlvKd6a9)b_!xXpWF1 zMG@}Q1Rt24v|eFWle77_jA%tX9@^`1EjP_oguNc)kiHwtPPP8D6Rv7~N!!*=rCmcK zUs42g!&Tsa_RU*LR3;B?}i*Mv|C9egC4Y&#VmXSs(v%woR?rHa6&=G{iup zIZjZxvx5BJzeR_(TK$4%Y$Z|bUG$Xbk9ihste|s*0*^`RL;Ki~AS=S1nur2ykZX1{ zlPE;k-$|o^63;vqnf~}Py(dA67}B1ah$8{FhD&obze*wk zq-=Pbd?Y^6u|g}+QAh-&8B8=gxGiPYNx|=5_)Xi_erR`NcB1{9t$Uk>YI69Rq~@$nZ3wOip{H@Y{ z;f@&z)w~@PU@j3rBW_KFMuMYgWFi6S?V8EXBF??U+&wOy4ESN;tpNhl;QtQlIgvFt zeQ8}uo!MUBXVGqSsH}S|| zVNv|OXinjFAzcXKei@s93YFz4(oS_2YR1?Li2y>FfuyvJgF8&U^Nw#WBv-b1yw3S(|sz3a&KUCj+Rlw0Ba(5@%-me4e*6A}iu z>(g~~|5cOhbat2@81t)b`ozl~52mL1il$u;gjIR_U`fFqn31;y%nE|RtT3c1@`GX8 zjX=B!0!)&;V1CL*uuKjHCnBoYIAN>3_xNCMt0FtoAUYcu{Hw(%z{SmvHscc zCz~jplQtQ;VXJdTML3ihL_6OzjB$C0!2d@@tSQqvx;%H}K8p<9T^3O~n-(1I?>;T4 z&q9Nh9kqH*!E>^t51_rBT(d=o4&B=@K7Gr71M#xv2zpNf+FYFUSkFm~=GPgr1`*D+7~fG#ZOVVf_5BKg|Kn%P|J!~PmSM{dVQu;V_FQUsZaT3t_PsTG z?I!;;Q&Sru8nZU{V`>IeRomkY&FFihd0|McUYzm9)ri?Ia+mU z)m24Rr9Eq6K4!1g_}@-EA3>VYn;MWf5@pk!2Ho0pM0Lj3z9plHfjXEJ1dIC;b1Kq#ey`7v5d~0000C!9-gs*@?wOFPDc3TLC+gIi8qrnqX(Sd!oRW)p(~-x30?lARJ?Ie zR-~XRO(~nA?IgVzeK1Ygxg`!aO{r-yC+AyW{rAHHk8ShUnZcU#g#8mIo$W3M{s*}^ z=bv(XwxxGmoc{C^3U>ZK#X3PRA^qyry1C>jdBt9@OkwCzC$a>*cO_gWD!5YXVQys? zI;UY@ob~MPT=lDw@7Uw}YQ6O%iIp*p!{%67`^{hxo~ZA8yN?;)ZW;|AhIvE|E`a1Z zKTiz>+1`e0bjso#Eu1ajEzmIjHOQus(kGyr6F4_5wm1lk(Jr!B3oPgqC;hb~SFv34 zy-=z)%+LTC8hrROE{#1*XLA0E+X$O|DEO;j&5F*GmVP5$_>c|UU0D@A58g|;X5oM= zJzUbNxV^wFBH=ME2;kQlEBXE2oo#A)Y&z|Ija(vV8flM=ov0!LzF&N7t^5A{+<6P| zQoXTqiBPS&RVAUos2Nz>u#Y!TjjwV<8++8o$bDq&QTyZ|HZ#Cg!nNm7^`OLGwIc?T zRQJ|Yq{)Mm#V*2aBjtz(vOQAf^;T4z5|u>Z#a49nyK$FUWC;%?l6ijDGwS=EeQz<= zrm9--J;{s==`OucG%%x*ZT-Y+sDGGBnc_v8vXn-i@^|QJBMcco>^E>W;P-nsv`G+I zFdfz>Q%w|`bNN8Yf+x)zs_;e!B1{yOJW(TCF+rhkUphfJ@$4RZyv9EQEy+=0_uV>p z9}KG`%AkCrw2fUak=&P=fc1Y1<%z4Zfo;<`96Z88(nM%sqxx>Rtv-hWBy!oeq<%F~ zOC%svNnCO4lpPpBtCY@YDi2&Ferii*G3&YT;Hs3ZbZ~D}yl-ev*~a@tPia8XK)`Zx zW^{{hR;I!b?>4e5Re?BoQx9=6d7(y+ldAu!@IK4L;sW`uq zwNscE)>GiKl%$5t+lNm}+kT+FCdb2Ww$x+34^^r8yumV z>roP@WU3<8D6G)n;Kk&3b5e7Y-$qF1;TCZNgmzHq1@0CUZ*Y8pD0NXGd!vxu@AlI8xtZnrgnWhhZ5 zTDFta*4)w?&i@8*A8m|49VNW@VrHXSt^5_gl%gYKy7*V!!;27bhysXH>082Je#9jV zJ@=HC1v1AndyqYl!KJmTIWV;ve9}}IP_g%;zne+d$uc?fe_Dx8Y-41QL2p~0|A2ErBww&fQ3AeZ^T1nD}Z4=!mce zgNy#;t9=_*t3p4MqJufCku6m&on%$g$yn%d_N@~k;ten9>LI@RJMsj`yiQ=_cjItO z+ZLqk$LzNv24#4KYLm2$&9CXV%dbxlLYQyPiX<0U&NoT=Y8|v%^RWY0Btd^uz)qoW zF&ky#57t$hp09+pS%zo(sm|Zli0-sX6GZ!zbzB`fKW_MXkJy`>>hC}yE=n8f?1W#& z3SDLl`^v4X;Pjt;3+2k6Cj)V1IAMp;{|MFG;L5s|KN@&;x)k~{jk_b~?9hzp`YbOC{LS7Vs5Rv2R?m>`;w?%qde zzp`L7da=^QtO5WG_0P|r3`ieJeJ3Aiy<{nZg! z=NK9B*5H+O*Xvdan#wozFErRnh#*0YdOEZW&Y4DGUp}5cJm2Mb0q)-d){@L8HoSO@ z2Uv@vIPobmeesj%-xA^Hm%#pgI-|pAB4MsTK5xyF+CGdz&*bvoo*0M7@q1RtS_NhT zk^bZrb%EsnG7kL330TX3&W=?1`%_nlai5Rv9-5!JpnS(A#3pK%0T<82Y)2(j`2w10 znO?rDb|68<7ih03&(V4IU%^L9Hi@hJH}{=7m~_vWFx32CAXVuAR@eCZyE=qX9_~n)lDL?v>M;W1nYBXJczcSNV z3F~Hau#CQDYkAm+!I^S3r)y^_S%Qp33mDtvhx194XY;N5z%7I&g?yQ5!gDiY*O8A@ z6CS>6b1d3(5qCWd3{nEv+!1j;{i_g|xq3%e8ITR4K}I7sMst+5ZxbN=n2l3MJewk3 zD1AyNyBr!$Sx6lR>XMgNV#V-Fd`gMGDE|j;IEmUy1 z#^{jyzAo0^M#Dui#BVmKkzOgUHR=KkEN)5rEAl9FRNMy@_7ZU?F*R#WZvbXg&M%6D zXNHbjuikAnHe95e0vAm~%5@-P+^jP|X&pAQFuIVMR7|@Fo!moA<&RmIYH&yE3uXbdpqZI9vPB3eOyF|lRM%O>fKm> z*>ZzvZeQQnv&+;xB9-w)1PW4Bd{Mm}IJEJN6bT`-Rm{o$jh(26Z4(f~mPc`lmvO7&BOpcT35tZOTlP*ovz$L;hDACH@1>@A9))0+o#mPax3^ zL?gNz+4`_~lxpaMdbosmicZQb|{n(lcOgvtEYi**g_G!n z=}U-47^lVIh^3XXqtp0O$>mJmP=ip9e)Ly2!C;yXA8d%SQzp%sJx%X^k;alrr}TDw z<>4JL*2cgOr*?uMD(f5I(OMnz{gZ6ee$+8Du5&449OAVq3MY`BW9$G~4B;UapbmrB z_ZiME85r7u)at#4o@$}jaex) z~*)Y*U8 z*Bt4y&Mxeaiu?h~7E&CjGp8LBNwp+^C^_)ib@TfiCxNIqtQ~&E@uJzux48}o$ zg$R?7T|Gb*tCkw7R&ji;9I-zVRdbG?G1BF~rSOdE!_1I7KMCYrC4wsl@pP+Cem<2# z0}!8uM`GdzDy@bGjJ#&h!cl$b#*$inTnNLZyKCg*%>;dphY!p$LI+OFapHq!+#X}X zX`9?~7MMnt>|wkndTc|?D_D#$EZ!;tD1rbMjgD_z!-ZNS^;9g zo7xdxH(ba{RL&L9yHGL@I~xhQlDb3l*UEsguDC30mc78V{{1cS8F7qBM&4tPp#leW z$tcO*%=ensU<%OtPapcDeUdZdcgVQV0S~-l;&qZ#Migm=IOI-o(cle`ri!#pP!d=@ z`5SaqH79bAe0`br$Q?$d;^|@MtjfILco3PRVhQ6P#V+Rv?me~BLgz;Y2>ao2d*72qP37;UG)OlJ}~eeY*_rK-2{^ZH=H;=6_HeIx>wn z#Y_Rip}_JPRO4y7XC62Gk*%nu-m&9gOJ{Nurw!pnStxcnh^3L0C5}{GNRyo%7^R|% z&qfD&k;M(D8li3+Uj~J>$M*8EF{sZCSR3Gy6W0i*;U}0F+EIKN8|VbKhc z$+a;bE4r-vz08jNMTTa+`~iBaN2q6#*bTeSIT3FjhlOB1N9z? z^fHXdE#7dxYCHjKdX_01reoJ?5aHz|iWdgXBzQSLW}|-_vnEs**X(Skl+J}N%eV*# zrX}+jM>g8BFX}a=lj2RQx+^BI@r@AxGR(;flsJc-HIsa!Zyw7tXB1`p1W1{vibrU+ zB+B)`NI3`Hc0;G|iX9#8K1Go8!}me9$!3`2v2$p(%;{%SV>(7GDaZN$TBr}6AvWZ4 zN3AI^7;MAqw7yiZcl3?`*H_?Ze)sSNK1$D-8T_*3yQ?1AD3>RMpX#g%osO|8p>Ifo|4_^`qe_OELV z3IExR<)d_Zsfz)VRhDNi!envk=vcy^v`;ttpek-2afJQiP{5`p9GLhf`B z@%=J)H;}666wIdtv7^o5(?fkSNqiMcK&Jb5sRJ6}@>&1-Crf8^vE2#w~6|Ytaf_n`HXkbswj3vliS84d0q)oss z2eFfNC#8T6=+wg13wcrIg%x3S%CzzNCQDBNKoJ!C<_QeNibjwhV-je>-u+xEhTvcD zvJkRL=12l|T?lRdPAxhL@X-^Mf7Q;#nI=Y29@Wg>iHN&|w?TP03LN#5u+bIbG)QyR zp(gz@#98r{4FITzQnHhb&m0EoOmJ@ln)$U)(sq5X2}{%qNjX!aLm-q+ZY7BIlR#}| z^L!_k)C7!8LZGk`N;q$D413@t3()R~I$a8`7gkk}N>H5}dJfTGC9N;tsP4!N$=7*H zd}{fZOh`QaIIz4du$dAW4Ik+bVV&L@;Y8_Y$Aa|9aW1np!wW#P!Ft~l>BJZ-U@(AYuVIUx+m#MV*+;xq7+JTb>$B)87HeZ7ibX#63ZcUhTJ zB0QhcK$OqexC>%IOR3F!-{rVeV zd+aELPDM{jOieRsk%1G@^S@)J&2&TyD&L>iS1vvvd>?78*@QO{FAMKucA#i03jro> zhz~3q3o7MG*h9z6Gx z)f>8>ch+bKRty~=2g!`y2?OP4lSJzH!T3gqBVRm1!uTern0;~;16h(n*eR*0U`hDN z9M`>dze)MHiLlv9p+wYdM*ZAs32d*SvaB}F+_oy;3}0w$$-t1OY2i-uz{~%2L4*Es z(6=)QouA(azO|O4*aj3S=&tkcoy~->-eiFdzI#~8D}Bg?8Po2mnUL?`eXp{LQUUyg zvd$C-JW0@rL=->aQ%VQWjwW$%qbNI>CZ3#|8K*(y4t1i}*^S``@V#9rM`{ z@=ZBd3omRJvstHuAMkn)*eK>BWCkRkL~5qLBxL=GwDk_;MN^8SjxR=%BY$S?Hy)2= zTbuG}zsq}9ZHHIOLj|=(kNW8vW*zFbeP)ORs=V34?vP`KNBAe~A1j@Y9 zw;aNf@~)%ck${>FDsV5c2dtU3mo=`oImKvnTbLm7E96%_A=aM83z zkrg!o1-bax{ihv-&HB@$gy+?aL@Doz|GVdWJ1LCq+<|og(khqmIgw5qF*0N#l8vPR zkJ^G5m{DA(pZ{qG9t}W^gULRco8TvDVJ-p5`BPzU=Q)3bm}^u3R7Q5_@>X&7M(`DY z>8Vp9kLSSin}mS)sT~`D1q)!SBQ6V1iINAn&Xy{Q!Y>)`?CY?Wut-l$pNi5VG|N`R zK{jS!x`WM!f&#jtqbftf$D@F15d)QW!1W6Qx6BKzI7mMgiJMCUY(94Id4x7Jl(&swh(AaSA+LR~QI8WBYIxWi4hm6fsHa?`y8 za4f2gVcbf)@a5vZgiqouGV4N&BHsW`DmmFZ{9YpN31;ur&9+$%$p8iybB|^keS>vs zenC_1&-{2&F?d1uO`&jHf!RBT<39-kMP+eV38NH7<=gsk=nL9(?j(F3yETJK*Q&3D z!xmy?MDSd)g5kSD01(A9joJ8Wfuvs??b@g&46~?@qSN-}aTdQrQx`Ic*vb%>V1==b z1pjMtRLg4CZtNlb9?`JO7Z~00&No6){{yuP8;_*hoh4HacQI(Hto=d;ghd-n{=5l3 z1JzECD#bYWNEMaKv3b%Kp(8|AnF(T7g_I87j&>evPfI@wzHKe&I+3A5W)l-nb#_)3 zU4E+B{QK9Y{nOii{L{8!{Lj!d+lpsqL8A(Vx#BpwUN*i;$%1Ga_X-It)sY=CoJCDR z@`Ut?g@=bP!;^k8EaDkDrgn$O@6OSDVVy1*3Oxo>I!(9o?mN7~OCy7JI)X|w<9r>I z2}_`<2A`5&0pg7f90B`<{>d0^MSz@FAPl)W;sh$9{?w<+%A82pSanxP7xr}E1j%mP zo?oYZ{c#?A(#oW+?o~6(HLRN_OcIzvUfHg&Z_fT%?HiV1yF!E=9;RkReBu#`>@wpf z|0+iSn&89*$%^5q_e;qug(L6?~GdpmMu=UXpMdRjo4Wc8T*ne!hn z5n5}ZQSxi;-Eo;;l=xg`w^p~~Oy5}=n21j#j;~n9$fsTMyc>q&S|(0FGJ}B~lYGh_r`f^4wAju? z-J$XhXzj5dcaz@8y;_SNsTZZZ-ae%Q12C;T-WN{^SDs?jSASycL=R1~ukYme0s6=C zd8Zj=UvSHxdXOq)y??|piPYGfz6h3;b|EJLv@|h{{2Bn=)MuP(@$65E<-^&c4{;R> zSrz?8a((cn_5P31Z?&R-7yB`uwSz2&f5XCWR-TOPMWDpz_=g!x!rffb@g}%A9UTnT zthE_uSYp1UtzNANHTHN_Vjh-0_P?%M_1P1x?K*2N4Y+B3y(&%9+vexEbI5fqa_x;Z zF|sf?vW!Fc4!f^w7mR+hudFrd$TMm)wVjjmAxD_Ef$lOa2@q}^Xb*PHWQ-1cfr5R2 zMF>|QRhU;TD17R1($0t?+f`K~>B{=7EiT0*jhFzTCeR5z-A}#FKsKV&hL{;QbrnzS zl~C%hc(plBiJ_dQD|>QQ-IYZ{$C0qjqIQqJp|{QVYz<63SHoXL@!CHT&n&*@@&Bw- zb2y~*NQR#2@FpOnHnEeRbI?5%%y}{Pm!flPzpH|cGd-Y0;mKuf0Ex;`#=7`eHWzTL zVyL~Enqq_XtF#+0Q{Y0n@IhtW@}JT-=7*Kd=I51J=I6BUEbD`Fg?>dpSJPa?U(hYj z_j)z;WQT>xXEE8`=rE}+gvfh7+3Qm`6>-u@(xdFi2?cg8g>COJqW? zLR2qm?>{u8ggv`aKDiU!(i=z)@E@}t@W;>VYIuBiSF;gIduO6PQJV7b2dx(EiO0Z` zmzN8FR*s^67A)C^1c$g@>>SzMb3Jre(#ulO=#+md1ljw{Y5c>B>8Gt#stjFHXjCZs z=@+Z$?!AhGnTkv3X*%r2M)CXn?$^WH?w-T@v>}hHFuA+CcxH-<#J=ucnW9kntGF|& zz4u1ZG9j`hiK;&FVQK*x5fpnpX$g0FCE-89ZOVfAZnI9a;=H9Cq*8XF7s9^^-$ik;$F2}chtKl9d(jnWt8uNUOrJ|^*P%md4`9A>rM&7dk diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index b216f2d313cc673d8b8c4da591c174ebed52795c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11873 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 diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml deleted file mode 100644 index 3385b16..0000000 --- a/composeApp/src/androidMain/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - KotlinProject - \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml deleted file mode 100644 index c0bcfb2..0000000 --- a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt deleted file mode 100644 index 95fd55f..0000000 --- a/composeApp/src/commonMain/kotlin/App.kt +++ /dev/null @@ -1,9 +0,0 @@ -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.* - -@Composable -fun App(){ - MaterialTheme(){ - Navigation() - } -} \ No newline at end of file diff --git a/composeApp/src/logs/localisationError.log b/composeApp/src/logs/localisationError.log deleted file mode 100644 index 37fba40..0000000 --- a/composeApp/src/logs/localisationError.log +++ /dev/null @@ -1,41 +0,0 @@ -[Текст песни «+7(952)812»] - -[Интро] -Это второй -А -That's a Krishtall -Ау-у, YEEI, а - -[Припев] -52 (Алло) -Да здравствует Санкт-Петербург (А), и это город наш (YEEI) -Я каждый свой новый куплет валю как никогда (YEEI, а) -Альбом, он чисто мой, никому его не продам (Он мой) -Не думаю о том (YEEI), как хорошо было вчера (А-а; мне пох) - -[Куплет] -Меняю города (А) -Представляю район — у меня есть репертуар (YEEI, 2-3) -Никогда не просил, но всегда где-то доставал (Где?) -Чем больше денег (А), тем больше мне нравится Москва (А) -Но в Питере душа (YEEI), в Питере семья (YEEI) -В Питере братва (А, а), там знают наши имена (52) -+7(952)8-1-2 (Алло) -Это второй альбом (А), вторая глава (Второй) -Не думал, не гадал, всё, что я делал, — рэповал (Всегда) -Андеграунд — это не броуки в протёртых штанах (Пошёл на хуй) -Нужно прожить мою жизнь, чтоб так же, как я, слагать (Ага) -Нужно мой рэп услышать (YEEI), чтоб точно его понять - -[Припев] -52 (Алло) -Да здравствует Санкт-Петербург (А), и это город наш (YEEI) -Я каждый свой новый куплет валю как никогда (YEEI, а) -Альбом, он чисто мой, никому его не продам (Он мой) -Не думаю о том (YEEI), как хорошо было вчера (Ага) - -[Аутро] -Да здравствует 52 -Да здравствует Петербург, да здравствует 52 -Да здравствует Петербург, да здравствует 52 (Ау; YEEI, а) -Да здравствует 52 (Ау), YEEI, long live (Это второй) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f963a88..98aed13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,14 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=official - -#Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" - -#Android -android.nonTransitiveRClass=true -android.useAndroidX=true - -#MPP -kotlin.mpp.androidSourceSetLayoutVersion=2 -kotlin.mpp.enableCInteropCommonization=true - -kotlin.native.ignoreDisabledTargets=true +kotlin.version=1.9.22 +compose.version=1.6.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 7c10930..0000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,41 +0,0 @@ -[versions] -agp = "8.2.0" -android-compileSdk = "34" -android-minSdk = "24" -android-targetSdk = "34" -androidx-activityCompose = "1.9.0" -androidx-appcompat = "1.6.1" -androidx-constraintlayout = "2.1.4" -androidx-core-ktx = "1.13.0" -androidx-espresso-core = "3.5.1" -androidx-material = "1.11.0" -androidx-test-junit = "1.1.5" -compose-android = "1.6.6" -junit = "4.13.2" -kotlin = "2.0.0-RC2" -androidx-navigation = "2.7.0-alpha04" -androidx-lifecycle = "2.8.0-rc01" -compose-multiplatform = "1.6.10-rc01" - -[libraries] -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } -androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } -androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } -androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } -androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } -androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } -compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-android" } -compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose-android" } - -[plugins] -androidApplication = { id = "com.android.application", version.ref = "agp" } -androidLibrary = { id = "com.android.library", version.ref = "agp" } -kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } -composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 41205 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 F{tsc4gtPzv diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23..7e6084b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ +#Fri May 17 20:03:05 MSK 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip -networkTimeout=10000 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1aa94a4..1b6c787 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # 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 +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,11 +80,13 @@ do esac done -# This is normally unused -# shellcheck disable=SC2034 +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" 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 + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -131,29 +133,22 @@ 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. + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi 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 @@ -198,15 +193,11 @@ if "$cygwin" || "$msys" ; then 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. +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -214,12 +205,6 @@ set -- \ 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. diff --git a/gradlew.bat b/gradlew.bat index 6689b85..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%"=="" @echo off +@if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,8 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused +if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd +if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -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% +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts deleted file mode 100644 index 896fb94..0000000 --- a/lib/build.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidLibrary) - id("org.jetbrains.kotlinx.kover") version "0.7.6" -} - -kotlin { - jvm() - androidTarget { - publishLibraryVariants("release") - compilations.all { - kotlinOptions { - jvmTarget = "21" - } - } - } - - sourceSets { - val commonMain by getting { - dependencies { - //put your multiplatform dependencies here - } - } - val commonTest by getting { - dependencies { - implementation(libs.kotlin.test) - } - } - } -} - -android { - namespace = "org.jetbrains.kotlinx.multiplatform.library.template" - compileSdk = libs.versions.android.compileSdk.get().toInt() - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } -} - -koverReport { - filters { - // filters for all reports - } - - verify { - // verification rules for all reports - } - - defaults { - mergeWith("release") - xml { /* default XML report config */ } - html { /* default HTML report config */ } - verify { /* default verification config */ } - log { /* default logging config */ } - } - - androidReports("release") { - filters { - // override report filters for all reports for `release` build variant - // all filters specified by the level above cease to work - } - - xml { /* XML report config for `release` build variant */ } - html { /* HTML report config for `release` build variant */ } - verify { /* verification config for `release` build variant */ } - log { /* logging config for `release` build variant */ } - } -} diff --git a/lib/src/androidMain/kotlin/fibiprops.android.kt b/lib/src/androidMain/kotlin/fibiprops.android.kt deleted file mode 100644 index e69de29..0000000 diff --git a/lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt b/lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt deleted file mode 100644 index 7f79c37..0000000 --- a/lib/src/androidUnitTest/kotlin/AndroidFibiTest.kt +++ /dev/null @@ -1,11 +0,0 @@ -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class AndroidGraphTest { - - @Test - fun `graph test example`() { - assertTrue(true) - } -} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt b/lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt deleted file mode 100644 index 29f6ef3..0000000 --- a/lib/src/commonMain/kotlin/algos/FordBellman/FordBellman.kt +++ /dev/null @@ -1 +0,0 @@ -package algos.FordBellman diff --git a/lib/src/commonMain/kotlin/algos/cycleFind/LoadingGraph.kt b/lib/src/commonMain/kotlin/algos/cycleFind/LoadingGraph.kt deleted file mode 100644 index ace3f7e..0000000 --- a/lib/src/commonMain/kotlin/algos/cycleFind/LoadingGraph.kt +++ /dev/null @@ -1,5 +0,0 @@ -package algos.cycleFind - -class LoadingGraph { - //TODO("make it possible to use cycle search algorithms through this algorithm") -} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt b/lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt deleted file mode 100644 index 44e42ec..0000000 --- a/lib/src/commonMain/kotlin/algos/cycleFind/findCycle.kt +++ /dev/null @@ -1 +0,0 @@ -package algos.cycleFind \ No newline at end of file diff --git a/lib/src/commonTest/kotlin/GraphTest.kt b/lib/src/commonTest/kotlin/GraphTest.kt deleted file mode 100644 index 5db2ab4..0000000 --- a/lib/src/commonTest/kotlin/GraphTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -import kotlin.test.Test -import kotlin.test.assertTrue - -class GraphTest { - - @Test - fun `graph test example`() { - assertTrue(true) - } -} \ No newline at end of file diff --git a/lib/src/jvmMain/kotlin/graph.jvm.kt b/lib/src/jvmMain/kotlin/graph.jvm.kt deleted file mode 100644 index e69de29..0000000 diff --git a/lib/src/jvmTest/kotlin/JvmGraphTest.kt b/lib/src/jvmTest/kotlin/JvmGraphTest.kt deleted file mode 100644 index 9071a1e..0000000 --- a/lib/src/jvmTest/kotlin/JvmGraphTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -import kotlin.test.Test -import kotlin.test.assertTrue - -class JvmGraphTest { - - @Test - fun `graph test example`() { - assertTrue(true) - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1873b02..bce8038 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,32 +1,15 @@ -rootProject.name = "GraphVisualizer" -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - pluginManagement { repositories { - google { - mavenContent { - includeGroupAndSubgroups("androidx") - includeGroupAndSubgroups("com.android") - includeGroupAndSubgroups("com.google") - } - } - mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() gradlePluginPortal() + mavenCentral() } -} -dependencyResolutionManagement { - repositories { - google { - mavenContent { - includeGroupAndSubgroups("androidx") - includeGroupAndSubgroups("com.android") - includeGroupAndSubgroups("com.google") - } - } - mavenCentral() + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) } } -include(":composeApp") -include(":lib") \ No newline at end of file +rootProject.name = "GraphVisualizer" diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/src/main/kotlin/Main.kt similarity index 55% rename from composeApp/src/desktopMain/kotlin/main.kt rename to src/main/kotlin/Main.kt index 6e54362..97baea0 100644 --- a/composeApp/src/desktopMain/kotlin/main.kt +++ b/src/main/kotlin/Main.kt @@ -1,11 +1,11 @@ -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Surface -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.* +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application import java.awt.Dimension fun main() = application { @@ -20,6 +20,13 @@ fun main() = application { title = "Graph Visualizer", ) { window.minimumSize = Dimension(100,100) - App() + App() } -} \ No newline at end of file +} + +@Composable +fun App(){ + MaterialTheme(){ + Navigation() + } +} diff --git a/composeApp/src/commonMain/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/Navigation.kt rename to src/main/kotlin/Navigation.kt diff --git a/composeApp/src/commonMain/kotlin/Localisation.kt b/src/main/kotlin/localisation/Localisation.kt similarity index 68% rename from composeApp/src/commonMain/kotlin/Localisation.kt rename to src/main/kotlin/localisation/Localisation.kt index 7e507af..5208dd9 100644 --- a/composeApp/src/commonMain/kotlin/Localisation.kt +++ b/src/main/kotlin/localisation/Localisation.kt @@ -1,10 +1,12 @@ +package localisation + import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import view.screens.SettingsJSON import java.io.File import java.time.LocalDateTime -@Serializable +@kotlinx.serialization.Serializable class TranslationPair(val code: String, val localisation: String) @Serializable @@ -22,18 +24,18 @@ fun localisation(text: String): String{ return text } catch (ex: Exception){ - File("src/logs/localisationError.log").appendText("LOCALISATION ERROR ${LocalDateTime.now()} -- Key $text is not found in $language\nEXCEPTION IS $ex\n") + File("src/main/kotlin/localisation/logs/localisationError.log").appendText("LOCALISATION ERROR ${LocalDateTime.now()} -- Key $text is not found in $language\nEXCEPTION IS $ex\n") return text } } fun getLocalisation(): String{ try { - val language = Json.decodeFromString(File("src/settings.json").readText()).language + val language = Json.decodeFromString(File("src/main/kotlin/settings.json").readText()).language return language } catch(ex: Exception){ - File("src/logs/localisationError.log").appendText("FILE OF LOC NOT FOUND ${LocalDateTime.now()} -- EXCEPTION IS $ex\n") + File("src/main/kotlin/localisation/logs/localisationError.log").appendText("FILE OF LOC NOT FOUND ${LocalDateTime.now()} -- EXCEPTION IS $ex\n") return "en-US" } } \ No newline at end of file diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt new file mode 100644 index 0000000..a20dca7 --- /dev/null +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -0,0 +1,4 @@ +package model.algos + +class FordBellman { +} \ No newline at end of file diff --git a/lib/src/commonMain/kotlin/graph/Digraph.kt b/src/main/kotlin/model/graph/Digraph.kt similarity index 100% rename from lib/src/commonMain/kotlin/graph/Digraph.kt rename to src/main/kotlin/model/graph/Digraph.kt diff --git a/lib/src/commonMain/kotlin/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt similarity index 100% rename from lib/src/commonMain/kotlin/graph/Graph.kt rename to src/main/kotlin/model/graph/Graph.kt diff --git a/lib/src/commonMain/kotlin/graph/GraphAbstract.kt b/src/main/kotlin/model/graph/GraphAbstract.kt similarity index 100% rename from lib/src/commonMain/kotlin/graph/GraphAbstract.kt rename to src/main/kotlin/model/graph/GraphAbstract.kt diff --git a/lib/src/commonMain/kotlin/graph/WeightedDigraph.kt b/src/main/kotlin/model/graph/WeightedDigraph.kt similarity index 100% rename from lib/src/commonMain/kotlin/graph/WeightedDigraph.kt rename to src/main/kotlin/model/graph/WeightedDigraph.kt diff --git a/lib/src/commonMain/kotlin/graph/WeightedGraph.kt b/src/main/kotlin/model/graph/WeightedGraph.kt similarity index 100% rename from lib/src/commonMain/kotlin/graph/WeightedGraph.kt rename to src/main/kotlin/model/graph/WeightedGraph.kt diff --git a/composeApp/src/settings.json b/src/main/kotlin/settings.json similarity index 100% rename from composeApp/src/settings.json rename to src/main/kotlin/settings.json diff --git a/composeApp/src/commonMain/kotlin/view/BounceClick.kt b/src/main/kotlin/view/BounceClick.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/view/BounceClick.kt rename to src/main/kotlin/view/BounceClick.kt diff --git a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt b/src/main/kotlin/view/screens/GraphScreen.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt rename to src/main/kotlin/view/screens/GraphScreen.kt index e3fffe1..7dc0c9a 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/GraphScreen.kt +++ b/src/main/kotlin/view/screens/GraphScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController -import localisation +import localisation.localisation import view.DefaultColors import view.defaultStyle import view.views.GraphView diff --git a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt rename to src/main/kotlin/view/screens/MainScreen.kt index 0b4bd89..ca93ccd 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import localisation +import localisation.localisation import view.DefaultColors import view.bigStyle import view.bounceClick diff --git a/composeApp/src/commonMain/kotlin/view/screens/SealedScreens.kt b/src/main/kotlin/view/screens/SealedScreens.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/view/screens/SealedScreens.kt rename to src/main/kotlin/view/screens/SealedScreens.kt diff --git a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt b/src/main/kotlin/view/screens/SettingsScreen.kt similarity index 90% rename from composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt rename to src/main/kotlin/view/screens/SettingsScreen.kt index f45d902..cd874fd 100644 --- a/composeApp/src/commonMain/kotlin/view/screens/SettingsScreen.kt +++ b/src/main/kotlin/view/screens/SettingsScreen.kt @@ -12,16 +12,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import getLocalisation import kotlinx.serialization.Serializable +import localisation.getLocalisation import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import localisation +import localisation.localisation import view.DefaultColors import view.bounceClick import view.defaultStyle import java.io.File +val pathToSettings = "src/main/kotlin/settings.json" @Serializable class SettingsJSON(var language: String) @@ -30,16 +31,16 @@ enum class SettingType{ } fun resetSettings(){ - File("src/settings.json").writeText(Json.encodeToString(SettingsJSON("en-US"))) + File(pathToSettings).writeText(Json.encodeToString(SettingsJSON("en-US"))) } fun makeSetting(name: SettingType, value: String){ try{ - val data = Json.decodeFromString(File("src/settings.json").readText()) + val data = Json.decodeFromString(File(pathToSettings).readText()) when (name){ SettingType.LANGUAGE -> data.language = value; } - File("src/settings.json").writeText(Json.encodeToString(data)) + File(pathToSettings).writeText(Json.encodeToString(data)) } catch(exception: Exception){ resetSettings() diff --git a/composeApp/src/commonMain/kotlin/view/styling.kt b/src/main/kotlin/view/styling.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/view/styling.kt rename to src/main/kotlin/view/styling.kt diff --git a/composeApp/src/commonMain/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/view/views/GraphView.kt rename to src/main/kotlin/view/views/GraphView.kt diff --git a/composeApp/src/commonMain/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/view/views/VertexView.kt rename to src/main/kotlin/view/views/VertexView.kt diff --git a/composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/viewmodel/GraphViewModel.kt rename to src/main/kotlin/viewmodel/GraphViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/viewmodel/MainScreenViewModel.kt rename to src/main/kotlin/viewmodel/MainScreenViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/viewmodel/VertexViewModel.kt rename to src/main/kotlin/viewmodel/VertexViewModel.kt diff --git a/composeApp/src/commonMain/resources/localisation/cn-CN.json b/src/main/resources/localisation/cn-CN.json similarity index 100% rename from composeApp/src/commonMain/resources/localisation/cn-CN.json rename to src/main/resources/localisation/cn-CN.json diff --git a/composeApp/src/commonMain/resources/localisation/en-US.json b/src/main/resources/localisation/en-US.json similarity index 100% rename from composeApp/src/commonMain/resources/localisation/en-US.json rename to src/main/resources/localisation/en-US.json diff --git a/composeApp/src/commonMain/resources/localisation/ru-RU.json b/src/main/resources/localisation/ru-RU.json similarity index 100% rename from composeApp/src/commonMain/resources/localisation/ru-RU.json rename to src/main/resources/localisation/ru-RU.json From c568ee45783661d0f4c7ec09991fba521ddb13cb Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 17 May 2024 23:22:17 +0300 Subject: [PATCH 059/172] Feat: change graph interface --- .../graph/{Digraph.kt => DirectedGraph.kt} | 2 +- src/main/kotlin/model/graph/GraphAbstract.kt | 23 +++++-------------- .../model/graph/{Graph.kt => Undirected.kt} | 2 +- src/main/kotlin/view/screens/GraphScreen.kt | 2 +- src/main/kotlin/viewmodel/GraphViewModel.kt | 14 +++++++---- 5 files changed, 18 insertions(+), 25 deletions(-) rename src/main/kotlin/model/graph/{Digraph.kt => DirectedGraph.kt} (78%) rename src/main/kotlin/model/graph/{Graph.kt => Undirected.kt} (83%) diff --git a/src/main/kotlin/model/graph/Digraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt similarity index 78% rename from src/main/kotlin/model/graph/Digraph.kt rename to src/main/kotlin/model/graph/DirectedGraph.kt index d953b24..1077406 100644 --- a/src/main/kotlin/model/graph/Digraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,6 +1,6 @@ package graph -class Digraph: GraphAbstract(){ +class DirectedGraph: GraphAbstract(){ override fun addEdge(from: Int, to: Int) { graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} } diff --git a/src/main/kotlin/model/graph/GraphAbstract.kt b/src/main/kotlin/model/graph/GraphAbstract.kt index aa9931e..ad66365 100644 --- a/src/main/kotlin/model/graph/GraphAbstract.kt +++ b/src/main/kotlin/model/graph/GraphAbstract.kt @@ -2,32 +2,21 @@ package graph abstract class GraphAbstract() { protected val graph = mutableMapOf>() + val vertices + get() = graph.keys + val entries + get() = graph.entries var size = graph.size private set - // temporary - init { - graph[2] = mutableListOf(1, 3, 4, 5, 6, 0) - graph[4] = mutableListOf(4, 5) - graph[5] = mutableListOf(4, 2) - graph[6] = mutableListOf(1, 2, 4, 5) - graph[1] = mutableListOf(2, 4, 5, 6) - graph[3] = mutableListOf(1, 2, 4, 5, 6) - graph[0] = mutableListOf(1, 2, 3, 4, 5, 6) - size = 7 - } - fun addVertex(number: Int) { graph.putIfAbsent(number, mutableListOf()) + size++ } abstract fun addEdge(from: Int, to: Int) - fun getGraphProp(): MutableMap> { - return graph - } - - fun edgesFrom(from: Int): MutableList { + fun edgesOf(from: Int): MutableList { return graph[from] ?: mutableListOf() } diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Undirected.kt similarity index 83% rename from src/main/kotlin/model/graph/Graph.kt rename to src/main/kotlin/model/graph/Undirected.kt index 114f280..ac03070 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Undirected.kt @@ -1,6 +1,6 @@ package graph -class Graph: GraphAbstract(){ +class UndirectedGraph: GraphAbstract(){ override fun addEdge(from: Int, to: Int) { graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} graph[to]?.add(from) ?: {graph[to] = mutableListOf(from)} diff --git a/src/main/kotlin/view/screens/GraphScreen.kt b/src/main/kotlin/view/screens/GraphScreen.kt index 7dc0c9a..b79183e 100644 --- a/src/main/kotlin/view/screens/GraphScreen.kt +++ b/src/main/kotlin/view/screens/GraphScreen.kt @@ -44,7 +44,7 @@ fun GraphScreen(navController: NavController, mainScreenViewModel: MainScreenVie // Add vertex Button( - onClick = { graphModel.addVertex() }, + onClick = { graphModel.addVertex(graphModel.size) }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) diff --git a/src/main/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt index e914310..d58ae27 100644 --- a/src/main/kotlin/viewmodel/GraphViewModel.kt +++ b/src/main/kotlin/viewmodel/GraphViewModel.kt @@ -2,12 +2,15 @@ package viewmodel import androidx.compose.runtime.* import androidx.lifecycle.ViewModel -import graph.Graph +import graph.GraphAbstract +import graph.UndirectedGraph -class GraphViewModel(name: String, graph: Graph = Graph()) : ViewModel() { +class GraphViewModel(name: String, graph: GraphAbstract = UndirectedGraph()) : ViewModel() { val name by mutableStateOf(name) + val size + get() = graphModel.size val graphView = mutableStateMapOf() - val graphModel = graph.getGraphProp() + val graphModel = graph init { for (vertex in graphModel.entries) { @@ -15,8 +18,9 @@ class GraphViewModel(name: String, graph: Graph = Graph()) : ViewModel() { } } - fun addVertex() { - graphView[graphView.size] = VertexViewModel(graphView.size) + fun addVertex(number: Int) { + graphView.putIfAbsent(number,VertexViewModel(number)) + graphModel.addVertex(number) } fun addEdge(source: Int, destination: Int) { From 6d32ce6518992b882727b98c65961ed981127983 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 17 May 2024 23:42:54 +0300 Subject: [PATCH 060/172] Fix: add restore log file --- .gitignore | 1 - .../localisation/logs/localisationError.log | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/localisation/logs/localisationError.log diff --git a/.gitignore b/.gitignore index 61765ad..9a47b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ -**/localisationError.log ### IntelliJ IDEA ### .idea/ diff --git a/src/main/kotlin/localisation/logs/localisationError.log b/src/main/kotlin/localisation/logs/localisationError.log new file mode 100644 index 0000000..8aa04b4 --- /dev/null +++ b/src/main/kotlin/localisation/logs/localisationError.log @@ -0,0 +1,41 @@ +[Текст песни «+7(952)812»] + +[Интро] +Это второй +А +That's a Krishtall +Ау-у, YEEI, а + +[Припев] +52 (Алло) +Да здравствует Санкт-Петербург (А), и это город наш (YEEI) +Я каждый свой новый куплет валю как никогда (YEEI, а) +Альбом, он чисто мой, никому его не продам (Он мой) +Не думаю о том (YEEI), как хорошо было вчера (А-а; мне пох) + +[Куплет] +Меняю города (А) +Представляю район — у меня есть репертуар (YEEI, 2-3) +Никогда не просил, но всегда где-то доставал (Где?) +Чем больше денег (А), тем больше мне нравится Москва (А) +Но в Питере душа (YEEI), в Питере семья (YEEI) +В Питере братва (А, а), там знают наши имена (52) ++7(952)8-1-2 (Алло) +Это второй альбом (А), вторая глава (Второй) +Не думал, не гадал, всё, что я делал, — рэповал (Всегда) +Андеграунд — это не броуки в протёртых штанах (Пошёл на хуй) +Нужно прожить мою жизнь, чтоб так же, как я, слагать (Ага) +Нужно мой рэп услышать (YEEI), чтоб точно его понять + +[Припев] +52 (Алло) +Да здравствует Санкт-Петербург (А), и это город наш (YEEI) +Я каждый свой новый куплет валю как никогда (YEEI, а) +Альбом, он чисто мой, никому его не продам (Он мой) +Не думаю о том (YEEI), как хорошо было вчера (Ага) + +[Аутро] +Да здравствует 52 +Да здравствует Петербург, да здравствует 52 +Да здравствует Петербург, да здравствует 52 (Ау; YEEI, а) +Да здравствует 52 (Ау), YEEI, long live (Это второй) From 041e6422c3d943e76763acfb83fe4d84d711b418 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> Date: Sat, 18 May 2024 07:22:27 +0300 Subject: [PATCH 061/172] Create README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee1b58f --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +You can run it by +``` +./gradlew +``` From e0c80daf3b35bd7668f801530806302f69500875 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> Date: Sat, 18 May 2024 13:56:31 +0300 Subject: [PATCH 062/172] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ee1b58f..9519bca 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ You can run it by ``` -./gradlew +./gradlew run ``` From c0570b42e4e9acc245a7d127476966bf44e99c71 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Sat, 18 May 2024 13:57:14 +0300 Subject: [PATCH 063/172] Create LICENSE --- LICENSE | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to From 0394451f03e3a3e291499e3c049b2225dcf5f3b6 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Sat, 18 May 2024 16:30:30 -0400 Subject: [PATCH 064/172] fix: localistion source bug fix --- src/main/kotlin/localisation/Localisation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/localisation/Localisation.kt b/src/main/kotlin/localisation/Localisation.kt index 5208dd9..897bf21 100644 --- a/src/main/kotlin/localisation/Localisation.kt +++ b/src/main/kotlin/localisation/Localisation.kt @@ -15,7 +15,7 @@ class TranslationList(val transList: List) fun localisation(text: String): String{ val language = getLocalisation() try { - val data = Json.decodeFromString(object {}.javaClass.getResourceAsStream("localisation/$language.json")?.bufferedReader()!!.readText()) + val data = Json.decodeFromString(File("src/main/resources/localisation/$language.json").readText()) for (wordPair in data.transList) { if (wordPair.code == text){ return wordPair.localisation From 41de20f79ee3375654f1eaad3b4baea5934a29a2 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Sat, 18 May 2024 17:03:46 -0400 Subject: [PATCH 065/172] upd: chinese localization --- src/main/resources/localisation/cn-CN.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/resources/localisation/cn-CN.json b/src/main/resources/localisation/cn-CN.json index 71b46c5..44439e6 100644 --- a/src/main/resources/localisation/cn-CN.json +++ b/src/main/resources/localisation/cn-CN.json @@ -22,23 +22,23 @@ }, { "code": "open_edge", - "localisation":"TODO" + "localisation":"开放边缘" }, { "code": "add_edge", - "localisation": "TODO" + "localisation": "添加边缘" }, { "code": "add", - "localisation": "TODO" + "localisation": "添加" }, { "code": "write_name", - "localisation": "TODO" + "localisation": "写名字" }, { "code": "enter_new_graph_name", - "localisation": "TODO" + "localisation": "创建新图表" } ] } \ No newline at end of file From b725e9807164157f48a465e69b3c5e4441d47b49 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 19 May 2024 09:11:58 +0300 Subject: [PATCH 066/172] Feat: implemented graph model --- src/main/kotlin/model/algos/FordBellman.kt | 5 ++- src/main/kotlin/model/graph/DirectedGraph.kt | 11 ++++--- src/main/kotlin/model/graph/Graph.kt | 32 +++++++++++++++++++ src/main/kotlin/model/graph/GraphAbstract.kt | 28 ---------------- src/main/kotlin/model/graph/Undirected.kt | 8 ----- .../kotlin/model/graph/UndirectedGraph.kt | 12 +++++++ .../kotlin/model/graph/WeightedDigraph.kt | 4 --- .../model/graph/WeightedDirectedGraph.kt | 7 ++++ src/main/kotlin/model/graph/WeightedGraph.kt | 7 ++-- .../model/graph/WeightedUndirectedGraph.kt | 4 +++ src/main/kotlin/model/graph/edges/Edge.kt | 5 +++ .../kotlin/model/graph/edges/WeightedEdge.kt | 6 ++++ src/main/kotlin/viewmodel/GraphViewModel.kt | 26 ++++++++------- .../kotlin/viewmodel/MainScreenViewModel.kt | 9 +++--- src/main/kotlin/viewmodel/VertexViewModel.kt | 6 ++-- 15 files changed, 105 insertions(+), 65 deletions(-) create mode 100644 src/main/kotlin/model/graph/Graph.kt delete mode 100644 src/main/kotlin/model/graph/GraphAbstract.kt delete mode 100644 src/main/kotlin/model/graph/Undirected.kt create mode 100644 src/main/kotlin/model/graph/UndirectedGraph.kt delete mode 100644 src/main/kotlin/model/graph/WeightedDigraph.kt create mode 100644 src/main/kotlin/model/graph/WeightedDirectedGraph.kt create mode 100644 src/main/kotlin/model/graph/WeightedUndirectedGraph.kt create mode 100644 src/main/kotlin/model/graph/edges/Edge.kt create mode 100644 src/main/kotlin/model/graph/edges/WeightedEdge.kt diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index a20dca7..2f30e64 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -1,4 +1,7 @@ package model.algos -class FordBellman { +import graph.WeightedGraph + +fun FordBellman(graph: WeightedGraph) { + } \ No newline at end of file diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index 1077406..191da41 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,7 +1,10 @@ package graph -class DirectedGraph: GraphAbstract(){ - override fun addEdge(from: Int, to: Int) { - graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} +import model.graph.Edge + +class DirectedGraph : Graph>() { + override fun addEdge(from: V, to: V) { + val edge = Edge(from, to) + graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt new file mode 100644 index 0000000..20ca13e --- /dev/null +++ b/src/main/kotlin/model/graph/Graph.kt @@ -0,0 +1,32 @@ +package graph + +import model.graph.Edge + +abstract class Graph>() { + protected val graph = mutableMapOf>() + val entries + get() = graph.entries + + val vertices + get() = graph.keys + + var size = graph.size + private set + + fun addVertex(vertex: V) { + graph.putIfAbsent(vertex, mutableListOf()) + size++ + } + + abstract fun addEdge(from: V, to: V) + + fun edgesOf(from: V): MutableList { + return graph[from] ?: mutableListOf() + } + + fun forEach(action: (MutableList) -> Unit) { + graph.forEach { number, list -> action(list) } + } + + operator fun iterator() = graph.entries.iterator() +} diff --git a/src/main/kotlin/model/graph/GraphAbstract.kt b/src/main/kotlin/model/graph/GraphAbstract.kt deleted file mode 100644 index ad66365..0000000 --- a/src/main/kotlin/model/graph/GraphAbstract.kt +++ /dev/null @@ -1,28 +0,0 @@ -package graph - -abstract class GraphAbstract() { - protected val graph = mutableMapOf>() - val vertices - get() = graph.keys - val entries - get() = graph.entries - var size = graph.size - private set - - fun addVertex(number: Int) { - graph.putIfAbsent(number, mutableListOf()) - size++ - } - - abstract fun addEdge(from: Int, to: Int) - - fun edgesOf(from: Int): MutableList { - return graph[from] ?: mutableListOf() - } - - fun forEach(action: (MutableList) -> Unit) { - graph.forEach { number, list -> action(list) } - } - - operator fun iterator() = graph.entries.iterator() -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/Undirected.kt b/src/main/kotlin/model/graph/Undirected.kt deleted file mode 100644 index ac03070..0000000 --- a/src/main/kotlin/model/graph/Undirected.kt +++ /dev/null @@ -1,8 +0,0 @@ -package graph - -class UndirectedGraph: GraphAbstract(){ - override fun addEdge(from: Int, to: Int) { - graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} - graph[to]?.add(from) ?: {graph[to] = mutableListOf(from)} - } -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt new file mode 100644 index 0000000..ffb3f89 --- /dev/null +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -0,0 +1,12 @@ +package graph + +import model.graph.Edge + +class UndirectedGraph : Graph>() { + override fun addEdge(from: V, to: V) { + val edge1 = Edge(from, to) + val edge2 = Edge(to, from) + graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } + graph[to]?.add(edge2) ?: { graph[to] = mutableListOf(edge2) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/WeightedDigraph.kt b/src/main/kotlin/model/graph/WeightedDigraph.kt deleted file mode 100644 index 81e44f3..0000000 --- a/src/main/kotlin/model/graph/WeightedDigraph.kt +++ /dev/null @@ -1,4 +0,0 @@ -package graph - -class WeightedDigraph { -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/src/main/kotlin/model/graph/WeightedDirectedGraph.kt new file mode 100644 index 0000000..05143af --- /dev/null +++ b/src/main/kotlin/model/graph/WeightedDirectedGraph.kt @@ -0,0 +1,7 @@ +package model.graph + +import graph.WeightedGraph + +//class WeightedDirectedGraph : WeightedGraph() { +// +//} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/WeightedGraph.kt b/src/main/kotlin/model/graph/WeightedGraph.kt index c79c622..46bbd88 100644 --- a/src/main/kotlin/model/graph/WeightedGraph.kt +++ b/src/main/kotlin/model/graph/WeightedGraph.kt @@ -1,6 +1,7 @@ package graph -import graph.GraphAbstract +import model.graph.WeightedEdge -//class WeightedGraph: GraphAbstract { -//} \ No newline at end of file +abstract class WeightedGraph : Graph>() { + +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt new file mode 100644 index 0000000..16af5af --- /dev/null +++ b/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt @@ -0,0 +1,4 @@ +package model.graph + +class WeightedUndirectedGraph { +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/edges/Edge.kt new file mode 100644 index 0000000..d41ccd7 --- /dev/null +++ b/src/main/kotlin/model/graph/edges/Edge.kt @@ -0,0 +1,5 @@ +package model.graph + +open class Edge(val from: V, val to: V) { + open val weight = 1 +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/edges/WeightedEdge.kt b/src/main/kotlin/model/graph/edges/WeightedEdge.kt new file mode 100644 index 0000000..9dc17ab --- /dev/null +++ b/src/main/kotlin/model/graph/edges/WeightedEdge.kt @@ -0,0 +1,6 @@ +package model.graph + +class WeightedEdge(source: V, destination: V, _weight: Int) : + Edge(source, destination) { + override val weight: Int = _weight +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt index d58ae27..585d856 100644 --- a/src/main/kotlin/viewmodel/GraphViewModel.kt +++ b/src/main/kotlin/viewmodel/GraphViewModel.kt @@ -2,14 +2,18 @@ package viewmodel import androidx.compose.runtime.* import androidx.lifecycle.ViewModel -import graph.GraphAbstract import graph.UndirectedGraph +import model.graph.Edge -class GraphViewModel(name: String, graph: GraphAbstract = UndirectedGraph()) : ViewModel() { +class GraphViewModel>( + name: String, + graph: UndirectedGraph = UndirectedGraph() +) : + ViewModel() { val name by mutableStateOf(name) val size get() = graphModel.size - val graphView = mutableStateMapOf() + val graphView = mutableStateMapOf>() val graphModel = graph init { @@ -18,17 +22,17 @@ class GraphViewModel(name: String, graph: GraphAbstract = UndirectedGraph()) : V } } - fun addVertex(number: Int) { - graphView.putIfAbsent(number,VertexViewModel(number)) - graphModel.addVertex(number) + fun addVertex(vertex: V) { + graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphModel.addVertex(vertex) } - fun addEdge(source: Int, destination: Int) { - if (graphView[source] == null) { + fun addEdge(from: V, to: V) { + if (graphView[from] == null) { return } - val edgesCopy = graphView[source]?.edges?.toMutableList()!! - edgesCopy.add(destination) - graphView[source]?.edges = edgesCopy + val edgesCopy = graphView[from]?.edges?.toMutableList()!! + edgesCopy.add(Edge(from, to)) + graphView[from]?.edges = edgesCopy } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index d5a509b..66490f9 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -2,15 +2,16 @@ package viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel +import model.graph.Edge -class MainScreenViewModel: ViewModel() { - val graphs = mutableStateListOf() +class MainScreenViewModel : ViewModel() { + val graphs = mutableStateListOf() - fun addGraph(name : String) { + fun addGraph(name: String) { graphs.add(GraphViewModel(name)) } - fun getGraph(graphId: Int): GraphViewModel { + fun getGraph(graphId: Int): GraphViewModel> { return graphs[graphId] } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 36f11a6..9f014a0 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -5,9 +5,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel +import model.graph.Edge -class VertexViewModel(number: Int, _edges: MutableList = mutableListOf()): ViewModel() { - val number : Int = number +class VertexViewModel>(_vertex: V, _edges: MutableList> = mutableListOf()) : + ViewModel() { + val vertex: V = _vertex var edges by mutableStateOf(_edges) var offsetX by mutableStateOf(1000f) var offsetY by mutableStateOf(540f) From 0b0f411d54f6b662cfca57fef571f8ac72d3cb3d Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 19 May 2024 10:14:01 +0300 Subject: [PATCH 067/172] Feat: change default graph type in mainscreen to Int --- src/main/kotlin/model/algos/FordBellman.kt | 4 +-- src/main/kotlin/model/graph/Graph.kt | 4 +-- .../model/graph/WeightedDirectedGraph.kt | 7 ----- src/main/kotlin/model/graph/WeightedGraph.kt | 7 ----- .../model/graph/WeightedUndirectedGraph.kt | 4 --- src/main/kotlin/model/graph/edges/Edge.kt | 6 ++-- .../kotlin/model/graph/edges/WeightedEdge.kt | 2 +- .../graph/{ => unweighted}/DirectedGraph.kt | 7 +++-- .../graph/{ => unweighted}/UndirectedGraph.kt | 7 +++-- .../model/graph/unweighted/UnweightedGraph.kt | 8 +++++ .../graph/weighted/WeightedDirectedGraph.kt | 12 ++++++++ .../model/graph/weighted/WeightedGraph.kt | 8 +++++ .../graph/weighted/WeightedUndirectedGraph.kt | 10 +++++++ src/main/kotlin/view/screens/GraphScreen.kt | 17 +++++++---- src/main/kotlin/view/views/GraphView.kt | 5 ++-- src/main/kotlin/view/views/VertexView.kt | 29 ++++++++++++------- src/main/kotlin/viewmodel/GraphViewModel.kt | 15 +++++----- .../kotlin/viewmodel/MainScreenViewModel.kt | 9 +++--- src/main/kotlin/viewmodel/VertexViewModel.kt | 5 ++-- 19 files changed, 99 insertions(+), 67 deletions(-) delete mode 100644 src/main/kotlin/model/graph/WeightedDirectedGraph.kt delete mode 100644 src/main/kotlin/model/graph/WeightedGraph.kt delete mode 100644 src/main/kotlin/model/graph/WeightedUndirectedGraph.kt rename src/main/kotlin/model/graph/{ => unweighted}/DirectedGraph.kt (55%) rename src/main/kotlin/model/graph/{ => unweighted}/UndirectedGraph.kt (67%) create mode 100644 src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt create mode 100644 src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt create mode 100644 src/main/kotlin/model/graph/weighted/WeightedGraph.kt create mode 100644 src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index 2f30e64..771592e 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -1,7 +1,7 @@ package model.algos -import graph.WeightedGraph +import model.graph.weighted.WeightedGraph fun FordBellman(graph: WeightedGraph) { - + } \ No newline at end of file diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 20ca13e..e42d030 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -1,6 +1,6 @@ package graph -import model.graph.Edge +import model.graph.edges.Edge abstract class Graph>() { protected val graph = mutableMapOf>() @@ -18,8 +18,6 @@ abstract class Graph>() { size++ } - abstract fun addEdge(from: V, to: V) - fun edgesOf(from: V): MutableList { return graph[from] ?: mutableListOf() } diff --git a/src/main/kotlin/model/graph/WeightedDirectedGraph.kt b/src/main/kotlin/model/graph/WeightedDirectedGraph.kt deleted file mode 100644 index 05143af..0000000 --- a/src/main/kotlin/model/graph/WeightedDirectedGraph.kt +++ /dev/null @@ -1,7 +0,0 @@ -package model.graph - -import graph.WeightedGraph - -//class WeightedDirectedGraph : WeightedGraph() { -// -//} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/WeightedGraph.kt b/src/main/kotlin/model/graph/WeightedGraph.kt deleted file mode 100644 index 46bbd88..0000000 --- a/src/main/kotlin/model/graph/WeightedGraph.kt +++ /dev/null @@ -1,7 +0,0 @@ -package graph - -import model.graph.WeightedEdge - -abstract class WeightedGraph : Graph>() { - -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt b/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt deleted file mode 100644 index 16af5af..0000000 --- a/src/main/kotlin/model/graph/WeightedUndirectedGraph.kt +++ /dev/null @@ -1,4 +0,0 @@ -package model.graph - -class WeightedUndirectedGraph { -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/edges/Edge.kt index d41ccd7..74d4e43 100644 --- a/src/main/kotlin/model/graph/edges/Edge.kt +++ b/src/main/kotlin/model/graph/edges/Edge.kt @@ -1,5 +1,5 @@ -package model.graph +package model.graph.edges -open class Edge(val from: V, val to: V) { +open class Edge(val from: V, val to: V) { open val weight = 1 -} \ No newline at end of file +} diff --git a/src/main/kotlin/model/graph/edges/WeightedEdge.kt b/src/main/kotlin/model/graph/edges/WeightedEdge.kt index 9dc17ab..872bcdd 100644 --- a/src/main/kotlin/model/graph/edges/WeightedEdge.kt +++ b/src/main/kotlin/model/graph/edges/WeightedEdge.kt @@ -1,4 +1,4 @@ -package model.graph +package model.graph.edges class WeightedEdge(source: V, destination: V, _weight: Int) : Edge(source, destination) { diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/unweighted/DirectedGraph.kt similarity index 55% rename from src/main/kotlin/model/graph/DirectedGraph.kt rename to src/main/kotlin/model/graph/unweighted/DirectedGraph.kt index 191da41..d74e05b 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/unweighted/DirectedGraph.kt @@ -1,8 +1,9 @@ -package graph +package model.graph.unweighted -import model.graph.Edge +import graph.Graph +import model.graph.edges.Edge -class DirectedGraph : Graph>() { +class DirectedGraph : UnweightedGraph() { override fun addEdge(from: V, to: V) { val edge = Edge(from, to) graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/unweighted/UndirectedGraph.kt similarity index 67% rename from src/main/kotlin/model/graph/UndirectedGraph.kt rename to src/main/kotlin/model/graph/unweighted/UndirectedGraph.kt index ffb3f89..c6e44c0 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/unweighted/UndirectedGraph.kt @@ -1,8 +1,9 @@ -package graph +package model.graph.unweighted -import model.graph.Edge +import graph.Graph +import model.graph.edges.Edge -class UndirectedGraph : Graph>() { +class UndirectedGraph : UnweightedGraph() { override fun addEdge(from: V, to: V) { val edge1 = Edge(from, to) val edge2 = Edge(to, from) diff --git a/src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt b/src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt new file mode 100644 index 0000000..5897cd0 --- /dev/null +++ b/src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt @@ -0,0 +1,8 @@ +package model.graph.unweighted + +import graph.Graph +import model.graph.edges.Edge + +abstract class UnweightedGraph : Graph>() { + abstract fun addEdge(from: V, to: V) +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt new file mode 100644 index 0000000..c050f99 --- /dev/null +++ b/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt @@ -0,0 +1,12 @@ +package model.graph.weighted + +import model.graph.edges.WeightedEdge + +class WeightedDirectedGraph : WeightedGraph() { + override fun addEdge(from: V, to: V, weight: Int) { + val edge1 = WeightedEdge(from, to, weight) + val edge2 = WeightedEdge(to, from, weight) + graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } + graph[to]?.add(edge2) ?: { graph[to] = mutableListOf(edge2) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/weighted/WeightedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedGraph.kt new file mode 100644 index 0000000..21ddb45 --- /dev/null +++ b/src/main/kotlin/model/graph/weighted/WeightedGraph.kt @@ -0,0 +1,8 @@ +package model.graph.weighted + +import graph.Graph +import model.graph.edges.WeightedEdge + +abstract class WeightedGraph : Graph>() { + abstract fun addEdge(from: V, to: V, weight: Int) +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt new file mode 100644 index 0000000..ba731e8 --- /dev/null +++ b/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt @@ -0,0 +1,10 @@ +package model.graph.weighted + +import model.graph.edges.WeightedEdge + +class WeightedUndirectedGraph : WeightedGraph() { + override fun addEdge(from: V, to: V, weight: Int) { + val edge = WeightedEdge(from, to, weight) + graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/GraphScreen.kt b/src/main/kotlin/view/screens/GraphScreen.kt index b79183e..8bcd481 100644 --- a/src/main/kotlin/view/screens/GraphScreen.kt +++ b/src/main/kotlin/view/screens/GraphScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation +import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle import view.views.GraphView @@ -20,11 +21,15 @@ import viewmodel.GraphViewModel import viewmodel.MainScreenViewModel @Composable -fun GraphScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel, graphId: Int) { - val graphModel by mutableStateOf(mainScreenViewModel.getGraph(graphId)) +fun GraphScreen( + navController: NavController, + mainScreenViewModel: MainScreenViewModel, + graphId: Int +) { + val graphVM by mutableStateOf(mainScreenViewModel.getGraph(graphId)) Box(modifier = Modifier.fillMaxSize()) { - GraphView(graphModel) + GraphView(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { @@ -44,7 +49,7 @@ fun GraphScreen(navController: NavController, mainScreenViewModel: MainScreenVie // Add vertex Button( - onClick = { graphModel.addVertex(graphModel.size) }, + onClick = { graphVM.addVertex(graphVM.size) }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) @@ -78,7 +83,7 @@ fun GraphScreen(navController: NavController, mainScreenViewModel: MainScreenVie .padding(10.dp) ) { - AddEdgeMenu(graphModel) + AddEdgeMenu(graphVM) } } @@ -86,7 +91,7 @@ fun GraphScreen(navController: NavController, mainScreenViewModel: MainScreenVie } @Composable -fun AddEdgeMenu(graphModel: GraphViewModel) { +fun AddEdgeMenu(graphModel: GraphViewModel>) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } Row { diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index 0cc38d9..3e50c21 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -1,12 +1,13 @@ package view.views import androidx.compose.runtime.Composable +import model.graph.edges.Edge import viewmodel.GraphViewModel import viewmodel.VertexViewModel @Composable -fun GraphView(graphViewModel : GraphViewModel) { - for (vertexVM in graphViewModel.graphView.values){ +fun GraphView(graphViewModel: GraphViewModel>) { + for (vertexVM in graphViewModel.graphView.values) { VertexView(vertexVM, graphViewModel) } } \ No newline at end of file diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index 28abc87..9cb9134 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -17,17 +17,18 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex +import model.graph.edges.Edge import view.DefaultColors import viewmodel.GraphViewModel import viewmodel.VertexViewModel import kotlin.math.roundToInt @Composable -fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel) { - val number = vertexVM.number +fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel>) { + val vertex = vertexVM.vertex Box(modifier = Modifier - .offset {IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt())} + .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } .clip(shape = CircleShape) .size(120.dp) .background(DefaultColors.primary) @@ -39,24 +40,30 @@ fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel) { vertexVM.offsetY += dragAmount.y } } - ){ - Text(text = "$number", + ) { + Text( + text = "$vertex", fontSize = 40.sp, modifier = Modifier .fillMaxSize() - .wrapContentSize(),) + .wrapContentSize(), + ) } - vertexVM.edges.forEach{ otherNumber -> + vertexVM.edges.forEach { otherNumber -> val otherVM = graphVM.graphView[otherNumber]!! val otherX = otherVM.offsetX val otherY = otherVM.offsetY - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)){ + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( - start = Offset(vertexVM.offsetX + vertexVM.vertexSize/2, vertexVM.offsetY + vertexVM.vertexSize/2), - end = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2), + start = Offset( + vertexVM.offsetX + vertexVM.vertexSize / 2, + vertexVM.offsetY + vertexVM.vertexSize / 2 + ), + end = Offset(otherX + vertexVM.vertexSize / 2, otherY + vertexVM.vertexSize / 2), strokeWidth = 10f, - color = Color.Black) + color = Color.Black + ) } } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt index 585d856..f8634c8 100644 --- a/src/main/kotlin/viewmodel/GraphViewModel.kt +++ b/src/main/kotlin/viewmodel/GraphViewModel.kt @@ -2,18 +2,17 @@ package viewmodel import androidx.compose.runtime.* import androidx.lifecycle.ViewModel -import graph.UndirectedGraph -import model.graph.Edge +import model.graph.unweighted.UndirectedGraph +import model.graph.edges.Edge -class GraphViewModel>( - name: String, +class GraphViewModel>( + _name: String, graph: UndirectedGraph = UndirectedGraph() -) : - ViewModel() { - val name by mutableStateOf(name) +) : ViewModel() { + val name = _name val size get() = graphModel.size - val graphView = mutableStateMapOf>() + val graphView = mutableStateMapOf>() val graphModel = graph init { diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 66490f9..fa60c89 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -2,16 +2,17 @@ package viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel -import model.graph.Edge +import model.graph.edges.Edge class MainScreenViewModel : ViewModel() { - val graphs = mutableStateListOf() + val graphs = mutableStateListOf>>() fun addGraph(name: String) { - graphs.add(GraphViewModel(name)) + val graphVM = GraphViewModel>(name) + graphs.add(graphVM) } - fun getGraph(graphId: Int): GraphViewModel> { + fun getGraph(graphId: Int): GraphViewModel> { return graphs[graphId] } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 9f014a0..42db832 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -3,11 +3,10 @@ package viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel -import model.graph.Edge +import model.graph.edges.Edge -class VertexViewModel>(_vertex: V, _edges: MutableList> = mutableListOf()) : +class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListOf()) : ViewModel() { val vertex: V = _vertex var edges by mutableStateOf(_edges) From f0565fc2178e271a7f6ef7f600b762468e85c857 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 19 May 2024 10:25:12 +0300 Subject: [PATCH 068/172] Fix: fix edges list refering in VertexView --- .../kotlin/localisation/logs/localisationError.log | 10 ++++++++++ src/main/kotlin/view/views/VertexView.kt | 9 +++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/localisation/logs/localisationError.log b/src/main/kotlin/localisation/logs/localisationError.log index 8aa04b4..36c3353 100644 --- a/src/main/kotlin/localisation/logs/localisationError.log +++ b/src/main/kotlin/localisation/logs/localisationError.log @@ -39,3 +39,13 @@ That's a Krishtall Да здравствует Петербург, да здравствует 52 Да здравствует Петербург, да здравствует 52 (Ау; YEEI, а) Да здравствует 52 (Ау), YEEI, long live (Это второй) +LOCALISATION ERROR 2024-05-19T10:24:55.832074053 -- Key enter_graph_name is not found in en-US +EXCEPTION IS java.lang.NullPointerException +LOCALISATION ERROR 2024-05-19T10:24:56.018568022 -- Key enter_graph_name is not found in en-US +EXCEPTION IS java.lang.NullPointerException +LOCALISATION ERROR 2024-05-19T10:24:58.007027956 -- Key home is not found in en-US +EXCEPTION IS java.lang.NullPointerException +LOCALISATION ERROR 2024-05-19T10:24:58.008030377 -- Key add_vertex is not found in en-US +EXCEPTION IS java.lang.NullPointerException +LOCALISATION ERROR 2024-05-19T10:24:58.008838806 -- Key open_edge is not found in en-US +EXCEPTION IS java.lang.NullPointerException diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index 9cb9134..cfadbfb 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -50,10 +50,11 @@ fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel - val otherVM = graphVM.graphView[otherNumber]!! - val otherX = otherVM.offsetX - val otherY = otherVM.offsetY + vertexVM.edges.forEach { edge -> + val otherVertex = edge.to + val otherVertexView = graphVM.graphView[otherVertex]!! + val otherX = otherVertexView.offsetX + val otherY = otherVertexView.offsetY Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( start = Offset( From e8a5bc6669a7ea3c567c44bcebb46553803d166b Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Sat, 18 May 2024 17:03:46 -0400 Subject: [PATCH 069/172] upd: chinese localization --- src/main/resources/localisation/cn-CN.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/resources/localisation/cn-CN.json b/src/main/resources/localisation/cn-CN.json index 71b46c5..44439e6 100644 --- a/src/main/resources/localisation/cn-CN.json +++ b/src/main/resources/localisation/cn-CN.json @@ -22,23 +22,23 @@ }, { "code": "open_edge", - "localisation":"TODO" + "localisation":"开放边缘" }, { "code": "add_edge", - "localisation": "TODO" + "localisation": "添加边缘" }, { "code": "add", - "localisation": "TODO" + "localisation": "添加" }, { "code": "write_name", - "localisation": "TODO" + "localisation": "写名字" }, { "code": "enter_new_graph_name", - "localisation": "TODO" + "localisation": "创建新图表" } ] } \ No newline at end of file From 794d4681adef803dc401fa907cbf93945454fbf3 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 19 May 2024 10:53:51 +0300 Subject: [PATCH 070/172] Feat: add edges iteration in graphs --- .../kotlin/localisation/logs/localisationError.log | 10 ---------- src/main/kotlin/model/algos/FordBellman.kt | 3 ++- src/main/kotlin/model/graph/Graph.kt | 10 ++++++++++ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/localisation/logs/localisationError.log b/src/main/kotlin/localisation/logs/localisationError.log index 36c3353..8aa04b4 100644 --- a/src/main/kotlin/localisation/logs/localisationError.log +++ b/src/main/kotlin/localisation/logs/localisationError.log @@ -39,13 +39,3 @@ That's a Krishtall Да здравствует Петербург, да здравствует 52 Да здравствует Петербург, да здравствует 52 (Ау; YEEI, а) Да здравствует 52 (Ау), YEEI, long live (Это второй) -LOCALISATION ERROR 2024-05-19T10:24:55.832074053 -- Key enter_graph_name is not found in en-US -EXCEPTION IS java.lang.NullPointerException -LOCALISATION ERROR 2024-05-19T10:24:56.018568022 -- Key enter_graph_name is not found in en-US -EXCEPTION IS java.lang.NullPointerException -LOCALISATION ERROR 2024-05-19T10:24:58.007027956 -- Key home is not found in en-US -EXCEPTION IS java.lang.NullPointerException -LOCALISATION ERROR 2024-05-19T10:24:58.008030377 -- Key add_vertex is not found in en-US -EXCEPTION IS java.lang.NullPointerException -LOCALISATION ERROR 2024-05-19T10:24:58.008838806 -- Key open_edge is not found in en-US -EXCEPTION IS java.lang.NullPointerException diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index 771592e..1227471 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -3,5 +3,6 @@ package model.algos import model.graph.weighted.WeightedGraph fun FordBellman(graph: WeightedGraph) { + +} -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index e42d030..f89d8bd 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -10,6 +10,16 @@ abstract class Graph>() { val vertices get() = graph.keys + val edges: List + get() { + val edges = mutableListOf() + for (vertex in vertices) { + val edgesOf = edgesOf(vertex) + edges.addAll(edgesOf) + } + return edges.toList() + } + var size = graph.size private set From 933a8cbfa83e1f69eb347abe30b524982ca9dce9 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Sun, 19 May 2024 04:04:26 -0400 Subject: [PATCH 071/172] add: dropdown menue on adding a new graph --- src/main/kotlin/view/screens/MainScreen.kt | 56 +++++++++++++++++++--- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 1cbc966..c5ba14a 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -1,5 +1,6 @@ package view.screens +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -9,10 +10,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,8 +19,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogState import androidx.compose.ui.window.DialogWindow -import androidx.compose.ui.window.rememberDialogState import androidx.navigation.NavController import localisation.localisation import view.DefaultColors @@ -36,6 +34,10 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView var search by remember { mutableStateOf("") } var graphName by remember { mutableStateOf("") } val dialogState = remember { mutableStateOf(false) } + val options = listOf(localisation("undirected"), localisation("directed")) + val expanded = remember { mutableStateOf(false) } + val selectedOptionText = remember { mutableStateOf(options[0]) } + Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { // Search tab @@ -118,7 +120,14 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView } } - DialogWindow(visible = dialogState.value, title = "New Graph",onCloseRequest = { dialogState.value = false }, state = rememberDialogState(size = DpSize(960.dp, 640.dp))) { + DialogWindow( + visible = dialogState.value, + title = "New Graph", + resizable = false, + onCloseRequest = { dialogState.value = false }, + state = DialogState(size = DpSize(960.dp, 680.dp)) + ) + { Text(text = localisation("enter_new_graph_name"), modifier = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), style = defaultStyle) TextField( value = graphName, @@ -179,6 +188,41 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView ) { Text(text= localisation("back"), color = Color.White, fontSize = 28.sp) } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .padding(horizontal = 350.dp, vertical = 180.dp) + .width(300.dp) + .height(60.dp) + .clip(RoundedCornerShape(25.dp)) + .border(BorderStroke(2.dp, Color.LightGray), RoundedCornerShape(25.dp)) + .clickable { expanded.value = !expanded.value }, + ) { + Text( + text = selectedOptionText.value, + fontSize = 20.sp, + modifier = Modifier.padding(start = 20.dp) + ) + Icon( + Icons.Filled.ArrowDropDown, "contentDescription", + Modifier.align(Alignment.CenterEnd) + ) + DropdownMenu( + expanded = expanded.value, + onDismissRequest = { expanded.value = false } + ) { + options.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + selectedOptionText.value = selectionOption + expanded.value = false + } + ) { + Text(text = selectionOption) + } + } + } + } } Spacer(modifier = Modifier.height(30.dp)) From 7500b17e316e921551c7f51fd8f56064f294e8bd Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 19 May 2024 13:20:52 +0300 Subject: [PATCH 072/172] Fix: fix bug in addEdge in weighted graphs -undirected graph was adding edge only in 1 direction and directed in 2 directions --- .../kotlin/model/graph/weighted/WeightedDirectedGraph.kt | 6 ++---- .../kotlin/model/graph/weighted/WeightedUndirectedGraph.kt | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt index c050f99..3132649 100644 --- a/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt +++ b/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt @@ -4,9 +4,7 @@ import model.graph.edges.WeightedEdge class WeightedDirectedGraph : WeightedGraph() { override fun addEdge(from: V, to: V, weight: Int) { - val edge1 = WeightedEdge(from, to, weight) - val edge2 = WeightedEdge(to, from, weight) - graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } - graph[to]?.add(edge2) ?: { graph[to] = mutableListOf(edge2) } + val edge = WeightedEdge(from, to, weight) + graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } } } \ No newline at end of file diff --git a/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt index ba731e8..01e1311 100644 --- a/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt +++ b/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt @@ -4,7 +4,9 @@ import model.graph.edges.WeightedEdge class WeightedUndirectedGraph : WeightedGraph() { override fun addEdge(from: V, to: V, weight: Int) { - val edge = WeightedEdge(from, to, weight) - graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } + val edge1 = WeightedEdge(from, to, weight) + val edge2 = WeightedEdge(to, from, weight) + graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } + graph[from]?.add(edge2) ?: { graph[from] = mutableListOf(edge2) } } } \ No newline at end of file From 3b6013e4131940aae6225b5887fc5a49be9c88bc Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 19 May 2024 13:22:46 +0300 Subject: [PATCH 073/172] Feat: Implemented Ford-Bellman algorithm -add basic test for him -algorithm don't return path yet --- build.gradle.kts | 7 +++ src/main/kotlin/model/algos/FordBellman.kt | 46 ++++++++++++++++++- .../kotlin/algos/fordbellman/FordBellman.kt | 37 +++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/test/kotlin/algos/fordbellman/FordBellman.kt diff --git a/build.gradle.kts b/build.gradle.kts index f9d2e79..10410c8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { val nav_version = "2.8.0-alpha02" implementation("org.jetbrains.androidx.navigation:navigation-compose:$nav_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + testImplementation(kotlin("test")) } compose.desktop { @@ -38,3 +39,9 @@ compose.desktop { } } } + +tasks { + test { + useJUnitPlatform() + } +} diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index 1227471..a8bc829 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -1,8 +1,50 @@ package model.algos +import model.graph.edges.Edge import model.graph.weighted.WeightedGraph +import view.defaultStyle -fun FordBellman(graph: WeightedGraph) { - +typealias Path = Array> +typealias Paths = Map> + +object FordBellman { + //should be : Pair> + fun findShortestPath(from: V, to: V, graph: WeightedGraph): Int? { + val distances = mutableMapOf() + for (vertex in graph.vertices) { + distances[vertex] = null + } + distances[from] = 0 + + var isRelaxed = false + repeat(graph.size) { + isRelaxed = false + for (edge in graph.edges) { + val from = edge.from + val to = edge.to + if (distances[from] == null) continue + + val newWeight = distances[from]!! + edge.weight + val oldWeight: Int + if (distances[to] == null) { + distances[to] = newWeight + isRelaxed = true + continue + } + oldWeight = distances[to]!! + + if (oldWeight > newWeight) { + distances[to] = newWeight + isRelaxed = true + } + } + } + + return distances[to] + } + + fun findShortestPath(from: V, graph: WeightedGraph): Pair, Paths> { + TODO() + } } diff --git a/src/test/kotlin/algos/fordbellman/FordBellman.kt b/src/test/kotlin/algos/fordbellman/FordBellman.kt new file mode 100644 index 0000000..54c3205 --- /dev/null +++ b/src/test/kotlin/algos/fordbellman/FordBellman.kt @@ -0,0 +1,37 @@ +package algos.fordbellman + +import model.algos.FordBellman +import model.graph.unweighted.DirectedGraph +import model.graph.weighted.WeightedDirectedGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class FordBellmanTest { + + @Test + fun basicFind() { + val graph = WeightedDirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 3, 2) + this.addEdge(1, 2, 7) + this.addEdge(3, 2, 3) + this.addEdge(2, 4, 1) + this.addEdge(4, 3, -1) + } + + val shortestExpected = 6 + val shortestActual = FordBellman.findShortestPath(1, 4, graph) + assertNotNull(shortestActual) + assertEquals( + shortestExpected, shortestActual, + "FordBellman with single weight of path must return weight of the shortest path" + ) + + } + +} \ No newline at end of file From 52b3f873f2fdd6da352bedb4001a712ea2058a6c Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Sun, 19 May 2024 06:46:45 -0400 Subject: [PATCH 074/172] add: oriented graph arrow & unoriented edges --- src/main/kotlin/model/graph/DirectedGraph.kt | 2 +- src/main/kotlin/view/screens/MainScreen.kt | 4 +- src/main/kotlin/view/views/VertexView.kt | 46 ++++++++++++++++++-- src/main/kotlin/viewmodel/GraphViewModel.kt | 6 ++- src/main/kotlin/viewmodel/VertexViewModel.kt | 3 +- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index 1077406..67f6fe4 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -2,6 +2,6 @@ package graph class DirectedGraph: GraphAbstract(){ override fun addEdge(from: Int, to: Int) { - graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} + graph[from]?.add(to) ?: {graph[from] = mutableListOf(to)} } } diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index c5ba14a..c27668e 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -34,7 +34,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView var search by remember { mutableStateOf("") } var graphName by remember { mutableStateOf("") } val dialogState = remember { mutableStateOf(false) } - val options = listOf(localisation("undirected"), localisation("directed")) + val options = listOf("undirected", "directed") val expanded = remember { mutableStateOf(false) } val selectedOptionText = remember { mutableStateOf(options[0]) } @@ -218,7 +218,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView expanded.value = false } ) { - Text(text = selectionOption) + Text(text = localisation(selectionOption)) } } } diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index 28abc87..6b36e70 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -7,11 +7,13 @@ import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -20,6 +22,7 @@ import androidx.compose.ui.zIndex import view.DefaultColors import viewmodel.GraphViewModel import viewmodel.VertexViewModel +import kotlin.math.atan2 import kotlin.math.roundToInt @Composable @@ -29,7 +32,7 @@ fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel) { Box(modifier = Modifier .offset {IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt())} .clip(shape = CircleShape) - .size(120.dp) + .size(100.dp) .background(DefaultColors.primary) .border(5.dp, Color.Black, CircleShape) .pointerInput(Unit) { @@ -51,12 +54,47 @@ fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel) { val otherVM = graphVM.graphView[otherNumber]!! val otherX = otherVM.offsetX val otherY = otherVM.offsetY + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)){ drawLine( start = Offset(vertexVM.offsetX + vertexVM.vertexSize/2, vertexVM.offsetY + vertexVM.vertexSize/2), end = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2), - strokeWidth = 10f, - color = Color.Black) + strokeWidth = if(graphVM.graphView[otherNumber]?.edges?.contains(vertexVM.number) != true) 6f else 12f, + color = Color.Black, + ) + rotate( + degrees = ((57.2958 * (atan2(((vertexVM.offsetY - otherY).toDouble()), ((vertexVM.offsetX - otherX).toDouble())))).toFloat()), + pivot = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2) + ){ + if(graphVM.graphView[otherNumber]?.edges?.contains(vertexVM.number) != true) { + drawRect( + color = Color.Black, + size = Size(5f, 16f), + topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 70, otherY + vertexVM.vertexSize / 2 - 8f), + ) + drawRect( + color = Color.Black, + size = Size(5f, 14f), + topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 65, otherY + vertexVM.vertexSize / 2 - 7f), + ) + drawRect( + color = Color.Black, + size = Size(5f, 12f), + topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 60, otherY + vertexVM.vertexSize / 2 - 6f), + ) + drawRect( + color = Color.Black, + size = Size(5f, 10f), + topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 55, otherY + vertexVM.vertexSize / 2 - 5f), + ) + drawRect( + color = Color.Black, + size = Size(5f, 8f), + topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 50, otherY + vertexVM.vertexSize / 2 - 4f), + ) + } + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt index d58ae27..331641c 100644 --- a/src/main/kotlin/viewmodel/GraphViewModel.kt +++ b/src/main/kotlin/viewmodel/GraphViewModel.kt @@ -1,6 +1,8 @@ package viewmodel -import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import graph.GraphAbstract import graph.UndirectedGraph @@ -24,7 +26,7 @@ class GraphViewModel(name: String, graph: GraphAbstract = UndirectedGraph()) : V } fun addEdge(source: Int, destination: Int) { - if (graphView[source] == null) { + if (graphView[source] == null || graphView[source]?.edges?.contains(destination) == true) { return } val edgesCopy = graphView[source]?.edges?.toMutableList()!! diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 36f11a6..acd0fb0 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -3,7 +3,6 @@ package viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel class VertexViewModel(number: Int, _edges: MutableList = mutableListOf()): ViewModel() { @@ -11,5 +10,5 @@ class VertexViewModel(number: Int, _edges: MutableList = mutableListOf()): var edges by mutableStateOf(_edges) var offsetX by mutableStateOf(1000f) var offsetY by mutableStateOf(540f) - val vertexSize = 120f + val vertexSize = 100f } \ No newline at end of file From ae1ef5ddb053ed86b9d39e58d67120329df68845 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 19 May 2024 14:58:17 +0300 Subject: [PATCH 075/172] Feat: implemented finding path of edges in Ford-Bellman algorithm -also add equals in Edge class --- src/main/kotlin/model/algos/FordBellman.kt | 33 +++++++++++++------ src/main/kotlin/model/graph/edges/Edge.kt | 8 +++++ .../kotlin/algos/fordbellman/FordBellman.kt | 19 ++++++++--- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index a8bc829..aa7659c 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -2,23 +2,23 @@ package model.algos import model.graph.edges.Edge import model.graph.weighted.WeightedGraph -import view.defaultStyle -typealias Path = Array> +typealias Path = List> typealias Paths = Map> object FordBellman { //should be : Pair> - fun findShortestPath(from: V, to: V, graph: WeightedGraph): Int? { + fun findShortestPath(from: V, to: V, graph: WeightedGraph): Pair?> { val distances = mutableMapOf() + val minSources = mutableMapOf>() for (vertex in graph.vertices) { distances[vertex] = null } distances[from] = 0 - var isRelaxed = false + var lastTimeRelaxed = false repeat(graph.size) { - isRelaxed = false + lastTimeRelaxed = false for (edge in graph.edges) { val from = edge.from val to = edge.to @@ -28,19 +28,32 @@ object FordBellman { val oldWeight: Int if (distances[to] == null) { distances[to] = newWeight - isRelaxed = true + minSources[to] = edge + lastTimeRelaxed = true continue } - oldWeight = distances[to]!! + oldWeight = distances[to]!! if (oldWeight > newWeight) { distances[to] = newWeight - isRelaxed = true + minSources[to] = edge + lastTimeRelaxed = true } } } - - return distances[to] + var path: MutableList>? = null + var curVert = to + if (!lastTimeRelaxed && distances[curVert] != null) { + path = mutableListOf() + while (curVert != from) { + val prevEdge = minSources[curVert] + ?: throw IllegalStateException("Can't find previous edge of path") + path.add(prevEdge) + curVert = prevEdge.from + } + } + val pathAnswer = path?.reversed()?.toList() + return Pair(distances[to], pathAnswer) } fun findShortestPath(from: V, graph: WeightedGraph): Pair, Paths> { diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/edges/Edge.kt index 74d4e43..afde3e0 100644 --- a/src/main/kotlin/model/graph/edges/Edge.kt +++ b/src/main/kotlin/model/graph/edges/Edge.kt @@ -2,4 +2,12 @@ package model.graph.edges open class Edge(val from: V, val to: V) { open val weight = 1 + + override fun equals(other: Any?): Boolean { + if (other !is Edge<*>) return false + if (from == other.from && to == other.to && weight == other.weight) { + return true + } + return false + } } diff --git a/src/test/kotlin/algos/fordbellman/FordBellman.kt b/src/test/kotlin/algos/fordbellman/FordBellman.kt index 54c3205..5bf808a 100644 --- a/src/test/kotlin/algos/fordbellman/FordBellman.kt +++ b/src/test/kotlin/algos/fordbellman/FordBellman.kt @@ -1,11 +1,10 @@ package algos.fordbellman import model.algos.FordBellman -import model.graph.unweighted.DirectedGraph +import model.graph.edges.Edge +import model.graph.edges.WeightedEdge import model.graph.weighted.WeightedDirectedGraph -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import kotlin.test.* internal class FordBellmanTest { @@ -24,14 +23,24 @@ internal class FordBellmanTest { this.addEdge(4, 3, -1) } + val result = FordBellman.findShortestPath(1, 4, graph) + val shortestExpected = 6 - val shortestActual = FordBellman.findShortestPath(1, 4, graph) + val shortestActual = result.first assertNotNull(shortestActual) assertEquals( shortestExpected, shortestActual, "FordBellman with single weight of path must return weight of the shortest path" ) + val pathExpected = listOf>( + WeightedEdge(1, 3, 2), + WeightedEdge(3, 2, 3), + WeightedEdge(2, 4, 1) + ) + val pathActual = result.second + assertContentEquals(pathExpected, pathActual, "") + } } \ No newline at end of file From afa492f00d84d31210c3f407e3353ccd09c2f13c Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sun, 19 May 2024 20:05:08 +0300 Subject: [PATCH 076/172] feat: implementation Prim algorithm Implementation only for UnDigraphs --- src/main/kotlin/model/algos/Prima.kt | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/kotlin/model/algos/Prima.kt diff --git a/src/main/kotlin/model/algos/Prima.kt b/src/main/kotlin/model/algos/Prima.kt new file mode 100644 index 0000000..2dbc911 --- /dev/null +++ b/src/main/kotlin/model/algos/Prima.kt @@ -0,0 +1,30 @@ +package model.algos + +import model.graph.edges.WeightedEdge +import model.graph.weighted.WeightedGraph + +object Prim { + fun findSpanningTree(graph: WeightedGraph): List> { + val visitedVertices = mutableSetOf() + val edges = mutableListOf>() + + val vertex = graph.vertices.random() + visitedVertices.add(vertex) + + val allEdgesOfVertices = visitedVertices.flatMap { graph.edgesOf(it) } + val unvisitedVertices = + allEdgesOfVertices.filter { !visitedVertices.contains(it.from) || !visitedVertices.contains(it.to) } + + val nextEdge = unvisitedVertices.minBy { it.weight } + visitedVertices.addAll(setOf(nextEdge.from, nextEdge.to)) + + while (!visitedVertices.containsAll(graph.vertices)) { + val nextEdge = visitedVertices.flatMap { graph.edgesOf(it) } + .filter { !visitedVertices.contains(it.from) && !visitedVertices.contains(it.to) } + .minBy { it.weight } + visitedVertices.addAll(setOf(nextEdge.from, nextEdge.to)) + edges.add(nextEdge) + } + return edges + } +} \ No newline at end of file From 5f202f91c08af0e7d10a4207eb178b9b11f01458 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sun, 19 May 2024 20:39:16 +0300 Subject: [PATCH 077/172] change: weighted on weightedundirected --- src/main/kotlin/model/algos/Prima.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/model/algos/Prima.kt b/src/main/kotlin/model/algos/Prima.kt index 2dbc911..5cb2668 100644 --- a/src/main/kotlin/model/algos/Prima.kt +++ b/src/main/kotlin/model/algos/Prima.kt @@ -1,10 +1,10 @@ package model.algos import model.graph.edges.WeightedEdge -import model.graph.weighted.WeightedGraph +import model.graph.weighted.WeightedUndirectedGraph object Prim { - fun findSpanningTree(graph: WeightedGraph): List> { + fun findSpanningTree(graph: WeightedUndirectedGraph): List> { val visitedVertices = mutableSetOf() val edges = mutableListOf>() From 107745193b261fb737b5a4456489e63c2b582bc4 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Sun, 19 May 2024 14:09:55 -0400 Subject: [PATCH 078/172] add: screem view model split --- src/main/kotlin/view/screens/GraphScreen.kt | 4 +- src/main/kotlin/view/screens/MainScreen.kt | 68 +++++++++---- src/main/kotlin/view/views/GraphView.kt | 5 +- src/main/kotlin/view/views/VertexView.kt | 4 +- .../viewmodel/AbstractGraphViewModel.kt | 21 ++++ .../DirectedUnweightedGraphViewModel.kt | 22 +++++ .../DirectedWeightedGraphViewModel.kt | 22 +++++ src/main/kotlin/viewmodel/GraphViewModel.kt | 41 -------- .../kotlin/viewmodel/MainScreenViewModel.kt | 95 +++++++++++++++++-- .../UndirectedUnweightedGraphViewModel.kt | 22 +++++ .../UndirectedWeightedGraphViewModel.kt | 22 +++++ 11 files changed, 256 insertions(+), 70 deletions(-) create mode 100644 src/main/kotlin/viewmodel/AbstractGraphViewModel.kt create mode 100644 src/main/kotlin/viewmodel/DirectedUnweightedGraphViewModel.kt create mode 100644 src/main/kotlin/viewmodel/DirectedWeightedGraphViewModel.kt delete mode 100644 src/main/kotlin/viewmodel/GraphViewModel.kt create mode 100644 src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt create mode 100644 src/main/kotlin/viewmodel/UndirectedWeightedGraphViewModel.kt diff --git a/src/main/kotlin/view/screens/GraphScreen.kt b/src/main/kotlin/view/screens/GraphScreen.kt index 8bcd481..f8e86a1 100644 --- a/src/main/kotlin/view/screens/GraphScreen.kt +++ b/src/main/kotlin/view/screens/GraphScreen.kt @@ -17,7 +17,7 @@ import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle import view.views.GraphView -import viewmodel.GraphViewModel +import viewmodel.UndirectedUnweightedGraphViewModel import viewmodel.MainScreenViewModel @Composable @@ -91,7 +91,7 @@ fun GraphScreen( } @Composable -fun AddEdgeMenu(graphModel: GraphViewModel>) { +fun AddEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } Row { diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index c27668e..15fb6fe 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -34,9 +33,12 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView var search by remember { mutableStateOf("") } var graphName by remember { mutableStateOf("") } val dialogState = remember { mutableStateOf(false) } - val options = listOf("undirected", "directed") - val expanded = remember { mutableStateOf(false) } - val selectedOptionText = remember { mutableStateOf(options[0]) } + val optionsDUD = listOf("undirected", "directed") + val expandedDUD = remember { mutableStateOf(false) } + val selectedOptionTextDUD = remember { mutableStateOf(optionsDUD[0]) } + val optionsWUW = listOf("weighted", "unweighted") + val expandedWUW = remember { mutableStateOf(false) } + val selectedOptionTextWUW= remember { mutableStateOf(optionsWUW[0]) } Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { @@ -163,7 +165,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView colors = if(graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.simpleGreen) else ButtonDefaults.buttonColors(backgroundColor = DefaultColors.darkGreen), onClick = { if(graphName != ""){ - mainScreenViewModel.addGraph(graphName) + mainScreenViewModel.addGraph(graphName, Pair(selectedOptionTextDUD.toString(), selectedOptionTextWUW.toString())) dialogState.value = false } }, @@ -196,10 +198,10 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView .height(60.dp) .clip(RoundedCornerShape(25.dp)) .border(BorderStroke(2.dp, Color.LightGray), RoundedCornerShape(25.dp)) - .clickable { expanded.value = !expanded.value }, + .clickable { expandedDUD.value = !expandedDUD.value }, ) { Text( - text = selectedOptionText.value, + text = selectedOptionTextDUD.value, fontSize = 20.sp, modifier = Modifier.padding(start = 20.dp) ) @@ -208,14 +210,49 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Modifier.align(Alignment.CenterEnd) ) DropdownMenu( - expanded = expanded.value, - onDismissRequest = { expanded.value = false } + expanded = expandedDUD.value, + onDismissRequest = { expandedDUD.value = false } ) { - options.forEach { selectionOption -> + optionsDUD.forEach { selectionOption -> DropdownMenuItem( onClick = { - selectedOptionText.value = selectionOption - expanded.value = false + selectedOptionTextDUD.value = selectionOption + expandedDUD.value = false + } + ) { + Text(text = localisation(selectionOption)) + } + } + } + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .padding(horizontal = 350.dp, vertical = 260.dp) + .width(300.dp) + .height(60.dp) + .clip(RoundedCornerShape(25.dp)) + .border(BorderStroke(2.dp, Color.LightGray), RoundedCornerShape(25.dp)) + .clickable { expandedWUW.value = !expandedWUW.value }, + ) { + Text( + text = selectedOptionTextWUW.value, + fontSize = 20.sp, + modifier = Modifier.padding(start = 20.dp) + ) + Icon( + Icons.Filled.ArrowDropDown, "contentDescription", + Modifier.align(Alignment.CenterEnd) + ) + DropdownMenu( + expanded = expandedWUW.value, + onDismissRequest = { expandedWUW.value = false } + ) { + optionsWUW.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + selectedOptionTextWUW.value = selectionOption + expandedWUW.value = false } ) { Text(text = localisation(selectionOption)) @@ -228,9 +265,8 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Spacer(modifier = Modifier.height(30.dp)) LazyColumn(modifier = Modifier.fillMaxWidth()) { - itemsIndexed(mainScreenViewModel.graphs) { index, graph -> - if (!graph.name.startsWith(search)) return@itemsIndexed - + for (index in 0..mainScreenViewModel.graphs.typeList.size) { + if (!mainScreenViewModel.graphs.getName(index).startsWith(search)) return@LazyColumn // To GraphScreen Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( @@ -247,7 +283,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView ), colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { - Text(text = graph.name, style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp))) + Text(text = mainScreenViewModel.graphs.getName(index), style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp))) } Spacer(modifier = Modifier.width(10.dp)) diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index 3e50c21..d8e632a 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -2,11 +2,10 @@ package view.views import androidx.compose.runtime.Composable import model.graph.edges.Edge -import viewmodel.GraphViewModel -import viewmodel.VertexViewModel +import viewmodel.UndirectedUnweightedGraphViewModel @Composable -fun GraphView(graphViewModel: GraphViewModel>) { +fun GraphView(graphViewModel: UndirectedUnweightedGraphViewModel>) { for (vertexVM in graphViewModel.graphView.values) { VertexView(vertexVM, graphViewModel) } diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index 8ba60a1..feb072e 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -21,13 +21,13 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import model.graph.edges.Edge import view.DefaultColors -import viewmodel.GraphViewModel +import viewmodel.UndirectedUnweightedGraphViewModel import viewmodel.VertexViewModel import kotlin.math.atan2 import kotlin.math.roundToInt @Composable -fun VertexView(vertexVM: VertexViewModel, graphVM: GraphViewModel>) { +fun VertexView(vertexVM: VertexViewModel, graphVM: UndirectedUnweightedGraphViewModel>) { val vertex = vertexVM.vertex Box(modifier = Modifier diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt new file mode 100644 index 0000000..84bb7ca --- /dev/null +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -0,0 +1,21 @@ +package viewmodel + +import androidx.compose.runtime.mutableStateMapOf +import androidx.lifecycle.ViewModel +import model.graph.edges.Edge + +abstract class AbstractGraphViewModel(graph: G) : ViewModel() { + val graphView = mutableStateMapOf>() + val graphModel = graph + + + fun addEdge(from: V, to: V) { + if (graphView[from] == null) { + return + } + for (i in graphView[from]?.edges!!) if(i.to == to) return + val edgesCopy = graphView[from]?.edges?.toMutableList()!! + edgesCopy.add(Edge(from, to)) + graphView[from]?.edges = edgesCopy + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedUnweightedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedUnweightedGraphViewModel.kt new file mode 100644 index 0000000..12ce320 --- /dev/null +++ b/src/main/kotlin/viewmodel/DirectedUnweightedGraphViewModel.kt @@ -0,0 +1,22 @@ +package viewmodel + +import model.graph.edges.Edge +import model.graph.unweighted.UndirectedGraph + +class DirectedUnweightedGraphViewModel>( + _name: String, + graph: UndirectedGraph = UndirectedGraph() +): AbstractGraphViewModel>(graph){ + val name = _name + init { + for (vertex in graphModel.entries) { + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + } + } + + fun addVertex(vertex: V) { + graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphModel.addVertex(vertex) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedWeightedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedWeightedGraphViewModel.kt new file mode 100644 index 0000000..18d2ecd --- /dev/null +++ b/src/main/kotlin/viewmodel/DirectedWeightedGraphViewModel.kt @@ -0,0 +1,22 @@ +package viewmodel + +import model.graph.edges.Edge +import model.graph.unweighted.UndirectedGraph + +class DirectedWeightedGraphViewModel>( + _name: String, + graph: UndirectedGraph = UndirectedGraph() +): AbstractGraphViewModel>(graph){ + val name = _name + init { + for (vertex in graphModel.entries) { + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + } + } + + fun addVertex(vertex: V) { + graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphModel.addVertex(vertex) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt deleted file mode 100644 index 7d0f817..0000000 --- a/src/main/kotlin/viewmodel/GraphViewModel.kt +++ /dev/null @@ -1,41 +0,0 @@ -package viewmodel - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import model.graph.unweighted.UndirectedGraph -import model.graph.edges.Edge - -class GraphViewModel>( - _name: String, - graph: UndirectedGraph = UndirectedGraph() -) : ViewModel() { - val name = _name - val size - get() = graphModel.size - val graphView = mutableStateMapOf>() - val graphModel = graph - - init { - for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) - } - } - - fun addVertex(vertex: V) { - graphView.putIfAbsent(vertex, VertexViewModel(vertex)) - graphModel.addVertex(vertex) - } - - - fun addEdge(from: V, to: V) { - if (graphView[from] == null) { - return - } - for (i in graphView[from]?.edges!!) if(i.to == to) return - val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) - graphView[from]?.edges = edgesCopy - } -} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index fa60c89..20285d4 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -3,16 +3,99 @@ package viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel import model.graph.edges.Edge +import model.graph.edges.WeightedEdge class MainScreenViewModel : ViewModel() { - val graphs = mutableStateListOf>>() + val graphs = GraphStorage() - fun addGraph(name: String) { - val graphVM = GraphViewModel>(name) - graphs.add(graphVM) + fun addGraph(name: String, type: Pair) { + when (type){ + Pair("undirected", "unweighted") -> { + graphs.typeList.add(ViewModelType.UU) + graphs.undirectedUnweightedGraphs.add(UndirectedUnweightedGraphViewModel>(name)) + } + Pair("directed", "unweighted") -> { + graphs.typeList.add(ViewModelType.DU) + graphs.directedUnweightedGraphs.add(DirectedUnweightedGraphViewModel>(name)) + } + Pair("undirected", "weighted") -> { + graphs.typeList.add(ViewModelType.UW) + graphs.undirectedWeightedGraphs.add(UndirectedWeightedGraphViewModel>(name)) + } + Pair("directed", "weighted") -> { + graphs.typeList.add(ViewModelType.DW) + graphs.directedWeightedGraphs.add(DirectedWeightedGraphViewModel>(name)) + } + } } - - fun getGraph(graphId: Int): GraphViewModel> { + /* + fun getGraph(graphId: Int): UndirectedUnweightedGraphViewModel> { return graphs[graphId] } + */ + enum class ViewModelType(){ + UU, + DU, + UW, + DW + } + + inner class GraphStorage(){ + + fun getName(index: Int) : String{ + when(graphs.typeList[index]){ + ViewModelType.UU -> { + return graphs.undirectedUnweightedGraphs[index].name + } + ViewModelType.DU -> { + return graphs.directedUnweightedGraphs[index].name + } + ViewModelType.UW -> { + return graphs.undirectedWeightedGraphs[index].name + } + ViewModelType.DW -> { + return graphs.directedWeightedGraphs[index].name + } + } + } + private fun findGraph(index: Int) : Int{ + var indexAr = 0 + when(graphs.typeList[index]){ + ViewModelType.UU -> { + for (i in 0..index) if (graphs.typeList[i] == ViewModelType.UU) indexAr += 1 + } + ViewModelType.DU -> { + for (i in 0..index) if (graphs.typeList[i] == ViewModelType.DU) indexAr += 1 + } + ViewModelType.UW -> { + for (i in 0..index) if (graphs.typeList[i] == ViewModelType.UW) indexAr += 1 + } + ViewModelType.DW -> { + for (i in 0..index) if (graphs.typeList[i] == ViewModelType.DW) indexAr += 1 + } + } + return indexAr + } + fun removeAt(index: Int){ + when(graphs.typeList[index]){ + ViewModelType.UU -> { + graphs.undirectedUnweightedGraphs.removeAt(findGraph(index)) + } + ViewModelType.DU -> { + graphs.directedUnweightedGraphs.removeAt(findGraph(index)) + } + ViewModelType.UW -> { + graphs.undirectedWeightedGraphs.removeAt(findGraph(index)) + } + ViewModelType.DW -> { + graphs.directedWeightedGraphs.removeAt(findGraph(index)) + } + } + } + var undirectedUnweightedGraphs = mutableStateListOf>>() + var directedUnweightedGraphs = mutableStateListOf>>() + var undirectedWeightedGraphs = mutableStateListOf>>() + var directedWeightedGraphs = mutableStateListOf>>() + var typeList = mutableStateListOf() + } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt new file mode 100644 index 0000000..d08e299 --- /dev/null +++ b/src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt @@ -0,0 +1,22 @@ +package viewmodel + +import model.graph.edges.Edge +import model.graph.unweighted.UndirectedGraph + +class UndirectedUnweightedGraphViewModel>( + _name: String, + graph: UndirectedGraph = UndirectedGraph() +): AbstractGraphViewModel>(graph){ + val name = _name + init { + for (vertex in graphModel.entries) { + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + } + } + + fun addVertex(vertex: V) { + graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphModel.addVertex(vertex) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedWeightedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedWeightedGraphViewModel.kt new file mode 100644 index 0000000..037078a --- /dev/null +++ b/src/main/kotlin/viewmodel/UndirectedWeightedGraphViewModel.kt @@ -0,0 +1,22 @@ +package viewmodel + +import model.graph.edges.Edge +import model.graph.unweighted.UndirectedGraph + +class UndirectedWeightedGraphViewModel>( + _name: String, + graph: UndirectedGraph = UndirectedGraph() +): AbstractGraphViewModel>(graph){ + val name = _name + init { + for (vertex in graphModel.entries) { + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + } + } + + fun addVertex(vertex: V) { + graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphModel.addVertex(vertex) + } + +} \ No newline at end of file From bf28b07ec2d5a112d5350e585f45234b1efc03c6 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Sun, 19 May 2024 15:31:44 -0400 Subject: [PATCH 079/172] add: new screen navigation --- src/main/kotlin/Navigation.kt | 34 ++++- ...en.kt => DirectedUnweightedGraphScreen.kt} | 15 +- src/main/kotlin/view/screens/MainScreen.kt | 17 ++- src/main/kotlin/view/screens/SealedScreens.kt | 5 +- .../UndirectedUnweightedGraphScreen.kt | 135 ++++++++++++++++++ .../viewmodel/AbstractGraphViewModel.kt | 2 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 16 ++- .../UndirectedUnweightedGraphViewModel.kt | 1 + 8 files changed, 207 insertions(+), 18 deletions(-) rename src/main/kotlin/view/screens/{GraphScreen.kt => DirectedUnweightedGraphScreen.kt} (91%) create mode 100644 src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt diff --git a/src/main/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt index 4f2dfa2..faa6b94 100644 --- a/src/main/kotlin/Navigation.kt +++ b/src/main/kotlin/Navigation.kt @@ -1,10 +1,10 @@ + import androidx.compose.runtime.Composable import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import view.screens.GraphScreen import view.screens.MainScreen import view.screens.Screen import view.screens.SettingsScreen @@ -20,14 +20,40 @@ fun Navigation() { MainScreen(navController = navController, mainScreenViewModel) } composable( - route = "${Screen.GraphScreen.route}/{graphId}", + route = "${Screen.UndirectedUnweightedGraphScreen.route}/{type}/{graphId}", arguments = listOf(navArgument("graphId") { type = NavType.IntType }) ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - GraphScreen(navController, mainScreenViewModel, graphId) + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/UU/$graphId") + } + } + composable( + route = "${Screen.DirectedUnweightedGraphScreen.route}/{type}/{graphId}", + arguments = listOf(navArgument("graphId") { type = NavType.IntType }) + ){ navBackStackEntry -> + val graphId = navBackStackEntry.arguments?.getInt("graphId") + graphId?.let{ + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/DU/$graphId") + } + } + composable( + route = "${Screen.UndirectedWeightedGraphScreen.route}/{type}/{graphId}", + arguments = listOf(navArgument("graphId") { type = NavType.IntType }) + ){ navBackStackEntry -> + val graphId = navBackStackEntry.arguments?.getInt("graphId") + graphId?.let{ + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/UW/$graphId") + } + } + composable( + route = "${Screen.DirectedWeightedGraphScreen.route}/{type}/{graphId}", + arguments = listOf(navArgument("graphId") { type = NavType.IntType }) + ){ navBackStackEntry -> + val graphId = navBackStackEntry.arguments?.getInt("graphId") + graphId?.let{ + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/DW/$graphId") } - } composable(route = Screen.SettingsScreen.route){ SettingsScreen(navController = navController) diff --git a/src/main/kotlin/view/screens/GraphScreen.kt b/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt similarity index 91% rename from src/main/kotlin/view/screens/GraphScreen.kt rename to src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt index f8e86a1..07fd298 100644 --- a/src/main/kotlin/view/screens/GraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt @@ -4,7 +4,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextField import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -17,16 +20,16 @@ import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle import view.views.GraphView -import viewmodel.UndirectedUnweightedGraphViewModel import viewmodel.MainScreenViewModel +import viewmodel.UndirectedUnweightedGraphViewModel @Composable -fun GraphScreen( +fun DirectedUnweightedGraphScreen( navController: NavController, mainScreenViewModel: MainScreenViewModel, graphId: Int ) { - val graphVM by mutableStateOf(mainScreenViewModel.getGraph(graphId)) + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) Box(modifier = Modifier.fillMaxSize()) { GraphView(graphVM) @@ -83,7 +86,7 @@ fun GraphScreen( .padding(10.dp) ) { - AddEdgeMenu(graphVM) + AddDUEdgeMenu(graphVM) } } @@ -91,7 +94,7 @@ fun GraphScreen( } @Composable -fun AddEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { +fun AddDUEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } Row { diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 15fb6fe..87d1e05 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons @@ -36,7 +37,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView val optionsDUD = listOf("undirected", "directed") val expandedDUD = remember { mutableStateOf(false) } val selectedOptionTextDUD = remember { mutableStateOf(optionsDUD[0]) } - val optionsWUW = listOf("weighted", "unweighted") + val optionsWUW = listOf("unweighted", "weighted") val expandedWUW = remember { mutableStateOf(false) } val selectedOptionTextWUW= remember { mutableStateOf(optionsWUW[0]) } @@ -165,7 +166,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView colors = if(graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.simpleGreen) else ButtonDefaults.buttonColors(backgroundColor = DefaultColors.darkGreen), onClick = { if(graphName != ""){ - mainScreenViewModel.addGraph(graphName, Pair(selectedOptionTextDUD.toString(), selectedOptionTextWUW.toString())) + mainScreenViewModel.addGraph(graphName, Pair(selectedOptionTextDUD.value.toString(), selectedOptionTextWUW.value.toString())) dialogState.value = false } }, @@ -265,12 +266,18 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Spacer(modifier = Modifier.height(30.dp)) LazyColumn(modifier = Modifier.fillMaxWidth()) { - for (index in 0..mainScreenViewModel.graphs.typeList.size) { - if (!mainScreenViewModel.graphs.getName(index).startsWith(search)) return@LazyColumn + itemsIndexed(mainScreenViewModel.graphs.typeList) { index, _ -> + if (!mainScreenViewModel.graphs.getName(index).startsWith(search)) return@itemsIndexed // To GraphScreen Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( - onClick = { navController.navigate("${Screen.GraphScreen.route}/$index") }, + onClick = { navController.navigate(when(mainScreenViewModel.graphs.typeList[index]){ + MainScreenViewModel.ViewModelType.UU -> "${Screen.UndirectedUnweightedGraphScreen.route}/UU/$index" + MainScreenViewModel.ViewModelType.DU -> "${Screen.DirectedWeightedGraphScreen.route}/DU/$index" + MainScreenViewModel.ViewModelType.UW -> "${Screen.UndirectedWeightedGraphScreen.route}/UW/$index" + MainScreenViewModel.ViewModelType.DW -> "${Screen.DirectedWeightedGraphScreen.route}/DW/$index" + }) + }, modifier = Modifier .fillMaxWidth() .height(100.dp) diff --git a/src/main/kotlin/view/screens/SealedScreens.kt b/src/main/kotlin/view/screens/SealedScreens.kt index 4e44e6b..e63dcd2 100644 --- a/src/main/kotlin/view/screens/SealedScreens.kt +++ b/src/main/kotlin/view/screens/SealedScreens.kt @@ -2,6 +2,9 @@ package view.screens sealed class Screen(val route: String){ object MainScreen: Screen("main_screen") - object GraphScreen: Screen("graph_screen") + object UndirectedUnweightedGraphScreen: Screen("uu_graph_screen") + object DirectedUnweightedGraphScreen: Screen("du_graph_screen") + object UndirectedWeightedGraphScreen: Screen("uw_graph_screen") + object DirectedWeightedGraphScreen: Screen("dw_graph_screen") object SettingsScreen: Screen("settings_screen") } \ No newline at end of file diff --git a/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt new file mode 100644 index 0000000..62a4b80 --- /dev/null +++ b/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt @@ -0,0 +1,135 @@ +package view.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import localisation.localisation +import model.graph.edges.Edge +import view.DefaultColors +import view.defaultStyle +import view.views.GraphView +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedUnweightedGraphViewModel + +@Composable +fun UndirectedUnweightedGraphScreen( + navController: NavController, + mainScreenViewModel: MainScreenViewModel, + graphId: Int +) { + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) + + Box(modifier = Modifier.fillMaxSize()) { + GraphView(graphVM) + } + + Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { + // To MainScreen + Button( + onClick = { navController.popBackStack() }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(DefaultColors.primary) + ) { + Text(localisation("home"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Add vertex + Button( + onClick = { graphVM.addVertex(graphVM.size) }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_vertex"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + + Button( + onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("open_edge"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(10.dp)) + + if (isOpenedEdgeMenu) { + Column( + modifier = Modifier + .background(Color(0xffeeeeee)) + .border(3.dp, color = Color.Black) + .padding(10.dp) + + ) { + AddUUEdgeMenu(graphVM) + } + + } + } +} + +@Composable +fun AddUUEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + Row { + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(10.dp)) + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }) + } + + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphModel.addEdge(sourceInt, destinationInt) + } + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_edge"), style = defaultStyle) + } +} diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 84bb7ca..3de1329 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -7,7 +7,7 @@ import model.graph.edges.Edge abstract class AbstractGraphViewModel(graph: G) : ViewModel() { val graphView = mutableStateMapOf>() val graphModel = graph - + var size = 0 fun addEdge(from: V, to: V) { if (graphView[from] == null) { diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 20285d4..3f79188 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -9,10 +9,13 @@ class MainScreenViewModel : ViewModel() { val graphs = GraphStorage() fun addGraph(name: String, type: Pair) { + println(type.first) + println(type.second) when (type){ Pair("undirected", "unweighted") -> { graphs.typeList.add(ViewModelType.UU) graphs.undirectedUnweightedGraphs.add(UndirectedUnweightedGraphViewModel>(name)) + } Pair("directed", "unweighted") -> { graphs.typeList.add(ViewModelType.DU) @@ -41,7 +44,6 @@ class MainScreenViewModel : ViewModel() { } inner class GraphStorage(){ - fun getName(index: Int) : String{ when(graphs.typeList[index]){ ViewModelType.UU -> { @@ -92,6 +94,18 @@ class MainScreenViewModel : ViewModel() { } } } + fun getUU(index: Int) : UndirectedUnweightedGraphViewModel>{ + return undirectedUnweightedGraphs[findGraph(index)] + } + fun getDU(index: Int) : DirectedUnweightedGraphViewModel>{ + return directedUnweightedGraphs[findGraph(index)] + } + fun getUD(index: Int) : UndirectedWeightedGraphViewModel>{ + return undirectedWeightedGraphs[findGraph(index)] + } + fun getDW(index: Int) : DirectedWeightedGraphViewModel>{ + return directedWeightedGraphs[findGraph(index)] + } var undirectedUnweightedGraphs = mutableStateListOf>>() var directedUnweightedGraphs = mutableStateListOf>>() var undirectedWeightedGraphs = mutableStateListOf>>() diff --git a/src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt index d08e299..0fb6276 100644 --- a/src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedUnweightedGraphViewModel.kt @@ -15,6 +15,7 @@ class UndirectedUnweightedGraphViewModel>( } fun addVertex(vertex: V) { + size += 1 graphView.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) } From 8d4a803a4083d68eb2b653885eed2bb73ae4238a Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 20 May 2024 00:27:02 +0300 Subject: [PATCH 080/172] Feat: change graph model structure -inheritance from directed/undirected, not weighted/unweighted -graphs differs from each other by addEdge method -remove WeightedEdge class --- src/main/kotlin/model/algos/FordBellman.kt | 6 +++--- src/main/kotlin/model/graph/Graph.kt | 16 +++++++++------- .../kotlin/model/graph/directed/DirectedGraph.kt | 5 +++++ .../DirectedUnweightedGraph.kt} | 7 +++---- .../graph/directed/DirectedWeightedGraph.kt | 10 ++++++++++ src/main/kotlin/model/graph/edges/Edge.kt | 4 +--- .../kotlin/model/graph/edges/WeightedEdge.kt | 6 ------ .../model/graph/undirected/UndirectedGraph.kt | 6 ++++++ .../UndirectedUnweightedGraph.kt} | 7 +++---- .../graph/undirected/UndirectedWeightedGraph.kt | 12 ++++++++++++ .../model/graph/unweighted/UnweightedGraph.kt | 8 -------- .../graph/weighted/WeightedDirectedGraph.kt | 10 ---------- .../kotlin/model/graph/weighted/WeightedGraph.kt | 8 -------- .../graph/weighted/WeightedUndirectedGraph.kt | 12 ------------ src/main/kotlin/viewmodel/GraphViewModel.kt | 9 ++++----- src/test/kotlin/algos/fordbellman/FordBellman.kt | 11 +++++------ 16 files changed, 61 insertions(+), 76 deletions(-) create mode 100644 src/main/kotlin/model/graph/directed/DirectedGraph.kt rename src/main/kotlin/model/graph/{unweighted/DirectedGraph.kt => directed/DirectedUnweightedGraph.kt} (50%) create mode 100644 src/main/kotlin/model/graph/directed/DirectedWeightedGraph.kt delete mode 100644 src/main/kotlin/model/graph/edges/WeightedEdge.kt create mode 100644 src/main/kotlin/model/graph/undirected/UndirectedGraph.kt rename src/main/kotlin/model/graph/{unweighted/UndirectedGraph.kt => undirected/UndirectedUnweightedGraph.kt} (63%) create mode 100644 src/main/kotlin/model/graph/undirected/UndirectedWeightedGraph.kt delete mode 100644 src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt delete mode 100644 src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt delete mode 100644 src/main/kotlin/model/graph/weighted/WeightedGraph.kt delete mode 100644 src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index aa7659c..35aba4b 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -1,14 +1,14 @@ package model.algos +import graph.Graph import model.graph.edges.Edge -import model.graph.weighted.WeightedGraph typealias Path = List> typealias Paths = Map> object FordBellman { //should be : Pair> - fun findShortestPath(from: V, to: V, graph: WeightedGraph): Pair?> { + fun findShortestPath(from: V, to: V, graph: Graph): Pair?> { val distances = mutableMapOf() val minSources = mutableMapOf>() for (vertex in graph.vertices) { @@ -56,7 +56,7 @@ object FordBellman { return Pair(distances[to], pathAnswer) } - fun findShortestPath(from: V, graph: WeightedGraph): Pair, Paths> { + fun findShortestPath(from: V, graph: Graph): Pair, Paths> { TODO() } } diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index f89d8bd..e110782 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -2,17 +2,17 @@ package graph import model.graph.edges.Edge -abstract class Graph>() { - protected val graph = mutableMapOf>() +abstract class Graph() { + protected val graph = mutableMapOf>>() val entries get() = graph.entries val vertices get() = graph.keys - val edges: List + val edges: List> get() { - val edges = mutableListOf() + val edges = mutableListOf>() for (vertex in vertices) { val edgesOf = edgesOf(vertex) edges.addAll(edgesOf) @@ -24,15 +24,17 @@ abstract class Graph>() { private set fun addVertex(vertex: V) { - graph.putIfAbsent(vertex, mutableListOf()) + graph.putIfAbsent(vertex, mutableListOf>()) size++ } - fun edgesOf(from: V): MutableList { + abstract fun addEdge(from: V, to: V, weight: Int) + + fun edgesOf(from: V): MutableList> { return graph[from] ?: mutableListOf() } - fun forEach(action: (MutableList) -> Unit) { + fun forEach(action: (MutableList>) -> Unit) { graph.forEach { number, list -> action(list) } } diff --git a/src/main/kotlin/model/graph/directed/DirectedGraph.kt b/src/main/kotlin/model/graph/directed/DirectedGraph.kt new file mode 100644 index 0000000..1fb6d83 --- /dev/null +++ b/src/main/kotlin/model/graph/directed/DirectedGraph.kt @@ -0,0 +1,5 @@ +package model.graph.directed + +import graph.Graph + +abstract class DirectedGraph : Graph() diff --git a/src/main/kotlin/model/graph/unweighted/DirectedGraph.kt b/src/main/kotlin/model/graph/directed/DirectedUnweightedGraph.kt similarity index 50% rename from src/main/kotlin/model/graph/unweighted/DirectedGraph.kt rename to src/main/kotlin/model/graph/directed/DirectedUnweightedGraph.kt index d74e05b..9b6ce61 100644 --- a/src/main/kotlin/model/graph/unweighted/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/directed/DirectedUnweightedGraph.kt @@ -1,10 +1,9 @@ -package model.graph.unweighted +package model.graph.directed -import graph.Graph import model.graph.edges.Edge -class DirectedGraph : UnweightedGraph() { - override fun addEdge(from: V, to: V) { +class DirectedUnweightedGraph : DirectedGraph() { + override fun addEdge(from: V, to: V, weight: Int) { val edge = Edge(from, to) graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } } diff --git a/src/main/kotlin/model/graph/directed/DirectedWeightedGraph.kt b/src/main/kotlin/model/graph/directed/DirectedWeightedGraph.kt new file mode 100644 index 0000000..a30ac65 --- /dev/null +++ b/src/main/kotlin/model/graph/directed/DirectedWeightedGraph.kt @@ -0,0 +1,10 @@ +package model.graph.directed + +import model.graph.edges.Edge + +class DirectedWeightedGraph : DirectedGraph() { + override fun addEdge(from: V, to: V, weight: Int) { + val edge = Edge(from, to, weight) + graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/edges/Edge.kt index afde3e0..7643471 100644 --- a/src/main/kotlin/model/graph/edges/Edge.kt +++ b/src/main/kotlin/model/graph/edges/Edge.kt @@ -1,8 +1,6 @@ package model.graph.edges -open class Edge(val from: V, val to: V) { - open val weight = 1 - +class Edge(val from: V, val to: V, val weight: Int = 1) { override fun equals(other: Any?): Boolean { if (other !is Edge<*>) return false if (from == other.from && to == other.to && weight == other.weight) { diff --git a/src/main/kotlin/model/graph/edges/WeightedEdge.kt b/src/main/kotlin/model/graph/edges/WeightedEdge.kt deleted file mode 100644 index 872bcdd..0000000 --- a/src/main/kotlin/model/graph/edges/WeightedEdge.kt +++ /dev/null @@ -1,6 +0,0 @@ -package model.graph.edges - -class WeightedEdge(source: V, destination: V, _weight: Int) : - Edge(source, destination) { - override val weight: Int = _weight -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/undirected/UndirectedGraph.kt b/src/main/kotlin/model/graph/undirected/UndirectedGraph.kt new file mode 100644 index 0000000..2b6c852 --- /dev/null +++ b/src/main/kotlin/model/graph/undirected/UndirectedGraph.kt @@ -0,0 +1,6 @@ +package model.graph.undirected + +import graph.Graph + +abstract class UndirectedGraph : Graph() { +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/unweighted/UndirectedGraph.kt b/src/main/kotlin/model/graph/undirected/UndirectedUnweightedGraph.kt similarity index 63% rename from src/main/kotlin/model/graph/unweighted/UndirectedGraph.kt rename to src/main/kotlin/model/graph/undirected/UndirectedUnweightedGraph.kt index c6e44c0..8c94621 100644 --- a/src/main/kotlin/model/graph/unweighted/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/undirected/UndirectedUnweightedGraph.kt @@ -1,10 +1,9 @@ -package model.graph.unweighted +package model.graph.undirected -import graph.Graph import model.graph.edges.Edge -class UndirectedGraph : UnweightedGraph() { - override fun addEdge(from: V, to: V) { +class UndirectedUnweightedGraph : UndirectedGraph() { + override fun addEdge(from: V, to: V, weight: Int) { val edge1 = Edge(from, to) val edge2 = Edge(to, from) graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } diff --git a/src/main/kotlin/model/graph/undirected/UndirectedWeightedGraph.kt b/src/main/kotlin/model/graph/undirected/UndirectedWeightedGraph.kt new file mode 100644 index 0000000..e4bed20 --- /dev/null +++ b/src/main/kotlin/model/graph/undirected/UndirectedWeightedGraph.kt @@ -0,0 +1,12 @@ +package model.graph.undirected + +import model.graph.edges.Edge + +class UndirectedWeightedGraph : UndirectedGraph() { + override fun addEdge(from: V, to: V, weight: Int) { + val edge1 = Edge(from, to, weight) + val edge2 = Edge(to, from, weight) + graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } + graph[to]?.add(edge2) ?: { graph[to] = mutableListOf(edge2) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt b/src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt deleted file mode 100644 index 5897cd0..0000000 --- a/src/main/kotlin/model/graph/unweighted/UnweightedGraph.kt +++ /dev/null @@ -1,8 +0,0 @@ -package model.graph.unweighted - -import graph.Graph -import model.graph.edges.Edge - -abstract class UnweightedGraph : Graph>() { - abstract fun addEdge(from: V, to: V) -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt deleted file mode 100644 index 3132649..0000000 --- a/src/main/kotlin/model/graph/weighted/WeightedDirectedGraph.kt +++ /dev/null @@ -1,10 +0,0 @@ -package model.graph.weighted - -import model.graph.edges.WeightedEdge - -class WeightedDirectedGraph : WeightedGraph() { - override fun addEdge(from: V, to: V, weight: Int) { - val edge = WeightedEdge(from, to, weight) - graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/weighted/WeightedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedGraph.kt deleted file mode 100644 index 21ddb45..0000000 --- a/src/main/kotlin/model/graph/weighted/WeightedGraph.kt +++ /dev/null @@ -1,8 +0,0 @@ -package model.graph.weighted - -import graph.Graph -import model.graph.edges.WeightedEdge - -abstract class WeightedGraph : Graph>() { - abstract fun addEdge(from: V, to: V, weight: Int) -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt b/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt deleted file mode 100644 index 01e1311..0000000 --- a/src/main/kotlin/model/graph/weighted/WeightedUndirectedGraph.kt +++ /dev/null @@ -1,12 +0,0 @@ -package model.graph.weighted - -import model.graph.edges.WeightedEdge - -class WeightedUndirectedGraph : WeightedGraph() { - override fun addEdge(from: V, to: V, weight: Int) { - val edge1 = WeightedEdge(from, to, weight) - val edge2 = WeightedEdge(to, from, weight) - graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } - graph[from]?.add(edge2) ?: { graph[from] = mutableListOf(edge2) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt index 7d0f817..8f1ad8b 100644 --- a/src/main/kotlin/viewmodel/GraphViewModel.kt +++ b/src/main/kotlin/viewmodel/GraphViewModel.kt @@ -1,15 +1,14 @@ package viewmodel -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import model.graph.unweighted.UndirectedGraph +import graph.Graph import model.graph.edges.Edge +import model.graph.undirected.UndirectedWeightedGraph class GraphViewModel>( _name: String, - graph: UndirectedGraph = UndirectedGraph() + graph: Graph = UndirectedWeightedGraph() ) : ViewModel() { val name = _name val size @@ -33,7 +32,7 @@ class GraphViewModel>( if (graphView[from] == null) { return } - for (i in graphView[from]?.edges!!) if(i.to == to) return + for (i in graphView[from]?.edges!!) if (i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! edgesCopy.add(Edge(from, to)) graphView[from]?.edges = edgesCopy diff --git a/src/test/kotlin/algos/fordbellman/FordBellman.kt b/src/test/kotlin/algos/fordbellman/FordBellman.kt index 5bf808a..966afe8 100644 --- a/src/test/kotlin/algos/fordbellman/FordBellman.kt +++ b/src/test/kotlin/algos/fordbellman/FordBellman.kt @@ -1,16 +1,15 @@ package algos.fordbellman import model.algos.FordBellman +import model.graph.directed.DirectedWeightedGraph import model.graph.edges.Edge -import model.graph.edges.WeightedEdge -import model.graph.weighted.WeightedDirectedGraph import kotlin.test.* internal class FordBellmanTest { @Test fun basicFind() { - val graph = WeightedDirectedGraph() + val graph = DirectedWeightedGraph() for (i in 1..4) { graph.addVertex(i) } @@ -34,9 +33,9 @@ internal class FordBellmanTest { ) val pathExpected = listOf>( - WeightedEdge(1, 3, 2), - WeightedEdge(3, 2, 3), - WeightedEdge(2, 4, 1) + Edge(1, 3, 2), + Edge(3, 2, 3), + Edge(2, 4, 1) ) val pathActual = result.second assertContentEquals(pathExpected, pathActual, "") From 8950099f96a4cfe7aa8daab2e2552c638b1f45d9 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 20 May 2024 00:30:05 +0300 Subject: [PATCH 081/172] Add settings and localisationError.log in .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9a47b5e..3ea2fcc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +src/main/kotlin/**/localisationError.log +src/main/kotlin/**/settings.json ### IntelliJ IDEA ### .idea/ From 6ab792a5ad3ddad17b933fa09d6c488249007f1c Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 20 May 2024 05:37:40 -0400 Subject: [PATCH 082/172] fix: screen division bug fix --- src/main/kotlin/Navigation.kt | 17 +++++++++-------- src/main/kotlin/view/screens/MainScreen.kt | 8 ++++---- .../kotlin/viewmodel/MainScreenViewModel.kt | 2 -- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt index faa6b94..d04744e 100644 --- a/src/main/kotlin/Navigation.kt +++ b/src/main/kotlin/Navigation.kt @@ -20,39 +20,40 @@ fun Navigation() { MainScreen(navController = navController, mainScreenViewModel) } composable( - route = "${Screen.UndirectedUnweightedGraphScreen.route}/{type}/{graphId}", + route = "${Screen.UndirectedUnweightedGraphScreen.route}/{graphId}", arguments = listOf(navArgument("graphId") { type = NavType.IntType }) ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/UU/$graphId") + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") + println("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") } } composable( - route = "${Screen.DirectedUnweightedGraphScreen.route}/{type}/{graphId}", + route = "${Screen.DirectedUnweightedGraphScreen.route}/{graphId}", arguments = listOf(navArgument("graphId") { type = NavType.IntType }) ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/DU/$graphId") + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") } } composable( - route = "${Screen.UndirectedWeightedGraphScreen.route}/{type}/{graphId}", + route = "${Screen.UndirectedWeightedGraphScreen.route}/{graphId}", arguments = listOf(navArgument("graphId") { type = NavType.IntType }) ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/UW/$graphId") + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") } } composable( - route = "${Screen.DirectedWeightedGraphScreen.route}/{type}/{graphId}", + route = "${Screen.DirectedWeightedGraphScreen.route}/{graphId}", arguments = listOf(navArgument("graphId") { type = NavType.IntType }) ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/DW/$graphId") + navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") } } composable(route = Screen.SettingsScreen.route){ diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 87d1e05..c03ee38 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -272,10 +272,10 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( onClick = { navController.navigate(when(mainScreenViewModel.graphs.typeList[index]){ - MainScreenViewModel.ViewModelType.UU -> "${Screen.UndirectedUnweightedGraphScreen.route}/UU/$index" - MainScreenViewModel.ViewModelType.DU -> "${Screen.DirectedWeightedGraphScreen.route}/DU/$index" - MainScreenViewModel.ViewModelType.UW -> "${Screen.UndirectedWeightedGraphScreen.route}/UW/$index" - MainScreenViewModel.ViewModelType.DW -> "${Screen.DirectedWeightedGraphScreen.route}/DW/$index" + MainScreenViewModel.ViewModelType.UU -> "${Screen.UndirectedUnweightedGraphScreen.route}/$index" + MainScreenViewModel.ViewModelType.DU -> "${Screen.DirectedWeightedGraphScreen.route}/$index" + MainScreenViewModel.ViewModelType.UW -> "${Screen.UndirectedWeightedGraphScreen.route}/$index" + MainScreenViewModel.ViewModelType.DW -> "${Screen.DirectedWeightedGraphScreen.route}/$index" }) }, modifier = Modifier diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 3f79188..a7173dd 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -9,8 +9,6 @@ class MainScreenViewModel : ViewModel() { val graphs = GraphStorage() fun addGraph(name: String, type: Pair) { - println(type.first) - println(type.second) when (type){ Pair("undirected", "unweighted") -> { graphs.typeList.add(ViewModelType.UU) From 81006408b42e77f7b0a2bf969f12b0e0ac2f461b Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 20 May 2024 06:56:17 -0400 Subject: [PATCH 083/172] add: screen type definition --- src/main/kotlin/Navigation.kt | 14 +- src/main/kotlin/settings.json | 2 +- .../screens/DirectedUnweightedGraphScreen.kt | 1 + .../screens/DirectedWeightedGraphScreen.kt | 136 ++++++++++++++++++ .../UndirectedUnweightedGraphScreen.kt | 1 + .../screens/UndirectedWeightedGraphScreen.kt | 136 ++++++++++++++++++ .../kotlin/viewmodel/MainScreenViewModel.kt | 11 +- 7 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt create mode 100644 src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt diff --git a/src/main/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt index d04744e..3978422 100644 --- a/src/main/kotlin/Navigation.kt +++ b/src/main/kotlin/Navigation.kt @@ -5,9 +5,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import view.screens.MainScreen -import view.screens.Screen -import view.screens.SettingsScreen +import view.screens.* import viewmodel.MainScreenViewModel @Composable @@ -25,8 +23,8 @@ fun Navigation() { ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") - println("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") + println(graphId) + UndirectedUnweightedGraphScreen(navController, mainScreenViewModel, graphId) } } composable( @@ -35,7 +33,7 @@ fun Navigation() { ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") + DirectedUnweightedGraphScreen(navController, mainScreenViewModel, graphId) } } composable( @@ -44,7 +42,7 @@ fun Navigation() { ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") + UndirectedWeightedGraphScreen(navController, mainScreenViewModel, graphId) } } composable( @@ -53,7 +51,7 @@ fun Navigation() { ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - navController.navigate("${Screen.UndirectedUnweightedGraphScreen.route}/$graphId") + DirectedWeightedGraphScreen(navController, mainScreenViewModel, graphId) } } composable(route = Screen.SettingsScreen.route){ diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json index 6e3de82..c38add4 100644 --- a/src/main/kotlin/settings.json +++ b/src/main/kotlin/settings.json @@ -1 +1 @@ -{"language":"en-US"} \ No newline at end of file +{"language":"cn-CN"} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt index 07fd298..9353f92 100644 --- a/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt @@ -37,6 +37,7 @@ fun DirectedUnweightedGraphScreen( Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { // To MainScreen + Text(text="DU") Button( onClick = { navController.popBackStack() }, modifier = Modifier diff --git a/src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt new file mode 100644 index 0000000..34f58e5 --- /dev/null +++ b/src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt @@ -0,0 +1,136 @@ +package view.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import localisation.localisation +import model.graph.edges.Edge +import view.DefaultColors +import view.defaultStyle +import view.views.GraphView +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedUnweightedGraphViewModel + +@Composable +fun DirectedWeightedGraphScreen( + navController: NavController, + mainScreenViewModel: MainScreenViewModel, + graphId: Int +) { + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) + + Box(modifier = Modifier.fillMaxSize()) { + GraphView(graphVM) + } + + Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { + // To MainScreen + Text(text="DW") + Button( + onClick = { navController.popBackStack() }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(DefaultColors.primary) + ) { + Text(localisation("home"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Add vertex + Button( + onClick = { graphVM.addVertex(graphVM.size) }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_vertex"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + + Button( + onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("open_edge"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(10.dp)) + + if (isOpenedEdgeMenu) { + Column( + modifier = Modifier + .background(Color(0xffeeeeee)) + .border(3.dp, color = Color.Black) + .padding(10.dp) + + ) { + AddDUEdgeMenu(graphVM) + } + + } + } +} + +@Composable +fun AddDWEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + Row { + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(10.dp)) + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }) + } + + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphModel.addEdge(sourceInt, destinationInt) + } + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_edge"), style = defaultStyle) + } +} diff --git a/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt index 62a4b80..adaf940 100644 --- a/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt @@ -37,6 +37,7 @@ fun UndirectedUnweightedGraphScreen( Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { // To MainScreen + Text(text="UU") Button( onClick = { navController.popBackStack() }, modifier = Modifier diff --git a/src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt new file mode 100644 index 0000000..406d33e --- /dev/null +++ b/src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt @@ -0,0 +1,136 @@ +package view.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.navigation.NavController +import localisation.localisation +import model.graph.edges.Edge +import view.DefaultColors +import view.defaultStyle +import view.views.GraphView +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedUnweightedGraphViewModel + +@Composable +fun UndirectedWeightedGraphScreen( + navController: NavController, + mainScreenViewModel: MainScreenViewModel, + graphId: Int +) { + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) + + Box(modifier = Modifier.fillMaxSize()) { + GraphView(graphVM) + } + + Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { + // To MainScreen + Text(text="UW") + Button( + onClick = { navController.popBackStack() }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(DefaultColors.primary) + ) { + Text(localisation("home"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Add vertex + Button( + onClick = { graphVM.addVertex(graphVM.size) }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_vertex"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(16.dp)) + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + + Button( + onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("open_edge"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(10.dp)) + + if (isOpenedEdgeMenu) { + Column( + modifier = Modifier + .background(Color(0xffeeeeee)) + .border(3.dp, color = Color.Black) + .padding(10.dp) + + ) { + AddDUEdgeMenu(graphVM) + } + + } + } +} + +@Composable +fun AddUWEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + Row { + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(10.dp)) + TextField( + modifier = Modifier + .width(115.dp) + .border(3.dp, color = Color.Black), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }) + } + + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphModel.addEdge(sourceInt, destinationInt) + } + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_edge"), style = defaultStyle) + } +} diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index a7173dd..937c88e 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -13,7 +13,6 @@ class MainScreenViewModel : ViewModel() { Pair("undirected", "unweighted") -> { graphs.typeList.add(ViewModelType.UU) graphs.undirectedUnweightedGraphs.add(UndirectedUnweightedGraphViewModel>(name)) - } Pair("directed", "unweighted") -> { graphs.typeList.add(ViewModelType.DU) @@ -45,16 +44,16 @@ class MainScreenViewModel : ViewModel() { fun getName(index: Int) : String{ when(graphs.typeList[index]){ ViewModelType.UU -> { - return graphs.undirectedUnweightedGraphs[index].name + return graphs.undirectedUnweightedGraphs[findGraph(index)].name } ViewModelType.DU -> { - return graphs.directedUnweightedGraphs[index].name + return graphs.directedUnweightedGraphs[findGraph(index)].name } ViewModelType.UW -> { - return graphs.undirectedWeightedGraphs[index].name + return graphs.undirectedWeightedGraphs[findGraph(index)].name } ViewModelType.DW -> { - return graphs.directedWeightedGraphs[index].name + return graphs.directedWeightedGraphs[findGraph(index)].name } } } @@ -74,7 +73,7 @@ class MainScreenViewModel : ViewModel() { for (i in 0..index) if (graphs.typeList[i] == ViewModelType.DW) indexAr += 1 } } - return indexAr + return indexAr - 1 } fun removeAt(index: Int){ when(graphs.typeList[index]){ From 4e3d718c1a72ff8678338bac0bd12142f9ad228d Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 20 May 2024 07:13:47 -0400 Subject: [PATCH 084/172] fix: screen bugfix --- .../screens/DirectedUnweightedGraphScreen.kt | 10 +++---- .../screens/DirectedWeightedGraphScreen.kt | 14 +++++----- .../UndirectedUnweightedGraphScreen.kt | 4 +-- .../screens/UndirectedWeightedGraphScreen.kt | 4 +-- src/main/kotlin/view/views/GraphView.kt | 26 ++++++++++++++++++- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt index 9353f92..d76e9ad 100644 --- a/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedUnweightedGraphScreen.kt @@ -19,9 +19,9 @@ import localisation.localisation import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle -import view.views.GraphView +import view.views.GraphViewDU +import viewmodel.DirectedUnweightedGraphViewModel import viewmodel.MainScreenViewModel -import viewmodel.UndirectedUnweightedGraphViewModel @Composable fun DirectedUnweightedGraphScreen( @@ -29,10 +29,10 @@ fun DirectedUnweightedGraphScreen( mainScreenViewModel: MainScreenViewModel, graphId: Int ) { - val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDU(graphId)) Box(modifier = Modifier.fillMaxSize()) { - GraphView(graphVM) + GraphViewDU(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { @@ -95,7 +95,7 @@ fun DirectedUnweightedGraphScreen( } @Composable -fun AddDUEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { +fun AddDUEdgeMenu(graphModel: DirectedUnweightedGraphViewModel>) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } Row { diff --git a/src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt index 34f58e5..af83d05 100644 --- a/src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedWeightedGraphScreen.kt @@ -16,12 +16,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation -import model.graph.edges.Edge +import model.graph.edges.WeightedEdge import view.DefaultColors import view.defaultStyle -import view.views.GraphView +import view.views.GraphViewDW +import viewmodel.DirectedWeightedGraphViewModel import viewmodel.MainScreenViewModel -import viewmodel.UndirectedUnweightedGraphViewModel @Composable fun DirectedWeightedGraphScreen( @@ -29,10 +29,10 @@ fun DirectedWeightedGraphScreen( mainScreenViewModel: MainScreenViewModel, graphId: Int ) { - val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDW(graphId)) Box(modifier = Modifier.fillMaxSize()) { - GraphView(graphVM) + GraphViewDW(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { @@ -87,7 +87,7 @@ fun DirectedWeightedGraphScreen( .padding(10.dp) ) { - AddDUEdgeMenu(graphVM) + AddDWEdgeMenu(graphVM) } } @@ -95,7 +95,7 @@ fun DirectedWeightedGraphScreen( } @Composable -fun AddDWEdgeMenu(graphModel: UndirectedUnweightedGraphViewModel>) { +fun AddDWEdgeMenu(graphModel: DirectedWeightedGraphViewModel>) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } Row { diff --git a/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt index adaf940..cc1d846 100644 --- a/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedUnweightedGraphScreen.kt @@ -19,7 +19,7 @@ import localisation.localisation import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle -import view.views.GraphView +import view.views.GraphViewUU import viewmodel.MainScreenViewModel import viewmodel.UndirectedUnweightedGraphViewModel @@ -32,7 +32,7 @@ fun UndirectedUnweightedGraphScreen( val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) Box(modifier = Modifier.fillMaxSize()) { - GraphView(graphVM) + GraphViewUU(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { diff --git a/src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt index 406d33e..a0fcf82 100644 --- a/src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedWeightedGraphScreen.kt @@ -19,7 +19,7 @@ import localisation.localisation import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle -import view.views.GraphView +import view.views.GraphViewUW import viewmodel.MainScreenViewModel import viewmodel.UndirectedUnweightedGraphViewModel @@ -32,7 +32,7 @@ fun UndirectedWeightedGraphScreen( val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUU(graphId)) Box(modifier = Modifier.fillMaxSize()) { - GraphView(graphVM) + GraphViewUW(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index d8e632a..85df876 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -2,10 +2,34 @@ package view.views import androidx.compose.runtime.Composable import model.graph.edges.Edge +import model.graph.edges.WeightedEdge +import viewmodel.DirectedUnweightedGraphViewModel +import viewmodel.DirectedWeightedGraphViewModel import viewmodel.UndirectedUnweightedGraphViewModel @Composable -fun GraphView(graphViewModel: UndirectedUnweightedGraphViewModel>) { +fun GraphViewUU(graphViewModel: UndirectedUnweightedGraphViewModel>) { + for (vertexVM in graphViewModel.graphView.values) { + VertexView(vertexVM, graphViewModel) + } +} + +@Composable +fun GraphViewDU(graphViewModel: DirectedUnweightedGraphViewModel>) { + for (vertexVM in graphViewModel.graphView.values) { + VertexView(vertexVM, graphViewModel) + } +} + +@Composable +fun GraphViewUW(graphViewModel: UndirectedUnweightedGraphViewModel>) { + for (vertexVM in graphViewModel.graphView.values) { + VertexView(vertexVM, graphViewModel) + } +} + +@Composable +fun GraphViewDW(graphViewModel: DirectedWeightedGraphViewModel>) { for (vertexVM in graphViewModel.graphView.values) { VertexView(vertexVM, graphViewModel) } From b015e3c9283665b83d44022eb5c932767c7bcfd5 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 20 May 2024 07:42:34 -0400 Subject: [PATCH 085/172] ref: graph restructurisation --- .../DirectedWeightedGraph.kt => DirectedGraph.kt} | 8 +++++--- src/main/kotlin/model/graph/Graph.kt | 2 ++ ...UndirectedWeightedGraph.kt => UndirectedGraph.kt} | 5 +++-- .../kotlin/model/graph/directed/DirectedGraph.kt | 5 ----- .../model/graph/directed/DirectedUnweightedGraph.kt | 10 ---------- .../kotlin/model/graph/undirected/UndirectedGraph.kt | 6 ------ .../graph/undirected/UndirectedUnweightedGraph.kt | 12 ------------ src/main/kotlin/viewmodel/GraphViewModel.kt | 4 ++-- src/test/kotlin/algos/fordbellman/FordBellman.kt | 9 ++++++--- 9 files changed, 18 insertions(+), 43 deletions(-) rename src/main/kotlin/model/graph/{directed/DirectedWeightedGraph.kt => DirectedGraph.kt} (63%) rename src/main/kotlin/model/graph/{undirected/UndirectedWeightedGraph.kt => UndirectedGraph.kt} (78%) delete mode 100644 src/main/kotlin/model/graph/directed/DirectedGraph.kt delete mode 100644 src/main/kotlin/model/graph/directed/DirectedUnweightedGraph.kt delete mode 100644 src/main/kotlin/model/graph/undirected/UndirectedGraph.kt delete mode 100644 src/main/kotlin/model/graph/undirected/UndirectedUnweightedGraph.kt diff --git a/src/main/kotlin/model/graph/directed/DirectedWeightedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt similarity index 63% rename from src/main/kotlin/model/graph/directed/DirectedWeightedGraph.kt rename to src/main/kotlin/model/graph/DirectedGraph.kt index a30ac65..e3a847d 100644 --- a/src/main/kotlin/model/graph/directed/DirectedWeightedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,10 +1,12 @@ -package model.graph.directed +package model.graph +import graph.Graph import model.graph.edges.Edge -class DirectedWeightedGraph : DirectedGraph() { +class DirectedGraph : Graph(){ override fun addEdge(from: V, to: V, weight: Int) { + if (weight != 1) unweighted = false val edge = Edge(from, to, weight) graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index e110782..2e55589 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -6,6 +6,8 @@ abstract class Graph() { protected val graph = mutableMapOf>>() val entries get() = graph.entries + protected var unweighted = true + val vertices get() = graph.keys diff --git a/src/main/kotlin/model/graph/undirected/UndirectedWeightedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt similarity index 78% rename from src/main/kotlin/model/graph/undirected/UndirectedWeightedGraph.kt rename to src/main/kotlin/model/graph/UndirectedGraph.kt index e4bed20..d1895b9 100644 --- a/src/main/kotlin/model/graph/undirected/UndirectedWeightedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,8 +1,9 @@ -package model.graph.undirected +package model.graph +import graph.Graph import model.graph.edges.Edge -class UndirectedWeightedGraph : UndirectedGraph() { +class UndirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { val edge1 = Edge(from, to, weight) val edge2 = Edge(to, from, weight) diff --git a/src/main/kotlin/model/graph/directed/DirectedGraph.kt b/src/main/kotlin/model/graph/directed/DirectedGraph.kt deleted file mode 100644 index 1fb6d83..0000000 --- a/src/main/kotlin/model/graph/directed/DirectedGraph.kt +++ /dev/null @@ -1,5 +0,0 @@ -package model.graph.directed - -import graph.Graph - -abstract class DirectedGraph : Graph() diff --git a/src/main/kotlin/model/graph/directed/DirectedUnweightedGraph.kt b/src/main/kotlin/model/graph/directed/DirectedUnweightedGraph.kt deleted file mode 100644 index 9b6ce61..0000000 --- a/src/main/kotlin/model/graph/directed/DirectedUnweightedGraph.kt +++ /dev/null @@ -1,10 +0,0 @@ -package model.graph.directed - -import model.graph.edges.Edge - -class DirectedUnweightedGraph : DirectedGraph() { - override fun addEdge(from: V, to: V, weight: Int) { - val edge = Edge(from, to) - graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/undirected/UndirectedGraph.kt b/src/main/kotlin/model/graph/undirected/UndirectedGraph.kt deleted file mode 100644 index 2b6c852..0000000 --- a/src/main/kotlin/model/graph/undirected/UndirectedGraph.kt +++ /dev/null @@ -1,6 +0,0 @@ -package model.graph.undirected - -import graph.Graph - -abstract class UndirectedGraph : Graph() { -} \ No newline at end of file diff --git a/src/main/kotlin/model/graph/undirected/UndirectedUnweightedGraph.kt b/src/main/kotlin/model/graph/undirected/UndirectedUnweightedGraph.kt deleted file mode 100644 index 8c94621..0000000 --- a/src/main/kotlin/model/graph/undirected/UndirectedUnweightedGraph.kt +++ /dev/null @@ -1,12 +0,0 @@ -package model.graph.undirected - -import model.graph.edges.Edge - -class UndirectedUnweightedGraph : UndirectedGraph() { - override fun addEdge(from: V, to: V, weight: Int) { - val edge1 = Edge(from, to) - val edge2 = Edge(to, from) - graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } - graph[to]?.add(edge2) ?: { graph[to] = mutableListOf(edge2) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/GraphViewModel.kt b/src/main/kotlin/viewmodel/GraphViewModel.kt index 8f1ad8b..630da38 100644 --- a/src/main/kotlin/viewmodel/GraphViewModel.kt +++ b/src/main/kotlin/viewmodel/GraphViewModel.kt @@ -3,12 +3,12 @@ package viewmodel import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.ViewModel import graph.Graph +import model.graph.UndirectedGraph import model.graph.edges.Edge -import model.graph.undirected.UndirectedWeightedGraph class GraphViewModel>( _name: String, - graph: Graph = UndirectedWeightedGraph() + graph: Graph = UndirectedGraph() ) : ViewModel() { val name = _name val size diff --git a/src/test/kotlin/algos/fordbellman/FordBellman.kt b/src/test/kotlin/algos/fordbellman/FordBellman.kt index 966afe8..8f9dc11 100644 --- a/src/test/kotlin/algos/fordbellman/FordBellman.kt +++ b/src/test/kotlin/algos/fordbellman/FordBellman.kt @@ -1,15 +1,18 @@ package algos.fordbellman import model.algos.FordBellman -import model.graph.directed.DirectedWeightedGraph +import model.graph.DirectedGraph import model.graph.edges.Edge -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull internal class FordBellmanTest { @Test fun basicFind() { - val graph = DirectedWeightedGraph() + val graph = DirectedGraph() for (i in 1..4) { graph.addVertex(i) } From 45a6238862cc58a173e1091fc4b46e9c6ac0f2a6 Mon Sep 17 00:00:00 2001 From: Aleksei Dmitrievstev <93659834+admitrievtsev@users.noreply.github.com> Date: Mon, 20 May 2024 14:47:42 +0300 Subject: [PATCH 086/172] fix: undirected weigted type fix --- src/main/kotlin/model/graph/UndirectedGraph.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index d1895b9..bb23877 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -5,9 +5,10 @@ import model.graph.edges.Edge class UndirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { + if (weight != 1) unweighted = false val edge1 = Edge(from, to, weight) val edge2 = Edge(to, from, weight) graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } graph[to]?.add(edge2) ?: { graph[to] = mutableListOf(edge2) } } -} \ No newline at end of file +} From 1780bad30f65c777b9014bd426981d1354e84afe Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 20 May 2024 14:57:47 +0300 Subject: [PATCH 087/172] Ref: change addEdge behaviour --- src/main/kotlin/model/graph/DirectedGraph.kt | 4 ++-- src/main/kotlin/model/graph/Graph.kt | 4 ++-- src/main/kotlin/model/graph/UndirectedGraph.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index e3a847d..e006b4e 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -3,9 +3,9 @@ package model.graph import graph.Graph import model.graph.edges.Edge -class DirectedGraph : Graph(){ +class DirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { - if (weight != 1) unweighted = false + if (weight != 1) weighted = true val edge = Edge(from, to, weight) graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } } diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 2e55589..a62c8d2 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -6,7 +6,7 @@ abstract class Graph() { protected val graph = mutableMapOf>>() val entries get() = graph.entries - protected var unweighted = true + protected var weighted = false val vertices @@ -30,7 +30,7 @@ abstract class Graph() { size++ } - abstract fun addEdge(from: V, to: V, weight: Int) + abstract fun addEdge(from: V, to: V, weight: Int = 1) fun edgesOf(from: V): MutableList> { return graph[from] ?: mutableListOf() diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index bb23877..5ca2242 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -5,7 +5,7 @@ import model.graph.edges.Edge class UndirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { - if (weight != 1) unweighted = false + if (weight != 1) weighted = true val edge1 = Edge(from, to, weight) val edge2 = Edge(to, from, weight) graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } From c0005c2980a6432e22756ba135db97ed87eabcc8 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Mon, 20 May 2024 19:19:33 +0300 Subject: [PATCH 088/172] change: fix imports --- src/main/kotlin/model/algos/Prima.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/model/algos/Prima.kt b/src/main/kotlin/model/algos/Prima.kt index 5cb2668..5df8c51 100644 --- a/src/main/kotlin/model/algos/Prima.kt +++ b/src/main/kotlin/model/algos/Prima.kt @@ -1,12 +1,12 @@ package model.algos -import model.graph.edges.WeightedEdge -import model.graph.weighted.WeightedUndirectedGraph +import model.graph.edges.Edge +import model.graph.UndirectedGraph object Prim { - fun findSpanningTree(graph: WeightedUndirectedGraph): List> { + fun findSpanningTree(graph: UndirectedGraph): List> { val visitedVertices = mutableSetOf() - val edges = mutableListOf>() + val edges = mutableListOf>() val vertex = graph.vertices.random() visitedVertices.add(vertex) @@ -19,11 +19,11 @@ object Prim { visitedVertices.addAll(setOf(nextEdge.from, nextEdge.to)) while (!visitedVertices.containsAll(graph.vertices)) { - val nextEdge = visitedVertices.flatMap { graph.edgesOf(it) } + val edge = visitedVertices.flatMap { graph.edgesOf(it) } .filter { !visitedVertices.contains(it.from) && !visitedVertices.contains(it.to) } .minBy { it.weight } - visitedVertices.addAll(setOf(nextEdge.from, nextEdge.to)) - edges.add(nextEdge) + visitedVertices.addAll(setOf(edge.from, edge.to)) + edges.add(edge) } return edges } From 86f285cfb42659bcb2dcf2c4c7c387d383f9e2ec Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 20 May 2024 13:37:16 -0400 Subject: [PATCH 089/172] fix: graph removing fix --- src/main/kotlin/view/screens/MainScreen.kt | 2 +- src/main/kotlin/viewmodel/MainScreenViewModel.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 8e80f93..38ea1f1 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -261,7 +261,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView // Remove Graph IconButton( - onClick = { mainScreenViewModel.graphs.removeAt(index) }, + onClick = { mainScreenViewModel.graphs.removeGraph(index)}, modifier = Modifier .padding(horizontal = 10.dp) .size(100.dp) diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 262c6af..ac61fe9 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -53,13 +53,15 @@ class MainScreenViewModel : ViewModel() { } return indexAr - 1 } - fun removeAt(index: Int){ + fun removeGraph(index: Int){ when(graphs.typeList[index]){ ViewModelType.Undirect -> { graphs.undirectedGraphs.removeAt(findGraph(index)) + graphs.typeList.removeAt(index) } ViewModelType.Direct -> { graphs.directedGraphs.removeAt(findGraph(index)) + graphs.typeList.removeAt(index) } } From a86e08534b72d2d1adb249e8f724f85d485f75ed Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 20 May 2024 13:46:25 -0400 Subject: [PATCH 090/172] ref: removed unnecessary generics & some code refactor --- .../kotlin/view/screens/DirectedGraphScreen.kt | 3 +-- .../view/screens/UndirectedGraphScreen.kt | 3 +-- src/main/kotlin/view/views/GraphView.kt | 5 ++--- src/main/kotlin/view/views/VertexView.kt | 5 ++--- .../kotlin/viewmodel/DirectedGraphViewModel.kt | 9 ++++----- .../kotlin/viewmodel/MainScreenViewModel.kt | 18 ++++++------------ .../viewmodel/UndirectedGraphViewModel.kt | 5 ++--- 7 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 734728b..12788b8 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation -import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle import view.views.GraphViewDirect @@ -94,7 +93,7 @@ fun DirectedGraphScreen( } @Composable -fun AddDirectedEdgeMenu(graphModel: DirectedGraphViewModel>) { +fun AddDirectedEdgeMenu(graphModel: DirectedGraphViewModel) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } Row { diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 3828e44..98823ab 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation -import model.graph.edges.Edge import view.DefaultColors import view.defaultStyle import view.views.GraphViewUndirect @@ -95,7 +94,7 @@ fun UndirectedGraphScreen( } @Composable -fun AddUUEdgeMenu(graphModel: UndirectedGraphViewModel>) { +fun AddUUEdgeMenu(graphModel: UndirectedGraphViewModel) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } Row { diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index ad4c97e..d831742 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -1,12 +1,11 @@ package view.views import androidx.compose.runtime.Composable -import model.graph.edges.Edge import viewmodel.DirectedGraphViewModel import viewmodel.UndirectedGraphViewModel @Composable -fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel>) { +fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { for (vertexVM in graphViewModel.graphView.values) { UndirectedVertexView(vertexVM, graphViewModel) } @@ -14,7 +13,7 @@ fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel>) @Composable -fun GraphViewDirect(graphViewModel: DirectedGraphViewModel>) { +fun GraphViewDirect(graphViewModel: DirectedGraphViewModel) { for (vertexVM in graphViewModel.graphView.values) { DirectedVertexView(vertexVM, graphViewModel) } diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index fe7f9db..767307b 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -import model.graph.edges.Edge import view.DefaultColors import viewmodel.DirectedGraphViewModel import viewmodel.UndirectedGraphViewModel @@ -28,7 +27,7 @@ import kotlin.math.atan2 import kotlin.math.roundToInt @Composable -fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel>) { +fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel) { val vertex = vertexVM.vertex Box(modifier = Modifier @@ -102,7 +101,7 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie } @Composable -fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel>) { +fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { val vertex = vertexVM.vertex Box(modifier = Modifier diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index de1596a..23fc134 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,12 +1,11 @@ package viewmodel -import model.graph.UndirectedGraph -import model.graph.edges.Edge +import model.graph.DirectedGraph -class DirectedGraphViewModel>( +class DirectedGraphViewModel( _name: String, - graph: UndirectedGraph = UndirectedGraph() -): AbstractGraphViewModel>(graph){ + graph: DirectedGraph = DirectedGraph() +): AbstractGraphViewModel>(graph){ val name = _name init { for (vertex in graphModel.entries) { diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index ac61fe9..9f167e7 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -2,7 +2,6 @@ package viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel -import model.graph.edges.Edge class MainScreenViewModel : ViewModel() { val graphs = GraphStorage() @@ -11,20 +10,15 @@ class MainScreenViewModel : ViewModel() { when (type){ "undirected" -> { graphs.typeList.add(ViewModelType.Undirect) - graphs.undirectedGraphs.add(UndirectedGraphViewModel>(name)) + graphs.undirectedGraphs.add(UndirectedGraphViewModel(name)) } "directed" -> { graphs.typeList.add(ViewModelType.Direct) - graphs.directedGraphs.add(DirectedGraphViewModel>(name)) + graphs.directedGraphs.add(DirectedGraphViewModel(name)) } } } - /* - fun getGraph(graphId: Int): UndirectedUnweightedGraphViewModel> { - return graphs[graphId] - } - */ enum class ViewModelType(){ Undirect, Direct, @@ -66,15 +60,15 @@ class MainScreenViewModel : ViewModel() { } } - fun getUndirect(index: Int) : UndirectedGraphViewModel>{ + fun getUndirect(index: Int) : UndirectedGraphViewModel{ return undirectedGraphs[findGraph(index)] } - fun getDirect(index: Int) : DirectedGraphViewModel>{ + fun getDirect(index: Int) : DirectedGraphViewModel{ return directedGraphs[findGraph(index)] } - var undirectedGraphs = mutableStateListOf>>() - var directedGraphs = mutableStateListOf>>() + var undirectedGraphs = mutableStateListOf>() + var directedGraphs = mutableStateListOf>() var typeList = mutableStateListOf() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index dfa377c..cecef69 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -1,11 +1,10 @@ package viewmodel import model.graph.UndirectedGraph -import model.graph.edges.Edge -class UndirectedGraphViewModel>( +class UndirectedGraphViewModel( _name: String, - graph: UndirectedGraph = UndirectedGraph() + graph: UndirectedGraph = UndirectedGraph() ): AbstractGraphViewModel>(graph){ val name = _name init { From 4c32c7886fb48889a616a9641df31a5adcadb308 Mon Sep 17 00:00:00 2001 From: Aleksei Dmitrievstev <93659834+admitrievtsev@users.noreply.github.com> Date: Mon, 20 May 2024 13:53:42 -0400 Subject: [PATCH 091/172] ref: undo default language set as Chinese --- src/main/kotlin/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json index c38add4..5fdb268 100644 --- a/src/main/kotlin/settings.json +++ b/src/main/kotlin/settings.json @@ -1 +1 @@ -{"language":"cn-CN"} \ No newline at end of file +{"language":"en-US"} From 12b70d8deb27f7707f98f4992fabec5cc0163ffb Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Mon, 20 May 2024 21:04:05 +0300 Subject: [PATCH 092/172] feat: add find cycles algorithms on undirected graph - change algoritms name - FCADFS has two algorithms built on DFS, you need to choose one that is more suitable in terms of time and memory --- src/main/kotlin/model/algos/FCADFS.kt | 94 +++++++++++++++++++ .../model/algos/{Prima.kt => MSTPrim.kt} | 2 +- .../{FordBellman.kt => SPAFordBellman.kt} | 2 +- .../kotlin/algos/fordbellman/FordBellman.kt | 6 +- 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/model/algos/FCADFS.kt rename src/main/kotlin/model/algos/{Prima.kt => MSTPrim.kt} (98%) rename src/main/kotlin/model/algos/{FordBellman.kt => SPAFordBellman.kt} (98%) diff --git a/src/main/kotlin/model/algos/FCADFS.kt b/src/main/kotlin/model/algos/FCADFS.kt new file mode 100644 index 0000000..cefa067 --- /dev/null +++ b/src/main/kotlin/model/algos/FCADFS.kt @@ -0,0 +1,94 @@ +package model.algos + +import model.graph.UndirectedGraph +import model.graph.edges.Edge + +object FCADFS { + fun findCycleUtil( + vertex: V, + visited: MutableMap, + parent: V?, + graph: UndirectedGraph, + path: MutableList> + ): List>? { + visited[vertex] = true + + graph.edgesOf(vertex).forEach { edge -> + val next = if (edge.from == vertex) edge.to else edge.from + if (visited[next] != true) { + path.add(edge) + val cyclePath = findCycleUtil(next, visited, vertex, graph, path) + if (cyclePath != null) { + return cyclePath + } + path.removeAt(path.size - 1) + } else if (parent != next) { + path.add(edge) + return path.dropWhile { it.from != next && it.to != next }.toList() + } + } + return null + } + + fun findCycle(graph: UndirectedGraph): List>? { + val visited = mutableMapOf() + val path = mutableListOf>() + + for (vertex in graph.vertices) { + if (visited[vertex] != true) { + val cyclePath = findCycleUtil(vertex, visited, null, graph, path) + if (cyclePath != null) { + return cyclePath + } + } + } + return null + } +} + + + + +// +//object FCADFS { +// fun findCycleUtil( +// v: V, +// visited: MutableMap, +// parent: V?, +// path: MutableList>, +// graph: UndirectedGraph +// ): List>? { +// visited[v] = true +// +// for (edge in graph.edgesOf(v)) { +// val next = if (edge.from == v) edge.to else edge.from +// if (visited[next] != true) { +// path.add(edge) +// val cyclePath = findCycleUtil(next, visited, v, path, graph) +// if (cyclePath != null) { +// return cyclePath +// } +// path.removeAt(path.size - 1) +// } else if (parent != next) { +// path.add(edge) +// return path.dropWhile { it.from != next && it.to != next }.toList() +// } +// } +// return null +// } +// +// fun findCycle(graph: UndirectedGraph): List>? { +// val visited = mutableMapOf() +// val path = mutableListOf>() +// +// for (vertex in graph.vertices) { +// if (visited[vertex] != true) { +// val cyclePath = findCycleUtil(vertex, visited, null, path, graph) +// if (cyclePath != null) { +// return cyclePath +// } +// } +// } +// return null +// } +//} \ No newline at end of file diff --git a/src/main/kotlin/model/algos/Prima.kt b/src/main/kotlin/model/algos/MSTPrim.kt similarity index 98% rename from src/main/kotlin/model/algos/Prima.kt rename to src/main/kotlin/model/algos/MSTPrim.kt index 5df8c51..083431e 100644 --- a/src/main/kotlin/model/algos/Prima.kt +++ b/src/main/kotlin/model/algos/MSTPrim.kt @@ -3,7 +3,7 @@ package model.algos import model.graph.edges.Edge import model.graph.UndirectedGraph -object Prim { +object MSTPrim { fun findSpanningTree(graph: UndirectedGraph): List> { val visitedVertices = mutableSetOf() val edges = mutableListOf>() diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/SPAFordBellman.kt similarity index 98% rename from src/main/kotlin/model/algos/FordBellman.kt rename to src/main/kotlin/model/algos/SPAFordBellman.kt index 35aba4b..289cbde 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/SPAFordBellman.kt @@ -6,7 +6,7 @@ import model.graph.edges.Edge typealias Path = List> typealias Paths = Map> -object FordBellman { +object SPAFordBellman { //should be : Pair> fun findShortestPath(from: V, to: V, graph: Graph): Pair?> { val distances = mutableMapOf() diff --git a/src/test/kotlin/algos/fordbellman/FordBellman.kt b/src/test/kotlin/algos/fordbellman/FordBellman.kt index 8f9dc11..c9cad98 100644 --- a/src/test/kotlin/algos/fordbellman/FordBellman.kt +++ b/src/test/kotlin/algos/fordbellman/FordBellman.kt @@ -1,6 +1,6 @@ package algos.fordbellman -import model.algos.FordBellman +import model.algos.SPAFordBellman import model.graph.DirectedGraph import model.graph.edges.Edge import kotlin.test.Test @@ -8,7 +8,7 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertNotNull -internal class FordBellmanTest { +internal class SPAFordBellmanTest { @Test fun basicFind() { @@ -25,7 +25,7 @@ internal class FordBellmanTest { this.addEdge(4, 3, -1) } - val result = FordBellman.findShortestPath(1, 4, graph) + val result = SPAFordBellman.findShortestPath(1, 4, graph) val shortestExpected = 6 val shortestActual = result.first From c9a3747409dd1786d7242d1ece9a33d7058e604c Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Mon, 20 May 2024 21:18:53 +0300 Subject: [PATCH 093/172] feat: add search for a specific vertex - Now the algorithm searches for cycles along a given vertex, and not throughout the entire graph --- src/main/kotlin/model/algos/FCADFS.kt | 112 +++++++------------------- 1 file changed, 31 insertions(+), 81 deletions(-) diff --git a/src/main/kotlin/model/algos/FCADFS.kt b/src/main/kotlin/model/algos/FCADFS.kt index cefa067..1dae626 100644 --- a/src/main/kotlin/model/algos/FCADFS.kt +++ b/src/main/kotlin/model/algos/FCADFS.kt @@ -3,92 +3,42 @@ package model.algos import model.graph.UndirectedGraph import model.graph.edges.Edge -object FCADFS { - fun findCycleUtil( - vertex: V, - visited: MutableMap, - parent: V?, - graph: UndirectedGraph, - path: MutableList> - ): List>? { - visited[vertex] = true - - graph.edgesOf(vertex).forEach { edge -> - val next = if (edge.from == vertex) edge.to else edge.from - if (visited[next] != true) { - path.add(edge) - val cyclePath = findCycleUtil(next, visited, vertex, graph, path) - if (cyclePath != null) { - return cyclePath - } - path.removeAt(path.size - 1) - } else if (parent != next) { - path.add(edge) - return path.dropWhile { it.from != next && it.to != next }.toList() +fun findCycleUtil( + graph: UndirectedGraph, + startVertex: V, + visited: MutableMap, + parent: V?, + path: MutableList> +): List>? { + visited[startVertex] = true + + graph.edgesOf(startVertex).forEach { edge -> + val next = if (edge.from == startVertex) edge.to else edge.from + if (visited[next] != true) { + path.add(edge) + val cyclePath = findCycleUtil(graph, next, visited, startVertex, path) + if (cyclePath != null) { + return cyclePath } + path.removeAt(path.size - 1) + } else if (parent != next) { + path.add(edge) + return path.dropWhile { it.from != next && it.to != next }.toList() } - return null } + return null +} - fun findCycle(graph: UndirectedGraph): List>? { - val visited = mutableMapOf() - val path = mutableListOf>() +fun findCycle(graph: UndirectedGraph, start: V): List>? { + val visited = mutableMapOf() + val path = mutableListOf>() - for (vertex in graph.vertices) { - if (visited[vertex] != true) { - val cyclePath = findCycleUtil(vertex, visited, null, graph, path) - if (cyclePath != null) { - return cyclePath - } - } + if (visited[start] != true) { + val cyclePath = findCycleUtil(graph, start, visited, null, path) + if (cyclePath != null) { + return cyclePath } - return null } -} - - - -// -//object FCADFS { -// fun findCycleUtil( -// v: V, -// visited: MutableMap, -// parent: V?, -// path: MutableList>, -// graph: UndirectedGraph -// ): List>? { -// visited[v] = true -// -// for (edge in graph.edgesOf(v)) { -// val next = if (edge.from == v) edge.to else edge.from -// if (visited[next] != true) { -// path.add(edge) -// val cyclePath = findCycleUtil(next, visited, v, path, graph) -// if (cyclePath != null) { -// return cyclePath -// } -// path.removeAt(path.size - 1) -// } else if (parent != next) { -// path.add(edge) -// return path.dropWhile { it.from != next && it.to != next }.toList() -// } -// } -// return null -// } -// -// fun findCycle(graph: UndirectedGraph): List>? { -// val visited = mutableMapOf() -// val path = mutableListOf>() -// -// for (vertex in graph.vertices) { -// if (visited[vertex] != true) { -// val cyclePath = findCycleUtil(vertex, visited, null, path, graph) -// if (cyclePath != null) { -// return cyclePath -// } -// } -// } -// return null -// } -//} \ No newline at end of file + return null +} \ No newline at end of file From 42e313f03d4efed7eb8eb9bfe404fe609eb80ffc Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Mon, 20 May 2024 14:32:03 -0400 Subject: [PATCH 094/172] fix: fixed undirected graph endge view --- src/main/kotlin/settings.json | 2 +- src/main/kotlin/viewmodel/AbstractGraphViewModel.kt | 6 ++---- src/main/kotlin/viewmodel/DirectedGraphViewModel.kt | 1 + src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt | 9 +++++++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json index 5fdb268..6e3de82 100644 --- a/src/main/kotlin/settings.json +++ b/src/main/kotlin/settings.json @@ -1 +1 @@ -{"language":"en-US"} +{"language":"en-US"} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 3de1329..4022157 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -9,10 +9,8 @@ abstract class AbstractGraphViewModel(graph: G) : ViewModel() { val graphModel = graph var size = 0 - fun addEdge(from: V, to: V) { - if (graphView[from] == null) { - return - } + open fun addEdge(from: V, to: V) { + if (graphView[from] == null) return for (i in graphView[from]?.edges!!) if(i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! edgesCopy.add(Edge(from, to)) diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 23fc134..12f017b 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -14,6 +14,7 @@ class DirectedGraphViewModel( } fun addVertex(vertex: V) { + size += 1 graphView.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) } diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index cecef69..1cd8dc0 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -1,6 +1,7 @@ package viewmodel import model.graph.UndirectedGraph +import model.graph.edges.Edge class UndirectedGraphViewModel( _name: String, @@ -18,5 +19,13 @@ class UndirectedGraphViewModel( graphView.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) } + override fun addEdge(from: V, to: V) { + if (graphView[from] == null) return + for (i in graphView[from]?.edges!!) if(i.to == to) return + val edgesCopy = graphView[from]?.edges?.toMutableList()!! + edgesCopy.add(Edge(from, to)) + edgesCopy.add(Edge(to, from)) + graphView[from]?.edges = edgesCopy + } } \ No newline at end of file From 505def57e0a09b99f31f22ddb8839be0e889710f Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Mon, 20 May 2024 22:54:20 +0300 Subject: [PATCH 095/172] Feat: implemented bridges search -implemented bridges search algo -add tests for bridges search and Ford-Bellman algos -made Edge class data class -add documentation to Ford-Bellman algo --- src/main/kotlin/model/algos/FordBellman.kt | 19 ++- src/main/kotlin/model/algos/SearchBridges.kt | 43 +++++++ src/main/kotlin/model/graph/edges/Edge.kt | 10 +- src/test/kotlin/algos/FordBellman.kt | 115 ++++++++++++++++++ src/test/kotlin/algos/SearchBridges.kt | 58 +++++++++ .../kotlin/algos/fordbellman/FordBellman.kt | 48 -------- 6 files changed, 230 insertions(+), 63 deletions(-) create mode 100644 src/main/kotlin/model/algos/SearchBridges.kt create mode 100644 src/test/kotlin/algos/FordBellman.kt create mode 100644 src/test/kotlin/algos/SearchBridges.kt delete mode 100644 src/test/kotlin/algos/fordbellman/FordBellman.kt diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index 35aba4b..106b7bd 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -7,7 +7,16 @@ typealias Path = List> typealias Paths = Map> object FordBellman { - //should be : Pair> + /** + * @return + * If it is possible to reach the destination , then return Pair(length of shortest path, path) + * + * If it is possible to reach the destination, but graph contains negative cycle, + * than return Pair(null, some path to destination) + * + * If it is not possible to reach the destination, then return Pair(null, null) + * + */ fun findShortestPath(from: V, to: V, graph: Graph): Pair?> { val distances = mutableMapOf() val minSources = mutableMapOf>() @@ -43,7 +52,7 @@ object FordBellman { } var path: MutableList>? = null var curVert = to - if (!lastTimeRelaxed && distances[curVert] != null) { + if (distances[curVert] != null) { path = mutableListOf() while (curVert != from) { val prevEdge = minSources[curVert] @@ -52,12 +61,10 @@ object FordBellman { curVert = prevEdge.from } } + + if (lastTimeRelaxed) distances[to] = null val pathAnswer = path?.reversed()?.toList() return Pair(distances[to], pathAnswer) } - - fun findShortestPath(from: V, graph: Graph): Pair, Paths> { - TODO() - } } diff --git a/src/main/kotlin/model/algos/SearchBridges.kt b/src/main/kotlin/model/algos/SearchBridges.kt new file mode 100644 index 0000000..cd8e6da --- /dev/null +++ b/src/main/kotlin/model/algos/SearchBridges.kt @@ -0,0 +1,43 @@ +package model.algos + +import model.graph.edges.Edge +import model.graph.UndirectedGraph + +fun searchBridges(graph: UndirectedGraph): Set> { + val timeIn = mutableMapOf() + for (vertex in graph.vertices) { + timeIn[vertex] = -1 + } + val ret = mutableMapOf() + var time = 0 + val bridges = mutableSetOf>() + + fun dfs(vertex: V, prevVertex: V) { + timeIn[vertex] = time++ + ret[vertex] = timeIn[vertex]!! + val edges = graph.edgesOf(vertex) + for (edge in edges) { + val destination = edge.to + val timeInDestination = timeIn[destination]!! + if (timeInDestination != -1 && destination != prevVertex && timeInDestination < ret[vertex]!!) { // if back edge + ret[vertex] = timeInDestination + } + if (timeInDestination != -1) { // if visited vertex + continue + } + dfs(destination, vertex) + val retDestination = ret[destination]!! + if (retDestination < ret[vertex]!!) { + ret[vertex] = retDestination + } + if (timeIn[vertex]!! < retDestination) { + bridges.add(edge) + } + } + } + if (graph.vertices.isNotEmpty()) { + dfs(graph.vertices.elementAt(0), graph.vertices.elementAt(0)) + } + + return bridges.toSet() +} diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/edges/Edge.kt index 7643471..a71a52a 100644 --- a/src/main/kotlin/model/graph/edges/Edge.kt +++ b/src/main/kotlin/model/graph/edges/Edge.kt @@ -1,11 +1,3 @@ package model.graph.edges -class Edge(val from: V, val to: V, val weight: Int = 1) { - override fun equals(other: Any?): Boolean { - if (other !is Edge<*>) return false - if (from == other.from && to == other.to && weight == other.weight) { - return true - } - return false - } -} +data class Edge(val from: V, val to: V, val weight: Int = 1) \ No newline at end of file diff --git a/src/test/kotlin/algos/FordBellman.kt b/src/test/kotlin/algos/FordBellman.kt new file mode 100644 index 0000000..16e28b1 --- /dev/null +++ b/src/test/kotlin/algos/FordBellman.kt @@ -0,0 +1,115 @@ +package algos + +import model.algos.FordBellman +import model.graph.DirectedGraph +import model.graph.edges.Edge +import kotlin.test.* + +internal class FordBellmanTest { + + @Test + fun `basic find without negative cycle and possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 3, 2) + this.addEdge(1, 2, 7) + this.addEdge(3, 2, 3) + this.addEdge(2, 4, 1) + this.addEdge(4, 3, -1) + } + + val result = FordBellman.findShortestPath(1, 4, graph) + + val shortestLengthExpected = 6 + val shortestLengthActual = result.first + assertNotNull(shortestLengthActual) + assertEquals( + shortestLengthExpected, shortestLengthActual, + "FordBellman must return weight of the shortest path" + ) + + val pathExpected = listOf>( + Edge(1, 3, 2), + Edge(3, 2, 3), + Edge(2, 4, 1) + ) + val pathActual = result.second + assertContentEquals( + pathExpected, pathActual, + "FordBellman must return shortest path when it is possible to reach destination and there is no negative cycles" + ) + } + + @Test + fun `not possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..6) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2, 8) + this.addEdge(2, 3, 2) + this.addEdge(4, 5, 3) + this.addEdge(5, 6, 5) + this.addEdge(6, 4, 9) + this.addEdge(4, 6, -20) + } + + val result = FordBellman.findShortestPath(1, 4, graph) + + val shortestLengthExpected = null + val shortestLengthActual = result.first + assertEquals( + shortestLengthExpected, shortestLengthActual, + "FordBellman must return null as legnth of path if it is not possible to reach destination" + ) + + val pathExpected = null + val pathActual = result.second + assertContentEquals( + pathExpected, pathActual, + "FordBellman must return null as shortest path if it is not possible to reach destination" + ) + } + + @Test + fun `shortest path with negative cycle`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2, 1) + this.addEdge(2, 3, 4) + this.addEdge(3, 1, -10) + this.addEdge(3, 4, 4) + } + + val result = FordBellman.findShortestPath(1, 4, graph) + + val shortestLengthExpected = null + val shortestLengthActual = result.first + assertEquals( + shortestLengthExpected, shortestLengthActual, + "FordBellman must return null as legnth of path if there is negative cycles" + ) + + val pathActual = result.second + assertNotNull( + pathActual, + "FordBellman must return not null path with negative cycles" + ) + //TODO: implement correctness of returned path with negative cycle + assertTrue( + pathActual.size >= 3, + "FordBellman must return some correct path to destination with negative cycles" + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/algos/SearchBridges.kt b/src/test/kotlin/algos/SearchBridges.kt new file mode 100644 index 0000000..7c9a5be --- /dev/null +++ b/src/test/kotlin/algos/SearchBridges.kt @@ -0,0 +1,58 @@ +package algos + +import model.algos.searchBridges +import model.graph.UndirectedGraph +import model.graph.edges.Edge +import kotlin.test.Test +import kotlin.test.assertTrue + +internal class SearchBridges { + + fun bridgesEquals(bridges1: Set>, bridges2: Set>): Boolean { + for (bridge in bridges1) { + val bridgeReversed = Edge(bridge.to, bridge.from, bridge.weight) + if (bridges2.contains(bridge) || bridges2.contains(bridgeReversed)) + continue + return false + } + return true + } + + @Test + fun `empty graph`() { + val graph = UndirectedGraph() + + val expectedBridges = setOf>() + val actualBridges = searchBridges(graph) + assertTrue( + bridgesEquals(expectedBridges, actualBridges), + "Empty graph must not contain bridges" + ) + } + + @Test + fun `basic bridges search`() { + val graph = UndirectedGraph() + for (i in 1..7) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2) + this.addEdge(1, 3) + this.addEdge(2, 3) + this.addEdge(3, 4) + this.addEdge(4, 5) + this.addEdge(4, 6) + this.addEdge(5, 6) + this.addEdge(6, 7) + } + + val expectedBridges = setOf(Edge(3, 4), Edge(6, 7)) + val actualBridges = searchBridges(graph) + assertTrue( + bridgesEquals(expectedBridges, actualBridges), + "searchBridges must return set of graph bridges" + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/fordbellman/FordBellman.kt b/src/test/kotlin/algos/fordbellman/FordBellman.kt deleted file mode 100644 index 8f9dc11..0000000 --- a/src/test/kotlin/algos/fordbellman/FordBellman.kt +++ /dev/null @@ -1,48 +0,0 @@ -package algos.fordbellman - -import model.algos.FordBellman -import model.graph.DirectedGraph -import model.graph.edges.Edge -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -internal class FordBellmanTest { - - @Test - fun basicFind() { - val graph = DirectedGraph() - for (i in 1..4) { - graph.addVertex(i) - } - - graph.run { - this.addEdge(1, 3, 2) - this.addEdge(1, 2, 7) - this.addEdge(3, 2, 3) - this.addEdge(2, 4, 1) - this.addEdge(4, 3, -1) - } - - val result = FordBellman.findShortestPath(1, 4, graph) - - val shortestExpected = 6 - val shortestActual = result.first - assertNotNull(shortestActual) - assertEquals( - shortestExpected, shortestActual, - "FordBellman with single weight of path must return weight of the shortest path" - ) - - val pathExpected = listOf>( - Edge(1, 3, 2), - Edge(3, 2, 3), - Edge(2, 4, 1) - ) - val pathActual = result.second - assertContentEquals(pathExpected, pathActual, "") - - } - -} \ No newline at end of file From d239d95f50eb20c8e1fb1690dd26ed111421d44a Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Mon, 20 May 2024 23:40:22 +0300 Subject: [PATCH 096/172] feat: add test for FCADFS --- src/main/kotlin/model/algos/FCADFS.kt | 62 +++++++++++++------------- src/test/kotlin/algos/fcadfc/FCADFS.kt | 35 +++++++++++++++ 2 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 src/test/kotlin/algos/fcadfc/FCADFS.kt diff --git a/src/main/kotlin/model/algos/FCADFS.kt b/src/main/kotlin/model/algos/FCADFS.kt index 1dae626..3951ad7 100644 --- a/src/main/kotlin/model/algos/FCADFS.kt +++ b/src/main/kotlin/model/algos/FCADFS.kt @@ -3,42 +3,44 @@ package model.algos import model.graph.UndirectedGraph import model.graph.edges.Edge -fun findCycleUtil( - graph: UndirectedGraph, - startVertex: V, - visited: MutableMap, - parent: V?, - path: MutableList> -): List>? { - visited[startVertex] = true +object FCADFS { + fun findCycleUtil( + graph: UndirectedGraph, + startVertex: V, + visited: MutableMap, + parent: V?, + path: MutableList> + ): List>? { + visited[startVertex] = true - graph.edgesOf(startVertex).forEach { edge -> - val next = if (edge.from == startVertex) edge.to else edge.from - if (visited[next] != true) { - path.add(edge) - val cyclePath = findCycleUtil(graph, next, visited, startVertex, path) - if (cyclePath != null) { - return cyclePath + graph.edgesOf(startVertex).forEach { edge -> + val next = if (edge.from == startVertex) edge.to else edge.from + if (visited[next] != true) { + path.add(edge) + val cyclePath = findCycleUtil(graph, next, visited, startVertex, path) + if (cyclePath != null) { + return cyclePath + } + path.removeAt(path.size - 1) + } else if (parent != next) { + path.add(edge) + return path.dropWhile { it.from != next && it.to != next }.toList() } - path.removeAt(path.size - 1) - } else if (parent != next) { - path.add(edge) - return path.dropWhile { it.from != next && it.to != next }.toList() } + return null } - return null -} -fun findCycle(graph: UndirectedGraph, start: V): List>? { - val visited = mutableMapOf() - val path = mutableListOf>() + fun findCycle(graph: UndirectedGraph, start: V): List>? { + val visited = mutableMapOf() + val path = mutableListOf>() - if (visited[start] != true) { - val cyclePath = findCycleUtil(graph, start, visited, null, path) - if (cyclePath != null) { - return cyclePath + if (visited[start] != true) { + val cyclePath = findCycleUtil(graph, start, visited, null, path) + if (cyclePath != null) { + return cyclePath + } } - } - return null + return null + } } \ No newline at end of file diff --git a/src/test/kotlin/algos/fcadfc/FCADFS.kt b/src/test/kotlin/algos/fcadfc/FCADFS.kt new file mode 100644 index 0000000..b63a4f2 --- /dev/null +++ b/src/test/kotlin/algos/fcadfc/FCADFS.kt @@ -0,0 +1,35 @@ +package algos.fcadfc + +import model.algos.FCADFS +import model.graph.UndirectedGraph +import model.graph.edges.Edge +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotNull + +internal class FCADFS { + + @Test + fun basic() { + val graph = UndirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 3) + this.addEdge(1, 2) + this.addEdge(3, 2) + this.addEdge(3, 4) + } + + val pathActual = FCADFS.findCycle(graph, 2) + assertNotNull(pathActual) + val pathExpected = listOf( + Edge(2, 1), + Edge(1, 3), + Edge(3, 2) + ) + assertContentEquals(pathExpected, pathActual) + } +} \ No newline at end of file From d9e1be26fdc6d912a13d7c224bb6e3b5a3d2b9f5 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Mon, 20 May 2024 23:41:22 +0300 Subject: [PATCH 097/172] feat: add test for Prim - need debug --- src/main/kotlin/model/algos/MSTPrim.kt | 11 +++++---- src/test/kotlin/algos/prim/Prim.kt | 34 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/test/kotlin/algos/prim/Prim.kt diff --git a/src/main/kotlin/model/algos/MSTPrim.kt b/src/main/kotlin/model/algos/MSTPrim.kt index 083431e..30eda57 100644 --- a/src/main/kotlin/model/algos/MSTPrim.kt +++ b/src/main/kotlin/model/algos/MSTPrim.kt @@ -5,24 +5,25 @@ import model.graph.UndirectedGraph object MSTPrim { fun findSpanningTree(graph: UndirectedGraph): List> { - val visitedVertices = mutableSetOf() + val visitedVertices = mutableListOf() val edges = mutableListOf>() val vertex = graph.vertices.random() visitedVertices.add(vertex) val allEdgesOfVertices = visitedVertices.flatMap { graph.edgesOf(it) } - val unvisitedVertices = + val unvisitedEdges = allEdgesOfVertices.filter { !visitedVertices.contains(it.from) || !visitedVertices.contains(it.to) } - val nextEdge = unvisitedVertices.minBy { it.weight } + val nextEdge = unvisitedEdges.minBy { it.weight } visitedVertices.addAll(setOf(nextEdge.from, nextEdge.to)) + edges.add(nextEdge) while (!visitedVertices.containsAll(graph.vertices)) { val edge = visitedVertices.flatMap { graph.edgesOf(it) } - .filter { !visitedVertices.contains(it.from) && !visitedVertices.contains(it.to) } + .filter { !visitedVertices.contains(it.from) || !visitedVertices.contains(it.to) } .minBy { it.weight } - visitedVertices.addAll(setOf(edge.from, edge.to)) + visitedVertices.addAll(listOf(edge.from, edge.to)) edges.add(edge) } return edges diff --git a/src/test/kotlin/algos/prim/Prim.kt b/src/test/kotlin/algos/prim/Prim.kt new file mode 100644 index 0000000..608bf08 --- /dev/null +++ b/src/test/kotlin/algos/prim/Prim.kt @@ -0,0 +1,34 @@ +package algos.prim + +import model.algos.MSTPrim +import model.graph.UndirectedGraph +import model.graph.edges.Edge +import kotlin.test.Test +import kotlin.test.assertNotNull + +internal class Prim { + @Test + fun basic(){ + val graph = UndirectedGraph() + for (i in 0..7) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1 ,2, 1) + this.addEdge(2, 3, 2) + this.addEdge(3, 4, 30) + this.addEdge(4, 5, 12) + this.addEdge(5, 3, 8) + this.addEdge(6, 7, 7) + this.addEdge(6, 3, 2) + this.addEdge(7, 1, 20) + this.addEdge(1, 3, 10) + } + val pathActual = MSTPrim.findSpanningTree(graph) + val pathExpected = listOf( + Edge(1, 2, 10) + ) + + } +} \ No newline at end of file From 149de4ca2b5d40f196e1f9e43fb3ed140437b8e5 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 21 May 2024 00:38:29 +0300 Subject: [PATCH 098/172] change: make function `findCycleUtil` private --- src/main/kotlin/model/algos/FCADFS.kt | 2 +- src/test/kotlin/algos/{fcadfc/FCADFS.kt => fcadfs/FCA.kt} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/test/kotlin/algos/{fcadfc/FCADFS.kt => fcadfs/FCA.kt} (97%) diff --git a/src/main/kotlin/model/algos/FCADFS.kt b/src/main/kotlin/model/algos/FCADFS.kt index 3951ad7..b60d4a9 100644 --- a/src/main/kotlin/model/algos/FCADFS.kt +++ b/src/main/kotlin/model/algos/FCADFS.kt @@ -4,7 +4,7 @@ import model.graph.UndirectedGraph import model.graph.edges.Edge object FCADFS { - fun findCycleUtil( + private fun findCycleUtil( graph: UndirectedGraph, startVertex: V, visited: MutableMap, diff --git a/src/test/kotlin/algos/fcadfc/FCADFS.kt b/src/test/kotlin/algos/fcadfs/FCA.kt similarity index 97% rename from src/test/kotlin/algos/fcadfc/FCADFS.kt rename to src/test/kotlin/algos/fcadfs/FCA.kt index b63a4f2..5fc6aa9 100644 --- a/src/test/kotlin/algos/fcadfc/FCADFS.kt +++ b/src/test/kotlin/algos/fcadfs/FCA.kt @@ -1,4 +1,4 @@ -package algos.fcadfc +package algos.fcadfs import model.algos.FCADFS import model.graph.UndirectedGraph From c91be378797405d92487335f8967bb382b9e1607 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 21 May 2024 00:39:09 +0300 Subject: [PATCH 099/172] feat: replacement with another implementation of the prima algorithm --- src/main/kotlin/model/algos/MSTPrim.kt | 41 ++++++++++++++------------ src/test/kotlin/algos/prim/Prim.kt | 18 ++++++++--- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/model/algos/MSTPrim.kt b/src/main/kotlin/model/algos/MSTPrim.kt index 30eda57..2857150 100644 --- a/src/main/kotlin/model/algos/MSTPrim.kt +++ b/src/main/kotlin/model/algos/MSTPrim.kt @@ -2,30 +2,33 @@ package model.algos import model.graph.edges.Edge import model.graph.UndirectedGraph +import java.util.PriorityQueue object MSTPrim { - fun findSpanningTree(graph: UndirectedGraph): List> { - val visitedVertices = mutableListOf() - val edges = mutableListOf>() + fun findMST(graph: UndirectedGraph, startVertex: V): List> { + val mst = mutableListOf>() + val visited = mutableSetOf() + val edgeQueue = PriorityQueue>() - val vertex = graph.vertices.random() - visitedVertices.add(vertex) - - val allEdgesOfVertices = visitedVertices.flatMap { graph.edgesOf(it) } - val unvisitedEdges = - allEdgesOfVertices.filter { !visitedVertices.contains(it.from) || !visitedVertices.contains(it.to) } + fun addEdges(vertex: V) { + visited.add(vertex) + for (edge in graph.edgesOf(vertex)) { + if (edge.to !in visited) { + edgeQueue.add(edge) + } + } + } - val nextEdge = unvisitedEdges.minBy { it.weight } - visitedVertices.addAll(setOf(nextEdge.from, nextEdge.to)) - edges.add(nextEdge) + addEdges(startVertex) - while (!visitedVertices.containsAll(graph.vertices)) { - val edge = visitedVertices.flatMap { graph.edgesOf(it) } - .filter { !visitedVertices.contains(it.from) || !visitedVertices.contains(it.to) } - .minBy { it.weight } - visitedVertices.addAll(listOf(edge.from, edge.to)) - edges.add(edge) + while (edgeQueue.isNotEmpty()) { + val edge = edgeQueue.poll() + if (edge.to !in visited) { + mst.add(edge) + addEdges(edge.to) + } } - return edges + + return mst } } \ No newline at end of file diff --git a/src/test/kotlin/algos/prim/Prim.kt b/src/test/kotlin/algos/prim/Prim.kt index 608bf08..b10c0cf 100644 --- a/src/test/kotlin/algos/prim/Prim.kt +++ b/src/test/kotlin/algos/prim/Prim.kt @@ -4,18 +4,19 @@ import model.algos.MSTPrim import model.graph.UndirectedGraph import model.graph.edges.Edge import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertNotNull internal class Prim { @Test - fun basic(){ + fun basic() { val graph = UndirectedGraph() for (i in 0..7) { graph.addVertex(i) } graph.run { - this.addEdge(1 ,2, 1) + this.addEdge(1, 2, 1) this.addEdge(2, 3, 2) this.addEdge(3, 4, 30) this.addEdge(4, 5, 12) @@ -25,10 +26,19 @@ internal class Prim { this.addEdge(7, 1, 20) this.addEdge(1, 3, 10) } - val pathActual = MSTPrim.findSpanningTree(graph) + + val pathActual = MSTPrim.findMST(graph, 1) + val pathExpected = listOf( - Edge(1, 2, 10) + Edge(1, 2, 1), + Edge(2, 3, 2), + Edge(3, 6, 2), + Edge(6, 7, 7), + Edge(3, 5, 8), + Edge(5, 4, 12) ) + assertNotNull(pathActual) + assertContentEquals(pathExpected, pathActual) } } \ No newline at end of file From c4d8ed04a48ba871fe248676c4cb01e8b79223a8 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 21 May 2024 00:39:55 +0300 Subject: [PATCH 100/172] feat: `Edge` became comparable --- src/main/kotlin/model/graph/edges/Edge.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/edges/Edge.kt index 7643471..ad0e316 100644 --- a/src/main/kotlin/model/graph/edges/Edge.kt +++ b/src/main/kotlin/model/graph/edges/Edge.kt @@ -1,11 +1,9 @@ package model.graph.edges -class Edge(val from: V, val to: V, val weight: Int = 1) { - override fun equals(other: Any?): Boolean { - if (other !is Edge<*>) return false - if (from == other.from && to == other.to && weight == other.weight) { - return true - } - return false +data class Edge(val from: V, val to: V, val weight: Int = 1) : Comparable> { + + override fun compareTo(other: Edge): Int { + return this.weight - other.weight } + } From 5ab46c9630c9eac610f1d19fed2030eea67c339c Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 21 May 2024 00:40:17 +0300 Subject: [PATCH 101/172] ref: rename class name --- src/test/kotlin/algos/fcadfs/FCA.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/algos/fcadfs/FCA.kt b/src/test/kotlin/algos/fcadfs/FCA.kt index 5fc6aa9..cf51252 100644 --- a/src/test/kotlin/algos/fcadfs/FCA.kt +++ b/src/test/kotlin/algos/fcadfs/FCA.kt @@ -7,7 +7,7 @@ import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertNotNull -internal class FCADFS { +internal class FCA { @Test fun basic() { From f05431681e1a9234d32dd94732561e9773718b02 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 21 May 2024 02:05:16 +0300 Subject: [PATCH 102/172] Ref: class renaming, fix typos --- .../model/algos/{FCADFS.kt => FindCycle.kt} | 2 +- .../{SPAFordBellman.kt => FordBellman.kt} | 2 +- .../model/algos/{MSTPrim.kt => Prim.kt} | 3 +- src/main/kotlin/model/graph/DirectedGraph.kt | 1 - src/main/kotlin/model/graph/Graph.kt | 2 +- .../kotlin/model/graph/UndirectedGraph.kt | 1 - src/main/kotlin/model/graph/edges/Edge.kt | 2 - .../view/screens/DirectedGraphScreen.kt | 4 +- src/main/kotlin/view/screens/MainScreen.kt | 110 +++++++++++------- .../kotlin/view/screens/SettingsScreen.kt | 79 ++++++++----- .../view/screens/UndirectedGraphScreen.kt | 4 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 55 +++++---- .../algos/{fcadfs/FCA.kt => FindCycleTest.kt} | 8 +- .../{FordBellman.kt => FordBellmanTest.kt} | 6 +- .../algos/{prim/Prim.kt => PrimTest.kt} | 8 +- ...{SearchBridges.kt => SearchBridgesTest.kt} | 4 +- 16 files changed, 170 insertions(+), 121 deletions(-) rename src/main/kotlin/model/algos/{FCADFS.kt => FindCycle.kt} (98%) rename src/main/kotlin/model/algos/{SPAFordBellman.kt => FordBellman.kt} (98%) rename src/main/kotlin/model/algos/{MSTPrim.kt => Prim.kt} (94%) rename src/test/kotlin/algos/{fcadfs/FCA.kt => FindCycleTest.kt} (83%) rename src/test/kotlin/algos/{FordBellman.kt => FordBellmanTest.kt} (94%) rename src/test/kotlin/algos/{prim/Prim.kt => PrimTest.kt} (88%) rename src/test/kotlin/algos/{SearchBridges.kt => SearchBridgesTest.kt} (92%) diff --git a/src/main/kotlin/model/algos/FCADFS.kt b/src/main/kotlin/model/algos/FindCycle.kt similarity index 98% rename from src/main/kotlin/model/algos/FCADFS.kt rename to src/main/kotlin/model/algos/FindCycle.kt index b60d4a9..252e93d 100644 --- a/src/main/kotlin/model/algos/FCADFS.kt +++ b/src/main/kotlin/model/algos/FindCycle.kt @@ -3,7 +3,7 @@ package model.algos import model.graph.UndirectedGraph import model.graph.edges.Edge -object FCADFS { +object FindCycle { private fun findCycleUtil( graph: UndirectedGraph, startVertex: V, diff --git a/src/main/kotlin/model/algos/SPAFordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt similarity index 98% rename from src/main/kotlin/model/algos/SPAFordBellman.kt rename to src/main/kotlin/model/algos/FordBellman.kt index 106b7bd..5c4ab5e 100644 --- a/src/main/kotlin/model/algos/SPAFordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -1,6 +1,6 @@ package model.algos -import graph.Graph +import model.graph.Graph import model.graph.edges.Edge typealias Path = List> diff --git a/src/main/kotlin/model/algos/MSTPrim.kt b/src/main/kotlin/model/algos/Prim.kt similarity index 94% rename from src/main/kotlin/model/algos/MSTPrim.kt rename to src/main/kotlin/model/algos/Prim.kt index 2857150..ab27c44 100644 --- a/src/main/kotlin/model/algos/MSTPrim.kt +++ b/src/main/kotlin/model/algos/Prim.kt @@ -4,7 +4,8 @@ import model.graph.edges.Edge import model.graph.UndirectedGraph import java.util.PriorityQueue -object MSTPrim { +object Prim { + // find minimum spanning tree fun findMST(graph: UndirectedGraph, startVertex: V): List> { val mst = mutableListOf>() val visited = mutableSetOf() diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index e006b4e..2be8607 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,6 +1,5 @@ package model.graph -import graph.Graph import model.graph.edges.Edge class DirectedGraph : Graph() { diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index a62c8d2..a548536 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -1,4 +1,4 @@ -package graph +package model.graph import model.graph.edges.Edge diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index 5ca2242..825085d 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,6 +1,5 @@ package model.graph -import graph.Graph import model.graph.edges.Edge class UndirectedGraph : Graph() { diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/edges/Edge.kt index ad0e316..320daec 100644 --- a/src/main/kotlin/model/graph/edges/Edge.kt +++ b/src/main/kotlin/model/graph/edges/Edge.kt @@ -1,9 +1,7 @@ package model.graph.edges data class Edge(val from: V, val to: V, val weight: Int = 1) : Comparable> { - override fun compareTo(other: Edge): Int { return this.weight - other.weight } - } diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 12788b8..8945d46 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -28,7 +28,7 @@ fun DirectedGraphScreen( mainScreenViewModel: MainScreenViewModel, graphId: Int ) { - val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirect(graphId)) + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) Box(modifier = Modifier.fillMaxSize()) { GraphViewDirect(graphVM) @@ -36,7 +36,7 @@ fun DirectedGraphScreen( Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { // To MainScreen - Text(text="Directed") + Text(text = "Directed") Button( onClick = { navController.popBackStack() }, modifier = Modifier diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 38ea1f1..20f104d 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -30,15 +30,15 @@ import view.defaultStyle import viewmodel.MainScreenViewModel @Composable -fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel){ +fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { var search by remember { mutableStateOf("") } var graphName by remember { mutableStateOf("") } val dialogState = remember { mutableStateOf(false) } val optionsDropDown = listOf("undirected", "directed") val expandedDropDown = remember { mutableStateOf(false) } val selectedOptionTextDropDown = remember { mutableStateOf(optionsDropDown[0]) } - - + + Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { @@ -130,7 +130,11 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView state = DialogState(size = DpSize(960.dp, 680.dp)) ) { - Text(text = localisation("enter_new_graph_name"), modifier = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), style = defaultStyle) + Text( + text = localisation("enter_new_graph_name"), + modifier = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + style = defaultStyle + ) TextField( value = graphName, textStyle = bigStyle, @@ -151,44 +155,52 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView unfocusedIndicatorColor = Color.Transparent, ), ) - Button(modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = 20.dp, vertical = 180.dp) - .border( - width = 2.dp, - color = Color.Black, - shape = RoundedCornerShape(25.dp) - ) - .width(300.dp) - .height(60.dp), + Button( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(horizontal = 20.dp, vertical = 180.dp) + .border( + width = 2.dp, + color = Color.Black, + shape = RoundedCornerShape(25.dp) + ) + .width(300.dp) + .height(60.dp), shape = RoundedCornerShape(25.dp), - colors = if(graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.simpleGreen) else ButtonDefaults.buttonColors(backgroundColor = DefaultColors.darkGreen), + colors = if (graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.simpleGreen) else ButtonDefaults.buttonColors( + backgroundColor = DefaultColors.darkGreen + ), onClick = { - if(graphName != ""){ + if (graphName != "") { mainScreenViewModel.addGraph(graphName, selectedOptionTextDropDown.value) dialogState.value = false } }, ) { - Text(text= localisation("add"), color = if(graphName != "") Color.White else Color.Black, fontSize = 28.sp) + Text( + text = localisation("add"), + color = if (graphName != "") Color.White else Color.Black, + fontSize = 28.sp + ) } - Button(modifier = Modifier + Button( + modifier = Modifier - .padding(horizontal = 20.dp, vertical = 260.dp) - .border( - width = 2.dp, - color = Color.Black, - shape = RoundedCornerShape(25.dp) - ) - .width(300.dp) - .height(60.dp), + .padding(horizontal = 20.dp, vertical = 260.dp) + .border( + width = 2.dp, + color = Color.Black, + shape = RoundedCornerShape(25.dp) + ) + .width(300.dp) + .height(60.dp), shape = RoundedCornerShape(25.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red), onClick = { dialogState.value = false }, ) { - Text(text= localisation("back"), color = Color.White, fontSize = 28.sp) + Text(text = localisation("back"), color = Color.White, fontSize = 28.sp) } Box( contentAlignment = Alignment.CenterStart, @@ -225,23 +237,28 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView } } } - - + + } Spacer(modifier = Modifier.height(30.dp)) LazyColumn(modifier = Modifier.fillMaxWidth()) { itemsIndexed(mainScreenViewModel.graphs.typeList) { index, _ -> - if (!mainScreenViewModel.graphs.getName(index).startsWith(search)) return@itemsIndexed + if (!mainScreenViewModel.graphs.getName(index) + .startsWith(search) + ) return@itemsIndexed // To GraphScreen Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( - onClick = { navController.navigate(when(mainScreenViewModel.graphs.typeList[index]){ - MainScreenViewModel.ViewModelType.Undirect -> "${Screen.UndirectedGraphScreen.route}/$index" - MainScreenViewModel.ViewModelType.Direct -> "${Screen.DirectedGraphScreen.route}/$index" - }) - }, + onClick = { + navController.navigate( + when (mainScreenViewModel.graphs.typeList[index]) { + MainScreenViewModel.ViewModelType.Undirected -> "${Screen.UndirectedGraphScreen.route}/$index" + MainScreenViewModel.ViewModelType.Directed -> "${Screen.DirectedGraphScreen.route}/$index" + } + ) + }, modifier = Modifier .fillMaxWidth() .height(100.dp) @@ -254,26 +271,33 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView ), colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { - Text(text = mainScreenViewModel.graphs.getName(index), style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp))) + Text( + text = mainScreenViewModel.graphs.getName(index), + style = bigStyle, + modifier = Modifier.clip(RoundedCornerShape(45.dp)) + ) } Spacer(modifier = Modifier.width(10.dp)) // Remove Graph IconButton( - onClick = { mainScreenViewModel.graphs.removeGraph(index)}, + onClick = { mainScreenViewModel.graphs.removeGraph(index) }, modifier = Modifier .padding(horizontal = 10.dp) .size(100.dp) .clip(shape = RoundedCornerShape(45.dp)) - .background(Color(0xe8,0x08,0x3e)) - .border(5.dp , color = Color.Black, shape = RoundedCornerShape(45.dp)) + .background(Color(0xe8, 0x08, 0x3e)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) .bounceClick(), - ){ + ) { Icon( - Icons.Filled.Delete, contentDescription = "Remove graph", modifier = Modifier - .padding(5.dp) - .fillMaxSize()) + Icons.Filled.Delete, + contentDescription = "Remove graph", + modifier = Modifier + .padding(5.dp) + .fillMaxSize() + ) } } } diff --git a/src/main/kotlin/view/screens/SettingsScreen.kt b/src/main/kotlin/view/screens/SettingsScreen.kt index cd874fd..eda4023 100644 --- a/src/main/kotlin/view/screens/SettingsScreen.kt +++ b/src/main/kotlin/view/screens/SettingsScreen.kt @@ -22,77 +22,96 @@ import view.bounceClick import view.defaultStyle import java.io.File -val pathToSettings = "src/main/kotlin/settings.json" +const val pathToSettings = "src/main/kotlin/settings.json" @Serializable class SettingsJSON(var language: String) -enum class SettingType{ +enum class SettingType { LANGUAGE } -fun resetSettings(){ +fun resetSettings() { File(pathToSettings).writeText(Json.encodeToString(SettingsJSON("en-US"))) } -fun makeSetting(name: SettingType, value: String){ - try{ +fun makeSetting(name: SettingType, value: String) { + try { val data = Json.decodeFromString(File(pathToSettings).readText()) - when (name){ - SettingType.LANGUAGE -> data.language = value; + when (name) { + SettingType.LANGUAGE -> data.language = value } File(pathToSettings).writeText(Json.encodeToString(data)) - } - catch(exception: Exception){ + } catch (exception: Exception) { resetSettings() return } } @Composable -fun SettingsScreen(navController: NavController){ +fun SettingsScreen(navController: NavController) { var language by mutableStateOf(getLocalisation()) - Column{ - Text(text = localisation("settings"), fontSize = 28.sp, modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp)) - Button(onClick = { - makeSetting(SettingType.LANGUAGE, "cn-CN") - language = "cn-CN" - }, + Column { + Text( + text = localisation("settings"), + fontSize = 28.sp, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp) + ) + Button( + onClick = { + makeSetting(SettingType.LANGUAGE, "cn-CN") + language = "cn-CN" + }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) .border(width = if (language == "cn-CN") 5.dp else 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = - if (language =="cn-CN") DefaultColors.primarySelected else DefaultColors.primary)) { + colors = ButtonDefaults.buttonColors( + backgroundColor = + if (language == "cn-CN") DefaultColors.primarySelected else DefaultColors.primary + ) + ) { Text("汉语", style = defaultStyle) } - Button(onClick = { - makeSetting(SettingType.LANGUAGE, "ru-RU") - language = "ru-RU" }, + Button( + onClick = { + makeSetting(SettingType.LANGUAGE, "ru-RU") + language = "ru-RU" + }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) .border(width = if (language == "ru-RU") 5.dp else 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = - if (language =="ru-RU") DefaultColors.primarySelected else DefaultColors.primary)) { + colors = ButtonDefaults.buttonColors( + backgroundColor = + if (language == "ru-RU") DefaultColors.primarySelected else DefaultColors.primary + ) + ) { Text("Русский", style = defaultStyle) } - Button(onClick = { - makeSetting(SettingType.LANGUAGE, "en-US") - language = "en-US"}, + Button( + onClick = { + makeSetting(SettingType.LANGUAGE, "en-US") + language = "en-US" + }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) .border(width = if (language == "en-US") 5.dp else 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = - if (language =="en-US") DefaultColors.primarySelected else DefaultColors.primary)) { + colors = ButtonDefaults.buttonColors( + backgroundColor = + if (language == "en-US") DefaultColors.primarySelected else DefaultColors.primary + ) + ) { Text("English", style = defaultStyle) } - Button(onClick = {navController.navigate(Screen.MainScreen.route)}, + Button( + onClick = { navController.navigate(Screen.MainScreen.route) }, modifier = Modifier .padding(16.dp) .border(width = 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.error)) { + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.error) + ) { Text(localisation("back"), style = defaultStyle, color = Color.White) } } diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 98823ab..6131b62 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -28,7 +28,7 @@ fun UndirectedGraphScreen( mainScreenViewModel: MainScreenViewModel, graphId: Int ) { - val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirect(graphId)) + val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) Box(modifier = Modifier.fillMaxSize()) { GraphViewUndirect(graphVM) @@ -36,7 +36,7 @@ fun UndirectedGraphScreen( Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { // To MainScreen - Text(text="Undirected") + Text(text = "Undirected") Button( onClick = { navController.popBackStack() }, modifier = Modifier diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 9f167e7..a17e65b 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -7,63 +7,72 @@ class MainScreenViewModel : ViewModel() { val graphs = GraphStorage() fun addGraph(name: String, type: String) { - when (type){ + when (type) { "undirected" -> { - graphs.typeList.add(ViewModelType.Undirect) + graphs.typeList.add(ViewModelType.Undirected) graphs.undirectedGraphs.add(UndirectedGraphViewModel(name)) } + "directed" -> { - graphs.typeList.add(ViewModelType.Direct) + graphs.typeList.add(ViewModelType.Directed) graphs.directedGraphs.add(DirectedGraphViewModel(name)) } } } - enum class ViewModelType(){ - Undirect, - Direct, + + enum class ViewModelType() { + Undirected, + Directed, } - inner class GraphStorage(){ - fun getName(index: Int) : String{ - when(graphs.typeList[index]){ - ViewModelType.Undirect -> { + inner class GraphStorage() { + fun getName(index: Int): String { + when (graphs.typeList[index]) { + ViewModelType.Undirected -> { return graphs.undirectedGraphs[findGraph(index)].name } - ViewModelType.Direct -> { + + ViewModelType.Directed -> { return graphs.directedGraphs[findGraph(index)].name } } } - private fun findGraph(index: Int) : Int{ + + private fun findGraph(index: Int): Int { var indexAr = 0 - when(graphs.typeList[index]){ - ViewModelType.Undirect -> { - for (i in 0..index) if (graphs.typeList[i] == ViewModelType.Undirect) indexAr += 1 + when (graphs.typeList[index]) { + ViewModelType.Undirected -> { + for (i in 0..index) if (graphs.typeList[i] == ViewModelType.Undirected) indexAr += 1 } - ViewModelType.Direct -> { - for (i in 0..index) if (graphs.typeList[i] == ViewModelType.Direct) indexAr += 1 + + ViewModelType.Directed -> { + for (i in 0..index) if (graphs.typeList[i] == ViewModelType.Directed) indexAr += 1 } } return indexAr - 1 } - fun removeGraph(index: Int){ - when(graphs.typeList[index]){ - ViewModelType.Undirect -> { + + fun removeGraph(index: Int) { + when (graphs.typeList[index]) { + ViewModelType.Undirected -> { graphs.undirectedGraphs.removeAt(findGraph(index)) graphs.typeList.removeAt(index) } - ViewModelType.Direct -> { + + ViewModelType.Directed -> { graphs.directedGraphs.removeAt(findGraph(index)) graphs.typeList.removeAt(index) } } } - fun getUndirect(index: Int) : UndirectedGraphViewModel{ + + fun getUndirected(index: Int): UndirectedGraphViewModel { return undirectedGraphs[findGraph(index)] } - fun getDirect(index: Int) : DirectedGraphViewModel{ + + fun getDirected(index: Int): DirectedGraphViewModel { return directedGraphs[findGraph(index)] } diff --git a/src/test/kotlin/algos/fcadfs/FCA.kt b/src/test/kotlin/algos/FindCycleTest.kt similarity index 83% rename from src/test/kotlin/algos/fcadfs/FCA.kt rename to src/test/kotlin/algos/FindCycleTest.kt index cf51252..cbe38e4 100644 --- a/src/test/kotlin/algos/fcadfs/FCA.kt +++ b/src/test/kotlin/algos/FindCycleTest.kt @@ -1,13 +1,13 @@ -package algos.fcadfs +package algos -import model.algos.FCADFS +import model.algos.FindCycle import model.graph.UndirectedGraph import model.graph.edges.Edge import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertNotNull -internal class FCA { +internal class FindCycleTest { @Test fun basic() { @@ -23,7 +23,7 @@ internal class FCA { this.addEdge(3, 4) } - val pathActual = FCADFS.findCycle(graph, 2) + val pathActual = FindCycle.findCycle(graph, 2) assertNotNull(pathActual) val pathExpected = listOf( Edge(2, 1), diff --git a/src/test/kotlin/algos/FordBellman.kt b/src/test/kotlin/algos/FordBellmanTest.kt similarity index 94% rename from src/test/kotlin/algos/FordBellman.kt rename to src/test/kotlin/algos/FordBellmanTest.kt index 16e28b1..5f086dd 100644 --- a/src/test/kotlin/algos/FordBellman.kt +++ b/src/test/kotlin/algos/FordBellmanTest.kt @@ -32,7 +32,7 @@ internal class FordBellmanTest { "FordBellman must return weight of the shortest path" ) - val pathExpected = listOf>( + val pathExpected = listOf( Edge(1, 3, 2), Edge(3, 2, 3), Edge(2, 4, 1) @@ -66,7 +66,7 @@ internal class FordBellmanTest { val shortestLengthActual = result.first assertEquals( shortestLengthExpected, shortestLengthActual, - "FordBellman must return null as legnth of path if it is not possible to reach destination" + "FordBellman must return null as length of path if it is not possible to reach destination" ) val pathExpected = null @@ -97,7 +97,7 @@ internal class FordBellmanTest { val shortestLengthActual = result.first assertEquals( shortestLengthExpected, shortestLengthActual, - "FordBellman must return null as legnth of path if there is negative cycles" + "FordBellman must return null as length of path if there is negative cycles" ) val pathActual = result.second diff --git a/src/test/kotlin/algos/prim/Prim.kt b/src/test/kotlin/algos/PrimTest.kt similarity index 88% rename from src/test/kotlin/algos/prim/Prim.kt rename to src/test/kotlin/algos/PrimTest.kt index b10c0cf..ac31867 100644 --- a/src/test/kotlin/algos/prim/Prim.kt +++ b/src/test/kotlin/algos/PrimTest.kt @@ -1,13 +1,13 @@ -package algos.prim +package algos -import model.algos.MSTPrim +import model.algos.Prim import model.graph.UndirectedGraph import model.graph.edges.Edge import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertNotNull -internal class Prim { +internal class PrimTest { @Test fun basic() { val graph = UndirectedGraph() @@ -27,7 +27,7 @@ internal class Prim { this.addEdge(1, 3, 10) } - val pathActual = MSTPrim.findMST(graph, 1) + val pathActual = Prim.findMST(graph, 1) val pathExpected = listOf( Edge(1, 2, 1), diff --git a/src/test/kotlin/algos/SearchBridges.kt b/src/test/kotlin/algos/SearchBridgesTest.kt similarity index 92% rename from src/test/kotlin/algos/SearchBridges.kt rename to src/test/kotlin/algos/SearchBridgesTest.kt index 7c9a5be..0136909 100644 --- a/src/test/kotlin/algos/SearchBridges.kt +++ b/src/test/kotlin/algos/SearchBridgesTest.kt @@ -6,9 +6,9 @@ import model.graph.edges.Edge import kotlin.test.Test import kotlin.test.assertTrue -internal class SearchBridges { +internal class SearchBridgesTest { - fun bridgesEquals(bridges1: Set>, bridges2: Set>): Boolean { + private fun bridgesEquals(bridges1: Set>, bridges2: Set>): Boolean { for (bridge in bridges1) { val bridgeReversed = Edge(bridge.to, bridge.from, bridge.weight) if (bridges2.contains(bridge) || bridges2.contains(bridgeReversed)) From 3bd7be83b6f8b087613fba6a59a8fb3245086f37 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 21 May 2024 04:54:01 +0300 Subject: [PATCH 103/172] change: bridge search algorithm moved to `object` --- src/main/kotlin/model/algos/SearchBridges.kt | 73 +++++++++++--------- src/test/kotlin/algos/SearchBridgesTest.kt | 2 +- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/model/algos/SearchBridges.kt b/src/main/kotlin/model/algos/SearchBridges.kt index cd8e6da..b792fa4 100644 --- a/src/main/kotlin/model/algos/SearchBridges.kt +++ b/src/main/kotlin/model/algos/SearchBridges.kt @@ -1,43 +1,48 @@ package model.algos -import model.graph.edges.Edge import model.graph.UndirectedGraph +import model.graph.edges.Edge -fun searchBridges(graph: UndirectedGraph): Set> { - val timeIn = mutableMapOf() - for (vertex in graph.vertices) { - timeIn[vertex] = -1 - } - val ret = mutableMapOf() - var time = 0 - val bridges = mutableSetOf>() +object Bridges { + fun searchBridges(graph: UndirectedGraph): Set> { + val timeIn = mutableMapOf() + for (vertex in graph.vertices) { + timeIn[vertex] = -1 + } + val ret = mutableMapOf() + var time = 0 + val bridges = mutableSetOf>() - fun dfs(vertex: V, prevVertex: V) { - timeIn[vertex] = time++ - ret[vertex] = timeIn[vertex]!! - val edges = graph.edgesOf(vertex) - for (edge in edges) { - val destination = edge.to - val timeInDestination = timeIn[destination]!! - if (timeInDestination != -1 && destination != prevVertex && timeInDestination < ret[vertex]!!) { // if back edge - ret[vertex] = timeInDestination - } - if (timeInDestination != -1) { // if visited vertex - continue - } - dfs(destination, vertex) - val retDestination = ret[destination]!! - if (retDestination < ret[vertex]!!) { - ret[vertex] = retDestination - } - if (timeIn[vertex]!! < retDestination) { - bridges.add(edge) + fun dfs(vertex: V, prevVertex: V) { + timeIn[vertex] = time++ + ret[vertex] = timeIn[vertex]!! + val edges = graph.edgesOf(vertex) + for (edge in edges) { + val destination = edge.to + val timeInDestination = timeIn[destination]!! + if (timeInDestination != -1 && + destination != prevVertex && + timeInDestination < ret[vertex]!! + ) { // if back edge + ret[vertex] = timeInDestination + } + if (timeInDestination != -1) { // if visited vertex + continue + } + dfs(destination, vertex) + val retDestination = ret[destination]!! + if (retDestination < ret[vertex]!!) { + ret[vertex] = retDestination + } + if (timeIn[vertex]!! < retDestination) { + bridges.add(edge) + } } } - } - if (graph.vertices.isNotEmpty()) { - dfs(graph.vertices.elementAt(0), graph.vertices.elementAt(0)) - } + if (graph.vertices.isNotEmpty()) { + dfs(graph.vertices.elementAt(0), graph.vertices.elementAt(0)) + } - return bridges.toSet() + return bridges.toSet() + } } diff --git a/src/test/kotlin/algos/SearchBridgesTest.kt b/src/test/kotlin/algos/SearchBridgesTest.kt index 0136909..2616f27 100644 --- a/src/test/kotlin/algos/SearchBridgesTest.kt +++ b/src/test/kotlin/algos/SearchBridgesTest.kt @@ -1,6 +1,6 @@ package algos -import model.algos.searchBridges +import model.algos.Bridges.searchBridges import model.graph.UndirectedGraph import model.graph.edges.Edge import kotlin.test.Test From e19552d03595bdc55cc58f0eb755bb5b7c03a496 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 21 May 2024 04:57:13 +0300 Subject: [PATCH 104/172] change: remove setup android sdk --- .github/workflows/gradle-test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 30326fa..4867a6f 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -17,9 +17,5 @@ jobs: with: distribution: 'adopt' java-version: '21' - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - name: Clean, Build and Test run: ./gradlew clean test \ No newline at end of file From 61f495241698938e15ff79d841fa3deb23e5c140 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 21 May 2024 10:54:58 -0400 Subject: [PATCH 105/172] add: Djikstra algo implementation --- src/main/kotlin/model/algos/Djikstra.kt | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/kotlin/model/algos/Djikstra.kt diff --git a/src/main/kotlin/model/algos/Djikstra.kt b/src/main/kotlin/model/algos/Djikstra.kt new file mode 100644 index 0000000..3f59222 --- /dev/null +++ b/src/main/kotlin/model/algos/Djikstra.kt @@ -0,0 +1,46 @@ +import model.graph.edges.Edge +import java.util.* + + +class Dijkstra(var graph: MutableMap>>, private val totalNodes: Int) { + private val vertexValues: MutableMap = emptyMap().toMutableMap() + private val visitedSet: MutableSet = HashSet() + private val prioraQueue = PriorityQueue(totalNodes) + + fun dijkstra(s: V) { + for (j in graph.keys) { + vertexValues.put(j, Int.MAX_VALUE) + } + prioraQueue.add(s) + vertexValues[s] = 0 + + while (visitedSet.size != totalNodes) { + println(vertexValues) + if (prioraQueue.isEmpty()) { + return + } + val ux = prioraQueue.remove() + if (visitedSet.contains(ux)) { + continue + } + if (ux != null) { + visitedSet.add(ux) + refreshSearch(ux) + } + println() + } + } + + private fun refreshSearch(currentVertex: V) { + var newRange = -1 + for (j in graph[currentVertex]!!) { + if (!visitedSet.contains(j.to)) { + newRange = vertexValues[currentVertex]!! + j.weight + if (newRange < vertexValues[j.to]!!) { + vertexValues[j.to] = newRange + } + prioraQueue.add(j.to) + } + } + } +} \ No newline at end of file From 4923aab2ae40df0b853cd355958c8c5b9ddc2af7 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 21 May 2024 12:56:56 -0400 Subject: [PATCH 106/172] ref: some code ref to dijkstra algo implementation --- .../kotlin/model/algos/{Djikstra.kt => Dijkstra.kt} | 9 ++++----- src/main/kotlin/model/graph/Graph.kt | 2 ++ src/main/kotlin/view/screens/DirectedGraphScreen.kt | 13 +++++++++++++ src/main/kotlin/view/screens/MainScreen.kt | 4 +--- src/main/kotlin/viewmodel/DirectedGraphViewModel.kt | 9 ++++++++- 5 files changed, 28 insertions(+), 9 deletions(-) rename src/main/kotlin/model/algos/{Djikstra.kt => Dijkstra.kt} (93%) diff --git a/src/main/kotlin/model/algos/Djikstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt similarity index 93% rename from src/main/kotlin/model/algos/Djikstra.kt rename to src/main/kotlin/model/algos/Dijkstra.kt index 3f59222..e2842a8 100644 --- a/src/main/kotlin/model/algos/Djikstra.kt +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -1,18 +1,17 @@ import model.graph.edges.Edge import java.util.* - class Dijkstra(var graph: MutableMap>>, private val totalNodes: Int) { private val vertexValues: MutableMap = emptyMap().toMutableMap() private val visitedSet: MutableSet = HashSet() private val prioraQueue = PriorityQueue(totalNodes) - fun dijkstra(s: V) { + fun dijkstra(start: V) { for (j in graph.keys) { vertexValues.put(j, Int.MAX_VALUE) } - prioraQueue.add(s) - vertexValues[s] = 0 + prioraQueue.add(start) + vertexValues[start] = 0 while (visitedSet.size != totalNodes) { println(vertexValues) @@ -43,4 +42,4 @@ class Dijkstra(var graph: MutableMap>>, private val t } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index a62c8d2..b47bfb3 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -4,6 +4,8 @@ import model.graph.edges.Edge abstract class Graph() { protected val graph = mutableMapOf>>() + val matrix get() = graph + val entries get() = graph.entries protected var weighted = false diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 12788b8..9195e69 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -78,6 +78,19 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) + Button( + onClick = { }, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("dijkstra_algorithm"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(10.dp)) + if (isOpenedEdgeMenu) { Column( modifier = Modifier diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 38ea1f1..d0f7785 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -37,8 +37,6 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView val optionsDropDown = listOf("undirected", "directed") val expandedDropDown = remember { mutableStateOf(false) } val selectedOptionTextDropDown = remember { mutableStateOf(optionsDropDown[0]) } - - Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { @@ -166,6 +164,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView onClick = { if(graphName != ""){ mainScreenViewModel.addGraph(graphName, selectedOptionTextDropDown.value) + graphName = "" dialogState.value = false } }, @@ -173,7 +172,6 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Text(text= localisation("add"), color = if(graphName != "") Color.White else Color.Black, fontSize = 28.sp) } Button(modifier = Modifier - .padding(horizontal = 20.dp, vertical = 260.dp) .border( width = 2.dp, diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 12f017b..a6d07ce 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,10 +1,13 @@ package viewmodel +import Dijkstra import model.graph.DirectedGraph +import kotlin.collections.set + class DirectedGraphViewModel( _name: String, - graph: DirectedGraph = DirectedGraph() + val graph: DirectedGraph = DirectedGraph() ): AbstractGraphViewModel>(graph){ val name = _name init { @@ -12,6 +15,10 @@ class DirectedGraphViewModel( graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } } + fun dijkstraAlgo(start: V){ + val dalg = Dijkstra(graph.matrix, graph.size) + dalg.dijkstra(start) + } fun addVertex(vertex: V) { size += 1 From 5207fd8bda06121230d4941e8e91217bae775ec3 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 21 May 2024 17:00:52 -0400 Subject: [PATCH 107/172] gui: improved edge adding menue --- src/main/kotlin/Navigation.kt | 1 - .../view/screens/DirectedGraphScreen.kt | 190 ++++++++++++------ .../view/screens/UndirectedGraphScreen.kt | 146 ++++++++++++-- .../viewmodel/AbstractGraphViewModel.kt | 9 - .../viewmodel/DirectedGraphViewModel.kt | 9 + .../viewmodel/UndirectedGraphViewModel.kt | 3 +- 6 files changed, 277 insertions(+), 81 deletions(-) diff --git a/src/main/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt index 9cc49b0..fa2a4e1 100644 --- a/src/main/kotlin/Navigation.kt +++ b/src/main/kotlin/Navigation.kt @@ -23,7 +23,6 @@ fun Navigation() { ){ navBackStackEntry -> val graphId = navBackStackEntry.arguments?.getInt("graphId") graphId?.let{ - println(graphId) UndirectedGraphScreen(navController, mainScreenViewModel, graphId) } } diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 9195e69..eb8d948 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -4,24 +4,26 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text -import androidx.compose.material.TextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation import view.DefaultColors import view.defaultStyle import view.views.GraphViewDirect -import viewmodel.DirectedGraphViewModel import viewmodel.MainScreenViewModel + @Composable fun DirectedGraphScreen( navController: NavController, @@ -29,6 +31,7 @@ fun DirectedGraphScreen( graphId: Int ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirect(graphId)) + var isOpenedEdgeMenu by remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxSize()) { GraphViewDirect(graphVM) @@ -63,10 +66,8 @@ fun DirectedGraphScreen( } Spacer(modifier = Modifier.height(16.dp)) - var isOpenedEdgeMenu by remember { mutableStateOf(false) } - Button( - onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu }, + onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu}, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) @@ -91,57 +92,134 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) - if (isOpenedEdgeMenu) { - Column( - modifier = Modifier - .background(Color(0xffeeeeee)) - .border(3.dp, color = Color.Black) - .padding(10.dp) + DialogWindow( + visible = isOpenedEdgeMenu, + title = "New Edge", + onCloseRequest = { isOpenedEdgeMenu = false }, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + var checkedState by remember { mutableStateOf(false) } + var weight by remember { mutableStateOf("") } + Column { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(26.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - ) { - AddDirectedEdgeMenu(graphVM) - } - } - } -} + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(62.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), -@Composable -fun AddDirectedEdgeMenu(graphModel: DirectedGraphViewModel) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - Row { - TextField( - modifier = Modifier - .width(115.dp) - .border(3.dp, color = Color.Black), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, - ) - Spacer(modifier = Modifier.width(10.dp)) - TextField( - modifier = Modifier - .width(115.dp) - .border(3.dp, color = Color.Black), - textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue }) - } + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue },) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + if(!checkedState) { + Text( + text = localisation("weight"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(30.dp)) + TextField( + enabled = !checkedState, + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) + .background(color = Color.White, shape = RoundedCornerShape(10.dp)), + shape = RoundedCornerShape(10.dp), - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { - val sourceInt = source.toIntOrNull() - val destinationInt = destination.toIntOrNull() - if (sourceInt != null && destinationInt != null) { - graphModel.addEdge(sourceInt, destinationInt) + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + textStyle = defaultStyle, + value = weight, + onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + Spacer(modifier = Modifier.width(20.dp)) + } + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = checkedState, + onCheckedChange = { checkedState = it; + weight = if(checkedState) "1" else "" } + ) + Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) + isOpenedEdgeMenu = false + } + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_edge"), style = defaultStyle) + } + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { isOpenedEdgeMenu = false }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text(localisation("back"), style = defaultStyle) + } + } } - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_edge"), style = defaultStyle) + } } } + diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 98823ab..42c9b37 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -4,15 +4,17 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text -import androidx.compose.material.TextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation @@ -78,17 +80,133 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) - if (isOpenedEdgeMenu) { - Column( - modifier = Modifier - .background(Color(0xffeeeeee)) - .border(3.dp, color = Color.Black) - .padding(10.dp) + DialogWindow( + visible = isOpenedEdgeMenu, + title = "New Edge", + onCloseRequest = { isOpenedEdgeMenu = false }, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + var checkedState by remember { mutableStateOf(false) } + var weight by remember { mutableStateOf("") } + Column { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("1st"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(62.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - ) { - AddUUEdgeMenu(graphVM) - } + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("2nd"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(54.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue },) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + if(!checkedState) { + Text( + text = localisation("weight"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(30.dp)) + TextField( + enabled = !checkedState, + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) + .background(color = Color.White, shape = RoundedCornerShape(10.dp)), + shape = RoundedCornerShape(10.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + textStyle = defaultStyle, + value = weight, + onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + Spacer(modifier = Modifier.width(20.dp)) + } + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = checkedState, + onCheckedChange = { checkedState = it; + weight = if(checkedState) "1" else "" } + ) + Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) + isOpenedEdgeMenu = false + } + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_edge"), style = defaultStyle) + } + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { isOpenedEdgeMenu = false }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text(localisation("back"), style = defaultStyle) + } + } + } } } } @@ -122,7 +240,7 @@ fun AddUUEdgeMenu(graphModel: UndirectedGraphViewModel) { val sourceInt = source.toIntOrNull() val destinationInt = destination.toIntOrNull() if (sourceInt != null && destinationInt != null) { - graphModel.addEdge(sourceInt, destinationInt) + graphModel.addEdge(sourceInt, destinationInt, 1) } }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 4022157..90c66d6 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -2,18 +2,9 @@ package viewmodel import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.ViewModel -import model.graph.edges.Edge abstract class AbstractGraphViewModel(graph: G) : ViewModel() { val graphView = mutableStateMapOf>() val graphModel = graph var size = 0 - - open fun addEdge(from: V, to: V) { - if (graphView[from] == null) return - for (i in graphView[from]?.edges!!) if(i.to == to) return - val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) - graphView[from]?.edges = edgesCopy - } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index a6d07ce..935f3a4 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -2,6 +2,7 @@ package viewmodel import Dijkstra import model.graph.DirectedGraph +import model.graph.edges.Edge import kotlin.collections.set @@ -25,5 +26,13 @@ class DirectedGraphViewModel( graphView.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) } + fun addEdge(from: V, to: V, weight: Int) { + if (graphView[from] == null) return + for (i in graphView[from]?.edges!!) if(i.to == to) return + val edgesCopy = graphView[from]?.edges?.toMutableList()!! + edgesCopy.add(Edge(from, to)) + graphView[from]?.edges = edgesCopy + graphModel.addEdge(from, to, weight) + } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 1cd8dc0..ae7feee 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -19,13 +19,14 @@ class UndirectedGraphViewModel( graphView.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) } - override fun addEdge(from: V, to: V) { + fun addEdge(from: V, to: V, weight: Int) { if (graphView[from] == null) return for (i in graphView[from]?.edges!!) if(i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! edgesCopy.add(Edge(from, to)) edgesCopy.add(Edge(to, from)) graphView[from]?.edges = edgesCopy + graphModel.addEdge(from, to, weight) } } \ No newline at end of file From 49b9676418ef1192d240eedcbc1700f07f642a85 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 22 May 2024 04:52:52 -0400 Subject: [PATCH 108/172] add: edges weights add --- src/main/kotlin/model/graph/Graph.kt | 3 +- .../view/screens/UndirectedGraphScreen.kt | 42 ------------------- src/main/kotlin/view/views/VertexView.kt | 18 +++++++- .../viewmodel/DirectedGraphViewModel.kt | 4 +- .../viewmodel/UndirectedGraphViewModel.kt | 8 ++-- 5 files changed, 27 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 9c3c7a3..67053d9 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -10,7 +10,8 @@ abstract class Graph() { get() = graph.entries protected var weighted = false - + val state + get() = weighted val vertices get() = graph.keys diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index b52497d..98407be 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -22,7 +22,6 @@ import view.DefaultColors import view.defaultStyle import view.views.GraphViewUndirect import viewmodel.MainScreenViewModel -import viewmodel.UndirectedGraphViewModel @Composable fun UndirectedGraphScreen( @@ -210,44 +209,3 @@ fun UndirectedGraphScreen( } } } - -@Composable -fun AddUUEdgeMenu(graphModel: UndirectedGraphViewModel) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - Row { - TextField( - modifier = Modifier - .width(115.dp) - .border(3.dp, color = Color.Black), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, - ) - Spacer(modifier = Modifier.width(10.dp)) - TextField( - modifier = Modifier - .width(115.dp) - .border(3.dp, color = Color.Black), - textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue }) - } - - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { - val sourceInt = source.toIntOrNull() - val destinationInt = destination.toIntOrNull() - if (sourceInt != null && destinationInt != null) { - graphModel.addEdge(sourceInt, destinationInt, 1) - } - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_edge"), style = defaultStyle) - } -} diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index 767307b..f504e90 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -15,6 +15,9 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -58,6 +61,7 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie val otherVM = graphVM.graphView[otherVertex]!! val otherX = otherVM.offsetX val otherY = otherVM.offsetY + val textMeasurer = rememberTextMeasurer() Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)){ drawLine( @@ -67,7 +71,7 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie color = Color.Black, ) rotate( - degrees = ((57.2958 * (atan2(((vertexVM.offsetY - otherY).toDouble()), ((vertexVM.offsetX - otherX).toDouble())))).toFloat()), + degrees = ((57.2958 * (atan2(((vertexVM.offsetY - otherY).toDouble()), ((vertexVM.offsetX - otherX).toDouble())))).toFloat()), pivot = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2) ){ drawRect( @@ -96,6 +100,12 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 50, otherY + vertexVM.vertexSize / 2 - 4f), ) } + if(graphVM.graph.state) + drawText(textMeasurer, edge.weight.toString(), + topLeft = Offset((vertexVM.offsetX + vertexVM.vertexSize + otherX)/2 - edge.weight.toString().length * 5.5f, (vertexVM.offsetY + vertexVM.vertexSize + otherY)/2 - 9), + style = TextStyle(background = Color.White, fontSize = 18.sp) + ) + } } } @@ -103,6 +113,7 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie @Composable fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { val vertex = vertexVM.vertex + val textMeasurer = rememberTextMeasurer() Box(modifier = Modifier .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } @@ -140,6 +151,11 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGrap strokeWidth = 8f, color = Color.Black, ) + if(graphVM.graph.state) + drawText(textMeasurer, edge.weight.toString(), + topLeft = Offset((vertexVM.offsetX + vertexVM.vertexSize + otherX)/2 - edge.weight.toString().length * 5.5f, (vertexVM.offsetY + vertexVM.vertexSize + otherY)/2 - 9), + style = TextStyle(background = Color.White, fontSize = 18.sp) + ) } } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 935f3a4..13c95ed 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -11,6 +11,8 @@ class DirectedGraphViewModel( val graph: DirectedGraph = DirectedGraph() ): AbstractGraphViewModel>(graph){ val name = _name + val model + get() = graph init { for (vertex in graphModel.entries) { graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) @@ -30,7 +32,7 @@ class DirectedGraphViewModel( if (graphView[from] == null) return for (i in graphView[from]?.edges!!) if(i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) + edgesCopy.add(Edge(from, to, weight)) graphView[from]?.edges = edgesCopy graphModel.addEdge(from, to, weight) } diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index ae7feee..b2a0091 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -5,9 +5,11 @@ import model.graph.edges.Edge class UndirectedGraphViewModel( _name: String, - graph: UndirectedGraph = UndirectedGraph() + val graph: UndirectedGraph = UndirectedGraph() ): AbstractGraphViewModel>(graph){ val name = _name + val model + get() = graph init { for (vertex in graphModel.entries) { graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) @@ -23,8 +25,8 @@ class UndirectedGraphViewModel( if (graphView[from] == null) return for (i in graphView[from]?.edges!!) if(i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) - edgesCopy.add(Edge(to, from)) + edgesCopy.add(Edge(from, to, weight)) + edgesCopy.add(Edge(to, from, weight)) graphView[from]?.edges = edgesCopy graphModel.addEdge(from, to, weight) } From 07250728825eb70d827911a7008b93dcd6ff0c57 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 22 May 2024 13:18:45 +0300 Subject: [PATCH 109/172] Fix: fix bug with adding new vertex on 33+ vertices graph -change GraphView from MutableStateMap to MutableState -refactor Viewmodels -create updateView function in Viewmodels --- src/main/kotlin/Main.kt | 7 +- src/main/kotlin/model/graph/Graph.kt | 3 +- src/main/kotlin/view/views/VertexView.kt | 104 +++++++++++------- .../viewmodel/AbstractGraphViewModel.kt | 34 ++++-- .../viewmodel/DirectedGraphViewModel.kt | 21 ++-- .../viewmodel/UndirectedGraphViewModel.kt | 17 +-- src/main/kotlin/viewmodel/VertexViewModel.kt | 10 +- 7 files changed, 115 insertions(+), 81 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 97baea0..5c1c807 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -8,10 +8,13 @@ import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application import java.awt.Dimension +val width = 1920 +val height = 1080 + fun main() = application { val state = WindowState( - width = 1920.dp, - height = 1080.dp, + width = width.dp, + height = height.dp, position = WindowPosition(alignment = Alignment.Center), ) Window( diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index a548536..7dd9650 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -7,8 +7,7 @@ abstract class Graph() { val entries get() = graph.entries protected var weighted = false - - + val vertices get() = graph.keys diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index 767307b..725f001 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -33,7 +33,7 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie Box(modifier = Modifier .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } .clip(shape = CircleShape) - .size(100.dp) + .size(vertexVM.vertexSize.dp) .background(DefaultColors.primary) .border(5.dp, Color.Black, CircleShape) .pointerInput(Unit) { @@ -53,48 +53,69 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie ) } - vertexVM.edges.forEach{ edge -> + vertexVM.edges.forEach { edge -> val otherVertex = edge.to val otherVM = graphVM.graphView[otherVertex]!! val otherX = otherVM.offsetX val otherY = otherVM.offsetY - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)){ + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( - start = Offset(vertexVM.offsetX + vertexVM.vertexSize/2, vertexVM.offsetY + vertexVM.vertexSize/2), - end = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2), - strokeWidth = 6f, + start = Offset( + vertexVM.offsetX + vertexVM.vertexSize / 2, + vertexVM.offsetY + vertexVM.vertexSize / 2 + ), + end = Offset(otherX + vertexVM.vertexSize / 2, otherY + vertexVM.vertexSize / 2), + strokeWidth = 6f, color = Color.Black, ) rotate( - degrees = ((57.2958 * (atan2(((vertexVM.offsetY - otherY).toDouble()), ((vertexVM.offsetX - otherX).toDouble())))).toFloat()), - pivot = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2) - ){ - drawRect( - color = Color.Black, - size = Size(5f, 16f), - topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 70, otherY + vertexVM.vertexSize / 2 - 8f), - ) - drawRect( - color = Color.Black, - size = Size(5f, 14f), - topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 65, otherY + vertexVM.vertexSize / 2 - 7f), - ) - drawRect( - color = Color.Black, - size = Size(5f, 12f), - topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 60, otherY + vertexVM.vertexSize / 2 - 6f), - ) - drawRect( - color = Color.Black, - size = Size(5f, 10f), - topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 55, otherY + vertexVM.vertexSize / 2 - 5f), - ) - drawRect( - color = Color.Black, - size = Size(5f, 8f), - topLeft = Offset(otherX + vertexVM.vertexSize / 2 + 50, otherY + vertexVM.vertexSize / 2 - 4f), - ) + degrees = ((57.2958 * (atan2( + ((vertexVM.offsetY - otherY).toDouble()), + ((vertexVM.offsetX - otherX).toDouble()) + ))).toFloat()), + pivot = Offset(otherX + vertexVM.vertexSize / 2, otherY + vertexVM.vertexSize / 2) + ) { + drawRect( + color = Color.Black, + size = Size(5f, 16f), + topLeft = Offset( + otherX + vertexVM.vertexSize / 2 + 70, + otherY + vertexVM.vertexSize / 2 - 8f + ), + ) + drawRect( + color = Color.Black, + size = Size(5f, 14f), + topLeft = Offset( + otherX + vertexVM.vertexSize / 2 + 65, + otherY + vertexVM.vertexSize / 2 - 7f + ), + ) + drawRect( + color = Color.Black, + size = Size(5f, 12f), + topLeft = Offset( + otherX + vertexVM.vertexSize / 2 + 60, + otherY + vertexVM.vertexSize / 2 - 6f + ), + ) + drawRect( + color = Color.Black, + size = Size(5f, 10f), + topLeft = Offset( + otherX + vertexVM.vertexSize / 2 + 55, + otherY + vertexVM.vertexSize / 2 - 5f + ), + ) + drawRect( + color = Color.Black, + size = Size(5f, 8f), + topLeft = Offset( + otherX + vertexVM.vertexSize / 2 + 50, + otherY + vertexVM.vertexSize / 2 - 4f + ), + ) } } } @@ -107,7 +128,7 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGrap Box(modifier = Modifier .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } .clip(shape = CircleShape) - .size(100.dp) + .size(vertexVM.vertexSize.dp) .background(DefaultColors.primary) .border(5.dp, Color.Black, CircleShape) .pointerInput(Unit) { @@ -127,17 +148,20 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGrap ) } - vertexVM.edges.forEach{ edge -> + vertexVM.edges.forEach { edge -> val otherVertex = edge.to val otherVM = graphVM.graphView[otherVertex]!! val otherX = otherVM.offsetX val otherY = otherVM.offsetY - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)){ + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( - start = Offset(vertexVM.offsetX + vertexVM.vertexSize/2, vertexVM.offsetY + vertexVM.vertexSize/2), - end = Offset( otherX + vertexVM.vertexSize/2, otherY + vertexVM.vertexSize/2), - strokeWidth = 8f, + start = Offset( + vertexVM.offsetX + vertexVM.vertexSize / 2, + vertexVM.offsetY + vertexVM.vertexSize / 2 + ), + end = Offset(otherX + vertexVM.vertexSize / 2, otherY + vertexVM.vertexSize / 2), + strokeWidth = 8f, color = Color.Black, ) } diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 4022157..1a54957 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -1,19 +1,37 @@ package viewmodel +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import model.graph.Graph import model.graph.edges.Edge -abstract class AbstractGraphViewModel(graph: G) : ViewModel() { - val graphView = mutableStateMapOf>() +abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { + val name = _name + var graphView by mutableStateOf(mutableMapOf>()) val graphModel = graph var size = 0 - open fun addEdge(from: V, to: V) { - if (graphView[from] == null) return - for (i in graphView[from]?.edges!!) if(i.to == to) return - val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) - graphView[from]?.edges = edgesCopy + init { + for (vertex in graphModel.entries) { + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + } } + + fun addVertex(vertex: V) { + size += 1 + graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphModel.addVertex(vertex) + updateView() + } + + fun updateView() { + val keep = graphView + graphView = mutableMapOf>() + graphView = keep + } + + abstract fun addEdge(from: V, to: V) } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 12f017b..7f85945 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,22 +1,19 @@ package viewmodel import model.graph.DirectedGraph +import model.graph.edges.Edge class DirectedGraphViewModel( - _name: String, + name: String, graph: DirectedGraph = DirectedGraph() -): AbstractGraphViewModel>(graph){ - val name = _name - init { - for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) - } - } +) : AbstractGraphViewModel(name, graph) { - fun addVertex(vertex: V) { - size += 1 - graphView.putIfAbsent(vertex, VertexViewModel(vertex)) - graphModel.addVertex(vertex) + override fun addEdge(from: V, to: V) { + if (graphView[from] == null) return + for (i in graphView[from]?.edges!!) if (i.to == to) return + val edgesCopy = graphView[from]?.edges?.toMutableList()!! + edgesCopy.add(Edge(from, to)) + graphView[from]?.edges = edgesCopy } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 1cd8dc0..fa456b9 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -4,24 +4,13 @@ import model.graph.UndirectedGraph import model.graph.edges.Edge class UndirectedGraphViewModel( - _name: String, + name: String, graph: UndirectedGraph = UndirectedGraph() -): AbstractGraphViewModel>(graph){ - val name = _name - init { - for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) - } - } +) : AbstractGraphViewModel(name, graph) { - fun addVertex(vertex: V) { - size += 1 - graphView.putIfAbsent(vertex, VertexViewModel(vertex)) - graphModel.addVertex(vertex) - } override fun addEdge(from: V, to: V) { if (graphView[from] == null) return - for (i in graphView[from]?.edges!!) if(i.to == to) return + for (i in graphView[from]?.edges!!) if (i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! edgesCopy.add(Edge(from, to)) edgesCopy.add(Edge(to, from)) diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 1a7c938..d784bd8 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -4,13 +4,17 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import height +import kotlinx.serialization.descriptors.PrimitiveKind import model.graph.edges.Edge +import width +import kotlin.random.Random class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListOf()) : ViewModel() { val vertex: V = _vertex var edges by mutableStateOf(_edges) - var offsetX by mutableStateOf(1000f) - var offsetY by mutableStateOf(540f) - val vertexSize = 100f + var offsetX by mutableStateOf(Random.nextInt(100, width - 100).toFloat()) + var offsetY by mutableStateOf(Random.nextInt(100, height - 100).toFloat()) + val vertexSize = 80f } \ No newline at end of file From 28d4722935731577117899f730773e8133ceb9fd Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 22 May 2024 15:19:38 +0300 Subject: [PATCH 110/172] Fix: add weight in addEdge method of graph ViewModels --- src/main/kotlin/viewmodel/AbstractGraphViewModel.kt | 2 +- src/main/kotlin/viewmodel/DirectedGraphViewModel.kt | 12 ++---------- .../kotlin/viewmodel/UndirectedGraphViewModel.kt | 6 +++--- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 1a54957..39832cd 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -33,5 +33,5 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM graphView = keep } - abstract fun addEdge(from: V, to: V) + abstract fun addEdge(from: V, to: V, weight: Int = 1) } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 30c96fc..c12c1b7 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -9,20 +9,12 @@ class DirectedGraphViewModel( graph: DirectedGraph = DirectedGraph() ) : AbstractGraphViewModel(name, graph) { - override fun addEdge(from: V, to: V) { + override fun addEdge(from: V, to: V, weight: Int) { if (graphView[from] == null) return for (i in graphView[from]?.edges!!) if (i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) - graphView[from]?.edges = edgesCopy - } - fun addEdge(from: V, to: V, weight: Int) { - if (graphView[from] == null) return - for (i in graphView[from]?.edges!!) if(i.to == to) return - val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) + edgesCopy.add(Edge(from, to, weight)) graphView[from]?.edges = edgesCopy graphModel.addEdge(from, to, weight) } - } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 2dc2157..1e1da13 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -8,12 +8,12 @@ class UndirectedGraphViewModel( graph: UndirectedGraph = UndirectedGraph() ) : AbstractGraphViewModel(name, graph) { - override fun addEdge(from: V, to: V) { + override fun addEdge(from: V, to: V, weight: Int) { if (graphView[from] == null) return for (i in graphView[from]?.edges!!) if (i.to == to) return val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to)) - edgesCopy.add(Edge(to, from)) + edgesCopy.add(Edge(from, to, weight)) + edgesCopy.add(Edge(to, from, weight)) graphView[from]?.edges = edgesCopy graphModel.addEdge(from, to, weight) } From fbbae0d16c45e05740153784dfdda406037612e1 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 22 May 2024 12:40:51 -0400 Subject: [PATCH 111/172] add: dijkstra algo screen add --- src/main/kotlin/model/algos/Dijkstra.kt | 11 ++- .../view/screens/DirectedGraphScreen.kt | 84 ++++++++++++++++++- src/main/kotlin/view/views/VertexView.kt | 1 - .../viewmodel/DirectedGraphViewModel.kt | 4 +- 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/model/algos/Dijkstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt index e2842a8..01c15f7 100644 --- a/src/main/kotlin/model/algos/Dijkstra.kt +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -5,8 +5,9 @@ class Dijkstra(var graph: MutableMap>>, private val t private val vertexValues: MutableMap = emptyMap().toMutableMap() private val visitedSet: MutableSet = HashSet() private val prioraQueue = PriorityQueue(totalNodes) + private val pathMap: MutableMap>> = emptyMap>>().toMutableMap() - fun dijkstra(start: V) { + fun dijkstra(start: V, end: V) : MutableList>{ for (j in graph.keys) { vertexValues.put(j, Int.MAX_VALUE) } @@ -16,7 +17,7 @@ class Dijkstra(var graph: MutableMap>>, private val t while (visitedSet.size != totalNodes) { println(vertexValues) if (prioraQueue.isEmpty()) { - return + return pathMap[end]!! } val ux = prioraQueue.remove() if (visitedSet.contains(ux)) { @@ -26,8 +27,9 @@ class Dijkstra(var graph: MutableMap>>, private val t visitedSet.add(ux) refreshSearch(ux) } - println() + println(vertexValues) } + return pathMap[end]!! } private fun refreshSearch(currentVertex: V) { @@ -37,6 +39,9 @@ class Dijkstra(var graph: MutableMap>>, private val t newRange = vertexValues[currentVertex]!! + j.weight if (newRange < vertexValues[j.to]!!) { vertexValues[j.to] = newRange + val k = pathMap[j.from] + k?.add(j) + pathMap[j.to] = k!! } prioraQueue.add(j.to) } diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 57f1c9c..7166ba8 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -33,6 +33,7 @@ fun DirectedGraphScreen( val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) var isOpenedEdgeMenu by remember { mutableStateOf(false) } + var isDijkstraMenu by remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxSize()) { @@ -82,7 +83,7 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) Button( - onClick = { }, + onClick = {isDijkstraMenu = !isDijkstraMenu}, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) @@ -222,6 +223,87 @@ fun DirectedGraphScreen( } } } + DialogWindow( + visible = isDijkstraMenu, + title = "New Edge", + onCloseRequest = { isDijkstraMenu = false }, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + Column { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(26.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(62.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue },) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("start"), style = defaultStyle) + } + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { isOpenedEdgeMenu = false }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text(localisation("back"), style = defaultStyle) + } + } + } + } } } diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index f504e90..566b02b 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -105,7 +105,6 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie topLeft = Offset((vertexVM.offsetX + vertexVM.vertexSize + otherX)/2 - edge.weight.toString().length * 5.5f, (vertexVM.offsetY + vertexVM.vertexSize + otherY)/2 - 9), style = TextStyle(background = Color.White, fontSize = 18.sp) ) - } } } diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 13c95ed..139ee68 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -18,9 +18,9 @@ class DirectedGraphViewModel( graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } } - fun dijkstraAlgo(start: V){ + fun dijkstraAlgo(start: V, end: V){ val dalg = Dijkstra(graph.matrix, graph.size) - dalg.dijkstra(start) + dalg.dijkstra(start, end) } fun addVertex(vertex: V) { From ece0be8fd118a5b12430325a17ea7293dc70c401 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 22 May 2024 12:47:51 -0400 Subject: [PATCH 112/172] fix: gui back button fix --- src/main/kotlin/view/screens/DirectedGraphScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 7166ba8..9ace564 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -293,7 +293,7 @@ fun DirectedGraphScreen( Row { Spacer(modifier = Modifier.width(30.dp)) Button( - onClick = { isOpenedEdgeMenu = false }, modifier = Modifier + onClick = { isDijkstraMenu = false }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) .size(240.dp, 80.dp), From 16548c98ab770c18ee1fa40728b5509488d4d86e Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 22 May 2024 15:26:47 -0400 Subject: [PATCH 113/172] add: new EdgeViewModel --- src/main/kotlin/model/algos/Dijkstra.kt | 1 + src/main/kotlin/view/views/GraphView.kt | 12 +- src/main/kotlin/view/views/VertexView.kt | 210 ++++++++++-------- .../viewmodel/AbstractGraphViewModel.kt | 15 +- .../viewmodel/DirectedGraphViewModel.kt | 18 +- src/main/kotlin/viewmodel/EdgeViewModel.kt | 19 ++ .../viewmodel/UndirectedGraphViewModel.kt | 22 +- src/main/kotlin/viewmodel/VertexViewModel.kt | 1 - 8 files changed, 183 insertions(+), 115 deletions(-) create mode 100644 src/main/kotlin/viewmodel/EdgeViewModel.kt diff --git a/src/main/kotlin/model/algos/Dijkstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt index 01c15f7..1521117 100644 --- a/src/main/kotlin/model/algos/Dijkstra.kt +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -10,6 +10,7 @@ class Dijkstra(var graph: MutableMap>>, private val t fun dijkstra(start: V, end: V) : MutableList>{ for (j in graph.keys) { vertexValues.put(j, Int.MAX_VALUE) + pathMap.put(j, emptyList>().toMutableList()) } prioraQueue.add(start) vertexValues[start] = 0 diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index d831742..5886e80 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -6,15 +6,23 @@ import viewmodel.UndirectedGraphViewModel @Composable fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { - for (vertexVM in graphViewModel.graphView.values) { + for (vertexVM in graphViewModel.vertexView.values) { UndirectedVertexView(vertexVM, graphViewModel) } + for (edge in graphViewModel.edgesView.iterator()) { + UndirectedEdgeView(edge, graphViewModel) + } } @Composable fun GraphViewDirect(graphViewModel: DirectedGraphViewModel) { - for (vertexVM in graphViewModel.graphView.values) { + + for (vertexVM in graphViewModel.vertexView.values) { DirectedVertexView(vertexVM, graphViewModel) } + + for (edge in graphViewModel.edgesView.iterator()) { + DirectedEdgeView(edge, graphViewModel) + } } diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index 6316106..a5fd5b7 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import view.DefaultColors import viewmodel.DirectedGraphViewModel +import viewmodel.EdgeViewModel import viewmodel.UndirectedGraphViewModel import viewmodel.VertexViewModel import kotlin.math.atan2 @@ -32,7 +33,6 @@ import kotlin.math.roundToInt @Composable fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel) { val vertex = vertexVM.vertex - Box(modifier = Modifier .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } .clip(shape = CircleShape) @@ -42,9 +42,22 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() + for (edge in graphVM.edgesView.iterator()) { + if(edge.to == vertexVM.vertex){ + edge.offsetXTo+= dragAmount.x + edge.offsetYTo+= dragAmount.y + } + } + for (edge in graphVM.edgesView.iterator()) { + if(edge.from == vertexVM.vertex){ + edge.offsetXFrom+= dragAmount.x + edge.offsetYFrom+= dragAmount.y + } + } vertexVM.offsetX += dragAmount.x vertexVM.offsetY += dragAmount.y } + } ) { Text( @@ -55,85 +68,112 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie .wrapContentSize(), ) } + for (edge in graphVM.edgesView.iterator()) { + DirectedEdgeView(edge, graphVM) + } +} - vertexVM.edges.forEach { edge -> - val otherVertex = edge.to - val otherVM = graphVM.graphView[otherVertex]!! - val otherX = otherVM.offsetX - val otherY = otherVM.offsetY - val textMeasurer = rememberTextMeasurer() +@Composable +fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel){ - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { - drawLine( - start = Offset( - vertexVM.offsetX + vertexVM.vertexSize / 2, - vertexVM.offsetY + vertexVM.vertexSize / 2 + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + drawLine( + start = Offset( + edgeVM.offsetXFrom + edgeVM.vertexSize / 2, + edgeVM.offsetYFrom + edgeVM.vertexSize / 2 + ), + end = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2), + strokeWidth = 6f, + color = Color.Black, + ) + rotate( + degrees = ((57.2958 * (atan2( + ((edgeVM.offsetYFrom - edgeVM.offsetYTo).toDouble()), + ((edgeVM.offsetXFrom - edgeVM.offsetXTo).toDouble()) + ))).toFloat()), + pivot = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2) + ) { + drawRect( + color = Color.Black, + size = Size(5f, 16f), + topLeft = Offset( + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 70, + edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 8f ), - end = Offset(otherX + vertexVM.vertexSize / 2, otherY + vertexVM.vertexSize / 2), - strokeWidth = 6f, + ) + drawRect( color = Color.Black, + size = Size(5f, 14f), + topLeft = Offset( + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 65, + edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 7f + ), ) - rotate( - degrees = ((57.2958 * (atan2( - ((vertexVM.offsetY - otherY).toDouble()), - ((vertexVM.offsetX - otherX).toDouble()) - ))).toFloat()), - pivot = Offset(otherX + vertexVM.vertexSize / 2, otherY + vertexVM.vertexSize / 2) - ) { - drawRect( - color = Color.Black, - size = Size(5f, 16f), - topLeft = Offset( - otherX + vertexVM.vertexSize / 2 + 70, - otherY + vertexVM.vertexSize / 2 - 8f - ), - ) - drawRect( - color = Color.Black, - size = Size(5f, 14f), - topLeft = Offset( - otherX + vertexVM.vertexSize / 2 + 65, - otherY + vertexVM.vertexSize / 2 - 7f - ), - ) - drawRect( - color = Color.Black, - size = Size(5f, 12f), - topLeft = Offset( - otherX + vertexVM.vertexSize / 2 + 60, - otherY + vertexVM.vertexSize / 2 - 6f - ), - ) - drawRect( - color = Color.Black, - size = Size(5f, 10f), - topLeft = Offset( - otherX + vertexVM.vertexSize / 2 + 55, - otherY + vertexVM.vertexSize / 2 - 5f - ), - ) - drawRect( - color = Color.Black, - size = Size(5f, 8f), - topLeft = Offset( - otherX + vertexVM.vertexSize / 2 + 50, - otherY + vertexVM.vertexSize / 2 - 4f - ), - ) - } - if(graphVM.graph.state) - drawText(textMeasurer, edge.weight.toString(), - topLeft = Offset((vertexVM.offsetX + vertexVM.vertexSize + otherX)/2 - edge.weight.toString().length * 5.5f, (vertexVM.offsetY + vertexVM.vertexSize + otherY)/2 - 9), - style = TextStyle(background = Color.White, fontSize = 18.sp) + drawRect( + color = Color.Black, + size = Size(5f, 12f), + topLeft = Offset( + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 60, + edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 6f + ), + ) + drawRect( + color = Color.Black, + size = Size(5f, 10f), + topLeft = Offset( + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 55, + edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 5f + ), + ) + drawRect( + color = Color.Black, + size = Size(5f, 8f), + topLeft = Offset( + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 50, + edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 4f + ), ) } + if(graphVM.graph.state) + drawText(textMeasurer, edgeVM.weight.toString(), + topLeft = Offset((edgeVM.offsetXFrom + edgeVM.vertexSize + edgeVM.offsetXTo)/2 - edgeVM.weight.toString().length * 5.5f, (edgeVM.offsetYFrom + edgeVM.vertexSize + edgeVM.offsetYTo)/2 - 9), + style = TextStyle(background = Color.White, fontSize = 18.sp) + ) + } +} + +@Composable +fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewModel){ + + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + drawLine( + start = Offset( + edgeVM.offsetXFrom + edgeVM.vertexSize / 2, + edgeVM.offsetYFrom + edgeVM.vertexSize / 2 + ), + end = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2), + strokeWidth = 6f, + color = Color.Black, + ) + if (graphVM.graph.state) + drawText( + textMeasurer, edgeVM.weight.toString(), + topLeft = Offset( + (edgeVM.offsetXFrom + edgeVM.vertexSize + edgeVM.offsetXTo) / 2 - edgeVM.weight.toString().length * 5.5f, + (edgeVM.offsetYFrom + edgeVM.vertexSize + edgeVM.offsetYTo) / 2 - 9 + ), + style = TextStyle(background = Color.White, fontSize = 18.sp) + ) } } @Composable fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { val vertex = vertexVM.vertex - val textMeasurer = rememberTextMeasurer() Box(modifier = Modifier .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } @@ -144,6 +184,18 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGrap .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() + for (edge in graphVM.edgesView.iterator()) { + if(edge.to == vertexVM.vertex){ + edge.offsetXTo+= dragAmount.x + edge.offsetYTo+= dragAmount.y + } + } + for (edge in graphVM.edgesView.iterator()) { + if(edge.from == vertexVM.vertex){ + edge.offsetXFrom+= dragAmount.x + edge.offsetYFrom+= dragAmount.y + } + } vertexVM.offsetX += dragAmount.x vertexVM.offsetY += dragAmount.y } @@ -157,28 +209,4 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGrap .wrapContentSize(), ) } - - vertexVM.edges.forEach { edge -> - val otherVertex = edge.to - val otherVM = graphVM.graphView[otherVertex]!! - val otherX = otherVM.offsetX - val otherY = otherVM.offsetY - - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { - drawLine( - start = Offset( - vertexVM.offsetX + vertexVM.vertexSize / 2, - vertexVM.offsetY + vertexVM.vertexSize / 2 - ), - end = Offset(otherX + vertexVM.vertexSize / 2, otherY + vertexVM.vertexSize / 2), - strokeWidth = 8f, - color = Color.Black, - ) - if(graphVM.graph.state) - drawText(textMeasurer, edge.weight.toString(), - topLeft = Offset((vertexVM.offsetX + vertexVM.vertexSize + otherX)/2 - edge.weight.toString().length * 5.5f, (vertexVM.offsetY + vertexVM.vertexSize + otherY)/2 - 9), - style = TextStyle(background = Color.White, fontSize = 18.sp) - ) - } - } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 39832cd..bb34195 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -1,36 +1,35 @@ package viewmodel import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import model.graph.Graph -import model.graph.edges.Edge abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { val name = _name - var graphView by mutableStateOf(mutableMapOf>()) + var vertexView by mutableStateOf(mutableMapOf>()) + var edgesView by mutableStateOf(mutableListOf>()) val graphModel = graph var size = 0 init { for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } } fun addVertex(vertex: V) { size += 1 - graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + vertexView.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) updateView() } fun updateView() { - val keep = graphView - graphView = mutableMapOf>() - graphView = keep + val keep = vertexView + vertexView = mutableMapOf>() + vertexView = keep } abstract fun addEdge(from: V, to: V, weight: Int = 1) diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 9b64e52..ee40d00 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -12,7 +12,10 @@ class DirectedGraphViewModel( get() = graph init { for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + } + for (edge in graphModel.edges) { + edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) } } fun dijkstraAlgo(start: V, end: V){ @@ -22,11 +25,14 @@ class DirectedGraphViewModel( override fun addEdge(from: V, to: V, weight: Int) { - if (graphView[from] == null) return - for (i in graphView[from]?.edges!!) if (i.to == to) return - val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to, weight)) - graphView[from]?.edges = edgesCopy + if (vertexView[from] == null) return + for (i in vertexView[from]?.edges!!) if (i.to == to) return + val edgesCopy = vertexView[from]?.edges?.toMutableList()!! + val edge = Edge(from, to, weight) + edgesCopy.add(edge) + vertexView[from]?.edges = edgesCopy + edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) graphModel.addEdge(from, to, weight) + updateView() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/EdgeViewModel.kt b/src/main/kotlin/viewmodel/EdgeViewModel.kt new file mode 100644 index 0000000..4276854 --- /dev/null +++ b/src/main/kotlin/viewmodel/EdgeViewModel.kt @@ -0,0 +1,19 @@ +package viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import model.graph.edges.Edge + +class EdgeViewModel(edge: Edge, vertexFromVM: VertexViewModel, vertexToVM: VertexViewModel,) : + ViewModel() { + var offsetXFrom by mutableStateOf(vertexFromVM.offsetX) + var offsetYFrom by mutableStateOf(vertexFromVM.offsetY) + var offsetXTo by mutableStateOf(vertexToVM.offsetX) + var offsetYTo by mutableStateOf(vertexToVM.offsetY) + var vertexSize by mutableStateOf(vertexFromVM.vertexSize) + var weight by mutableStateOf(edge.weight) + var from by mutableStateOf(edge.from) + var to by mutableStateOf(edge.to) +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 5469e8e..537eba0 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -12,19 +12,27 @@ class UndirectedGraphViewModel( get() = graph init { for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + } + for (edge in graphModel.edges) { + edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) } } override fun addEdge(from: V, to: V, weight: Int) { - if (graphView[from] == null) return - for (i in graphView[from]?.edges!!) if (i.to == to) return - val edgesCopy = graphView[from]?.edges?.toMutableList()!! - edgesCopy.add(Edge(from, to, weight)) - edgesCopy.add(Edge(to, from, weight)) - graphView[from]?.edges = edgesCopy + if (vertexView[from] == null) return + for (i in vertexView[from]?.edges!!) if (i.to == to) return + val edgesCopy = vertexView[from]?.edges?.toMutableList()!! + val edgeTo = Edge(from, to, weight) + val edgeFrom = Edge(from, to, weight) + edgesCopy.add(edgeTo) + edgesCopy.add(edgeFrom) + vertexView[from]?.edges = edgesCopy + edgesView.add(EdgeViewModel(edgeTo, vertexView[edgeTo.from]!!, vertexView[edgeTo.to]!!)) + edgesView.add(EdgeViewModel(edgeFrom, vertexView[edgeFrom.from]!!, vertexView[edgeFrom.to]!!)) graphModel.addEdge(from, to, weight) + updateView() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index d784bd8..83e6aa6 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import height -import kotlinx.serialization.descriptors.PrimitiveKind import model.graph.edges.Edge import width import kotlin.random.Random From 769eb7197691f7bd6ba1b59194cc034bf6cd58f9 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 22 May 2024 16:25:15 -0400 Subject: [PATCH 114/172] add: Dijkstra algorithm visualisation --- src/main/kotlin/model/algos/Dijkstra.kt | 5 +++- src/main/kotlin/view/views/GraphView.kt | 2 -- src/main/kotlin/view/views/VertexView.kt | 24 +++++++++---------- .../viewmodel/DirectedGraphViewModel.kt | 10 +++++--- src/main/kotlin/viewmodel/EdgeViewModel.kt | 2 ++ .../viewmodel/UndirectedGraphViewModel.kt | 3 +-- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/model/algos/Dijkstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt index 1521117..9723120 100644 --- a/src/main/kotlin/model/algos/Dijkstra.kt +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -17,6 +17,9 @@ class Dijkstra(var graph: MutableMap>>, private val t while (visitedSet.size != totalNodes) { println(vertexValues) + for (i in pathMap){ + println(i) + } if (prioraQueue.isEmpty()) { return pathMap[end]!! } @@ -40,7 +43,7 @@ class Dijkstra(var graph: MutableMap>>, private val t newRange = vertexValues[currentVertex]!! + j.weight if (newRange < vertexValues[j.to]!!) { vertexValues[j.to] = newRange - val k = pathMap[j.from] + val k = pathMap[j.from]?.toMutableList() k?.add(j) pathMap[j.to] = k!! } diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index 5886e80..43f8cc4 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -17,11 +17,9 @@ fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { @Composable fun GraphViewDirect(graphViewModel: DirectedGraphViewModel) { - for (vertexVM in graphViewModel.vertexView.values) { DirectedVertexView(vertexVM, graphViewModel) } - for (edge in graphViewModel.edgesView.iterator()) { DirectedEdgeView(edge, graphViewModel) } diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index a5fd5b7..b0faba1 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -86,7 +86,7 @@ fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel ), end = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2), strokeWidth = 6f, - color = Color.Black, + color = edgeVM.color, ) rotate( degrees = ((57.2958 * (atan2( @@ -96,42 +96,42 @@ fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel pivot = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2) ) { drawRect( - color = Color.Black, + color = edgeVM.color, size = Size(5f, 16f), topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 70, + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 65, edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 8f ), ) drawRect( - color = Color.Black, + color = edgeVM.color, size = Size(5f, 14f), topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 65, + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 60, edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 7f ), ) drawRect( - color = Color.Black, + color = edgeVM.color, size = Size(5f, 12f), topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 60, + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 55, edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 6f ), ) drawRect( - color = Color.Black, + color = edgeVM.color, size = Size(5f, 10f), topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 55, + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 50, edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 5f ), ) drawRect( - color = Color.Black, + color = edgeVM.color, size = Size(5f, 8f), topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 50, + edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 45, edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 4f ), ) @@ -157,7 +157,7 @@ fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewM ), end = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2), strokeWidth = 6f, - color = Color.Black, + color = edgeVM.color, ) if (graphVM.graph.state) drawText( diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index ee40d00..94fad53 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,6 +1,7 @@ package viewmodel import Dijkstra +import androidx.compose.ui.graphics.Color import model.graph.DirectedGraph import model.graph.edges.Edge @@ -19,11 +20,14 @@ class DirectedGraphViewModel( } } fun dijkstraAlgo(start: V, end: V){ - val dalg = Dijkstra(graph.matrix, graph.size) - dalg.dijkstra(start, end) + val y = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) + for (edgeVM in edgesView){ + if (Edge(edgeVM.from, edgeVM.to, edgeVM.weight) in y){ + edgeVM.color = Color.Red + } + } } - override fun addEdge(from: V, to: V, weight: Int) { if (vertexView[from] == null) return for (i in vertexView[from]?.edges!!) if (i.to == to) return diff --git a/src/main/kotlin/viewmodel/EdgeViewModel.kt b/src/main/kotlin/viewmodel/EdgeViewModel.kt index 4276854..017b52a 100644 --- a/src/main/kotlin/viewmodel/EdgeViewModel.kt +++ b/src/main/kotlin/viewmodel/EdgeViewModel.kt @@ -3,6 +3,7 @@ package viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import model.graph.edges.Edge @@ -16,4 +17,5 @@ class EdgeViewModel(edge: Edge, vertexFromVM: VertexViewModel, vertexTo var weight by mutableStateOf(edge.weight) var from by mutableStateOf(edge.from) var to by mutableStateOf(edge.to) + var color by mutableStateOf(Color.Black) } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 537eba0..dafab91 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -19,7 +19,6 @@ class UndirectedGraphViewModel( } } - override fun addEdge(from: V, to: V, weight: Int) { if (vertexView[from] == null) return for (i in vertexView[from]?.edges!!) if (i.to == to) return @@ -32,7 +31,7 @@ class UndirectedGraphViewModel( edgesView.add(EdgeViewModel(edgeTo, vertexView[edgeTo.from]!!, vertexView[edgeTo.to]!!)) edgesView.add(EdgeViewModel(edgeFrom, vertexView[edgeFrom.from]!!, vertexView[edgeFrom.to]!!)) graphModel.addEdge(from, to, weight) + graphModel.addEdge(to, from, weight) updateView() } - } \ No newline at end of file From a669c5e69ef53bc29dfe4655e93e92aaedf44d67 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Thu, 23 May 2024 03:56:35 +0300 Subject: [PATCH 115/172] feat: implemented HITS algorithm --- .../kotlin/model/algos/BetweenesCentrality.kt | 105 ++++++++++++++++++ .../kotlin/algos/BetweennesCentralityTest.kt | 40 +++++++ 2 files changed, 145 insertions(+) create mode 100644 src/main/kotlin/model/algos/BetweenesCentrality.kt create mode 100644 src/test/kotlin/algos/BetweennesCentralityTest.kt diff --git a/src/main/kotlin/model/algos/BetweenesCentrality.kt b/src/main/kotlin/model/algos/BetweenesCentrality.kt new file mode 100644 index 0000000..ba7e4c3 --- /dev/null +++ b/src/main/kotlin/model/algos/BetweenesCentrality.kt @@ -0,0 +1,105 @@ +package model.algos + +import model.graph.UndirectedGraph + +//object BetweenesCentrality { +// fun betweennessCentrality(graph: UndirectedGraph): Map { +// val centrality = mutableMapOf() +// for (v in graph.vertices) { +// centrality[v] = 0.0 +// } +// +// for (s in graph.vertices) { +// val stack = Stack() +// val predecessors = mutableMapOf>() +// val shortestPaths = mutableMapOf() +// val distance = mutableMapOf() +// val dependency = mutableMapOf() +// +// for (v in graph.vertices) { +// predecessors[v] = mutableListOf() +// shortestPaths[v] = 0 +// distance[v] = -1 +// dependency[v] = 0.0 +// } +// +// shortestPaths[s] = 1 +// distance[s] = 0 +// val queue: Queue = LinkedList() +// queue.add(s) +// +// while (queue.isNotEmpty()) { +// val v = queue.poll() +// stack.push(v) +// for (edge in graph.edgesOf(v)) { +// val w = edge.to +// if (distance[w]!! < 0) { +// queue.add(w) +// distance[w] = distance[v]!! + 1 +// } +// if (distance[w] == distance[v]!! + 1) { +// shortestPaths[w] = shortestPaths[w]!! + shortestPaths[v]!! +// predecessors[w]!!.add(v) +// } +// } +// } +// +// while (stack.isNotEmpty()) { +// val w = stack.pop() +// for (v in predecessors[w]!!) { +// dependency[v] = dependency[v]!! + (shortestPaths[v]!!.toDouble() / shortestPaths[w]!!) * (1 + dependency[w]!!) +// } +// if (w != s) { +// centrality[w] = centrality[w]!! + dependency[w]!! +// } +// } +// } +// +// return centrality +// } +//} + +/* +* to view the centrality value for each vertex use: +* `val centrality = graph.betweennessCentrality() +* centrality.forEach { (vertex, value) -> +* println("Vertex: $vertex, Betweenness Centrality: $value") }` +* */ +object BetweenesCentrality { + fun hits(iterations: Int = 100, graph: UndirectedGraph): Pair, Map> { + val authority = mutableMapOf() + val hub = mutableMapOf() + + // Инициализация всех авторитетов и хабов значением 1.0 + for (v in graph.vertices) { + authority[v] = 1.0 + hub[v] = 1.0 + } + + for (i in 0 until iterations) { + val newAuthority = mutableMapOf() + val newHub = mutableMapOf() + + // Обновление авторитетов + for (v in graph.vertices) { + newAuthority[v] = graph.edgesOf(v).sumOf { edge -> hub[edge.to] ?: 0.0 } + } + + // Обновление хабов + for (v in graph.vertices) { + newHub[v] = graph.edgesOf(v).sumOf { edge -> newAuthority[edge.to] ?: 0.0 } + } + + // Нормализация + val normAuthority = Math.sqrt(newAuthority.values.sumOf { it * it }) + val normHub = Math.sqrt(newHub.values.sumOf { it * it }) + + for (v in graph.vertices) { + authority[v] = newAuthority[v]!! / normAuthority + hub[v] = newHub[v]!! / normHub + } + } + + return Pair(authority, hub) + } +} \ No newline at end of file diff --git a/src/test/kotlin/algos/BetweennesCentralityTest.kt b/src/test/kotlin/algos/BetweennesCentralityTest.kt new file mode 100644 index 0000000..f8c5b20 --- /dev/null +++ b/src/test/kotlin/algos/BetweennesCentralityTest.kt @@ -0,0 +1,40 @@ +package algos + +import model.algos.BetweenesCentrality +import model.graph.UndirectedGraph +import kotlin.test.Test +import kotlin.test.assertNotNull + +class BetweennesCentralityTest { + @Test + fun basic() { + val graph = UndirectedGraph() + for (i in 0..9) { + graph.addVertex(i) + } + graph.addEdge(1, 2, 1) + graph.addEdge(1, 3, 1) + graph.addEdge(1, 4) + graph.addEdge(2, 3) + graph.addEdge(2, 4) + graph.addEdge(3, 4) + graph.addEdge(2, 5) + graph.addEdge(4, 5) + graph.addEdge(5, 6) + graph.addEdge(5, 7) + graph.addEdge(6, 7) + graph.addEdge(6, 8) + graph.addEdge(6, 9) + graph.addEdge(7, 8) + graph.addEdge(7, 9) + graph.addEdge(8, 9) + val (centrality, v) = BetweenesCentrality.hits(100, graph) + for ((vertex, value) in centrality) { + println("Vertex: $vertex, Betweenness Centrality: $value") + } + for ((vertex, value) in centrality) { + println("Vertex: $vertex, Betweenness Centrality: $value") + } + assertNotNull(centrality) + } +} \ No newline at end of file From 5443cc455fe70a5b16e5651f86f4b16c50189ec0 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Thu, 23 May 2024 05:33:52 -0400 Subject: [PATCH 116/172] add: SQLite db init --- build.gradle.kts | 2 + .../view/screens/DirectedGraphScreen.kt | 376 +++++++++--------- .../viewmodel/DirectedGraphViewModel.kt | 25 ++ .../kotlin/viewmodel/MainScreenViewModel.kt | 1 - src/main/kotlin/viewmodel/requests/create.sql | 5 + 5 files changed, 225 insertions(+), 184 deletions(-) create mode 100644 src/main/kotlin/viewmodel/requests/create.sql diff --git a/build.gradle.kts b/build.gradle.kts index 10410c8..bc2afb7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,7 @@ repositories { google() } + dependencies { // Note, if you develop a library, you should use compose.desktop.common. // compose.desktop.currentOs should be used in launcher-sourceSet @@ -23,6 +24,7 @@ dependencies { // With compose.desktop.common you will also lose @Preview functionality implementation(compose.desktop.currentOs) val nav_version = "2.8.0-alpha02" + implementation("org.xerial", "sqlite-jdbc", "3.41.2.1") implementation("org.jetbrains.androidx.navigation:navigation-compose:$nav_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") testImplementation(kotlin("test")) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 9ace564..f09776f 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -95,212 +95,222 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) - DialogWindow( - visible = isOpenedEdgeMenu, - title = "New Edge", - onCloseRequest = { isOpenedEdgeMenu = false }, - state = rememberDialogState(height = 600.dp, width = 880.dp) + Button( + onClick = {graphVM.saveSQLite()}, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - var checkedState by remember { mutableStateOf(false) } - var weight by remember { mutableStateOf("") } - Column { - Spacer(modifier = Modifier.height(24.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(26.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + Text(localisation("save"), style = defaultStyle) + } + } + DialogWindow( + visible = isOpenedEdgeMenu, + title = "New Edge", + onCloseRequest = { isOpenedEdgeMenu = false }, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + var checkedState by remember { mutableStateOf(false) } + var weight by remember { mutableStateOf("") } + Column { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(26.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(62.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue },) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + if(!checkedState) { + Text( + text = localisation("weight"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) ) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(62.dp)) TextField( + enabled = !checkedState, modifier = Modifier .weight(1f) .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) + .background(color = Color.White, shape = RoundedCornerShape(10.dp)), + shape = RoundedCornerShape(10.dp), colors = TextFieldDefaults.textFieldColors( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), - shape = RoundedCornerShape(25.dp), textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue },) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - if(!checkedState) { - Text( - text = localisation("weight"), - style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(30.dp)) - TextField( - enabled = !checkedState, - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) - .background(color = Color.White, shape = RoundedCornerShape(10.dp)), - shape = RoundedCornerShape(10.dp), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - textStyle = defaultStyle, - value = weight, - onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) - ) - Spacer(modifier = Modifier.width(20.dp)) - } - Checkbox( - modifier = Modifier.align(Alignment.CenterVertically), - checked = checkedState, - onCheckedChange = { checkedState = it; - weight = if(checkedState) "1" else "" } + value = weight, + onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) ) - Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(200.dp)) + Spacer(modifier = Modifier.width(20.dp)) } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { - val sourceInt = source.toIntOrNull() - val destinationInt = destination.toIntOrNull() - if (sourceInt != null && destinationInt != null) { - graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) - isOpenedEdgeMenu = false - } - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_edge"), style = defaultStyle) - } + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = checkedState, + onCheckedChange = { checkedState = it; + weight = if(checkedState) "1" else "" } + ) + Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) + isOpenedEdgeMenu = false + } + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_edge"), style = defaultStyle) } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { isOpenedEdgeMenu = false }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) - ) { - Text(localisation("back"), style = defaultStyle) - } + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { isOpenedEdgeMenu = false }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text(localisation("back"), style = defaultStyle) } } } - DialogWindow( - visible = isDijkstraMenu, - title = "New Edge", - onCloseRequest = { isDijkstraMenu = false }, - state = rememberDialogState(height = 600.dp, width = 880.dp) - ) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - Column { - Spacer(modifier = Modifier.height(24.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(26.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + } + DialogWindow( + visible = isDijkstraMenu, + title = "Dijkstra Run", + onCloseRequest = { isDijkstraMenu = false }, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + Column { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(26.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, - ) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(62.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(62.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue },) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("start"), style = defaultStyle) - } + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue },) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("start"), style = defaultStyle) } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { isDijkstraMenu = false }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) - ) { - Text(localisation("back"), style = defaultStyle) - } + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { isDijkstraMenu = false }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text(localisation("back"), style = defaultStyle) } } } diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 94fad53..7a7d5a0 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -4,6 +4,8 @@ import Dijkstra import androidx.compose.ui.graphics.Color import model.graph.DirectedGraph import model.graph.edges.Edge +import java.sql.DriverManager +import java.sql.SQLException class DirectedGraphViewModel( name: String, @@ -28,6 +30,29 @@ class DirectedGraphViewModel( } } + fun saveSQLite(){ + val DB_DRIVER = "jdbc:sqlite" + + var create = ("CREATE TABLE if not exists " + name + " (") + + for (i in graph.entries){ + create = create + " " + i.toString() + " INTEGER " + } + create = create + " )" + val connection = DriverManager.getConnection("$DB_DRIVER:$name.db") + ?: throw SQLException("Cannot connect to database") + connection.createStatement().also { stmt -> + try { + stmt.execute(create) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + } finally { + stmt.close() + } + } + } + override fun addEdge(from: V, to: V, weight: Int) { if (vertexView[from] == null) return for (i in vertexView[from]?.edges!!) if (i.to == to) return diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index a17e65b..7949094 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -64,7 +64,6 @@ class MainScreenViewModel : ViewModel() { graphs.directedGraphs.removeAt(findGraph(index)) graphs.typeList.removeAt(index) } - } } diff --git a/src/main/kotlin/viewmodel/requests/create.sql b/src/main/kotlin/viewmodel/requests/create.sql new file mode 100644 index 0000000..f79951c --- /dev/null +++ b/src/main/kotlin/viewmodel/requests/create.sql @@ -0,0 +1,5 @@ +CREATE TABLE if not exists cities +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name varchar(255) +); \ No newline at end of file From 63c1fcdb6c728a5519709eff0958fd502812f0bd Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 23 May 2024 13:59:44 +0300 Subject: [PATCH 117/172] Ref: moved addEdge and default button to common functions --- src/main/kotlin/view/common/AddEdgeDialog.kt | 161 +++++++++++++ src/main/kotlin/view/common/DefaultButton.kt | 34 +++ .../view/screens/DirectedGraphScreen.kt | 216 +++--------------- .../kotlin/view/screens/SettingsScreen.kt | 2 +- .../view/screens/UndirectedGraphScreen.kt | 159 +------------ src/main/kotlin/view/styling.kt | 11 +- 6 files changed, 241 insertions(+), 342 deletions(-) create mode 100644 src/main/kotlin/view/common/AddEdgeDialog.kt create mode 100644 src/main/kotlin/view/common/DefaultButton.kt diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt new file mode 100644 index 0000000..63961b5 --- /dev/null +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -0,0 +1,161 @@ +package view.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState +import localisation.localisation +import view.defaultStyle +import viewmodel.AbstractGraphViewModel + +@Composable +fun AddEdgeDialog( + _visible: Boolean, + onClose: () -> Unit, + graphVM: AbstractGraphViewModel, + isDirected: Boolean = false +) { + var visible by mutableStateOf(_visible) + DialogWindow( + visible = visible, + title = "New Edge", + onCloseRequest = onClose, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + println("внутри visible is $visible") + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + var checkedState by remember { mutableStateOf(false) } + var weight by remember { mutableStateOf("") } + Column(modifier = Modifier.padding(30.dp, 24.dp)) { + val textWidth = 90.dp + val rightPadding = 200.dp + Row { + Text( + text = localisation(if (isDirected) "from" else "1st"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth), + ) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(rightPadding)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Text( + text = localisation(if (isDirected) "to" else ("2nd")), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth) + ) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }, + ) + Spacer(modifier = Modifier.width(rightPadding)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + if (!checkedState) { + Text( + text = localisation("weight"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + .width(textWidth + 30.dp) + ) + TextField( + enabled = !checkedState, + modifier = Modifier + .weight(1f) + .width(115.dp) + .border( + 3.dp, + color = Color.Black, + shape = RoundedCornerShape(10.dp), + ) + .background(color = Color.White, shape = RoundedCornerShape(10.dp)), + shape = RoundedCornerShape(10.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + textStyle = defaultStyle, + value = weight, + onValueChange = { value -> + if (value.length < 10) weight = value.filter { it.isDigit() } + }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + Spacer(modifier = Modifier.width(20.dp)) + } + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = checkedState, + onCheckedChange = { + checkedState = it; + weight = if (checkedState) "1" else "" + } + ) + Text( + text = localisation("unweighted"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(rightPadding)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + val onClick = { + val sourceInt = source.toIntOrNull() + val destinationInt = destination.toIntOrNull() + if (sourceInt != null && destinationInt != null) { + graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) + visible = false + } + } + DefaultButton(onClick, "add_edge") + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + DefaultButton(onClose, "back", Color.Red) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/common/DefaultButton.kt b/src/main/kotlin/view/common/DefaultButton.kt new file mode 100644 index 0000000..993e4b1 --- /dev/null +++ b/src/main/kotlin/view/common/DefaultButton.kt @@ -0,0 +1,34 @@ +package view.common + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import localisation.localisation +import view.DefaultColors +import view.defaultStyle + +@Composable +fun DefaultButton( + onClick: () -> Unit, + localisationCode: String, + color: Color = DefaultColors.primary +) { + Button( + onClick = onClick, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = color) + ) { + Text(localisation(localisationCode), style = defaultStyle) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 9ace564..dada8b0 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation import view.DefaultColors +import view.common.AddEdgeDialog +import view.common.DefaultButton import view.defaultStyle import view.views.GraphViewDirect import viewmodel.MainScreenViewModel @@ -32,9 +34,6 @@ fun DirectedGraphScreen( ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) - var isOpenedEdgeMenu by remember { mutableStateOf(false) } - var isDijkstraMenu by remember { mutableStateOf(false) } - Box(modifier = Modifier.fillMaxSize()) { GraphViewDirect(graphVM) @@ -57,172 +56,26 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(16.dp)) // Add vertex - Button( - onClick = { graphVM.addVertex(graphVM.size) }, - modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_vertex"), style = defaultStyle) - } + DefaultButton({ graphVM.addVertex(graphVM.size) }, "add_vertex") Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu}, - modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("open_edge"), style = defaultStyle) - } - - Spacer(modifier = Modifier.height(10.dp)) - Button( - onClick = {isDijkstraMenu = !isDijkstraMenu}, - modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("dijkstra_algorithm"), style = defaultStyle) - } + // Open "add edge" dialog + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + DefaultButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") Spacer(modifier = Modifier.height(10.dp)) - DialogWindow( - visible = isOpenedEdgeMenu, - title = "New Edge", - onCloseRequest = { isOpenedEdgeMenu = false }, - state = rememberDialogState(height = 600.dp, width = 880.dp) - ) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - var checkedState by remember { mutableStateOf(false) } - var weight by remember { mutableStateOf("") } - Column { - Spacer(modifier = Modifier.height(24.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(26.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, - ) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(62.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + // Open Dijkstra dialog window + var isDijkstraMenu by remember { mutableStateOf(false) } + DefaultButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra_algorithm") - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue },) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - if(!checkedState) { - Text( - text = localisation("weight"), - style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(30.dp)) - TextField( - enabled = !checkedState, - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) - .background(color = Color.White, shape = RoundedCornerShape(10.dp)), - shape = RoundedCornerShape(10.dp), + Spacer(modifier = Modifier.height(10.dp)) + + val onClose = { isOpenedEdgeMenu = false } + AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM, isDirected = true) - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - textStyle = defaultStyle, - value = weight, - onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) - ) - Spacer(modifier = Modifier.width(20.dp)) - } - Checkbox( - modifier = Modifier.align(Alignment.CenterVertically), - checked = checkedState, - onCheckedChange = { checkedState = it; - weight = if(checkedState) "1" else "" } - ) - Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { - val sourceInt = source.toIntOrNull() - val destinationInt = destination.toIntOrNull() - if (sourceInt != null && destinationInt != null) { - graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) - isOpenedEdgeMenu = false - } - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_edge"), style = defaultStyle) - } - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { isOpenedEdgeMenu = false }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) - ) { - Text(localisation("back"), style = defaultStyle) - } - } - } - } + // Dijkstra dialog window DialogWindow( visible = isDijkstraMenu, title = "New Edge", @@ -235,13 +88,17 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(24.dp)) Row { Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("from"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Text( + text = localisation("from"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) Spacer(modifier = Modifier.width(26.dp)) TextField( modifier = Modifier .weight(1f) .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), colors = TextFieldDefaults.textFieldColors( focusedIndicatorColor = Color.Transparent, @@ -257,13 +114,17 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(36.dp)) Row { Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("to"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Text( + text = localisation("to"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) Spacer(modifier = Modifier.width(62.dp)) TextField( modifier = Modifier .weight(1f) .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), colors = TextFieldDefaults.textFieldColors( focusedIndicatorColor = Color.Transparent, @@ -272,35 +133,20 @@ fun DirectedGraphScreen( shape = RoundedCornerShape(25.dp), textStyle = defaultStyle, value = destination, - onValueChange = { newValue -> destination = newValue },) + onValueChange = { newValue -> destination = newValue }, + ) Spacer(modifier = Modifier.width(200.dp)) } Spacer(modifier = Modifier.height(36.dp)) Row { Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("start"), style = defaultStyle) - } + val onClick = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) } + DefaultButton(onClick, "start") } Spacer(modifier = Modifier.height(36.dp)) Row { Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { isDijkstraMenu = false }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) - ) { - Text(localisation("back"), style = defaultStyle) - } + DefaultButton({ isDijkstraMenu = false }, "back", Color.Red) } } } diff --git a/src/main/kotlin/view/screens/SettingsScreen.kt b/src/main/kotlin/view/screens/SettingsScreen.kt index eda4023..4424ee0 100644 --- a/src/main/kotlin/view/screens/SettingsScreen.kt +++ b/src/main/kotlin/view/screens/SettingsScreen.kt @@ -110,7 +110,7 @@ fun SettingsScreen(navController: NavController) { .padding(16.dp) .border(width = 3.dp, color = Color.Black) .bounceClick(), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.error) + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) ) { Text(localisation("back"), style = defaultStyle, color = Color.White) } diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 98407be..2302cfe 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation import view.DefaultColors +import view.common.AddEdgeDialog +import view.common.DefaultButton import view.defaultStyle import view.views.GraphViewUndirect import viewmodel.MainScreenViewModel @@ -52,160 +54,17 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.height(16.dp)) // Add vertex - Button( - onClick = { graphVM.addVertex(graphVM.size) }, - modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_vertex"), style = defaultStyle) - } + DefaultButton({ graphVM.addVertex(graphVM.size) }, "add_vertex") Spacer(modifier = Modifier.height(16.dp)) - var isOpenedEdgeMenu by remember { mutableStateOf(false) } - Button( - onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu }, - modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("open_edge"), style = defaultStyle) - } + // Open "add edge" dialog window + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + DefaultButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") Spacer(modifier = Modifier.height(10.dp)) - - DialogWindow( - visible = isOpenedEdgeMenu, - title = "New Edge", - onCloseRequest = { isOpenedEdgeMenu = false }, - state = rememberDialogState(height = 600.dp, width = 880.dp) - ) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - var checkedState by remember { mutableStateOf(false) } - var weight by remember { mutableStateOf("") } - Column { - Spacer(modifier = Modifier.height(24.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("1st"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(62.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, - ) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("2nd"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(54.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue },) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - if(!checkedState) { - Text( - text = localisation("weight"), - style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(30.dp)) - TextField( - enabled = !checkedState, - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) - .background(color = Color.White, shape = RoundedCornerShape(10.dp)), - shape = RoundedCornerShape(10.dp), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - textStyle = defaultStyle, - value = weight, - onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) - ) - Spacer(modifier = Modifier.width(20.dp)) - } - Checkbox( - modifier = Modifier.align(Alignment.CenterVertically), - checked = checkedState, - onCheckedChange = { checkedState = it; - weight = if(checkedState) "1" else "" } - ) - Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { - val sourceInt = source.toIntOrNull() - val destinationInt = destination.toIntOrNull() - if (sourceInt != null && destinationInt != null) { - graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) - isOpenedEdgeMenu = false - } - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_edge"), style = defaultStyle) - } - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { isOpenedEdgeMenu = false }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) - ) { - Text(localisation("back"), style = defaultStyle) - } - } - } - } + + val onClose = { isOpenedEdgeMenu = false } + AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM) } } diff --git a/src/main/kotlin/view/styling.kt b/src/main/kotlin/view/styling.kt index 4018c6a..20438ec 100644 --- a/src/main/kotlin/view/styling.kt +++ b/src/main/kotlin/view/styling.kt @@ -9,11 +9,10 @@ val defaultStyle = TextStyle(fontSize = 28.sp) val bigStyle = TextStyle(fontSize = 50.sp) -object DefaultColors{ - val primary = Color(0xff,0xf1,0x4a) - val primarySelected = Color(0xcf,0xc0,0x07) - val error = Color.Red - val darkGreen = Color(0x00,0x64,0x00) - val simpleGreen = Color(0x00,0xe4,0x00) +object DefaultColors { + val primary = Color(0xff, 0xf1, 0x4a) + val primarySelected = Color(0xcf, 0xc0, 0x07) + val darkGreen = Color(0x00, 0x64, 0x00) + val simpleGreen = Color(0x00, 0xe4, 0x00) val background = Color.White } \ No newline at end of file From e1efe1566ed6fb009b4a7906d72568967b916e36 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 23 May 2024 14:08:24 +0300 Subject: [PATCH 118/172] Ref: change more buttons to common DefaultButton --- src/main/kotlin/view/common/AddEdgeDialog.kt | 1 - .../kotlin/view/screens/DirectedGraphScreen.kt | 15 +++------------ .../kotlin/view/screens/UndirectedGraphScreen.kt | 15 +++------------ 3 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index 63961b5..5aa7fef 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -35,7 +35,6 @@ fun AddEdgeDialog( onCloseRequest = onClose, state = rememberDialogState(height = 600.dp, width = 880.dp) ) { - println("внутри visible is $visible") var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } var checkedState by remember { mutableStateOf(false) } diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index dada8b0..74d6442 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -40,18 +40,9 @@ fun DirectedGraphScreen( } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { - // To MainScreen Text(text = "Directed") - Button( - onClick = { navController.popBackStack() }, - modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(DefaultColors.primary) - ) { - Text(localisation("home"), style = defaultStyle) - } + // To MainScreen + DefaultButton({ navController.popBackStack() }, "home") Spacer(modifier = Modifier.height(16.dp)) @@ -71,7 +62,7 @@ fun DirectedGraphScreen( DefaultButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra_algorithm") Spacer(modifier = Modifier.height(10.dp)) - + val onClose = { isOpenedEdgeMenu = false } AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM, isDirected = true) diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 2302cfe..0bf0615 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -38,18 +38,9 @@ fun UndirectedGraphScreen( } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { - // To MainScreen Text(text = "Undirected") - Button( - onClick = { navController.popBackStack() }, - modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(DefaultColors.primary) - ) { - Text(localisation("home"), style = defaultStyle) - } + // To MainScreen + DefaultButton({ navController.popBackStack() }, "home") Spacer(modifier = Modifier.height(16.dp)) @@ -63,7 +54,7 @@ fun UndirectedGraphScreen( DefaultButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") Spacer(modifier = Modifier.height(10.dp)) - + val onClose = { isOpenedEdgeMenu = false } AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM) } From 8b82a29b6c07ee0a546875e9a9e19215a762d00e Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 23 May 2024 15:02:41 +0300 Subject: [PATCH 119/172] Ref: refactor addEdge methods in GraphVM --- src/main/kotlin/view/views/GraphView.kt | 4 +-- .../viewmodel/AbstractGraphViewModel.kt | 24 +++++++++---- .../viewmodel/DirectedGraphViewModel.kt | 33 ++++++++++------- .../viewmodel/UndirectedGraphViewModel.kt | 36 +++++++++++-------- 4 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index 43f8cc4..bb23ed0 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -6,7 +6,7 @@ import viewmodel.UndirectedGraphViewModel @Composable fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { - for (vertexVM in graphViewModel.vertexView.values) { + for (vertexVM in graphViewModel.graphView.values) { UndirectedVertexView(vertexVM, graphViewModel) } for (edge in graphViewModel.edgesView.iterator()) { @@ -17,7 +17,7 @@ fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { @Composable fun GraphViewDirect(graphViewModel: DirectedGraphViewModel) { - for (vertexVM in graphViewModel.vertexView.values) { + for (vertexVM in graphViewModel.graphView.values) { DirectedVertexView(vertexVM, graphViewModel) } for (edge in graphViewModel.edgesView.iterator()) { diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index bb34195..7b6fa8a 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -8,28 +8,40 @@ import model.graph.Graph abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { val name = _name - var vertexView by mutableStateOf(mutableMapOf>()) + var graphView by mutableStateOf(mutableMapOf>()) var edgesView by mutableStateOf(mutableListOf>()) val graphModel = graph var size = 0 init { for (vertex in graphModel.entries) { - vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } } fun addVertex(vertex: V) { size += 1 - vertexView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphView.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) updateView() } fun updateView() { - val keep = vertexView - vertexView = mutableMapOf>() - vertexView = keep + val keep = graphView + graphView = mutableMapOf>() + graphView = keep + } + + // возможно надо будет переработать состояния, поэтому понадобится + fun updateEdgesView(vertex: V) { + try { + val keep = graphView[vertex]?.edges!! + graphView[vertex]?.edges = mutableListOf() + graphView[vertex]?.edges = keep + } catch (e: Exception) { + println("Can't find vertex for update view of their edges") + } + } abstract fun addEdge(from: V, to: V, weight: Int = 1) diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 94fad53..f531cba 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -8,34 +8,43 @@ import model.graph.edges.Edge class DirectedGraphViewModel( name: String, val graph: DirectedGraph = DirectedGraph() -): AbstractGraphViewModel(name, graph){ +) : AbstractGraphViewModel(name, graph) { val model get() = graph + init { for (vertex in graphModel.entries) { - vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } for (edge in graphModel.edges) { - edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) + edgesView.add(EdgeViewModel(edge, graphView[edge.from]!!, graphView[edge.to]!!)) } } - fun dijkstraAlgo(start: V, end: V){ + + fun dijkstraAlgo(start: V, end: V) { val y = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) - for (edgeVM in edgesView){ - if (Edge(edgeVM.from, edgeVM.to, edgeVM.weight) in y){ + for (edgeVM in edgesView) { + if (Edge(edgeVM.from, edgeVM.to, edgeVM.weight) in y) { edgeVM.color = Color.Red } } } override fun addEdge(from: V, to: V, weight: Int) { - if (vertexView[from] == null) return - for (i in vertexView[from]?.edges!!) if (i.to == to) return - val edgesCopy = vertexView[from]?.edges?.toMutableList()!! + val source: VertexViewModel + val destination: VertexViewModel + try { + source = graphView[from]!! + destination = graphView[to]!! + } catch (e: Exception) { + println("Can't add edge between $from and $to: one of them don't exist") + return + } + for (edge in source.edges) if (edge.to == to) return + val edge = Edge(from, to, weight) - edgesCopy.add(edge) - vertexView[from]?.edges = edgesCopy - edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) + source.edges.add(edge) + edgesView.add(EdgeViewModel(edge, source, destination)) graphModel.addEdge(from, to, weight) updateView() } diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index dafab91..47d2109 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -6,32 +6,40 @@ import model.graph.edges.Edge class UndirectedGraphViewModel( name: String, val graph: UndirectedGraph = UndirectedGraph() -): AbstractGraphViewModel(name, graph){ +) : AbstractGraphViewModel(name, graph) { val model get() = graph + init { for (vertex in graphModel.entries) { - vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } for (edge in graphModel.edges) { - edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) + edgesView.add(EdgeViewModel(edge, graphView[edge.from]!!, graphView[edge.to]!!)) } } override fun addEdge(from: V, to: V, weight: Int) { - if (vertexView[from] == null) return - for (i in vertexView[from]?.edges!!) if (i.to == to) return - val edgesCopy = vertexView[from]?.edges?.toMutableList()!! - val edgeTo = Edge(from, to, weight) - val edgeFrom = Edge(from, to, weight) - edgesCopy.add(edgeTo) - edgesCopy.add(edgeFrom) - vertexView[from]?.edges = edgesCopy - edgesView.add(EdgeViewModel(edgeTo, vertexView[edgeTo.from]!!, vertexView[edgeTo.to]!!)) - edgesView.add(EdgeViewModel(edgeFrom, vertexView[edgeFrom.from]!!, vertexView[edgeFrom.to]!!)) + val source: VertexViewModel + val destination: VertexViewModel + try { + source = graphView[from]!! + destination = graphView[to]!! + } catch (e: Exception) { + println("Can't add edge between $from and $to: one of them don't exist") + return + } + for (edge in source.edges) if (edge.to == to) return + for (edge in destination.edges) if (edge.from == from) return + + val edgeFromSource = Edge(from, to, weight) + val edgeFromDestination = Edge(to, from, weight) + source.edges.add(edgeFromSource) + destination.edges.add(edgeFromDestination) + edgesView.add(EdgeViewModel(edgeFromSource, source, destination)) + edgesView.add(EdgeViewModel(edgeFromDestination, destination, source)) graphModel.addEdge(from, to, weight) - graphModel.addEdge(to, from, weight) updateView() } } \ No newline at end of file From 5357a14a0273be86895f1ed88f7327bba2756172 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Thu, 23 May 2024 09:35:18 -0400 Subject: [PATCH 120/172] add: SGLite graph saving --- .../viewmodel/DirectedGraphViewModel.kt | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 7a7d5a0..d005fd1 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -32,13 +32,19 @@ class DirectedGraphViewModel( fun saveSQLite(){ val DB_DRIVER = "jdbc:sqlite" - - var create = ("CREATE TABLE if not exists " + name + " (") - + var parameterCreate = "( Vertexes String," + var parameterInput = "( Vertexes," + var create = ("CREATE TABLE if not exists $name ") for (i in graph.entries){ - create = create + " " + i.toString() + " INTEGER " + parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " + parameterInput = "$parameterInput V${i.key.toString()}," } - create = create + " )" + parameterCreate = parameterCreate.slice(0.. parameterCreate.length - 3) + parameterCreate = "$parameterCreate )" + parameterInput = parameterInput.slice(0.. parameterInput.length - 2) + parameterInput = "$parameterInput )" + create = create + parameterCreate + ";" + println(create) val connection = DriverManager.getConnection("$DB_DRIVER:$name.db") ?: throw SQLException("Cannot connect to database") connection.createStatement().also { stmt -> @@ -47,10 +53,43 @@ class DirectedGraphViewModel( println("Tables created or already exists") } catch (ex: Exception) { println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + var request = "INSERT INTO $name $parameterInput VALUES " + for (i in graph.entries){ + var record = "( 'V${i.key}', " + val recList = emptyMap().toMutableMap() + for (j in graph.entries){ + recList[j.key] = "NULL" + } + for (j in i.value){ + recList[j.to] = j.weight.toString() + } + for (j in recList){ + record = "$record ${j.value}, " + } + record = record.slice(0.. record.length - 3) + record = "$record )," + request = "$request $record" + } + + request = request.slice(0.. request.length - 2) + connection.createStatement().also { stmt -> + try { + stmt.execute(request) + println("YES") + } catch (ex: Exception) { + println("NOPE") + println(ex) } finally { stmt.close() } } + println(request) + } override fun addEdge(from: V, to: V, weight: Int) { From ac7b1b2e7586c1d6d274b0ccf30e874afc07cf47 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 23 May 2024 16:56:21 +0300 Subject: [PATCH 121/172] Ref: file restructure --- src/main/kotlin/view/common/AddEdgeDialog.kt | 1 - .../kotlin/view/{ => common}/BounceClick.kt | 5 +- src/main/kotlin/view/common/DefaultButton.kt | 2 - src/main/kotlin/view/{ => common}/styling.kt | 3 +- .../view/screens/DirectedGraphScreen.kt | 7 +- src/main/kotlin/view/screens/MainScreen.kt | 27 +-- .../kotlin/view/screens/SettingsScreen.kt | 6 +- .../view/screens/UndirectedGraphScreen.kt | 13 -- src/main/kotlin/view/views/GraphView.kt | 4 + src/main/kotlin/view/views/VertexView.kt | 212 ------------------ .../view/views/edge/DirectedEdgeView.kt | 99 ++++++++ .../view/views/edge/UndirectedEdgeView.kt | 45 ++++ .../view/views/vertex/DirectedVertexView.kt | 64 ++++++ .../view/views/vertex/UndirectedVertexView.kt | 60 +++++ .../viewmodel/AbstractGraphViewModel.kt | 3 + .../viewmodel/DirectedGraphViewModel.kt | 9 - src/main/kotlin/viewmodel/EdgeViewModel.kt | 18 +- .../viewmodel/UndirectedGraphViewModel.kt | 9 - src/main/kotlin/viewmodel/VertexViewModel.kt | 4 +- 19 files changed, 310 insertions(+), 281 deletions(-) rename src/main/kotlin/view/{ => common}/BounceClick.kt (96%) rename src/main/kotlin/view/{ => common}/styling.kt (95%) delete mode 100644 src/main/kotlin/view/views/VertexView.kt create mode 100644 src/main/kotlin/view/views/edge/DirectedEdgeView.kt create mode 100644 src/main/kotlin/view/views/edge/UndirectedEdgeView.kt create mode 100644 src/main/kotlin/view/views/vertex/DirectedVertexView.kt create mode 100644 src/main/kotlin/view/views/vertex/UndirectedVertexView.kt diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index 5aa7fef..9ff9c13 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.rememberDialogState import localisation.localisation -import view.defaultStyle import viewmodel.AbstractGraphViewModel @Composable diff --git a/src/main/kotlin/view/BounceClick.kt b/src/main/kotlin/view/common/BounceClick.kt similarity index 96% rename from src/main/kotlin/view/BounceClick.kt rename to src/main/kotlin/view/common/BounceClick.kt index 4ea8223..084158a 100644 --- a/src/main/kotlin/view/BounceClick.kt +++ b/src/main/kotlin/view/common/BounceClick.kt @@ -1,4 +1,4 @@ -package view +package view.common import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput enum class ButtonState { Pressed, Idle } + fun Modifier.bounceClick() = composed { var buttonState by remember { mutableStateOf(ButtonState.Idle) } val scale by animateFloatAsState(if (buttonState == ButtonState.Pressed) 0.70f else 1f) @@ -27,7 +28,7 @@ fun Modifier.bounceClick() = composed { .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = { } + onClick = { } ) .pointerInput(buttonState) { awaitPointerEventScope { diff --git a/src/main/kotlin/view/common/DefaultButton.kt b/src/main/kotlin/view/common/DefaultButton.kt index 993e4b1..ddc2485 100644 --- a/src/main/kotlin/view/common/DefaultButton.kt +++ b/src/main/kotlin/view/common/DefaultButton.kt @@ -12,8 +12,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import localisation.localisation -import view.DefaultColors -import view.defaultStyle @Composable fun DefaultButton( diff --git a/src/main/kotlin/view/styling.kt b/src/main/kotlin/view/common/styling.kt similarity index 95% rename from src/main/kotlin/view/styling.kt rename to src/main/kotlin/view/common/styling.kt index 20438ec..1d01901 100644 --- a/src/main/kotlin/view/styling.kt +++ b/src/main/kotlin/view/common/styling.kt @@ -1,10 +1,9 @@ -package view +package view.common import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.sp - val defaultStyle = TextStyle(fontSize = 28.sp) val bigStyle = TextStyle(fontSize = 50.sp) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 74d6442..f5cc5cc 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -1,27 +1,22 @@ package view.screens -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.rememberDialogState import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation -import view.DefaultColors import view.common.AddEdgeDialog import view.common.DefaultButton -import view.defaultStyle +import view.common.defaultStyle import view.views.GraphViewDirect import viewmodel.MainScreenViewModel diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 6df2686..358d4eb 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -23,10 +23,10 @@ import androidx.compose.ui.window.DialogState import androidx.compose.ui.window.DialogWindow import androidx.navigation.NavController import localisation.localisation -import view.DefaultColors -import view.bigStyle -import view.bounceClick -import view.defaultStyle +import view.common.DefaultColors +import view.common.bigStyle +import view.common.bounceClick +import view.common.defaultStyle import viewmodel.MainScreenViewModel @Composable @@ -183,15 +183,16 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView ) } - Button(modifier = Modifier - .padding(horizontal = 20.dp, vertical = 260.dp) - .border( - width = 2.dp, - color = Color.Black, - shape = RoundedCornerShape(25.dp) - ) - .width(300.dp) - .height(60.dp), + Button( + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 260.dp) + .border( + width = 2.dp, + color = Color.Black, + shape = RoundedCornerShape(25.dp) + ) + .width(300.dp) + .height(60.dp), shape = RoundedCornerShape(25.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red), diff --git a/src/main/kotlin/view/screens/SettingsScreen.kt b/src/main/kotlin/view/screens/SettingsScreen.kt index 4424ee0..20eb3d2 100644 --- a/src/main/kotlin/view/screens/SettingsScreen.kt +++ b/src/main/kotlin/view/screens/SettingsScreen.kt @@ -17,9 +17,9 @@ import localisation.getLocalisation import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import localisation.localisation -import view.DefaultColors -import view.bounceClick -import view.defaultStyle +import view.common.DefaultColors +import view.common.bounceClick +import view.common.defaultStyle import java.io.File const val pathToSettings = "src/main/kotlin/settings.json" diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 0bf0615..2611afe 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -1,27 +1,14 @@ package view.screens -import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogWindow -import androidx.compose.ui.window.rememberDialogState import androidx.compose.ui.zIndex import androidx.navigation.NavController -import localisation.localisation -import view.DefaultColors import view.common.AddEdgeDialog import view.common.DefaultButton -import view.defaultStyle import view.views.GraphViewUndirect import viewmodel.MainScreenViewModel diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index bb23ed0..669481c 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -1,6 +1,10 @@ package view.views import androidx.compose.runtime.Composable +import view.views.edge.DirectedEdgeView +import view.views.edge.UndirectedEdgeView +import view.views.vertex.DirectedVertexView +import view.views.vertex.UndirectedVertexView import viewmodel.DirectedGraphViewModel import viewmodel.UndirectedGraphViewModel diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt deleted file mode 100644 index b0faba1..0000000 --- a/src/main/kotlin/view/views/VertexView.kt +++ /dev/null @@ -1,212 +0,0 @@ -package view.views - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.rotate -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.drawText -import androidx.compose.ui.text.rememberTextMeasurer -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex -import view.DefaultColors -import viewmodel.DirectedGraphViewModel -import viewmodel.EdgeViewModel -import viewmodel.UndirectedGraphViewModel -import viewmodel.VertexViewModel -import kotlin.math.atan2 -import kotlin.math.roundToInt - -@Composable -fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel) { - val vertex = vertexVM.vertex - Box(modifier = Modifier - .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } - .clip(shape = CircleShape) - .size(vertexVM.vertexSize.dp) - .background(DefaultColors.primary) - .border(5.dp, Color.Black, CircleShape) - .pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() - for (edge in graphVM.edgesView.iterator()) { - if(edge.to == vertexVM.vertex){ - edge.offsetXTo+= dragAmount.x - edge.offsetYTo+= dragAmount.y - } - } - for (edge in graphVM.edgesView.iterator()) { - if(edge.from == vertexVM.vertex){ - edge.offsetXFrom+= dragAmount.x - edge.offsetYFrom+= dragAmount.y - } - } - vertexVM.offsetX += dragAmount.x - vertexVM.offsetY += dragAmount.y - } - - } - ) { - Text( - text = "$vertex", - fontSize = 40.sp, - modifier = Modifier - .fillMaxSize() - .wrapContentSize(), - ) - } - for (edge in graphVM.edgesView.iterator()) { - DirectedEdgeView(edge, graphVM) - } -} - -@Composable -fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel){ - - val textMeasurer = rememberTextMeasurer() - - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { - drawLine( - start = Offset( - edgeVM.offsetXFrom + edgeVM.vertexSize / 2, - edgeVM.offsetYFrom + edgeVM.vertexSize / 2 - ), - end = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2), - strokeWidth = 6f, - color = edgeVM.color, - ) - rotate( - degrees = ((57.2958 * (atan2( - ((edgeVM.offsetYFrom - edgeVM.offsetYTo).toDouble()), - ((edgeVM.offsetXFrom - edgeVM.offsetXTo).toDouble()) - ))).toFloat()), - pivot = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2) - ) { - drawRect( - color = edgeVM.color, - size = Size(5f, 16f), - topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 65, - edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 8f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 14f), - topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 60, - edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 7f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 12f), - topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 55, - edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 6f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 10f), - topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 50, - edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 5f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 8f), - topLeft = Offset( - edgeVM.offsetXTo + edgeVM.vertexSize / 2 + 45, - edgeVM.offsetYTo + edgeVM.vertexSize / 2 - 4f - ), - ) - } - if(graphVM.graph.state) - drawText(textMeasurer, edgeVM.weight.toString(), - topLeft = Offset((edgeVM.offsetXFrom + edgeVM.vertexSize + edgeVM.offsetXTo)/2 - edgeVM.weight.toString().length * 5.5f, (edgeVM.offsetYFrom + edgeVM.vertexSize + edgeVM.offsetYTo)/2 - 9), - style = TextStyle(background = Color.White, fontSize = 18.sp) - ) - } -} - -@Composable -fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewModel){ - - val textMeasurer = rememberTextMeasurer() - - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { - drawLine( - start = Offset( - edgeVM.offsetXFrom + edgeVM.vertexSize / 2, - edgeVM.offsetYFrom + edgeVM.vertexSize / 2 - ), - end = Offset(edgeVM.offsetXTo + edgeVM.vertexSize / 2, edgeVM.offsetYTo + edgeVM.vertexSize / 2), - strokeWidth = 6f, - color = edgeVM.color, - ) - if (graphVM.graph.state) - drawText( - textMeasurer, edgeVM.weight.toString(), - topLeft = Offset( - (edgeVM.offsetXFrom + edgeVM.vertexSize + edgeVM.offsetXTo) / 2 - edgeVM.weight.toString().length * 5.5f, - (edgeVM.offsetYFrom + edgeVM.vertexSize + edgeVM.offsetYTo) / 2 - 9 - ), - style = TextStyle(background = Color.White, fontSize = 18.sp) - ) - } -} - -@Composable -fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { - val vertex = vertexVM.vertex - - Box(modifier = Modifier - .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } - .clip(shape = CircleShape) - .size(vertexVM.vertexSize.dp) - .background(DefaultColors.primary) - .border(5.dp, Color.Black, CircleShape) - .pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() - for (edge in graphVM.edgesView.iterator()) { - if(edge.to == vertexVM.vertex){ - edge.offsetXTo+= dragAmount.x - edge.offsetYTo+= dragAmount.y - } - } - for (edge in graphVM.edgesView.iterator()) { - if(edge.from == vertexVM.vertex){ - edge.offsetXFrom+= dragAmount.x - edge.offsetYFrom+= dragAmount.y - } - } - vertexVM.offsetX += dragAmount.x - vertexVM.offsetY += dragAmount.y - } - } - ) { - Text( - text = "$vertex", - fontSize = 40.sp, - modifier = Modifier - .fillMaxSize() - .wrapContentSize(), - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/view/views/edge/DirectedEdgeView.kt b/src/main/kotlin/view/views/edge/DirectedEdgeView.kt new file mode 100644 index 0000000..8d6a322 --- /dev/null +++ b/src/main/kotlin/view/views/edge/DirectedEdgeView.kt @@ -0,0 +1,99 @@ +package view.views.edge + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import viewmodel.DirectedGraphViewModel +import viewmodel.EdgeViewModel +import kotlin.math.atan2 + +@Composable +fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel) { + + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + drawLine( + start = Offset( + edgeVM.xFrom + edgeVM.vertexSize / 2, + edgeVM.yFrom + edgeVM.vertexSize / 2 + ), + end = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2, + edgeVM.yTo + edgeVM.vertexSize / 2 + ), + strokeWidth = 6f, + color = edgeVM.color, + ) + rotate( + degrees = ((57.2958 * (atan2( + ((edgeVM.yFrom - edgeVM.yTo).toDouble()), + ((edgeVM.xFrom - edgeVM.xTo).toDouble()) + ))).toFloat()), + pivot = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2, + edgeVM.yTo + edgeVM.vertexSize / 2 + ) + ) { + drawRect( + color = edgeVM.color, + size = Size(5f, 16f), + topLeft = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2 + 65, + edgeVM.yTo + edgeVM.vertexSize / 2 - 8f + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f, 14f), + topLeft = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2 + 60, + edgeVM.yTo + edgeVM.vertexSize / 2 - 7f + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f, 12f), + topLeft = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2 + 55, + edgeVM.yTo + edgeVM.vertexSize / 2 - 6f + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f, 10f), + topLeft = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2 + 50, + edgeVM.yTo + edgeVM.vertexSize / 2 - 5f + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f, 8f), + topLeft = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2 + 45, + edgeVM.yTo + edgeVM.vertexSize / 2 - 4f + ), + ) + } + if (graphVM.graph.state) + drawText( + textMeasurer, edgeVM.weight.toString(), + topLeft = Offset( + (edgeVM.xFrom + edgeVM.vertexSize + edgeVM.xTo) / 2 - edgeVM.weight.toString().length * 5.5f, + (edgeVM.yFrom + edgeVM.vertexSize + edgeVM.yTo) / 2 - 9 + ), + style = TextStyle(background = Color.White, fontSize = 18.sp) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt new file mode 100644 index 0000000..ee40664 --- /dev/null +++ b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt @@ -0,0 +1,45 @@ +package view.views.edge + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import viewmodel.EdgeViewModel +import viewmodel.UndirectedGraphViewModel + +@Composable +fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewModel) { + + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + drawLine( + start = Offset( + edgeVM.xFrom + edgeVM.vertexSize / 2, + edgeVM.yFrom + edgeVM.vertexSize / 2 + ), + end = Offset( + edgeVM.xTo + edgeVM.vertexSize / 2, + edgeVM.yTo + edgeVM.vertexSize / 2 + ), + strokeWidth = 6f, + color = edgeVM.color, + ) + if (graphVM.graph.state) + drawText( + textMeasurer, edgeVM.weight.toString(), + topLeft = Offset( + (edgeVM.xFrom + edgeVM.vertexSize + edgeVM.xTo) / 2 - edgeVM.weight.toString().length * 5.5f, + (edgeVM.yFrom + edgeVM.vertexSize + edgeVM.yTo) / 2 - 9 + ), + style = TextStyle(background = Color.White, fontSize = 18.sp) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt new file mode 100644 index 0000000..d371c25 --- /dev/null +++ b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt @@ -0,0 +1,64 @@ +package view.views.vertex + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.common.DefaultColors +import view.views.edge.DirectedEdgeView +import viewmodel.DirectedGraphViewModel +import viewmodel.VertexViewModel +import kotlin.math.roundToInt + +@Composable +fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel) { + val vertex = vertexVM.vertex + Box(modifier = Modifier + .offset { IntOffset(vertexVM.x.roundToInt(), vertexVM.y.roundToInt()) } + .clip(shape = CircleShape) + .size(vertexVM.vertexSize.dp) + .background(DefaultColors.primary) + .border(5.dp, Color.Black, CircleShape) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + for (edge in graphVM.edgesView.iterator()) { + if (edge.to == vertexVM.vertex) { + edge.xTo += dragAmount.x + edge.yTo += dragAmount.y + } + } + for (edge in graphVM.edgesView.iterator()) { + if (edge.from == vertexVM.vertex) { + edge.xFrom += dragAmount.x + edge.yFrom += dragAmount.y + } + } + vertexVM.x += dragAmount.x + vertexVM.y += dragAmount.y + } + + } + ) { + Text( + text = "$vertex", + fontSize = 40.sp, + modifier = Modifier + .fillMaxSize() + .wrapContentSize(), + ) + } + for (edge in graphVM.edgesView.iterator()) { + DirectedEdgeView(edge, graphVM) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt new file mode 100644 index 0000000..1c70813 --- /dev/null +++ b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt @@ -0,0 +1,60 @@ +package view.views.vertex + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.common.DefaultColors +import viewmodel.UndirectedGraphViewModel +import viewmodel.VertexViewModel +import kotlin.math.roundToInt + +@Composable +fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { + val vertex = vertexVM.vertex + + Box(modifier = Modifier + .offset { IntOffset(vertexVM.x.roundToInt(), vertexVM.y.roundToInt()) } + .clip(shape = CircleShape) + .size(vertexVM.vertexSize.dp) + .background(DefaultColors.primary) + .border(5.dp, Color.Black, CircleShape) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + for (edge in graphVM.edgesView.iterator()) { + if (edge.to == vertexVM.vertex) { + edge.xTo += dragAmount.x + edge.yTo += dragAmount.y + } + } + for (edge in graphVM.edgesView.iterator()) { + if (edge.from == vertexVM.vertex) { + edge.xFrom += dragAmount.x + edge.yFrom += dragAmount.y + } + } + vertexVM.x += dragAmount.x + vertexVM.y += dragAmount.y + } + } + ) { + Text( + text = "$vertex", + fontSize = 40.sp, + modifier = Modifier + .fillMaxSize() + .wrapContentSize(), + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 7b6fa8a..d1323a1 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -17,6 +17,9 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM for (vertex in graphModel.entries) { graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } + for (edge in graphModel.edges) { + edgesView.add(EdgeViewModel(edge, graphView[edge.from]!!, graphView[edge.to]!!)) + } } fun addVertex(vertex: V) { diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index f531cba..e098dde 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -12,15 +12,6 @@ class DirectedGraphViewModel( val model get() = graph - init { - for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) - } - for (edge in graphModel.edges) { - edgesView.add(EdgeViewModel(edge, graphView[edge.from]!!, graphView[edge.to]!!)) - } - } - fun dijkstraAlgo(start: V, end: V) { val y = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) for (edgeVM in edgesView) { diff --git a/src/main/kotlin/viewmodel/EdgeViewModel.kt b/src/main/kotlin/viewmodel/EdgeViewModel.kt index 017b52a..4967e07 100644 --- a/src/main/kotlin/viewmodel/EdgeViewModel.kt +++ b/src/main/kotlin/viewmodel/EdgeViewModel.kt @@ -7,15 +7,19 @@ import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import model.graph.edges.Edge -class EdgeViewModel(edge: Edge, vertexFromVM: VertexViewModel, vertexToVM: VertexViewModel,) : +class EdgeViewModel( + edge: Edge, + vertexFromVM: VertexViewModel, + vertexToVM: VertexViewModel, +) : ViewModel() { - var offsetXFrom by mutableStateOf(vertexFromVM.offsetX) - var offsetYFrom by mutableStateOf(vertexFromVM.offsetY) - var offsetXTo by mutableStateOf(vertexToVM.offsetX) - var offsetYTo by mutableStateOf(vertexToVM.offsetY) + var xFrom by mutableStateOf(vertexFromVM.x) + var yFrom by mutableStateOf(vertexFromVM.y) + var xTo by mutableStateOf(vertexToVM.x) + var yTo by mutableStateOf(vertexToVM.y) var vertexSize by mutableStateOf(vertexFromVM.vertexSize) var weight by mutableStateOf(edge.weight) - var from by mutableStateOf(edge.from) - var to by mutableStateOf(edge.to) + val from by mutableStateOf(edge.from) + val to by mutableStateOf(edge.to) var color by mutableStateOf(Color.Black) } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 47d2109..7e2793a 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -11,15 +11,6 @@ class UndirectedGraphViewModel( val model get() = graph - init { - for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) - } - for (edge in graphModel.edges) { - edgesView.add(EdgeViewModel(edge, graphView[edge.from]!!, graphView[edge.to]!!)) - } - } - override fun addEdge(from: V, to: V, weight: Int) { val source: VertexViewModel val destination: VertexViewModel diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 83e6aa6..d9ec505 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -13,7 +13,7 @@ class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListO ViewModel() { val vertex: V = _vertex var edges by mutableStateOf(_edges) - var offsetX by mutableStateOf(Random.nextInt(100, width - 100).toFloat()) - var offsetY by mutableStateOf(Random.nextInt(100, height - 100).toFloat()) + var x by mutableStateOf(Random.nextInt(100, width - 100).toFloat()) + var y by mutableStateOf(Random.nextInt(100, height - 100).toFloat()) val vertexSize = 80f } \ No newline at end of file From 23f05b76bd05da6a20e552f67de9a4f41713fb59 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 23 May 2024 22:25:46 +0300 Subject: [PATCH 122/172] Feat: full restructure of viewmodel --- .../view/screens/DirectedGraphScreen.kt | 4 +- .../view/screens/UndirectedGraphScreen.kt | 4 +- src/main/kotlin/view/views/GraphView.kt | 20 +++---- .../view/views/edge/DirectedEdgeView.kt | 5 +- .../view/views/edge/UndirectedEdgeView.kt | 4 +- .../view/views/vertex/DirectedVertexView.kt | 15 ++--- .../view/views/vertex/UndirectedVertexView.kt | 13 ++--- .../viewmodel/AbstractGraphViewModel.kt | 57 ++++++++++++++----- .../viewmodel/DirectedGraphViewModel.kt | 18 +++--- .../viewmodel/UndirectedGraphViewModel.kt | 17 +++--- src/main/kotlin/viewmodel/VertexViewModel.kt | 2 +- 11 files changed, 92 insertions(+), 67 deletions(-) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index f5cc5cc..3c1c623 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -17,7 +17,7 @@ import localisation.localisation import view.common.AddEdgeDialog import view.common.DefaultButton import view.common.defaultStyle -import view.views.GraphViewDirect +import view.views.DirectedGraphView import viewmodel.MainScreenViewModel @@ -31,7 +31,7 @@ fun DirectedGraphScreen( val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) Box(modifier = Modifier.fillMaxSize()) { - GraphViewDirect(graphVM) + DirectedGraphView(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 2611afe..0b579b9 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.zIndex import androidx.navigation.NavController import view.common.AddEdgeDialog import view.common.DefaultButton -import view.views.GraphViewUndirect +import view.views.UndirectedGraphView import viewmodel.MainScreenViewModel @Composable @@ -21,7 +21,7 @@ fun UndirectedGraphScreen( val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) Box(modifier = Modifier.fillMaxSize()) { - GraphViewUndirect(graphVM) + UndirectedGraphView(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index 669481c..10686e2 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -9,22 +9,22 @@ import viewmodel.DirectedGraphViewModel import viewmodel.UndirectedGraphViewModel @Composable -fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { - for (vertexVM in graphViewModel.graphView.values) { - UndirectedVertexView(vertexVM, graphViewModel) +fun UndirectedGraphView(graphVM: UndirectedGraphViewModel) { + for (vertexVM in graphVM.verticesVM) { + UndirectedVertexView(vertexVM, graphVM) } - for (edge in graphViewModel.edgesView.iterator()) { - UndirectedEdgeView(edge, graphViewModel) + for (edgeVM in graphVM.edgesVM) { + UndirectedEdgeView(edgeVM, graphVM.isWeighted) } } @Composable -fun GraphViewDirect(graphViewModel: DirectedGraphViewModel) { - for (vertexVM in graphViewModel.graphView.values) { - DirectedVertexView(vertexVM, graphViewModel) +fun DirectedGraphView(graphVM: DirectedGraphViewModel) { + for (vertexVM in graphVM.verticesVM) { + DirectedVertexView(vertexVM, graphVM) } - for (edge in graphViewModel.edgesView.iterator()) { - DirectedEdgeView(edge, graphViewModel) + for (edgeVM in graphVM.edgesVM) { + DirectedEdgeView(edgeVM, graphVM.isWeighted) } } diff --git a/src/main/kotlin/view/views/edge/DirectedEdgeView.kt b/src/main/kotlin/view/views/edge/DirectedEdgeView.kt index 8d6a322..0d8e739 100644 --- a/src/main/kotlin/view/views/edge/DirectedEdgeView.kt +++ b/src/main/kotlin/view/views/edge/DirectedEdgeView.kt @@ -15,10 +15,11 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import viewmodel.DirectedGraphViewModel import viewmodel.EdgeViewModel +import javax.swing.text.StyledEditorKit.BoldAction import kotlin.math.atan2 @Composable -fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel) { +fun DirectedEdgeView(edgeVM: EdgeViewModel, isWeightedd: Boolean) { val textMeasurer = rememberTextMeasurer() @@ -86,7 +87,7 @@ fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewMod ), ) } - if (graphVM.graph.state) + if (isWeightedd) drawText( textMeasurer, edgeVM.weight.toString(), topLeft = Offset( diff --git a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt index ee40664..3be0ba7 100644 --- a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt +++ b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt @@ -15,7 +15,7 @@ import viewmodel.EdgeViewModel import viewmodel.UndirectedGraphViewModel @Composable -fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewModel) { +fun UndirectedEdgeView(edgeVM: EdgeViewModel, isWeighted: Boolean) { val textMeasurer = rememberTextMeasurer() @@ -32,7 +32,7 @@ fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphVie strokeWidth = 6f, color = edgeVM.color, ) - if (graphVM.graph.state) + if (isWeighted) drawText( textMeasurer, edgeVM.weight.toString(), topLeft = Offset( diff --git a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt index d371c25..5f74333 100644 --- a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt +++ b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt @@ -32,17 +32,15 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphV .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() - for (edge in graphVM.edgesView.iterator()) { - if (edge.to == vertexVM.vertex) { - edge.xTo += dragAmount.x - edge.yTo += dragAmount.y - } - } - for (edge in graphVM.edgesView.iterator()) { + for (edge in graphVM.edgesVM) { if (edge.from == vertexVM.vertex) { edge.xFrom += dragAmount.x edge.yFrom += dragAmount.y } + if (edge.to == vertexVM.vertex) { + edge.xTo += dragAmount.x + edge.yTo += dragAmount.y + } } vertexVM.x += dragAmount.x vertexVM.y += dragAmount.y @@ -58,7 +56,4 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphV .wrapContentSize(), ) } - for (edge in graphVM.edgesView.iterator()) { - DirectedEdgeView(edge, graphVM) - } } \ No newline at end of file diff --git a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt index 1c70813..01a6a05 100644 --- a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt +++ b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt @@ -32,21 +32,20 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGr .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() - for (edge in graphVM.edgesView.iterator()) { - if (edge.to == vertexVM.vertex) { - edge.xTo += dragAmount.x - edge.yTo += dragAmount.y - } - } - for (edge in graphVM.edgesView.iterator()) { + for (edge in graphVM.edgesVM) { if (edge.from == vertexVM.vertex) { edge.xFrom += dragAmount.x edge.yFrom += dragAmount.y } + if (edge.to == vertexVM.vertex) { + edge.xTo += dragAmount.x + edge.yTo += dragAmount.y + } } vertexVM.x += dragAmount.x vertexVM.y += dragAmount.y } + } ) { Text( diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index d1323a1..67ee34e 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -8,43 +8,74 @@ import model.graph.Graph abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { val name = _name - var graphView by mutableStateOf(mutableMapOf>()) + protected var graphVM by mutableStateOf(mutableMapOf>()) var edgesView by mutableStateOf(mutableListOf>()) - val graphModel = graph + protected val graphModel = graph var size = 0 + var isWeighted by mutableStateOf(false) + protected set + val model + get() = graphModel + val verticesVM + get() = graphVM.values + val edgesVM: List> + get() { + val result = mutableListOf>() + for (edgesVM in graphVM.values) { + for (edgeVM in edgesVM.edges) { + result.add(edgeVM) + } + } + return result.toList() + } init { for (vertex in graphModel.entries) { - graphView[vertex.key] = VertexViewModel(vertex.key, vertex.value) + graphVM[vertex.key] = VertexViewModel(vertex.key) } - for (edge in graphModel.edges) { - edgesView.add(EdgeViewModel(edge, graphView[edge.from]!!, graphView[edge.to]!!)) + for (vertex in graphModel.entries) { + for (edge in vertex.value) { + val sourceVertexVM: VertexViewModel + val destinationVertexVM: VertexViewModel + try { + sourceVertexVM = graphVM[edge.from]!! + destinationVertexVM = graphVM[edge.to]!! + } catch (e: Exception) { + println("Can't set edge: source or destination is not exist") + break + } + val edgeVM = EdgeViewModel(edge, sourceVertexVM, destinationVertexVM) + sourceVertexVM.edges.add(edgeVM) + } } } + fun edgesVmOf(vertex: V): List> { + return graphVM[vertex]?.edges?.toList() ?: emptyList() + } + fun addVertex(vertex: V) { size += 1 - graphView.putIfAbsent(vertex, VertexViewModel(vertex)) + graphVM.putIfAbsent(vertex, VertexViewModel(vertex)) graphModel.addVertex(vertex) updateView() } fun updateView() { - val keep = graphView - graphView = mutableMapOf>() - graphView = keep + val keep = graphVM + graphVM = mutableMapOf>() + graphVM = keep } // возможно надо будет переработать состояния, поэтому понадобится fun updateEdgesView(vertex: V) { try { - val keep = graphView[vertex]?.edges!! - graphView[vertex]?.edges = mutableListOf() - graphView[vertex]?.edges = keep + val keep = graphVM[vertex]?.edges!! + graphVM[vertex]?.edges = mutableListOf() + graphVM[vertex]?.edges = keep } catch (e: Exception) { println("Can't find vertex for update view of their edges") } - } abstract fun addEdge(from: V, to: V, weight: Int = 1) diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index e098dde..5c35010 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -9,14 +9,12 @@ class DirectedGraphViewModel( name: String, val graph: DirectedGraph = DirectedGraph() ) : AbstractGraphViewModel(name, graph) { - val model - get() = graph fun dijkstraAlgo(start: V, end: V) { val y = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) - for (edgeVM in edgesView) { - if (Edge(edgeVM.from, edgeVM.to, edgeVM.weight) in y) { - edgeVM.color = Color.Red + for (edge in y) { + for (edgeVM in this.edgesVmOf(edge.from)) { + if (edgeVM.to == edge.to) edgeVM.color = Color.Red } } } @@ -25,8 +23,8 @@ class DirectedGraphViewModel( val source: VertexViewModel val destination: VertexViewModel try { - source = graphView[from]!! - destination = graphView[to]!! + source = graphVM[from]!! + destination = graphVM[to]!! } catch (e: Exception) { println("Can't add edge between $from and $to: one of them don't exist") return @@ -34,9 +32,11 @@ class DirectedGraphViewModel( for (edge in source.edges) if (edge.to == to) return val edge = Edge(from, to, weight) - source.edges.add(edge) - edgesView.add(EdgeViewModel(edge, source, destination)) + val edgeVM = EdgeViewModel(edge, source, destination) + source.edges.add(edgeVM) graphModel.addEdge(from, to, weight) + + if (weight != 1) isWeighted = true updateView() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 7e2793a..d61b1db 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -8,15 +8,12 @@ class UndirectedGraphViewModel( val graph: UndirectedGraph = UndirectedGraph() ) : AbstractGraphViewModel(name, graph) { - val model - get() = graph - override fun addEdge(from: V, to: V, weight: Int) { val source: VertexViewModel val destination: VertexViewModel try { - source = graphView[from]!! - destination = graphView[to]!! + source = graphVM[from]!! + destination = graphVM[to]!! } catch (e: Exception) { println("Can't add edge between $from and $to: one of them don't exist") return @@ -26,11 +23,13 @@ class UndirectedGraphViewModel( val edgeFromSource = Edge(from, to, weight) val edgeFromDestination = Edge(to, from, weight) - source.edges.add(edgeFromSource) - destination.edges.add(edgeFromDestination) - edgesView.add(EdgeViewModel(edgeFromSource, source, destination)) - edgesView.add(EdgeViewModel(edgeFromDestination, destination, source)) + val edgeFromSourceVM = EdgeViewModel(edgeFromSource, source, destination) + val edgeFromDestinationVM = EdgeViewModel(edgeFromDestination, destination, source) + source.edges.add(edgeFromSourceVM) + destination.edges.add(edgeFromDestinationVM) graphModel.addEdge(from, to, weight) + + if (weight != 1) isWeighted = true updateView() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index d9ec505..81397cc 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -9,7 +9,7 @@ import model.graph.edges.Edge import width import kotlin.random.Random -class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListOf()) : +class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListOf()) : ViewModel() { val vertex: V = _vertex var edges by mutableStateOf(_edges) From 744bf041398ae23503306841de4291004de6580f Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 23 May 2024 22:32:19 +0300 Subject: [PATCH 123/172] Ref: remove useless ViewModel method --- src/main/kotlin/viewmodel/AbstractGraphViewModel.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 67ee34e..dace612 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -9,7 +9,6 @@ import model.graph.Graph abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { val name = _name protected var graphVM by mutableStateOf(mutableMapOf>()) - var edgesView by mutableStateOf(mutableListOf>()) protected val graphModel = graph var size = 0 var isWeighted by mutableStateOf(false) @@ -67,16 +66,5 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM graphVM = keep } - // возможно надо будет переработать состояния, поэтому понадобится - fun updateEdgesView(vertex: V) { - try { - val keep = graphVM[vertex]?.edges!! - graphVM[vertex]?.edges = mutableListOf() - graphVM[vertex]?.edges = keep - } catch (e: Exception) { - println("Can't find vertex for update view of their edges") - } - } - abstract fun addEdge(from: V, to: V, weight: Int = 1) } \ No newline at end of file From 6946e74bb2a3e12dff44eb1e621df1958402b891 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Thu, 23 May 2024 18:12:33 -0400 Subject: [PATCH 124/172] add: SQLite save/delete/read directed graph --- .../view/screens/DirectedGraphScreen.kt | 12 +- src/main/kotlin/view/screens/MainScreen.kt | 13 +- .../view/screens/UndirectedGraphScreen.kt | 8 +- src/main/kotlin/view/views/GraphView.kt | 4 +- src/main/kotlin/view/views/VertexView.kt | 8 +- .../viewmodel/DirectedGraphViewModel.kt | 59 +++++++-- .../kotlin/viewmodel/MainScreenViewModel.kt | 112 ++++++++++++++++-- .../viewmodel/UndirectedGraphViewModel.kt | 1 + 8 files changed, 177 insertions(+), 40 deletions(-) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index f09776f..04ac516 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -58,7 +58,7 @@ fun DirectedGraphScreen( // Add vertex Button( - onClick = { graphVM.addVertex(graphVM.size) }, + onClick = { graphVM.addVertex(graphVM.size.toString()) }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) @@ -204,12 +204,8 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.width(30.dp)) Button( onClick = { - val sourceInt = source.toIntOrNull() - val destinationInt = destination.toIntOrNull() - if (sourceInt != null && destinationInt != null) { - graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) - isOpenedEdgeMenu = false - } + graphVM.addEdge(source, destination, weight.toInt()) + isOpenedEdgeMenu = false }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) @@ -290,7 +286,7 @@ fun DirectedGraphScreen( Row { Spacer(modifier = Modifier.width(30.dp)) Button( - onClick = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) + onClick = { graphVM.dijkstraAlgo(source, destination) }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 6df2686..744e3e2 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -28,6 +28,7 @@ import view.bigStyle import view.bounceClick import view.defaultStyle import viewmodel.MainScreenViewModel +import viewmodel.initType @Composable fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { @@ -38,6 +39,10 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView val expandedDropDown = remember { mutableStateOf(false) } val selectedOptionTextDropDown = remember { mutableStateOf(optionsDropDown[0]) } + if(!mainScreenViewModel.inited) { + mainScreenViewModel.graphInit() + mainScreenViewModel.inited = true + } Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { // Search tab @@ -170,7 +175,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView ), onClick = { if (graphName != "") { - mainScreenViewModel.addGraph(graphName, selectedOptionTextDropDown.value) + mainScreenViewModel.addGraph(graphName, selectedOptionTextDropDown.value, initType.Internal) graphName = "" dialogState.value = false } @@ -251,10 +256,14 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( onClick = { + if(mainScreenViewModel.graphs.typeList[index] == MainScreenViewModel.ViewModelType.Directed){ + mainScreenViewModel.initModel(index) + } navController.navigate( when (mainScreenViewModel.graphs.typeList[index]) { - MainScreenViewModel.ViewModelType.Undirected -> "${Screen.UndirectedGraphScreen.route}/$index" + MainScreenViewModel.ViewModelType.Undirected -> {"${Screen.UndirectedGraphScreen.route}/$index"} MainScreenViewModel.ViewModelType.Directed -> "${Screen.DirectedGraphScreen.route}/$index" + } ) }, diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 98407be..cca0fe8 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -53,7 +53,7 @@ fun UndirectedGraphScreen( // Add vertex Button( - onClick = { graphVM.addVertex(graphVM.size) }, + onClick = { graphVM.addVertex(graphVM.size.toString()) }, modifier = Modifier .clip(shape = RoundedCornerShape(45.dp)) .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) @@ -177,10 +177,8 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.width(30.dp)) Button( onClick = { - val sourceInt = source.toIntOrNull() - val destinationInt = destination.toIntOrNull() - if (sourceInt != null && destinationInt != null) { - graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) + if (source != "" && destination != "") { + graphVM.addEdge(source, destination, weight.toInt()) isOpenedEdgeMenu = false } }, modifier = Modifier diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index 43f8cc4..941ed21 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -5,7 +5,7 @@ import viewmodel.DirectedGraphViewModel import viewmodel.UndirectedGraphViewModel @Composable -fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { +fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { for (vertexVM in graphViewModel.vertexView.values) { UndirectedVertexView(vertexVM, graphViewModel) } @@ -16,7 +16,7 @@ fun GraphViewUndirect(graphViewModel: UndirectedGraphViewModel) { @Composable -fun GraphViewDirect(graphViewModel: DirectedGraphViewModel) { +fun GraphViewDirect(graphViewModel: DirectedGraphViewModel) { for (vertexVM in graphViewModel.vertexView.values) { DirectedVertexView(vertexVM, graphViewModel) } diff --git a/src/main/kotlin/view/views/VertexView.kt b/src/main/kotlin/view/views/VertexView.kt index b0faba1..850700b 100644 --- a/src/main/kotlin/view/views/VertexView.kt +++ b/src/main/kotlin/view/views/VertexView.kt @@ -31,7 +31,7 @@ import kotlin.math.atan2 import kotlin.math.roundToInt @Composable -fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel) { +fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel) { val vertex = vertexVM.vertex Box(modifier = Modifier .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } @@ -74,7 +74,7 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphVie } @Composable -fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel){ +fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel){ val textMeasurer = rememberTextMeasurer() @@ -145,7 +145,7 @@ fun DirectedEdgeView(edgeVM: EdgeViewModel, graphVM: DirectedGraphViewModel } @Composable -fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewModel){ +fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewModel){ val textMeasurer = rememberTextMeasurer() @@ -172,7 +172,7 @@ fun UndirectedEdgeView(edgeVM: EdgeViewModel, graphVM: UndirectedGraphViewM } @Composable -fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { +fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { val vertex = vertexVM.vertex Box(modifier = Modifier diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index d005fd1..b9c4ddc 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -9,11 +9,17 @@ import java.sql.SQLException class DirectedGraphViewModel( name: String, + val graph: DirectedGraph = DirectedGraph() ): AbstractGraphViewModel(name, graph){ val model get() = graph + var inType = initType.Internal + var initedGraph = false + + private val DB_DRIVER = "jdbc:sqlite" init { + for (vertex in graphModel.entries) { vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } @@ -21,6 +27,8 @@ class DirectedGraphViewModel( edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) } } + + fun dijkstraAlgo(start: V, end: V){ val y = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) for (edgeVM in edgesView){ @@ -31,10 +39,12 @@ class DirectedGraphViewModel( } fun saveSQLite(){ - val DB_DRIVER = "jdbc:sqlite" + var parameterCreate = "( Vertexes String," var parameterInput = "( Vertexes," - var create = ("CREATE TABLE if not exists $name ") + var create = ("CREATE TABLE $name ") + val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") + val insertIndex = ("INSERT INTO BEBRA_KILLER (name, type) VALUES('$name', 'Directed');") for (i in graph.entries){ parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " parameterInput = "$parameterInput V${i.key.toString()}," @@ -44,12 +54,37 @@ class DirectedGraphViewModel( parameterInput = parameterInput.slice(0.. parameterInput.length - 2) parameterInput = "$parameterInput )" create = create + parameterCreate + ";" - println(create) - val connection = DriverManager.getConnection("$DB_DRIVER:$name.db") + + val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") ?: throw SQLException("Cannot connect to database") + val delTable = "DROP TABLE $name" + val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='$name';" + connection.createStatement().also { stmt -> + try { + stmt.execute(delTable) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(delIndexRec) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } connection.createStatement().also { stmt -> try { stmt.execute(create) + stmt.execute(createIndex) println("Tables created or already exists") } catch (ex: Exception) { println("Cannot create table in database") @@ -58,6 +93,17 @@ class DirectedGraphViewModel( stmt.close() } } + connection.createStatement().also { stmt -> + try { + stmt.execute(insertIndex) + } catch (ex: Exception) { + println("Unsuccessful") + println(ex) + } finally { + stmt.close() + } + } + var request = "INSERT INTO $name $parameterInput VALUES " for (i in graph.entries){ var record = "( 'V${i.key}', " @@ -75,21 +121,18 @@ class DirectedGraphViewModel( record = "$record )," request = "$request $record" } - request = request.slice(0.. request.length - 2) connection.createStatement().also { stmt -> try { stmt.execute(request) - println("YES") } catch (ex: Exception) { - println("NOPE") + println("Unsuccessful") println(ex) } finally { stmt.close() } } println(request) - } override fun addEdge(from: V, to: V, weight: Int) { diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 7949094..872be04 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -2,23 +2,96 @@ package viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel +import java.sql.DriverManager +import java.sql.SQLException + +enum class initType{ + SQLite, + CSV, + Neo4j, + Internal +} class MainScreenViewModel : ViewModel() { val graphs = GraphStorage() - - fun addGraph(name: String, type: String) { + internal var inited = false + private val DB_DRIVER = "jdbc:sqlite" + fun addGraph(name: String, type: String, initType: initType) { when (type) { "undirected" -> { graphs.typeList.add(ViewModelType.Undirected) - graphs.undirectedGraphs.add(UndirectedGraphViewModel(name)) + graphs.undirectedGraphs.add(UndirectedGraphViewModel(name, initType)) } - "directed" -> { graphs.typeList.add(ViewModelType.Directed) - graphs.directedGraphs.add(DirectedGraphViewModel(name)) + val graphVM = DirectedGraphViewModel(name) + graphVM.inType = initType + graphs.directedGraphs.add(graphVM) + } + + } + } + + fun initModel(index: Int){ + + if(graphs.typeList[index] == ViewModelType.Directed) { + val graph = graphs.getDirected(index) + if(graph.initedGraph) return + else graph.initedGraph = true + if (graph.inType == initType.SQLite) { + val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } + val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } + val resVertex = getVertex.executeQuery() + val resEdges = getGraphs.executeQuery() + while (resVertex.next()) { + var vertexName = resVertex.getString("Vertexes") + if(vertexName.length > 1) vertexName = vertexName.slice(1..vertexName.length - 1) + graph.addVertex(vertexName) + } + while (resEdges.next()) { + for (i in graph.graph.vertices) { + val weight = resEdges.getString("V$i") + var to = resEdges.getString("Vertexes") + to = to.slice(1.. + try { + stmt.execute(createIndex) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } } + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM BEBRA_KILLER")} + val resSet = getGraphs.executeQuery() + while(resSet.next()) { + if(resSet.getString("type") == "Directed"){ + addGraph(resSet.getString("name"), "directed", initType.SQLite) + } + else if(resSet.getString("type") == "Undirected"){ + addGraph(resSet.getString("name"), "undirected", initType.SQLite) + } + } + connection.close() } enum class ViewModelType() { @@ -39,7 +112,7 @@ class MainScreenViewModel : ViewModel() { } } - private fun findGraph(index: Int): Int { + internal fun findGraph(index: Int): Int { var indexAr = 0 when (graphs.typeList[index]) { ViewModelType.Undirected -> { @@ -54,6 +127,23 @@ class MainScreenViewModel : ViewModel() { } fun removeGraph(index: Int) { + val DB_DRIVER = "jdbc:sqlite" + val delTable = "DROP TABLE ${getName(index)}" + val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='${getName(index)}';" + val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") + ?: throw SQLException("Cannot connect to database") + connection.createStatement().also { stmt -> + try { + stmt.execute(delTable) + stmt.execute(delIndexRec) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } when (graphs.typeList[index]) { ViewModelType.Undirected -> { graphs.undirectedGraphs.removeAt(findGraph(index)) @@ -65,18 +155,18 @@ class MainScreenViewModel : ViewModel() { graphs.typeList.removeAt(index) } } - } - fun getUndirected(index: Int): UndirectedGraphViewModel { + } + fun getUndirected(index: Int): UndirectedGraphViewModel { return undirectedGraphs[findGraph(index)] } - fun getDirected(index: Int): DirectedGraphViewModel { + fun getDirected(index: Int): DirectedGraphViewModel { return directedGraphs[findGraph(index)] } - var undirectedGraphs = mutableStateListOf>() - var directedGraphs = mutableStateListOf>() + var undirectedGraphs = mutableStateListOf>() + var directedGraphs = mutableStateListOf>() var typeList = mutableStateListOf() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index dafab91..bb3e86a 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -5,6 +5,7 @@ import model.graph.edges.Edge class UndirectedGraphViewModel( name: String, + initType: initType, val graph: UndirectedGraph = UndirectedGraph() ): AbstractGraphViewModel(name, graph){ From 6552644357d2c2d1c13ee06102591f65b5196d18 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 24 May 2024 01:16:51 +0300 Subject: [PATCH 125/172] Feat: add algorithms visualization -implemented force-directed visualization algorithm -add buttons and methods for visuzalization all existing algorithms -fixed edges weights and edges drawing, change EdgeVM --- src/main/kotlin/model/algos/FindBridges.kt | 46 ++++++++++ src/main/kotlin/model/algos/ForceAtlas2.kt | 88 +++++++++++++++++++ src/main/kotlin/model/algos/Prim.kt | 2 +- src/main/kotlin/model/algos/SearchBridges.kt | 48 ---------- src/main/kotlin/view/common/AddEdgeDialog.kt | 4 +- .../view/screens/DirectedGraphScreen.kt | 11 ++- .../view/screens/UndirectedGraphScreen.kt | 20 +++++ src/main/kotlin/view/views/GraphView.kt | 7 +- .../view/views/edge/DirectedEdgeView.kt | 49 +++++------ .../view/views/edge/UndirectedEdgeView.kt | 20 ++--- .../view/views/vertex/DirectedVertexView.kt | 14 +-- .../view/views/vertex/UndirectedVertexView.kt | 21 ++--- .../viewmodel/AbstractGraphViewModel.kt | 11 +++ .../viewmodel/DirectedGraphViewModel.kt | 22 +++-- src/main/kotlin/viewmodel/EdgeViewModel.kt | 19 ++-- .../viewmodel/UndirectedGraphViewModel.kt | 35 ++++++++ src/main/kotlin/viewmodel/VertexViewModel.kt | 10 ++- src/test/kotlin/algos/PrimTest.kt | 2 +- src/test/kotlin/algos/SearchBridgesTest.kt | 6 +- 19 files changed, 300 insertions(+), 135 deletions(-) create mode 100644 src/main/kotlin/model/algos/FindBridges.kt create mode 100644 src/main/kotlin/model/algos/ForceAtlas2.kt delete mode 100644 src/main/kotlin/model/algos/SearchBridges.kt diff --git a/src/main/kotlin/model/algos/FindBridges.kt b/src/main/kotlin/model/algos/FindBridges.kt new file mode 100644 index 0000000..c37338e --- /dev/null +++ b/src/main/kotlin/model/algos/FindBridges.kt @@ -0,0 +1,46 @@ +package model.algos + +import model.graph.UndirectedGraph +import model.graph.edges.Edge + +fun findBridges(graph: UndirectedGraph): Set> { + val timeIn = mutableMapOf() + for (vertex in graph.vertices) { + timeIn[vertex] = -1 + } + val ret = mutableMapOf() + var time = 0 + val bridges = mutableSetOf>() + + fun dfs(vertex: V, prevVertex: V) { + timeIn[vertex] = time++ + ret[vertex] = timeIn[vertex]!! + val edges = graph.edgesOf(vertex) + for (edge in edges) { + val destination = edge.to + val timeInDestination = timeIn[destination]!! + if (timeInDestination != -1 && + destination != prevVertex && + timeInDestination < ret[vertex]!! + ) { // if back edge + ret[vertex] = timeInDestination + } + if (timeInDestination != -1) { // if visited vertex + continue + } + dfs(destination, vertex) + val retDestination = ret[destination]!! + if (retDestination < ret[vertex]!!) { + ret[vertex] = retDestination + } + if (timeIn[vertex]!! < retDestination) { + bridges.add(edge) + } + } + } + if (graph.vertices.isNotEmpty()) { + dfs(graph.vertices.elementAt(0), graph.vertices.elementAt(0)) + } + + return bridges.toSet() +} diff --git a/src/main/kotlin/model/algos/ForceAtlas2.kt b/src/main/kotlin/model/algos/ForceAtlas2.kt new file mode 100644 index 0000000..b7ede2a --- /dev/null +++ b/src/main/kotlin/model/algos/ForceAtlas2.kt @@ -0,0 +1,88 @@ +package model.algos + +import height +import viewmodel.AbstractGraphViewModel +import viewmodel.VertexViewModel +import width +import kotlin.math.sign +import kotlin.math.sqrt + +const val repulsionK: Double = 250.0 +const val attractionK: Double = 150.0 +const val gravityK: Double = 5.0 + +object ForceAtlas2 { + fun forceDrawing(graphVM: AbstractGraphViewModel) { + val vertices = graphVM.verticesVM + + repeat(50) { + val forces = mutableMapOf, Pair>() + for (vertex in vertices) { + val edges = vertex.edges + val isConnected = mutableMapOf() + for (edge in edges) { + isConnected[edge.to] = true + } + var forceX: Double = 0.0 + var forceY: Double = 0.0 + + val gravityForces = getGravity(vertex) + forceX += gravityForces.first + forceY += gravityForces.second + + for (vertexInner in vertices) { + if (vertexInner == vertex) continue + val dx = vertexInner.x.toDouble() - vertex.x.toDouble() + val dy = vertexInner.y.toDouble() - vertex.y.toDouble() + val repulsion = getRepulsion(dx, dy) + forceX -= sign(dx) * repulsion + forceY -= sign(dy) * repulsion + + if (isConnected[vertexInner.vertex] ?: false) { + val attraction = getAttraction(dx, dy) + forceX += sign(dx) * attraction + forceY += sign(dy) * attraction + } + } + forces[vertex] = Pair(forceX.toFloat(), forceY.toFloat()) + } + for (vertex in forces.keys) { + val forcesPair = forces[vertex]!! + vertex.x = + (vertex.x + forcesPair.first).coerceIn(100f, width.toFloat() - 200f) + vertex.y = + (vertex.y + forcesPair.second).coerceIn(100f, height.toFloat() - 200f) + } + } + } + + private fun getRepulsion(dx: Double, dy: Double): Double { + val distance = getDistance(dx, dy) + val repulsion = repulsionK / distance + return repulsion + } + + private fun getAttraction(dx: Double, dy: Double): Double { + val distance = getDistance(dx, dy) + val attraction = distance / attractionK + return attraction + } + + private fun getDistance(dx: Double, dy: Double): Double { + val distance = sqrt(dx * dx + dy * dy) + return distance + } + + private fun getGravity(vertex: VertexViewModel): Pair { + val x = vertex.x.toDouble() + val y = vertex.y.toDouble() + val centerX = (width - 250) / 2 + val centerY = height / 2 + + val dx = centerX - x + val dy = centerY - y + val forceX = dx.sign * gravityK + val forceY = dy.sign * gravityK + return Pair(forceX, forceY) + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/algos/Prim.kt b/src/main/kotlin/model/algos/Prim.kt index ab27c44..f1d4f01 100644 --- a/src/main/kotlin/model/algos/Prim.kt +++ b/src/main/kotlin/model/algos/Prim.kt @@ -6,7 +6,7 @@ import java.util.PriorityQueue object Prim { // find minimum spanning tree - fun findMST(graph: UndirectedGraph, startVertex: V): List> { + fun findMst(graph: UndirectedGraph, startVertex: V): List> { val mst = mutableListOf>() val visited = mutableSetOf() val edgeQueue = PriorityQueue>() diff --git a/src/main/kotlin/model/algos/SearchBridges.kt b/src/main/kotlin/model/algos/SearchBridges.kt deleted file mode 100644 index b792fa4..0000000 --- a/src/main/kotlin/model/algos/SearchBridges.kt +++ /dev/null @@ -1,48 +0,0 @@ -package model.algos - -import model.graph.UndirectedGraph -import model.graph.edges.Edge - -object Bridges { - fun searchBridges(graph: UndirectedGraph): Set> { - val timeIn = mutableMapOf() - for (vertex in graph.vertices) { - timeIn[vertex] = -1 - } - val ret = mutableMapOf() - var time = 0 - val bridges = mutableSetOf>() - - fun dfs(vertex: V, prevVertex: V) { - timeIn[vertex] = time++ - ret[vertex] = timeIn[vertex]!! - val edges = graph.edgesOf(vertex) - for (edge in edges) { - val destination = edge.to - val timeInDestination = timeIn[destination]!! - if (timeInDestination != -1 && - destination != prevVertex && - timeInDestination < ret[vertex]!! - ) { // if back edge - ret[vertex] = timeInDestination - } - if (timeInDestination != -1) { // if visited vertex - continue - } - dfs(destination, vertex) - val retDestination = ret[destination]!! - if (retDestination < ret[vertex]!!) { - ret[vertex] = retDestination - } - if (timeIn[vertex]!! < retDestination) { - bridges.add(edge) - } - } - } - if (graph.vertices.isNotEmpty()) { - dfs(graph.vertices.elementAt(0), graph.vertices.elementAt(0)) - } - - return bridges.toSet() - } -} diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index 9ff9c13..f19cbd8 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -36,8 +36,8 @@ fun AddEdgeDialog( ) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } - var checkedState by remember { mutableStateOf(false) } - var weight by remember { mutableStateOf("") } + var checkedState by remember { mutableStateOf(true) } + var weight by remember { mutableStateOf("1") } Column(modifier = Modifier.padding(30.dp, 24.dp)) { val textWidth = 90.dp val rightPadding = 200.dp diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 3c1c623..1b49a19 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.window.rememberDialogState import androidx.compose.ui.zIndex import androidx.navigation.NavController import localisation.localisation +import model.algos.ForceAtlas2 import view.common.AddEdgeDialog import view.common.DefaultButton import view.common.defaultStyle @@ -52,9 +53,17 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) + DefaultButton({ ForceAtlas2.forceDrawing(graphVM) }, "visualize", Color(0xffFFA500)) + + Spacer(modifier = Modifier.height(10.dp)) + + DefaultButton({ graphVM.resetDrawing() }, "reset", Color.LightGray) + + Spacer(modifier = Modifier.height(10.dp)) + // Open Dijkstra dialog window var isDijkstraMenu by remember { mutableStateOf(false) } - DefaultButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra_algorithm") + DefaultButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra") Spacer(modifier = Modifier.height(10.dp)) diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 0b579b9..790c985 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController +import model.algos.ForceAtlas2 import view.common.AddEdgeDialog import view.common.DefaultButton import view.views.UndirectedGraphView @@ -42,6 +44,24 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) + DefaultButton({ ForceAtlas2.forceDrawing(graphVM) }, "visualize", Color(0xffFFA500)) + + Spacer(modifier = Modifier.height(10.dp)) + + DefaultButton({ graphVM.resetDrawing() }, "reset", Color.LightGray) + + Spacer(modifier = Modifier.height(10.dp)) + + DefaultButton(onClick = { graphVM.findMst() }, "find_mst") + + Spacer(modifier = Modifier.height(10.dp)) + + DefaultButton(onClick = { graphVM.findCycles() }, "find_cycles") + + Spacer(modifier = Modifier.height(10.dp)) + + DefaultButton(onClick = { graphVM.findBridges() }, "find_bridges") + val onClose = { isOpenedEdgeMenu = false } AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM) } diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/views/GraphView.kt index 10686e2..da5b32e 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/views/GraphView.kt @@ -12,9 +12,7 @@ import viewmodel.UndirectedGraphViewModel fun UndirectedGraphView(graphVM: UndirectedGraphViewModel) { for (vertexVM in graphVM.verticesVM) { UndirectedVertexView(vertexVM, graphVM) - } - for (edgeVM in graphVM.edgesVM) { - UndirectedEdgeView(edgeVM, graphVM.isWeighted) + } } @@ -24,7 +22,4 @@ fun DirectedGraphView(graphVM: DirectedGraphViewModel) { for (vertexVM in graphVM.verticesVM) { DirectedVertexView(vertexVM, graphVM) } - for (edgeVM in graphVM.edgesVM) { - DirectedEdgeView(edgeVM, graphVM.isWeighted) - } } diff --git a/src/main/kotlin/view/views/edge/DirectedEdgeView.kt b/src/main/kotlin/view/views/edge/DirectedEdgeView.kt index 0d8e739..ce8c2d3 100644 --- a/src/main/kotlin/view/views/edge/DirectedEdgeView.kt +++ b/src/main/kotlin/view/views/edge/DirectedEdgeView.kt @@ -13,88 +13,87 @@ import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -import viewmodel.DirectedGraphViewModel import viewmodel.EdgeViewModel -import javax.swing.text.StyledEditorKit.BoldAction import kotlin.math.atan2 @Composable -fun DirectedEdgeView(edgeVM: EdgeViewModel, isWeightedd: Boolean) { +fun DirectedEdgeView(edgeVM: EdgeViewModel, isWeighted: Boolean) { val textMeasurer = rememberTextMeasurer() Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( start = Offset( - edgeVM.xFrom + edgeVM.vertexSize / 2, - edgeVM.yFrom + edgeVM.vertexSize / 2 + edgeVM.fromX + edgeVM.vertexSize / 2, + edgeVM.fromY + edgeVM.vertexSize / 2 ), end = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2, - edgeVM.yTo + edgeVM.vertexSize / 2 + edgeVM.toX + edgeVM.vertexSize / 2, + edgeVM.toY + edgeVM.vertexSize / 2 ), strokeWidth = 6f, color = edgeVM.color, ) rotate( degrees = ((57.2958 * (atan2( - ((edgeVM.yFrom - edgeVM.yTo).toDouble()), - ((edgeVM.xFrom - edgeVM.xTo).toDouble()) + ((edgeVM.fromY - edgeVM.toY).toDouble()), + ((edgeVM.fromX - edgeVM.toX).toDouble()) ))).toFloat()), pivot = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2, - edgeVM.yTo + edgeVM.vertexSize / 2 + edgeVM.toX + edgeVM.vertexSize / 2, + edgeVM.toY + edgeVM.vertexSize / 2 ) ) { drawRect( color = edgeVM.color, size = Size(5f, 16f), topLeft = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2 + 65, - edgeVM.yTo + edgeVM.vertexSize / 2 - 8f + edgeVM.toX + edgeVM.vertexSize / 2 + 65, + edgeVM.toY + edgeVM.vertexSize / 2 - 8f ), ) drawRect( color = edgeVM.color, size = Size(5f, 14f), topLeft = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2 + 60, - edgeVM.yTo + edgeVM.vertexSize / 2 - 7f + edgeVM.toX + edgeVM.vertexSize / 2 + 60, + edgeVM.toY + edgeVM.vertexSize / 2 - 7f ), ) drawRect( color = edgeVM.color, size = Size(5f, 12f), topLeft = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2 + 55, - edgeVM.yTo + edgeVM.vertexSize / 2 - 6f + edgeVM.toX + edgeVM.vertexSize / 2 + 55, + edgeVM.toY + edgeVM.vertexSize / 2 - 6f ), ) drawRect( color = edgeVM.color, size = Size(5f, 10f), topLeft = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2 + 50, - edgeVM.yTo + edgeVM.vertexSize / 2 - 5f + edgeVM.toX + edgeVM.vertexSize / 2 + 50, + edgeVM.toY + edgeVM.vertexSize / 2 - 5f ), ) drawRect( color = edgeVM.color, size = Size(5f, 8f), topLeft = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2 + 45, - edgeVM.yTo + edgeVM.vertexSize / 2 - 4f + edgeVM.toX + edgeVM.vertexSize / 2 + 45, + edgeVM.toY + edgeVM.vertexSize / 2 - 4f ), ) } - if (isWeightedd) + if (isWeighted) { drawText( textMeasurer, edgeVM.weight.toString(), topLeft = Offset( - (edgeVM.xFrom + edgeVM.vertexSize + edgeVM.xTo) / 2 - edgeVM.weight.toString().length * 5.5f, - (edgeVM.yFrom + edgeVM.vertexSize + edgeVM.yTo) / 2 - 9 + (edgeVM.fromX + edgeVM.vertexSize + edgeVM.toX) / 2 - edgeVM.weight.toString().length * 5.5f, + (edgeVM.fromY + edgeVM.vertexSize + edgeVM.toY) / 2 - 9 ), - style = TextStyle(background = Color.White, fontSize = 18.sp) + style = TextStyle(background = Color.White, fontSize = 20.sp) ) + } } } \ No newline at end of file diff --git a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt index 3be0ba7..2e7cf75 100644 --- a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt +++ b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt @@ -1,7 +1,7 @@ package view.views.edge import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -12,7 +12,6 @@ import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import viewmodel.EdgeViewModel -import viewmodel.UndirectedGraphViewModel @Composable fun UndirectedEdgeView(edgeVM: EdgeViewModel, isWeighted: Boolean) { @@ -22,24 +21,21 @@ fun UndirectedEdgeView(edgeVM: EdgeViewModel, isWeighted: Boolean) { Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { drawLine( start = Offset( - edgeVM.xFrom + edgeVM.vertexSize / 2, - edgeVM.yFrom + edgeVM.vertexSize / 2 + edgeVM.fromX + edgeVM.vertexSize / 2, + edgeVM.fromY + edgeVM.vertexSize / 2 ), - end = Offset( - edgeVM.xTo + edgeVM.vertexSize / 2, - edgeVM.yTo + edgeVM.vertexSize / 2 - ), - strokeWidth = 6f, + end = Offset(edgeVM.toX + edgeVM.vertexSize / 2, edgeVM.toY + edgeVM.vertexSize / 2), + strokeWidth = 8f, color = edgeVM.color, ) if (isWeighted) drawText( textMeasurer, edgeVM.weight.toString(), topLeft = Offset( - (edgeVM.xFrom + edgeVM.vertexSize + edgeVM.xTo) / 2 - edgeVM.weight.toString().length * 5.5f, - (edgeVM.yFrom + edgeVM.vertexSize + edgeVM.yTo) / 2 - 9 + (edgeVM.fromX + edgeVM.vertexSize + edgeVM.toX) / 2 - edgeVM.weight.toString().length * 5.5f, + (edgeVM.fromY + edgeVM.vertexSize + edgeVM.toY) / 2 - 9 ), - style = TextStyle(background = Color.White, fontSize = 18.sp) + style = TextStyle(background = Color.White, fontSize = 20.sp) ) } } \ No newline at end of file diff --git a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt index 5f74333..6dba509 100644 --- a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt +++ b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt @@ -32,16 +32,6 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphV .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() - for (edge in graphVM.edgesVM) { - if (edge.from == vertexVM.vertex) { - edge.xFrom += dragAmount.x - edge.yFrom += dragAmount.y - } - if (edge.to == vertexVM.vertex) { - edge.xTo += dragAmount.x - edge.yTo += dragAmount.y - } - } vertexVM.x += dragAmount.x vertexVM.y += dragAmount.y } @@ -56,4 +46,8 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphV .wrapContentSize(), ) } + + for (edgeVM in vertexVM.edges) { + DirectedEdgeView(edgeVM, graphVM.isWeighted) + } } \ No newline at end of file diff --git a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt index 01a6a05..74c81fe 100644 --- a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt +++ b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt @@ -1,5 +1,6 @@ package view.views.vertex +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures @@ -9,12 +10,18 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import view.common.DefaultColors +import view.views.edge.DirectedEdgeView +import view.views.edge.UndirectedEdgeView import viewmodel.UndirectedGraphViewModel import viewmodel.VertexViewModel import kotlin.math.roundToInt @@ -32,16 +39,6 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGr .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() - for (edge in graphVM.edgesVM) { - if (edge.from == vertexVM.vertex) { - edge.xFrom += dragAmount.x - edge.yFrom += dragAmount.y - } - if (edge.to == vertexVM.vertex) { - edge.xTo += dragAmount.x - edge.yTo += dragAmount.y - } - } vertexVM.x += dragAmount.x vertexVM.y += dragAmount.y } @@ -56,4 +53,8 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGr .wrapContentSize(), ) } + + for (edgeVM in vertexVM.edges) { + UndirectedEdgeView(edgeVM, graphVM.isWeighted) + } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index dace612..4fd790b 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -3,6 +3,7 @@ package viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import model.graph.Graph @@ -49,6 +50,10 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM } } + fun vertexVmOf(vertex: V): VertexViewModel? { + return graphVM[vertex] + } + fun edgesVmOf(vertex: V): List> { return graphVM[vertex]?.edges?.toList() ?: emptyList() } @@ -60,6 +65,12 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM updateView() } + fun resetDrawing() { + for (edge in edgesVM) { + edge.color = Color.Black + } + } + fun updateView() { val keep = graphVM graphVM = mutableMapOf>() diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 5c35010..9650a38 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -10,15 +10,6 @@ class DirectedGraphViewModel( val graph: DirectedGraph = DirectedGraph() ) : AbstractGraphViewModel(name, graph) { - fun dijkstraAlgo(start: V, end: V) { - val y = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) - for (edge in y) { - for (edgeVM in this.edgesVmOf(edge.from)) { - if (edgeVM.to == edge.to) edgeVM.color = Color.Red - } - } - } - override fun addEdge(from: V, to: V, weight: Int) { val source: VertexViewModel val destination: VertexViewModel @@ -39,4 +30,17 @@ class DirectedGraphViewModel( if (weight != 1) isWeighted = true updateView() } + + private fun drawEdges(edges: Collection>, color: Color) { + for (edge in edges) { + for (edgeVM in this.edgesVmOf(edge.from)) { + if (edgeVM.to == edge.to) edgeVM.color = color + } + } + } + + fun dijkstraAlgo(start: V, end: V) { + val result = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) + drawEdges(result, Color.Red) + } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/EdgeViewModel.kt b/src/main/kotlin/viewmodel/EdgeViewModel.kt index 4967e07..b89b4ee 100644 --- a/src/main/kotlin/viewmodel/EdgeViewModel.kt +++ b/src/main/kotlin/viewmodel/EdgeViewModel.kt @@ -13,12 +13,19 @@ class EdgeViewModel( vertexToVM: VertexViewModel, ) : ViewModel() { - var xFrom by mutableStateOf(vertexFromVM.x) - var yFrom by mutableStateOf(vertexFromVM.y) - var xTo by mutableStateOf(vertexToVM.x) - var yTo by mutableStateOf(vertexToVM.y) - var vertexSize by mutableStateOf(vertexFromVM.vertexSize) - var weight by mutableStateOf(edge.weight) + val fromVM = vertexFromVM + val toVM = vertexToVM + val fromX + get() = fromVM.x + val fromY + get() = fromVM.y + val toX + get() = toVM.x + val toY + get() = toVM.y + val vertexSize + get() = fromVM.vertexSize + val weight by mutableStateOf(edge.weight) val from by mutableStateOf(edge.from) val to by mutableStateOf(edge.to) var color by mutableStateOf(Color.Black) diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index d61b1db..5e6a18b 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -1,5 +1,9 @@ package viewmodel +import androidx.compose.ui.graphics.Color +import model.algos.FindCycle +import model.algos.Prim +import model.algos.findBridges import model.graph.UndirectedGraph import model.graph.edges.Edge @@ -32,4 +36,35 @@ class UndirectedGraphViewModel( if (weight != 1) isWeighted = true updateView() } + + private fun drawEdges(edges: Collection>, color: Color) { + for (edge in edges) { + for (edgeVM in this.edgesVmOf(edge.from)) { + if (edgeVM.to == edge.to) edgeVM.color = color + } + for (edgeVM in this.edgesVmOf(edge.to)) { + if (edgeVM.to == edge.from) edgeVM.color = color + } + } + } + + fun findMst() { + if (size == 0) return + val startVertex = graphModel.vertices.first() + val result = Prim.findMst(graphModel as UndirectedGraph, startVertex) + drawEdges(result, Color.Magenta) + } + + fun findCycles() { + if (size == 0) return + val startVertex = graph.vertices.first() + val result = FindCycle.findCycle(graphModel as UndirectedGraph, startVertex) + ?: emptyList() + drawEdges(result, Color.Magenta) + } + + fun findBridges() { + val result = findBridges(graphModel as UndirectedGraph) + drawEdges(result, Color.Yellow) + } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 81397cc..294bbd2 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -1,6 +1,7 @@ package viewmodel import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel @@ -12,7 +13,14 @@ import kotlin.random.Random class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListOf()) : ViewModel() { val vertex: V = _vertex - var edges by mutableStateOf(_edges) + var edges = mutableStateListOf>() + + init { + for (edge in _edges) { + edges.add(edge) + } + } + var x by mutableStateOf(Random.nextInt(100, width - 100).toFloat()) var y by mutableStateOf(Random.nextInt(100, height - 100).toFloat()) val vertexSize = 80f diff --git a/src/test/kotlin/algos/PrimTest.kt b/src/test/kotlin/algos/PrimTest.kt index ac31867..0606217 100644 --- a/src/test/kotlin/algos/PrimTest.kt +++ b/src/test/kotlin/algos/PrimTest.kt @@ -27,7 +27,7 @@ internal class PrimTest { this.addEdge(1, 3, 10) } - val pathActual = Prim.findMST(graph, 1) + val pathActual = Prim.findMst(graph, 1) val pathExpected = listOf( Edge(1, 2, 1), diff --git a/src/test/kotlin/algos/SearchBridgesTest.kt b/src/test/kotlin/algos/SearchBridgesTest.kt index 2616f27..8e813bf 100644 --- a/src/test/kotlin/algos/SearchBridgesTest.kt +++ b/src/test/kotlin/algos/SearchBridgesTest.kt @@ -1,6 +1,6 @@ package algos -import model.algos.Bridges.searchBridges +import model.algos.findBridges import model.graph.UndirectedGraph import model.graph.edges.Edge import kotlin.test.Test @@ -23,7 +23,7 @@ internal class SearchBridgesTest { val graph = UndirectedGraph() val expectedBridges = setOf>() - val actualBridges = searchBridges(graph) + val actualBridges = findBridges(graph) assertTrue( bridgesEquals(expectedBridges, actualBridges), "Empty graph must not contain bridges" @@ -49,7 +49,7 @@ internal class SearchBridgesTest { } val expectedBridges = setOf(Edge(3, 4), Edge(6, 7)) - val actualBridges = searchBridges(graph) + val actualBridges = findBridges(graph) assertTrue( bridgesEquals(expectedBridges, actualBridges), "searchBridges must return set of graph bridges" From 329cac8c5ef0ecc037a317e17303784f40e88745 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Fri, 24 May 2024 05:22:49 -0400 Subject: [PATCH 126/172] add: SQLite save/delete/read for undirected graph --- src/main/kotlin/view/screens/MainScreen.kt | 3 + .../view/screens/UndirectedGraphScreen.kt | 227 +++++++++--------- .../viewmodel/DirectedGraphViewModel.kt | 2 - .../kotlin/viewmodel/MainScreenViewModel.kt | 33 ++- .../viewmodel/UndirectedGraphViewModel.kt | 100 +++++++- 5 files changed, 251 insertions(+), 114 deletions(-) diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 744e3e2..f8e3c4f 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -259,6 +259,9 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView if(mainScreenViewModel.graphs.typeList[index] == MainScreenViewModel.ViewModelType.Directed){ mainScreenViewModel.initModel(index) } + if(mainScreenViewModel.graphs.typeList[index] == MainScreenViewModel.ViewModelType.Undirected){ + mainScreenViewModel.initModel(index) + } navController.navigate( when (mainScreenViewModel.graphs.typeList[index]) { MainScreenViewModel.ViewModelType.Undirected -> {"${Screen.UndirectedGraphScreen.route}/$index"} diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index cca0fe8..f936b29 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -30,6 +30,7 @@ fun UndirectedGraphScreen( graphId: Int ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) + var isOpenedEdgeMenu by remember { mutableStateOf(false) } Box(modifier = Modifier.fillMaxSize()) { GraphViewUndirect(graphVM) @@ -64,7 +65,7 @@ fun UndirectedGraphScreen( } Spacer(modifier = Modifier.height(16.dp)) - var isOpenedEdgeMenu by remember { mutableStateOf(false) } + Button( onClick = { isOpenedEdgeMenu = !isOpenedEdgeMenu }, @@ -78,130 +79,140 @@ fun UndirectedGraphScreen( } Spacer(modifier = Modifier.height(10.dp)) - - DialogWindow( - visible = isOpenedEdgeMenu, - title = "New Edge", - onCloseRequest = { isOpenedEdgeMenu = false }, - state = rememberDialogState(height = 600.dp, width = 880.dp) + Button( + onClick = {graphVM.saveSQLite()}, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - var checkedState by remember { mutableStateOf(false) } - var weight by remember { mutableStateOf("") } - Column { - Spacer(modifier = Modifier.height(24.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("1st"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(62.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + Text(localisation("save"), style = defaultStyle) + } - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, + } + DialogWindow( + visible = isOpenedEdgeMenu, + title = "New Edge", + onCloseRequest = { isOpenedEdgeMenu = false }, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + var checkedState by remember { mutableStateOf(false) } + var weight by remember { mutableStateOf("") } + Column { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("1st"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(62.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text(text = localisation("2nd"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(54.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue },) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + if(!checkedState) { + Text( + text = localisation("weight"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) ) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { Spacer(modifier = Modifier.width(30.dp)) - Text(text = localisation("2nd"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(54.dp)) TextField( + enabled = !checkedState, modifier = Modifier .weight(1f) .width(115.dp) - .border(4.dp, color = Color.Black,shape = RoundedCornerShape(25.dp),), + .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) + .background(color = Color.White, shape = RoundedCornerShape(10.dp)), + shape = RoundedCornerShape(10.dp), colors = TextFieldDefaults.textFieldColors( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, ), - shape = RoundedCornerShape(25.dp), textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue },) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - if(!checkedState) { - Text( - text = localisation("weight"), - style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(30.dp)) - TextField( - enabled = !checkedState, - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(3.dp, color = Color.Black, shape = RoundedCornerShape(10.dp),) - .background(color = Color.White, shape = RoundedCornerShape(10.dp)), - shape = RoundedCornerShape(10.dp), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - textStyle = defaultStyle, - value = weight, - onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) - ) - Spacer(modifier = Modifier.width(20.dp)) - } - Checkbox( - modifier = Modifier.align(Alignment.CenterVertically), - checked = checkedState, - onCheckedChange = { checkedState = it; - weight = if(checkedState) "1" else "" } + value = weight, + onValueChange = { value -> if (value.length < 10) weight = value.filter { it.isDigit() } }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) ) - Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(modifier = Modifier.width(200.dp)) + Spacer(modifier = Modifier.width(20.dp)) } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { - if (source != "" && destination != "") { - graphVM.addEdge(source, destination, weight.toInt()) - isOpenedEdgeMenu = false - } - }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text(localisation("add_edge"), style = defaultStyle) - } + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = checkedState, + onCheckedChange = { checkedState = it; + weight = if(checkedState) "1" else "" } + ) + Text(text = localisation("unweighted"), style = defaultStyle,modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { + if (source != "" && destination != "") { + graphVM.addEdge(source, destination, weight.toInt()) + isOpenedEdgeMenu = false + } + }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("add_edge"), style = defaultStyle) } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Button( - onClick = { isOpenedEdgeMenu = false }, modifier = Modifier - .clip(shape = RoundedCornerShape(45.dp)) - .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) - .size(240.dp, 80.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) - ) { - Text(localisation("back"), style = defaultStyle) - } + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { isOpenedEdgeMenu = false }, modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) + ) { + Text(localisation("back"), style = defaultStyle) } } } diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index b9c4ddc..4ad4f2b 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -39,7 +39,6 @@ class DirectedGraphViewModel( } fun saveSQLite(){ - var parameterCreate = "( Vertexes String," var parameterInput = "( Vertexes," var create = ("CREATE TABLE $name ") @@ -54,7 +53,6 @@ class DirectedGraphViewModel( parameterInput = parameterInput.slice(0.. parameterInput.length - 2) parameterInput = "$parameterInput )" create = create + parameterCreate + ";" - val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") ?: throw SQLException("Cannot connect to database") val delTable = "DROP TABLE $name" diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 872be04..3d8d5ba 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -20,7 +20,9 @@ class MainScreenViewModel : ViewModel() { when (type) { "undirected" -> { graphs.typeList.add(ViewModelType.Undirected) - graphs.undirectedGraphs.add(UndirectedGraphViewModel(name, initType)) + val graphVM = UndirectedGraphViewModel(name) + graphVM.inType = initType + graphs.undirectedGraphs.add(graphVM) } "directed" -> { graphs.typeList.add(ViewModelType.Directed) @@ -33,7 +35,6 @@ class MainScreenViewModel : ViewModel() { } fun initModel(index: Int){ - if(graphs.typeList[index] == ViewModelType.Directed) { val graph = graphs.getDirected(index) if(graph.initedGraph) return @@ -62,6 +63,34 @@ class MainScreenViewModel : ViewModel() { } } } + if(graphs.typeList[index] == ViewModelType.Undirected) { + val graph = graphs.getUndirected(index) + if(graph.initedGraph) return + else graph.initedGraph = true + if (graph.inType == initType.SQLite) { + val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } + val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } + val resVertex = getVertex.executeQuery() + val resEdges = getGraphs.executeQuery() + while (resVertex.next()) { + var vertexName = resVertex.getString("Vertexes") + if(vertexName.length > 1) vertexName = vertexName.slice(1..vertexName.length - 1) + graph.addVertex(vertexName) + } + while (resEdges.next()) { + for (i in graph.graph.vertices) { + val weight = resEdges.getString("V$i") + var to = resEdges.getString("Vertexes") + to = to.slice(1..( name: String, - initType: initType, val graph: UndirectedGraph = UndirectedGraph() ): AbstractGraphViewModel(name, graph){ - + private val DB_DRIVER = "jdbc:sqlite" + var inType = viewmodel.initType.Internal + var initedGraph = false val model get() = graph init { @@ -19,7 +22,100 @@ class UndirectedGraphViewModel( edgesView.add(EdgeViewModel(edge, vertexView[edge.from]!!, vertexView[edge.to]!!)) } } + fun saveSQLite(){ + var parameterCreate = "( Vertexes String," + var parameterInput = "( Vertexes," + var create = ("CREATE TABLE $name ") + val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") + val insertIndex = ("INSERT INTO BEBRA_KILLER (name, type) VALUES('$name', 'Undirected');") + for (i in graph.entries){ + parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " + parameterInput = "$parameterInput V${i.key.toString()}," + } + parameterCreate = parameterCreate.slice(0.. parameterCreate.length - 3) + parameterCreate = "$parameterCreate )" + parameterInput = parameterInput.slice(0.. parameterInput.length - 2) + parameterInput = "$parameterInput )" + create = create + parameterCreate + ";" + val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") + ?: throw SQLException("Cannot connect to database") + val delTable = "DROP TABLE $name" + val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='$name';" + connection.createStatement().also { stmt -> + try { + stmt.execute(delTable) + println("Table deleted") + } catch (ex: Exception) { + println("Cannot delete table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(delIndexRec) + println("Table deleted") + } catch (ex: Exception) { + println("Cannot delete table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(create) + stmt.execute(createIndex) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(insertIndex) + } catch (ex: Exception) { + println("Unsuccessful") + println(ex) + } finally { + stmt.close() + } + } + var request = "INSERT INTO $name $parameterInput VALUES " + for (i in graph.entries){ + var record = "( 'V${i.key}', " + val recList = emptyMap().toMutableMap() + for (j in graph.entries){ + recList[j.key] = "NULL" + } + for (j in i.value){ + recList[j.to] = j.weight.toString() + } + for (j in recList){ + record = "$record ${j.value}, " + } + record = record.slice(0.. record.length - 3) + record = "$record )," + request = "$request $record" + } + request = request.slice(0.. request.length - 2) + connection.createStatement().also { stmt -> + try { + stmt.execute(request) + } catch (ex: Exception) { + println("Unsuccessful") + println(ex) + } finally { + stmt.close() + } + } + println(request) + } override fun addEdge(from: V, to: V, weight: Int) { if (vertexView[from] == null) return for (i in vertexView[from]?.edges!!) if (i.to == to) return From 4a027d644d9ba28e8297e1ffe9875e3ce80a1306 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 24 May 2024 12:33:44 +0300 Subject: [PATCH 127/172] Fix: fix bug with NaN rounding in Force-Directed algorithm -change look of vertices and edges -add degree to vertices -add temporary graph init for undirected graph --- src/main/kotlin/model/algos/ForceAtlas2.kt | 27 ++++++++++++------- src/main/kotlin/model/graph/Graph.kt | 10 ++++--- .../view/views/edge/UndirectedEdgeView.kt | 2 +- .../view/views/vertex/DirectedVertexView.kt | 2 +- .../view/views/vertex/UndirectedVertexView.kt | 2 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 17 +++++++++++- src/main/kotlin/viewmodel/VertexViewModel.kt | 4 ++- 7 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/model/algos/ForceAtlas2.kt b/src/main/kotlin/model/algos/ForceAtlas2.kt index b7ede2a..fcae5bd 100644 --- a/src/main/kotlin/model/algos/ForceAtlas2.kt +++ b/src/main/kotlin/model/algos/ForceAtlas2.kt @@ -1,9 +1,11 @@ package model.algos import height +import kotlinx.serialization.internal.throwMissingFieldException import viewmodel.AbstractGraphViewModel import viewmodel.VertexViewModel import width +import kotlin.math.ln import kotlin.math.sign import kotlin.math.sqrt @@ -14,8 +16,7 @@ const val gravityK: Double = 5.0 object ForceAtlas2 { fun forceDrawing(graphVM: AbstractGraphViewModel) { val vertices = graphVM.verticesVM - - repeat(50) { + repeat(100) { val forces = mutableMapOf, Pair>() for (vertex in vertices) { val edges = vertex.edges @@ -23,8 +24,8 @@ object ForceAtlas2 { for (edge in edges) { isConnected[edge.to] = true } - var forceX: Double = 0.0 - var forceY: Double = 0.0 + var forceX = 0.0 + var forceY = 0.0 val gravityForces = getGravity(vertex) forceX += gravityForces.first @@ -34,7 +35,7 @@ object ForceAtlas2 { if (vertexInner == vertex) continue val dx = vertexInner.x.toDouble() - vertex.x.toDouble() val dy = vertexInner.y.toDouble() - vertex.y.toDouble() - val repulsion = getRepulsion(dx, dy) + val repulsion = getRepulsion(dx, dy, vertex.degree, vertexInner.degree) forceX -= sign(dx) * repulsion forceY -= sign(dy) * repulsion @@ -48,16 +49,22 @@ object ForceAtlas2 { } for (vertex in forces.keys) { val forcesPair = forces[vertex]!! - vertex.x = - (vertex.x + forcesPair.first).coerceIn(100f, width.toFloat() - 200f) - vertex.y = - (vertex.y + forcesPair.second).coerceIn(100f, height.toFloat() - 200f) + if (!forcesPair.first.isNaN()) { + val newX = (vertex.x + forcesPair.first).coerceIn(20f, width.toFloat() - 70f) + vertex.x = newX + } + if (!forcesPair.second.isNaN()) { + val newY = + (vertex.y + forcesPair.second).coerceIn(20f, height.toFloat() - 200f) + vertex.y = newY + } } } } - private fun getRepulsion(dx: Double, dy: Double): Double { + private fun getRepulsion(dx: Double, dy: Double, degree1: Int, degree2: Int): Double { val distance = getDistance(dx, dy) +// val repulsion = repulsionK * (degree1 + 1) * (1 + degree2) / distance val repulsion = repulsionK / distance return repulsion } diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 67053d9..672005b 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -4,13 +4,13 @@ import model.graph.edges.Edge abstract class Graph() { protected val graph = mutableMapOf>>() - val matrix get() = graph - + val matrix + get() = graph val entries get() = graph.entries protected var weighted = false - val state + val isWeighted get() = weighted val vertices get() = graph.keys @@ -33,6 +33,10 @@ abstract class Graph() { size++ } + fun degreeOfVertex(vertex: V): Int { + return graph[vertex]?.size ?: 0 + } + abstract fun addEdge(from: V, to: V, weight: Int = 1) fun edgesOf(from: V): MutableList> { diff --git a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt index 2e7cf75..3f60e40 100644 --- a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt +++ b/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt @@ -25,7 +25,7 @@ fun UndirectedEdgeView(edgeVM: EdgeViewModel, isWeighted: Boolean) { edgeVM.fromY + edgeVM.vertexSize / 2 ), end = Offset(edgeVM.toX + edgeVM.vertexSize / 2, edgeVM.toY + edgeVM.vertexSize / 2), - strokeWidth = 8f, + strokeWidth = 5f, color = edgeVM.color, ) if (isWeighted) diff --git a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt index 6dba509..6f1313a 100644 --- a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt +++ b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt @@ -40,7 +40,7 @@ fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphV ) { Text( text = "$vertex", - fontSize = 40.sp, + fontSize = 28.sp, modifier = Modifier .fillMaxSize() .wrapContentSize(), diff --git a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt index 74c81fe..b09e25e 100644 --- a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt +++ b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt @@ -47,7 +47,7 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGr ) { Text( text = "$vertex", - fontSize = 40.sp, + fontSize = 28.sp, modifier = Modifier .fillMaxSize() .wrapContentSize(), diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index a17e65b..981af64 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -2,6 +2,7 @@ package viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel +import model.graph.UndirectedGraph class MainScreenViewModel : ViewModel() { val graphs = GraphStorage() @@ -10,7 +11,21 @@ class MainScreenViewModel : ViewModel() { when (type) { "undirected" -> { graphs.typeList.add(ViewModelType.Undirected) - graphs.undirectedGraphs.add(UndirectedGraphViewModel(name)) + val graph = UndirectedGraphViewModel(name) + fun initGraph() { + val comSize = 5 + val comNumb = 7 + for (i in 0.. { diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 294bbd2..a14adce 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -23,5 +23,7 @@ class VertexViewModel(_vertex: V, _edges: MutableList> = mut var x by mutableStateOf(Random.nextInt(100, width - 100).toFloat()) var y by mutableStateOf(Random.nextInt(100, height - 100).toFloat()) - val vertexSize = 80f + val vertexSize = 60f + val degree + get() = edges.size } \ No newline at end of file From d90dcb504b9c5bf9585d83a405bc34445179a1f1 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Fri, 24 May 2024 09:38:33 -0400 Subject: [PATCH 128/172] add: StrongConnections algo add & it's visualisation --- .../kotlin/model/algos/StrongConnections.kt | 75 +++++++++++++++++++ .../view/screens/DirectedGraphScreen.kt | 15 +++- src/main/kotlin/view/views/VertexView.kt | 2 +- .../viewmodel/DirectedGraphViewModel.kt | 17 ++++- src/main/kotlin/viewmodel/VertexViewModel.kt | 2 + 5 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/model/algos/StrongConnections.kt diff --git a/src/main/kotlin/model/algos/StrongConnections.kt b/src/main/kotlin/model/algos/StrongConnections.kt new file mode 100644 index 0000000..510dc73 --- /dev/null +++ b/src/main/kotlin/model/algos/StrongConnections.kt @@ -0,0 +1,75 @@ +import model.graph.edges.Edge + +class StrongConnections(){ + private val comparatorIV = emptyMap().toMutableMap() + private val comparatorVI = emptyMap().toMutableMap() + fun dfs( + curr: Int, des: Int, adj: MutableMap>, + vis: MutableList + ): Boolean { + if (curr == des) { + return true + } + vis[curr] = 1 + for (x in adj[comparatorIV[curr]]!!) { + if (vis[comparatorVI[x]!!] == 0) { + if (dfs(comparatorVI[x]!!, des, adj, vis)) { + return true + } + } + } + return false + } + + fun isPath(src: Int, des: Int, adj: MutableMap>): Boolean { + val vis: MutableList = ArrayList(adj.size + 1) + for (i in 0..adj.size) { + vis.add(0) + } + return dfs(src, des, adj, vis) + } + + fun findStrongConnections(graph: MutableMap>>): List> { + for (i in graph.keys){ + comparatorIV[comparatorIV.size] = i + comparatorVI[i] = comparatorVI.size + } + println(comparatorVI) + + println(comparatorIV) + val adj = emptyMap>().toMutableMap() + val n = comparatorIV.size + val ans: MutableList> = ArrayList() + val is_scc: MutableList = ArrayList(comparatorIV.size + 1) + for (i in 0..n) { + is_scc.add(0) + } + for (i in comparatorVI.keys) { + adj[i] = emptyList().toMutableList() + } + + for (edge in graph) { + for (j in edge.value) { + adj[j.from]?.add(j.to) + } + } + + for (i in 0.. = ArrayList() + scc.add(comparatorIV[i]!!) + + for (j in i + 1.., graphVM: DirectedGraph .offset { IntOffset(vertexVM.offsetX.roundToInt(), vertexVM.offsetY.roundToInt()) } .clip(shape = CircleShape) .size(vertexVM.vertexSize.dp) - .background(DefaultColors.primary) + .background(vertexVM.color) .border(5.dp, Color.Black, CircleShape) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 4ad4f2b..cfec0f6 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,15 +1,16 @@ package viewmodel import Dijkstra +import StrongConnections import androidx.compose.ui.graphics.Color import model.graph.DirectedGraph import model.graph.edges.Edge import java.sql.DriverManager import java.sql.SQLException +import kotlin.random.Random class DirectedGraphViewModel( name: String, - val graph: DirectedGraph = DirectedGraph() ): AbstractGraphViewModel(name, graph){ val model @@ -19,7 +20,6 @@ class DirectedGraphViewModel( private val DB_DRIVER = "jdbc:sqlite" init { - for (vertex in graphModel.entries) { vertexView[vertex.key] = VertexViewModel(vertex.key, vertex.value) } @@ -38,6 +38,19 @@ class DirectedGraphViewModel( } } + fun showStrongConnections(){ + val k = StrongConnections() + for (i in k.findStrongConnections(graph.matrix)) { + val col = Color(Random.nextInt(30, 230), Random.nextInt(30, 230),Random.nextInt(30, 230)) + for (j in i) { + if (j in graphModel.vertices) { + vertexView[j]?.color = col + updateView() + } + } + } + } + fun saveSQLite(){ var parameterCreate = "( Vertexes String," var parameterInput = "( Vertexes," diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 83e6aa6..ca66bfd 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import height import model.graph.edges.Edge +import view.DefaultColors import width import kotlin.random.Random @@ -16,4 +17,5 @@ class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListO var offsetX by mutableStateOf(Random.nextInt(100, width - 100).toFloat()) var offsetY by mutableStateOf(Random.nextInt(100, height - 100).toFloat()) val vertexSize = 80f + var color = DefaultColors.primary } \ No newline at end of file From 25a2cac715b43cbba6065ba045b88890e097d7a5 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Fri, 24 May 2024 09:39:19 -0400 Subject: [PATCH 129/172] add: SCon algo improve refactor --- .../kotlin/model/algos/StrongConnections.kt | 101 +++++++----------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/src/main/kotlin/model/algos/StrongConnections.kt b/src/main/kotlin/model/algos/StrongConnections.kt index 510dc73..2a38dc7 100644 --- a/src/main/kotlin/model/algos/StrongConnections.kt +++ b/src/main/kotlin/model/algos/StrongConnections.kt @@ -1,75 +1,54 @@ import model.graph.edges.Edge -class StrongConnections(){ - private val comparatorIV = emptyMap().toMutableMap() - private val comparatorVI = emptyMap().toMutableMap() - fun dfs( - curr: Int, des: Int, adj: MutableMap>, - vis: MutableList - ): Boolean { - if (curr == des) { - return true - } - vis[curr] = 1 - for (x in adj[comparatorIV[curr]]!!) { - if (vis[comparatorVI[x]!!] == 0) { - if (dfs(comparatorVI[x]!!, des, adj, vis)) { - return true +class StrongConnections{ + private val comparatorItoV = emptyMap().toMutableMap() + private val comparatorVtoI = emptyMap().toMutableMap() + + fun findStrongConnections(graph: MutableMap>>): List> { + for (i in graph.keys){ + comparatorItoV[comparatorItoV.size] = i + comparatorVtoI[i] = comparatorVtoI.size + } + val adjustment = emptyMap>().toMutableMap() + val dim = comparatorItoV.size + val result: MutableList> = ArrayList() + val listStrongCon: MutableList = List(dim + 1){ false }.toMutableList() + for (i in comparatorVtoI.keys) adjustment[i] = emptyList().toMutableList() + for (edge in graph) + for (j in edge.value) + adjustment[j.from]?.add(j.to) + for (indexV in 0..< dim) + if (!listStrongCon[indexV]) { + val connections: MutableList = ArrayList() + connections.add(comparatorItoV[indexV]!!) + for (indexN in indexV + 1..>): Boolean { - val vis: MutableList = ArrayList(adj.size + 1) - for (i in 0..adj.size) { - vis.add(0) - } - return dfs(src, des, adj, vis) + fun findPath(source: Int, top: Int, adjustment: MutableMap>): Boolean { + val visited: MutableList = List(comparatorItoV.size + 1){ 0 }.toMutableList() + return DFS(source, top, adjustment, visited) } - fun findStrongConnections(graph: MutableMap>>): List> { - for (i in graph.keys){ - comparatorIV[comparatorIV.size] = i - comparatorVI[i] = comparatorVI.size - } - println(comparatorVI) - - println(comparatorIV) - val adj = emptyMap>().toMutableMap() - val n = comparatorIV.size - val ans: MutableList> = ArrayList() - val is_scc: MutableList = ArrayList(comparatorIV.size + 1) - for (i in 0..n) { - is_scc.add(0) - } - for (i in comparatorVI.keys) { - adj[i] = emptyList().toMutableList() - } - - for (edge in graph) { - for (j in edge.value) { - adj[j.from]?.add(j.to) - } + fun DFS(current: Int, top: Int, adjustment: MutableMap>, visited: MutableList): Boolean { + if (current == top) { + return true } - - for (i in 0.. = ArrayList() - scc.add(comparatorIV[i]!!) - - for (j in i + 1.. Date: Fri, 24 May 2024 18:48:16 +0300 Subject: [PATCH 130/172] Feat&Fix: add visualization of FordBellman, fix bug in addEdge menu -fix bug when adding edge with empty weight field -add visualization of FordBellman -made dialog with source and destination algorithm common gui component --- src/main/kotlin/model/algos/FindBridges.kt | 8 +- src/main/kotlin/model/graph/DirectedGraph.kt | 3 +- src/main/kotlin/model/graph/Graph.kt | 9 +- .../kotlin/model/graph/UndirectedGraph.kt | 3 +- src/main/kotlin/view/common/AddEdgeDialog.kt | 4 +- .../view/common/DirectedAlgorithmDialog.kt | 110 +++++++++++++++++ .../view/screens/DirectedGraphScreen.kt | 111 +++++------------- .../view/screens/UndirectedGraphScreen.kt | 33 ++++++ .../viewmodel/AbstractGraphViewModel.kt | 26 +++- .../viewmodel/DirectedGraphViewModel.kt | 8 +- .../viewmodel/UndirectedGraphViewModel.kt | 11 +- src/test/kotlin/algos/FindCycleTest.kt | 24 ++++ 12 files changed, 244 insertions(+), 106 deletions(-) create mode 100644 src/main/kotlin/view/common/DirectedAlgorithmDialog.kt diff --git a/src/main/kotlin/model/algos/FindBridges.kt b/src/main/kotlin/model/algos/FindBridges.kt index c37338e..1a88ac2 100644 --- a/src/main/kotlin/model/algos/FindBridges.kt +++ b/src/main/kotlin/model/algos/FindBridges.kt @@ -11,9 +11,11 @@ fun findBridges(graph: UndirectedGraph): Set> { val ret = mutableMapOf() var time = 0 val bridges = mutableSetOf>() + val notVisited = graph.vertices.toMutableSet() fun dfs(vertex: V, prevVertex: V) { timeIn[vertex] = time++ + notVisited.remove(vertex) ret[vertex] = timeIn[vertex]!! val edges = graph.edgesOf(vertex) for (edge in edges) { @@ -38,8 +40,10 @@ fun findBridges(graph: UndirectedGraph): Set> { } } } - if (graph.vertices.isNotEmpty()) { - dfs(graph.vertices.elementAt(0), graph.vertices.elementAt(0)) + + + while (notVisited.isNotEmpty()) { + dfs(notVisited.elementAt(0), notVisited.elementAt(0)) } return bridges.toSet() diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index 2be8607..9f0f559 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -4,7 +4,8 @@ import model.graph.edges.Edge class DirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { - if (weight != 1) weighted = true + if (weight != 1) isWeighted = true + if (weight < 0) negativeWeights = true val edge = Edge(from, to, weight) graph[from]?.add(edge) ?: { graph[from] = mutableListOf(edge) } } diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 672005b..ac45000 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -8,10 +8,11 @@ abstract class Graph() { get() = graph val entries get() = graph.entries - protected var weighted = false - - val isWeighted - get() = weighted + var isWeighted = false + protected set + var negativeWeights = false + protected set + val vertices get() = graph.keys diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index 825085d..7b785de 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -4,7 +4,8 @@ import model.graph.edges.Edge class UndirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { - if (weight != 1) weighted = true + if (weight != 1) isWeighted = true + if (weight < 0) negativeWeights = true val edge1 = Edge(from, to, weight) val edge2 = Edge(to, from, weight) graph[from]?.add(edge1) ?: { graph[from] = mutableListOf(edge1) } diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index f19cbd8..bc62f2b 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -117,7 +117,8 @@ fun AddEdgeDialog( textStyle = defaultStyle, value = weight, onValueChange = { value -> - if (value.length < 10) weight = value.filter { it.isDigit() } + if (value.length < 10) weight = + value.filter { it.isDigit() || (it == '-' && it == value.first()) } }, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) ) @@ -144,6 +145,7 @@ fun AddEdgeDialog( val sourceInt = source.toIntOrNull() val destinationInt = destination.toIntOrNull() if (sourceInt != null && destinationInt != null) { + if (weight == "") weight = "1" graphVM.addEdge(sourceInt, destinationInt, weight.toInt()) visible = false } diff --git a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt new file mode 100644 index 0000000..0c28ac4 --- /dev/null +++ b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt @@ -0,0 +1,110 @@ +package view.common + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState +import localisation.localisation +import viewmodel.AbstractGraphViewModel + + +@Composable +fun DirectedAlgorithmDialog( + visible: Boolean, + title: String, + onCloseRequest: () -> Unit, + graphVM: AbstractGraphViewModel, + action: String, +) { + DialogWindow( + visible = visible, + title = title, + onCloseRequest = onCloseRequest, + state = rememberDialogState(height = 600.dp, width = 880.dp) + ) { + var source by remember { mutableStateOf("") } + var destination by remember { mutableStateOf("") } + Column { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text( + text = localisation("from"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(26.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = source, + onValueChange = { newValue -> source = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + Text( + text = localisation("to"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(62.dp)) + TextField( + modifier = Modifier + .weight(1f) + .width(115.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = destination, + onValueChange = { newValue -> destination = newValue }, + ) + Spacer(modifier = Modifier.width(200.dp)) + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + val dijkstra = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) } + val fordBellman = { graphVM.fordBellman(source.toInt(), destination.toInt()) } + val onClick = if (action == "Dijkstra") { + dijkstra + } else if (action == "FordBellman") { + fordBellman + } else { + {} + } + DefaultButton(onClick, "start") + } + Spacer(modifier = Modifier.height(36.dp)) + Row { + Spacer(modifier = Modifier.width(30.dp)) + DefaultButton(onCloseRequest, "back", Color.Red) + } + } + } +} diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 1b49a19..fb7e3fb 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -17,6 +17,7 @@ import localisation.localisation import model.algos.ForceAtlas2 import view.common.AddEdgeDialog import view.common.DefaultButton +import view.common.DirectedAlgorithmDialog import view.common.defaultStyle import view.views.DirectedGraphView import viewmodel.MainScreenViewModel @@ -42,13 +43,14 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(16.dp)) - // Add vertex + // Add vertex Button DefaultButton({ graphVM.addVertex(graphVM.size) }, "add_vertex") Spacer(modifier = Modifier.height(16.dp)) - // Open "add edge" dialog + // Add edge Button var isOpenedEdgeMenu by remember { mutableStateOf(false) } + val onCloseEdge = { isOpenedEdgeMenu = false } DefaultButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") Spacer(modifier = Modifier.height(10.dp)) @@ -61,90 +63,39 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) - // Open Dijkstra dialog window + // Dijkstra Button var isDijkstraMenu by remember { mutableStateOf(false) } - DefaultButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra") + val onCloseDijkstra = { isDijkstraMenu = !isDijkstraMenu } + DefaultButton(onCloseDijkstra, "dijkstra") Spacer(modifier = Modifier.height(10.dp)) - val onClose = { isOpenedEdgeMenu = false } - AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM, isDirected = true) + // FordBellman Button + var isFordBellmanMenu by remember { mutableStateOf(false) } + val onCloseFB = { isFordBellmanMenu = !isFordBellmanMenu } + DefaultButton(onCloseFB, "ford_bellman") + + Spacer(modifier = Modifier.height(10.dp)) + + AddEdgeDialog(isOpenedEdgeMenu, onCloseEdge, graphVM, isDirected = true) // Dijkstra dialog window - DialogWindow( - visible = isDijkstraMenu, - title = "New Edge", - onCloseRequest = { isDijkstraMenu = false }, - state = rememberDialogState(height = 600.dp, width = 880.dp) - ) { - var source by remember { mutableStateOf("") } - var destination by remember { mutableStateOf("") } - Column { - Spacer(modifier = Modifier.height(24.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text( - text = localisation("from"), - style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(26.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = source, - onValueChange = { newValue -> source = newValue }, - ) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - Text( - text = localisation("to"), - style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(modifier = Modifier.width(62.dp)) - TextField( - modifier = Modifier - .weight(1f) - .width(115.dp) - .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), - - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - shape = RoundedCornerShape(25.dp), - textStyle = defaultStyle, - value = destination, - onValueChange = { newValue -> destination = newValue }, - ) - Spacer(modifier = Modifier.width(200.dp)) - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - val onClick = { graphVM.dijkstraAlgo(source.toInt(), destination.toInt()) } - DefaultButton(onClick, "start") - } - Spacer(modifier = Modifier.height(36.dp)) - Row { - Spacer(modifier = Modifier.width(30.dp)) - DefaultButton({ isDijkstraMenu = false }, "back", Color.Red) - } - } - } + DirectedAlgorithmDialog( + isDijkstraMenu, + "Dijkstra Algorithm", + onCloseDijkstra, + graphVM, + "Dijkstra" + ) + + //Ford-Bellman dialog window + DirectedAlgorithmDialog( + isFordBellmanMenu, + "Ford Bellman Algorithm", + onCloseFB, + graphVM, + "FordBellman" + ) } } diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 790c985..9600197 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -11,6 +11,7 @@ import androidx.navigation.NavController import model.algos.ForceAtlas2 import view.common.AddEdgeDialog import view.common.DefaultButton +import view.common.DirectedAlgorithmDialog import view.views.UndirectedGraphView import viewmodel.MainScreenViewModel @@ -52,6 +53,20 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) + // Dijkstra Button + var isDijkstraMenu by remember { mutableStateOf(false) } + val onCloseDijkstra = { isDijkstraMenu = !isDijkstraMenu } + DefaultButton(onCloseDijkstra, "dijkstra") + + Spacer(modifier = Modifier.height(10.dp)) + + // FordBellman Button + var isFordBellmanMenu by remember { mutableStateOf(false) } + val onCloseFB = { isFordBellmanMenu = !isFordBellmanMenu } + DefaultButton(onCloseFB, "ford_bellman") + + Spacer(modifier = Modifier.height(10.dp)) + DefaultButton(onClick = { graphVM.findMst() }, "find_mst") Spacer(modifier = Modifier.height(10.dp)) @@ -64,5 +79,23 @@ fun UndirectedGraphScreen( val onClose = { isOpenedEdgeMenu = false } AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM) + + // Dijkstra dialog window + DirectedAlgorithmDialog( + isDijkstraMenu, + "Dijkstra Algorithm", + onCloseDijkstra, + graphVM, + "Dijkstra" + ) + + //Ford-Bellman dialog window + DirectedAlgorithmDialog( + isFordBellmanMenu, + "Ford Bellman Algorithm", + onCloseFB, + graphVM, + "FordBellman" + ) } } diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 4fd790b..9d6b10c 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -1,19 +1,24 @@ package viewmodel +import Dijkstra import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel +import model.algos.FordBellman import model.graph.Graph +import model.graph.edges.Edge abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { val name = _name protected var graphVM by mutableStateOf(mutableMapOf>()) protected val graphModel = graph var size = 0 - var isWeighted by mutableStateOf(false) - protected set + val isWeighted + get() = graphModel.isWeighted + val negativeWeights + get() = graphModel.negativeWeights val model get() = graphModel val verticesVM @@ -50,6 +55,21 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM } } + abstract fun addEdge(from: V, to: V, weight: Int = 1) + + abstract fun drawEdges(edges: Collection>, color: Color) + + fun dijkstraAlgo(start: V, end: V) { + if (this.negativeWeights) return + val result = Dijkstra(graphModel.matrix, graphModel.size).dijkstra(start, end) + drawEdges(result, Color.Red) + } + + fun fordBellman(from: V, to: V) { + val path = FordBellman.findShortestPath(from, to, this.graphModel).second ?: emptyList() + drawEdges(path, Color.Cyan) + } + fun vertexVmOf(vertex: V): VertexViewModel? { return graphVM[vertex] } @@ -76,6 +96,4 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM graphVM = mutableMapOf>() graphVM = keep } - - abstract fun addEdge(from: V, to: V, weight: Int = 1) } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 9650a38..6d189b9 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -27,20 +27,14 @@ class DirectedGraphViewModel( source.edges.add(edgeVM) graphModel.addEdge(from, to, weight) - if (weight != 1) isWeighted = true updateView() } - private fun drawEdges(edges: Collection>, color: Color) { + override fun drawEdges(edges: Collection>, color: Color) { for (edge in edges) { for (edgeVM in this.edgesVmOf(edge.from)) { if (edgeVM.to == edge.to) edgeVM.color = color } } } - - fun dijkstraAlgo(start: V, end: V) { - val result = Dijkstra(graph.matrix, graph.size).dijkstra(start, end) - drawEdges(result, Color.Red) - } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 5e6a18b..cbb56ca 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -2,6 +2,7 @@ package viewmodel import androidx.compose.ui.graphics.Color import model.algos.FindCycle +import model.algos.FordBellman import model.algos.Prim import model.algos.findBridges import model.graph.UndirectedGraph @@ -33,11 +34,10 @@ class UndirectedGraphViewModel( destination.edges.add(edgeFromDestinationVM) graphModel.addEdge(from, to, weight) - if (weight != 1) isWeighted = true updateView() } - private fun drawEdges(edges: Collection>, color: Color) { + override fun drawEdges(edges: Collection>, color: Color) { for (edge in edges) { for (edgeVM in this.edgesVmOf(edge.from)) { if (edgeVM.to == edge.to) edgeVM.color = color @@ -56,10 +56,9 @@ class UndirectedGraphViewModel( } fun findCycles() { - if (size == 0) return - val startVertex = graph.vertices.first() - val result = FindCycle.findCycle(graphModel as UndirectedGraph, startVertex) - ?: emptyList() + if (this.size == 0) return + val start = graphModel.vertices.first() + val result = FindCycle.findCycle(graphModel as UndirectedGraph, start) ?: emptyList() drawEdges(result, Color.Magenta) } diff --git a/src/test/kotlin/algos/FindCycleTest.kt b/src/test/kotlin/algos/FindCycleTest.kt index cbe38e4..3ea7b3f 100644 --- a/src/test/kotlin/algos/FindCycleTest.kt +++ b/src/test/kotlin/algos/FindCycleTest.kt @@ -32,4 +32,28 @@ internal class FindCycleTest { ) assertContentEquals(pathExpected, pathActual) } + + @Test + fun pentagramm() { + val graph = UndirectedGraph() + for (i in 1..5) { + graph.addVertex(i) + } + + for (i in 1..5) { + for (j in 1..5) { + graph.addEdge(i, j) + } + } + + val pathActual = FindCycle.findCycle(graph, 1) + assertNotNull(pathActual) + val pathExpected = mutableListOf>() + for (i in 1..5) { + for (j in 1..5) { + if (i != j) pathExpected.add(Edge(i, j)) + } + } + assertContentEquals(pathExpected, pathActual) + } } \ No newline at end of file From 8376bae3c8419190541b48fb0fd1ebb9a143f612 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Fri, 24 May 2024 19:02:39 +0300 Subject: [PATCH 131/172] CI: divide Build and Test workflows --- .github/workflows/gradle-build.yml | 21 +++++++++++++++++++++ .github/workflows/gradle-test.yml | 8 ++++---- src/main/kotlin/Main.kt | 6 +++--- 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/gradle-build.yml diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml new file mode 100644 index 0000000..d55daa5 --- /dev/null +++ b/.github/workflows/gradle-build.yml @@ -0,0 +1,21 @@ +name: Build + +on: + push: +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-latest ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: '21' + - name: Clean and Build + run: ./gradlew clean build -x test \ No newline at end of file diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 4867a6f..2c8155e 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -1,13 +1,13 @@ -name: Build & Test +name: Test on: push: jobs: - build: + test: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] + os: [ ubuntu-latest, macos-latest ] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -17,5 +17,5 @@ jobs: with: distribution: 'adopt' java-version: '21' - - name: Clean, Build and Test + - name: Clean and Test run: ./gradlew clean test \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 5c1c807..c55356e 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -22,14 +22,14 @@ fun main() = application { onCloseRequest = ::exitApplication, title = "Graph Visualizer", ) { - window.minimumSize = Dimension(100,100) + window.minimumSize = Dimension(100, 100) App() } } @Composable -fun App(){ - MaterialTheme(){ +fun App() { + MaterialTheme() { Navigation() } } From 5d2eefa6c6bd82250ede7696d73620c75f8b4779 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Fri, 24 May 2024 15:59:05 -0400 Subject: [PATCH 132/172] add: China Whisper algorithm implementation & visualisation --- build.gradle.kts | 3 +- .../view/screens/DirectedGraphScreen.kt | 13 ++++++++ .../viewmodel/DirectedGraphViewModel.kt | 30 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index bc2afb7..bcd4ab7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ version = "1.0-SNAPSHOT" repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://jitpack.io") google() } @@ -27,13 +28,13 @@ dependencies { implementation("org.xerial", "sqlite-jdbc", "3.41.2.1") implementation("org.jetbrains.androidx.navigation:navigation-compose:$nav_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("com.github.uhh-lt:chinese-whispers:-SNAPSHOT") testImplementation(kotlin("test")) } compose.desktop { application { mainClass = "MainKt" - nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "GraphVisualizer" diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 1699ecf..fbd3c53 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -108,6 +108,19 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) + Button( + onClick = {graphVM.chinaWhisperCluster()}, + modifier = Modifier + .clip(shape = RoundedCornerShape(45.dp)) + .border(5.dp, color = Color.Black, shape = RoundedCornerShape(45.dp)) + .size(240.dp, 80.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) + ) { + Text(localisation("find_strong_connections"), style = defaultStyle) + } + + Spacer(modifier = Modifier.height(10.dp)) + Button( onClick = {graphVM.saveSQLite()}, modifier = Modifier diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index cfec0f6..37ce37c 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -3,6 +3,9 @@ package viewmodel import Dijkstra import StrongConnections import androidx.compose.ui.graphics.Color +import de.tudarmstadt.lt.cw.graph.ArrayBackedGraph +import de.tudarmstadt.lt.cw.graph.ArrayBackedGraphCW +import de.tudarmstadt.lt.cw.graph.Graph import model.graph.DirectedGraph import model.graph.edges.Edge import java.sql.DriverManager @@ -38,6 +41,33 @@ class DirectedGraphViewModel( } } + fun chinaWhisperCluster(){ + val comparatorItoV = emptyMap().toMutableMap() + val comparatorVtoI = emptyMap().toMutableMap() + for (i in graph.vertices){ + comparatorItoV[comparatorItoV.size] = i + comparatorVtoI[i] = comparatorVtoI.size + } + val cwGraph :Graph = ArrayBackedGraph(comparatorVtoI.size, comparatorVtoI.size) + for (i in comparatorItoV){ + cwGraph.addNode(i.key) + } + for (i in graph.edges){ + cwGraph.addEdge(comparatorVtoI[i.from], comparatorVtoI[i.to],i.weight.toFloat()) + } + + val cw = ArrayBackedGraphCW(comparatorItoV.size) + + val findClusters = cw.findClusters(cwGraph) + for (k in findClusters.values) { + val col = Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + for (j in k) { + vertexView[comparatorItoV[j]]?.color = col + updateView() + } + } + } + fun showStrongConnections(){ val k = StrongConnections() for (i in k.findStrongConnections(graph.matrix)) { From a56d80d566dc35f68cca8688dd814ee2205c2c61 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sat, 25 May 2024 09:04:12 +0300 Subject: [PATCH 133/172] feat: implement new algorithm for find cycle --- src/main/kotlin/model/algos/FindCycle.kt | 100 +++++++++++++++-------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/src/main/kotlin/model/algos/FindCycle.kt b/src/main/kotlin/model/algos/FindCycle.kt index 252e93d..baf9cf9 100644 --- a/src/main/kotlin/model/algos/FindCycle.kt +++ b/src/main/kotlin/model/algos/FindCycle.kt @@ -1,46 +1,82 @@ package model.algos -import model.graph.UndirectedGraph import model.graph.edges.Edge object FindCycle { - private fun findCycleUtil( - graph: UndirectedGraph, - startVertex: V, - visited: MutableMap, - parent: V?, - path: MutableList> - ): List>? { - visited[startVertex] = true - - graph.edgesOf(startVertex).forEach { edge -> - val next = if (edge.from == startVertex) edge.to else edge.from - if (visited[next] != true) { - path.add(edge) - val cyclePath = findCycleUtil(graph, next, visited, startVertex, path) - if (cyclePath != null) { - return cyclePath - } - path.removeAt(path.size - 1) - } else if (parent != next) { - path.add(edge) - return path.dropWhile { it.from != next && it.to != next }.toList() + fun findCycles(graph: MutableMap>>, startVertex: V): List> { + val blockedSet = mutableSetOf() + val blockedMap = mutableMapOf>() + val stack = mutableListOf() + val preResult = mutableListOf>() + val sccs = StrongConnections() + val sccResult = sccs.findStrongConnections(graph) + + for (subGraph in sccResult) { + if (subGraph.size > 1) { + val startNode = subGraph.first() + findCyclesInSCC(startNode, startNode, graph, blockedSet, blockedMap, stack, preResult) + blockedSet.clear() + blockedMap.clear() } } - return null + + val result = mutableListOf>() + for (res in preResult){ + if(res.contains(startVertex)){ + result.add(res) + } + } + return result } - fun findCycle(graph: UndirectedGraph, start: V): List>? { - val visited = mutableMapOf() - val path = mutableListOf>() + private fun findCyclesInSCC( + start: V, + current: V, + graph: MutableMap>>, + blockedSet: MutableSet, + blockedMap: MutableMap>, + stack: MutableList, + result: MutableList> + ): Boolean { + var foundCycle = false + stack.add(current) + blockedSet.add(current) - if (visited[start] != true) { - val cyclePath = findCycleUtil(graph, start, visited, null, path) - if (cyclePath != null) { - return cyclePath + for (edge in graph[current] ?: mutableListOf()) { + val neighbor = edge.to + if (neighbor == start) { + val cycle = stack.toList() + result.add(cycle) + foundCycle = true + } else if (neighbor !in blockedSet) { + if (findCyclesInSCC(start, neighbor, graph, blockedSet, blockedMap, stack, result)) { + foundCycle = true + } } } - return null + if (foundCycle) { + unblock(current, blockedSet, blockedMap) + } else { + for (edge in graph[current] ?: mutableListOf()) { + val neighbor = edge.to + blockedMap.computeIfAbsent(neighbor) { mutableSetOf() }.add(current) + } + } + + stack.removeAt(stack.size - 1) + return foundCycle + } + + private fun unblock(node: V, blockedSet: MutableSet, blockedMap: MutableMap>) { + val stack = mutableListOf(node) + while (stack.isNotEmpty()) { + val current = stack.removeAt(stack.size - 1) + if (current in blockedSet) { + blockedSet.remove(current) + blockedMap[current]?.forEach { stack.add(it) } + blockedMap.remove(current) + } + } } -} \ No newline at end of file +} From d4a0786575eff8bb2c3941573d5f7d4b85d1df87 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sat, 25 May 2024 09:05:38 +0300 Subject: [PATCH 134/172] ref: `public` on `private` functions | codestyle - DFS -> dfs --- src/main/kotlin/model/algos/StrongConnections.kt | 10 ++++++---- src/main/kotlin/viewmodel/DirectedGraphViewModel.kt | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/model/algos/StrongConnections.kt b/src/main/kotlin/model/algos/StrongConnections.kt index 2a38dc7..0e7aef6 100644 --- a/src/main/kotlin/model/algos/StrongConnections.kt +++ b/src/main/kotlin/model/algos/StrongConnections.kt @@ -1,3 +1,5 @@ +package model.algos + import model.graph.edges.Edge class StrongConnections{ @@ -32,19 +34,19 @@ class StrongConnections{ return result } - fun findPath(source: Int, top: Int, adjustment: MutableMap>): Boolean { + private fun findPath(source: Int, top: Int, adjustment: MutableMap>): Boolean { val visited: MutableList = List(comparatorItoV.size + 1){ 0 }.toMutableList() - return DFS(source, top, adjustment, visited) + return dfs(source, top, adjustment, visited) } - fun DFS(current: Int, top: Int, adjustment: MutableMap>, visited: MutableList): Boolean { + private fun dfs(current: Int, top: Int, adjustment: MutableMap>, visited: MutableList): Boolean { if (current == top) { return true } visited[current] = 1 for (x in adjustment[comparatorItoV[current]]!!) { if (visited[comparatorVtoI[x]!!] == 0) { - if (DFS(comparatorVtoI[x]!!, top, adjustment, visited)) { + if (dfs(comparatorVtoI[x]!!, top, adjustment, visited)) { return true } } diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 2f910a2..16f4276 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,7 +1,7 @@ package viewmodel -import StrongConnections import androidx.compose.ui.graphics.Color +import model.algos.StrongConnections import model.graph.DirectedGraph import model.graph.edges.Edge import java.sql.DriverManager From f828b956f0979e39c0fd34b74190cf0847725204 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sat, 25 May 2024 09:52:04 +0300 Subject: [PATCH 135/172] feat: visualization for find cycles algorithm --- src/main/kotlin/model/algos/FindCycle.kt | 2 +- .../kotlin/viewmodel/DirectedGraphViewModel.kt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/model/algos/FindCycle.kt b/src/main/kotlin/model/algos/FindCycle.kt index baf9cf9..be3b61c 100644 --- a/src/main/kotlin/model/algos/FindCycle.kt +++ b/src/main/kotlin/model/algos/FindCycle.kt @@ -3,7 +3,7 @@ package model.algos import model.graph.edges.Edge object FindCycle { - fun findCycles(graph: MutableMap>>, startVertex: V): List> { + fun findCycles(graph: MutableMap>>, startVertex: Int): List> { val blockedSet = mutableSetOf() val blockedMap = mutableMapOf>() val stack = mutableListOf() diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 16f4276..877a84d 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,6 +1,7 @@ package viewmodel import androidx.compose.ui.graphics.Color +import model.algos.FindCycle import model.algos.StrongConnections import model.graph.DirectedGraph import model.graph.edges.Edge @@ -58,6 +59,21 @@ class DirectedGraphViewModel( } } + fun showFindCycles() { + val k = FindCycle + for (i in k.findCycles(graph.matrix, startVertex = 2)) { + val col = + Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + for (j in i) { + if (j in graphModel.vertices) { + graphVM[j]?.color = col + println(graphVM[j]?.color) + updateView() + } + } + } + } + fun saveSQLite() { var parameterCreate = "( Vertexes String," var parameterInput = "( Vertexes," From a920d83f0c40750bf1d816644e0582095bef27fd Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sat, 25 May 2024 10:00:43 +0300 Subject: [PATCH 136/172] feat: add visual for find cycle in undirected graph --- .../viewmodel/UndirectedGraphViewModel.kt | 21 +++++++++++++------ src/test/kotlin/algos/FindCycleTest.kt | 6 ++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 8c08921..db03ef7 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -8,13 +8,14 @@ import model.graph.UndirectedGraph import model.graph.edges.Edge import java.sql.DriverManager import java.sql.SQLException +import kotlin.random.Random class UndirectedGraphViewModel( name: String, val graph: UndirectedGraph = UndirectedGraph() ) : AbstractGraphViewModel(name, graph) { private val DB_DRIVER = "jdbc:sqlite" - var inType = viewmodel.initType.Internal + var inType = initType.Internal var initedGraph = false override fun addEdge(from: V, to: V, weight: Int) { @@ -57,11 +58,19 @@ class UndirectedGraphViewModel( drawEdges(result, Color.Magenta) } - fun findCycles() { - if (this.size == 0) return - val start = graphModel.vertices.first() - val result = FindCycle.findCycle(graphModel as UndirectedGraph, start) ?: emptyList() - drawEdges(result, Color.Magenta) + fun showFindCycles(startVertex: Int) { + val k = FindCycle + for (i in k.findCycles(graph.matrix, startVertex)) { + val col = + Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + for (j in i) { + if (j in graphModel.vertices) { + graphVM[j]?.color = col + println(graphVM[j]?.color) + updateView() + } + } + } } fun findBridges() { diff --git a/src/test/kotlin/algos/FindCycleTest.kt b/src/test/kotlin/algos/FindCycleTest.kt index 3ea7b3f..1b0455d 100644 --- a/src/test/kotlin/algos/FindCycleTest.kt +++ b/src/test/kotlin/algos/FindCycleTest.kt @@ -23,14 +23,13 @@ internal class FindCycleTest { this.addEdge(3, 4) } - val pathActual = FindCycle.findCycle(graph, 2) + val pathActual = FindCycle.findCycles(graph.matrix, 2) assertNotNull(pathActual) val pathExpected = listOf( Edge(2, 1), Edge(1, 3), Edge(3, 2) ) - assertContentEquals(pathExpected, pathActual) } @Test @@ -46,7 +45,7 @@ internal class FindCycleTest { } } - val pathActual = FindCycle.findCycle(graph, 1) + val pathActual = FindCycle.findCycles(graph.matrix, 1) assertNotNull(pathActual) val pathExpected = mutableListOf>() for (i in 1..5) { @@ -54,6 +53,5 @@ internal class FindCycleTest { if (i != j) pathExpected.add(Edge(i, j)) } } - assertContentEquals(pathExpected, pathActual) } } \ No newline at end of file From b8f13a754963e0fbae9ace681cba2a12de634ea8 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sat, 25 May 2024 17:53:24 +0300 Subject: [PATCH 137/172] ref: change parametr `startVertex` from `Int` to `V` --- src/main/kotlin/model/algos/FindCycle.kt | 2 +- src/main/kotlin/viewmodel/DirectedGraphViewModel.kt | 4 ++-- src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/model/algos/FindCycle.kt b/src/main/kotlin/model/algos/FindCycle.kt index be3b61c..baf9cf9 100644 --- a/src/main/kotlin/model/algos/FindCycle.kt +++ b/src/main/kotlin/model/algos/FindCycle.kt @@ -3,7 +3,7 @@ package model.algos import model.graph.edges.Edge object FindCycle { - fun findCycles(graph: MutableMap>>, startVertex: Int): List> { + fun findCycles(graph: MutableMap>>, startVertex: V): List> { val blockedSet = mutableSetOf() val blockedMap = mutableMapOf>() val stack = mutableListOf() diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 877a84d..22fd6a0 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -59,9 +59,9 @@ class DirectedGraphViewModel( } } - fun showFindCycles() { + fun showFindCycles(startVertex: V) { val k = FindCycle - for (i in k.findCycles(graph.matrix, startVertex = 2)) { + for (i in k.findCycles(graph.matrix, startVertex)) { val col = Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) for (j in i) { diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index db03ef7..717b7f2 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -58,7 +58,7 @@ class UndirectedGraphViewModel( drawEdges(result, Color.Magenta) } - fun showFindCycles(startVertex: Int) { + fun showFindCycles(startVertex: V) { val k = FindCycle for (i in k.findCycles(graph.matrix, startVertex)) { val col = From 831e181fce7e1dfa31d179c91ef8c9a5ac4d6102 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Sat, 25 May 2024 17:58:21 +0300 Subject: [PATCH 138/172] ref: change the function call to the current one --- src/main/kotlin/view/screens/UndirectedGraphScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 6b32b89..a4ebc82 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -67,7 +67,7 @@ fun UndirectedGraphScreen( DefaultShortButton(onClick = { graphVM.findMst() }, "find_mst") Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.findCycles() }, "find_cycles") + DefaultShortButton(onClick = { graphVM.showFindCycles("1") }, "find_cycles") Spacer(modifier = Modifier.height(10.dp)) DefaultShortButton(onClick = { graphVM.findBridges() }, "find_bridges") From a1ba88660f83383cd0f66b7ac6e39d94ad3ceba0 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 25 May 2024 19:35:47 +0300 Subject: [PATCH 139/172] Ref: change variables and methods naming, remove direct access to matrix of graph --- src/main/kotlin/model/algos/Dijkstra.kt | 36 +++++++------- src/main/kotlin/model/algos/FindBridges.kt | 2 +- src/main/kotlin/model/algos/FindCycle.kt | 41 ++++++++++++---- src/main/kotlin/model/algos/FordBellman.kt | 2 +- src/main/kotlin/model/algos/Prim.kt | 2 +- .../kotlin/model/algos/StrongConnections.kt | 46 +++++++++++------ src/main/kotlin/model/graph/DirectedGraph.kt | 2 - .../kotlin/model/graph/{edges => }/Edge.kt | 2 +- src/main/kotlin/model/graph/Graph.kt | 6 +-- .../kotlin/model/graph/UndirectedGraph.kt | 2 - .../view/common/DirectedAlgorithmDialog.kt | 4 +- .../view/screens/DirectedGraphScreen.kt | 9 ++-- .../view/screens/UndirectedGraphScreen.kt | 6 +-- .../viewmodel/AbstractGraphViewModel.kt | 35 +++++++++---- .../viewmodel/DirectedGraphViewModel.kt | 49 ++++++------------- src/main/kotlin/viewmodel/EdgeViewModel.kt | 2 +- .../viewmodel/UndirectedGraphViewModel.kt | 23 ++------- src/test/kotlin/algos/FindCycleTest.kt | 46 ++++------------- src/test/kotlin/algos/FordBellmanTest.kt | 2 +- src/test/kotlin/algos/PrimTest.kt | 2 +- src/test/kotlin/algos/SearchBridgesTest.kt | 2 +- 21 files changed, 153 insertions(+), 168 deletions(-) rename src/main/kotlin/model/graph/{edges => }/Edge.kt (87%) diff --git a/src/main/kotlin/model/algos/Dijkstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt index 9723120..ec00371 100644 --- a/src/main/kotlin/model/algos/Dijkstra.kt +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -1,23 +1,25 @@ -import model.graph.edges.Edge +import model.graph.Edge +import model.graph.Graph import java.util.* -class Dijkstra(var graph: MutableMap>>, private val totalNodes: Int) { +class Dijkstra(val graph: Graph, private val totalNodes: Int) { private val vertexValues: MutableMap = emptyMap().toMutableMap() private val visitedSet: MutableSet = HashSet() private val prioraQueue = PriorityQueue(totalNodes) - private val pathMap: MutableMap>> = emptyMap>>().toMutableMap() + private val pathMap: MutableMap>> = + emptyMap>>().toMutableMap() - fun dijkstra(start: V, end: V) : MutableList>{ - for (j in graph.keys) { - vertexValues.put(j, Int.MAX_VALUE) - pathMap.put(j, emptyList>().toMutableList()) + fun dijkstra(start: V, end: V): MutableList> { + for (vertex in graph.vertices) { + vertexValues.put(vertex, Int.MAX_VALUE) + pathMap.put(vertex, emptyList>().toMutableList()) } prioraQueue.add(start) vertexValues[start] = 0 while (visitedSet.size != totalNodes) { println(vertexValues) - for (i in pathMap){ + for (i in pathMap) { println(i) } if (prioraQueue.isEmpty()) { @@ -38,16 +40,16 @@ class Dijkstra(var graph: MutableMap>>, private val t private fun refreshSearch(currentVertex: V) { var newRange = -1 - for (j in graph[currentVertex]!!) { - if (!visitedSet.contains(j.to)) { - newRange = vertexValues[currentVertex]!! + j.weight - if (newRange < vertexValues[j.to]!!) { - vertexValues[j.to] = newRange - val k = pathMap[j.from]?.toMutableList() - k?.add(j) - pathMap[j.to] = k!! + for (edge in graph.edgesOf(currentVertex)) { + if (!visitedSet.contains(edge.to)) { + newRange = vertexValues[currentVertex]!! + edge.weight + if (newRange < vertexValues[edge.to]!!) { + vertexValues[edge.to] = newRange + val k = pathMap[edge.from]?.toMutableList() + k?.add(edge) + pathMap[edge.to] = k!! } - prioraQueue.add(j.to) + prioraQueue.add(edge.to) } } } diff --git a/src/main/kotlin/model/algos/FindBridges.kt b/src/main/kotlin/model/algos/FindBridges.kt index 1a88ac2..acc86b9 100644 --- a/src/main/kotlin/model/algos/FindBridges.kt +++ b/src/main/kotlin/model/algos/FindBridges.kt @@ -1,7 +1,7 @@ package model.algos import model.graph.UndirectedGraph -import model.graph.edges.Edge +import model.graph.Edge fun findBridges(graph: UndirectedGraph): Set> { val timeIn = mutableMapOf() diff --git a/src/main/kotlin/model/algos/FindCycle.kt b/src/main/kotlin/model/algos/FindCycle.kt index baf9cf9..6803b00 100644 --- a/src/main/kotlin/model/algos/FindCycle.kt +++ b/src/main/kotlin/model/algos/FindCycle.kt @@ -1,9 +1,9 @@ package model.algos -import model.graph.edges.Edge +import model.graph.Graph object FindCycle { - fun findCycles(graph: MutableMap>>, startVertex: V): List> { + fun findCycles(graph: Graph, startVertex: V): List> { val blockedSet = mutableSetOf() val blockedMap = mutableMapOf>() val stack = mutableListOf() @@ -14,15 +14,23 @@ object FindCycle { for (subGraph in sccResult) { if (subGraph.size > 1) { val startNode = subGraph.first() - findCyclesInSCC(startNode, startNode, graph, blockedSet, blockedMap, stack, preResult) + findCyclesInSCC( + startNode, + startNode, + graph, + blockedSet, + blockedMap, + stack, + preResult + ) blockedSet.clear() blockedMap.clear() } } val result = mutableListOf>() - for (res in preResult){ - if(res.contains(startVertex)){ + for (res in preResult) { + if (res.contains(startVertex)) { result.add(res) } } @@ -32,7 +40,7 @@ object FindCycle { private fun findCyclesInSCC( start: V, current: V, - graph: MutableMap>>, + graph: Graph, blockedSet: MutableSet, blockedMap: MutableMap>, stack: MutableList, @@ -42,14 +50,23 @@ object FindCycle { stack.add(current) blockedSet.add(current) - for (edge in graph[current] ?: mutableListOf()) { + for (edge in graph.edgesOf(current)) { val neighbor = edge.to if (neighbor == start) { val cycle = stack.toList() result.add(cycle) foundCycle = true } else if (neighbor !in blockedSet) { - if (findCyclesInSCC(start, neighbor, graph, blockedSet, blockedMap, stack, result)) { + if (findCyclesInSCC( + start, + neighbor, + graph, + blockedSet, + blockedMap, + stack, + result + ) + ) { foundCycle = true } } @@ -58,7 +75,7 @@ object FindCycle { if (foundCycle) { unblock(current, blockedSet, blockedMap) } else { - for (edge in graph[current] ?: mutableListOf()) { + for (edge in graph.edgesOf(current)) { val neighbor = edge.to blockedMap.computeIfAbsent(neighbor) { mutableSetOf() }.add(current) } @@ -68,7 +85,11 @@ object FindCycle { return foundCycle } - private fun unblock(node: V, blockedSet: MutableSet, blockedMap: MutableMap>) { + private fun unblock( + node: V, + blockedSet: MutableSet, + blockedMap: MutableMap> + ) { val stack = mutableListOf(node) while (stack.isNotEmpty()) { val current = stack.removeAt(stack.size - 1) diff --git a/src/main/kotlin/model/algos/FordBellman.kt b/src/main/kotlin/model/algos/FordBellman.kt index 5c4ab5e..9e9e241 100644 --- a/src/main/kotlin/model/algos/FordBellman.kt +++ b/src/main/kotlin/model/algos/FordBellman.kt @@ -1,7 +1,7 @@ package model.algos import model.graph.Graph -import model.graph.edges.Edge +import model.graph.Edge typealias Path = List> typealias Paths = Map> diff --git a/src/main/kotlin/model/algos/Prim.kt b/src/main/kotlin/model/algos/Prim.kt index f1d4f01..944845d 100644 --- a/src/main/kotlin/model/algos/Prim.kt +++ b/src/main/kotlin/model/algos/Prim.kt @@ -1,6 +1,6 @@ package model.algos -import model.graph.edges.Edge +import model.graph.Edge import model.graph.UndirectedGraph import java.util.PriorityQueue diff --git a/src/main/kotlin/model/algos/StrongConnections.kt b/src/main/kotlin/model/algos/StrongConnections.kt index 0e7aef6..e20e281 100644 --- a/src/main/kotlin/model/algos/StrongConnections.kt +++ b/src/main/kotlin/model/algos/StrongConnections.kt @@ -1,30 +1,37 @@ package model.algos -import model.graph.edges.Edge +import model.graph.DirectedGraph +import model.graph.Edge +import model.graph.Graph -class StrongConnections{ +class StrongConnections { private val comparatorItoV = emptyMap().toMutableMap() private val comparatorVtoI = emptyMap().toMutableMap() - fun findStrongConnections(graph: MutableMap>>): List> { - for (i in graph.keys){ - comparatorItoV[comparatorItoV.size] = i - comparatorVtoI[i] = comparatorVtoI.size + fun findStrongConnections(graph: Graph): List> { + for (vertex in graph.vertices) { + comparatorItoV[comparatorItoV.size] = vertex + comparatorVtoI[vertex] = comparatorVtoI.size } val adjustment = emptyMap>().toMutableMap() val dim = comparatorItoV.size val result: MutableList> = ArrayList() - val listStrongCon: MutableList = List(dim + 1){ false }.toMutableList() + val listStrongCon: MutableList = List(dim + 1) { false }.toMutableList() for (i in comparatorVtoI.keys) adjustment[i] = emptyList().toMutableList() - for (edge in graph) - for (j in edge.value) - adjustment[j.from]?.add(j.to) - for (indexV in 0..< dim) + for (vertex in graph.vertices) + for (edge in graph.edgesOf(vertex)) + adjustment[edge.from]?.add(edge.to) + for (indexV in 0.. = ArrayList() connections.add(comparatorItoV[indexV]!!) for (indexN in indexV + 1..{ return result } - private fun findPath(source: Int, top: Int, adjustment: MutableMap>): Boolean { - val visited: MutableList = List(comparatorItoV.size + 1){ 0 }.toMutableList() + private fun findPath( + source: Int, + top: Int, + adjustment: MutableMap> + ): Boolean { + val visited: MutableList = List(comparatorItoV.size + 1) { 0 }.toMutableList() return dfs(source, top, adjustment, visited) } - private fun dfs(current: Int, top: Int, adjustment: MutableMap>, visited: MutableList): Boolean { + private fun dfs( + current: Int, + top: Int, + adjustment: MutableMap>, + visited: MutableList + ): Boolean { if (current == top) { return true } diff --git a/src/main/kotlin/model/graph/DirectedGraph.kt b/src/main/kotlin/model/graph/DirectedGraph.kt index 9f0f559..9c1f751 100644 --- a/src/main/kotlin/model/graph/DirectedGraph.kt +++ b/src/main/kotlin/model/graph/DirectedGraph.kt @@ -1,7 +1,5 @@ package model.graph -import model.graph.edges.Edge - class DirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { if (weight != 1) isWeighted = true diff --git a/src/main/kotlin/model/graph/edges/Edge.kt b/src/main/kotlin/model/graph/Edge.kt similarity index 87% rename from src/main/kotlin/model/graph/edges/Edge.kt rename to src/main/kotlin/model/graph/Edge.kt index 320daec..9145767 100644 --- a/src/main/kotlin/model/graph/edges/Edge.kt +++ b/src/main/kotlin/model/graph/Edge.kt @@ -1,4 +1,4 @@ -package model.graph.edges +package model.graph data class Edge(val from: V, val to: V, val weight: Int = 1) : Comparable> { override fun compareTo(other: Edge): Int { diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index ac45000..1e3b4f5 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -1,18 +1,14 @@ package model.graph -import model.graph.edges.Edge - abstract class Graph() { protected val graph = mutableMapOf>>() - val matrix - get() = graph val entries get() = graph.entries var isWeighted = false protected set var negativeWeights = false protected set - + val vertices get() = graph.keys diff --git a/src/main/kotlin/model/graph/UndirectedGraph.kt b/src/main/kotlin/model/graph/UndirectedGraph.kt index 7b785de..f4d984c 100644 --- a/src/main/kotlin/model/graph/UndirectedGraph.kt +++ b/src/main/kotlin/model/graph/UndirectedGraph.kt @@ -1,7 +1,5 @@ package model.graph -import model.graph.edges.Edge - class UndirectedGraph : Graph() { override fun addEdge(from: V, to: V, weight: Int) { if (weight != 1) isWeighted = true diff --git a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt index 9cc19c7..3f93090 100644 --- a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt +++ b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt @@ -89,8 +89,8 @@ fun DirectedAlgorithmDialog( Spacer(modifier = Modifier.height(36.dp)) Row { Spacer(modifier = Modifier.width(30.dp)) - val dijkstra = { graphVM.dijkstraAlgo(source, destination) } - val fordBellman = { graphVM.fordBellman(source, destination) } + val dijkstra = { graphVM.drawDijkstra(source, destination) } + val fordBellman = { graphVM.drawFordBellman(source, destination) } val onClick = if (action == "Dijkstra") { dijkstra } else if (action == "FordBellman") { diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 481a501..5ec3c2f 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -50,15 +50,13 @@ fun DirectedGraphScreen( DefaultShortButton({ graphVM.resetColors() }, "reset", Color.LightGray) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton({ graphVM.showStrongConnections() }, "find_strong_connections") + DefaultShortButton({ graphVM.drawStrongConnections() }, "find_strong_connections") Spacer(modifier = Modifier.height(10.dp)) DefaultShortButton({ graphVM.chinaWhisperCluster() }, "find_clusters") Spacer(modifier = Modifier.height(10.dp)) - - - // Dijkstra Button + // Dijkstra Button var isDijkstraMenu by remember { mutableStateOf(false) } val onCloseDijkstra = { isDijkstraMenu = !isDijkstraMenu } DefaultShortButton(onCloseDijkstra, "dijkstra") @@ -70,6 +68,9 @@ fun DirectedGraphScreen( DefaultShortButton(onCloseFB, "ford_bellman") Spacer(modifier = Modifier.height(10.dp)) + DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles") + Spacer(modifier = Modifier.height(10.dp)) + AddEdgeDialog(isOpenedEdgeMenu, onCloseEdge, graphVM, isDirected = true) // Dijkstra dialog window diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index a4ebc82..34cb3ce 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -64,13 +64,13 @@ fun UndirectedGraphScreen( DefaultShortButton(onCloseFB, "ford_bellman") Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.findMst() }, "find_mst") + DefaultShortButton(onClick = { graphVM.drawMst() }, "find_mst") Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.showFindCycles("1") }, "find_cycles") + DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles") Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.findBridges() }, "find_bridges") + DefaultShortButton(onClick = { graphVM.drawBridges() }, "find_bridges") Spacer(modifier = Modifier.height(10.dp)) val onClose = { isOpenedEdgeMenu = false } diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index bc9c634..27b5369 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -6,10 +6,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel +import model.algos.FindCycle import model.algos.FordBellman import model.graph.Graph -import model.graph.edges.Edge +import model.graph.Edge import view.common.DefaultColors +import kotlin.random.Random abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { val name = _name @@ -60,17 +62,36 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM abstract fun drawEdges(edges: Collection>, color: Color) - fun dijkstraAlgo(start: V, end: V) { + fun updateView() { + val keep = graphVM + graphVM = mutableMapOf>() + graphVM = keep + } + + fun drawDijkstra(start: V, end: V) { if (this.negativeWeights) return - val result = Dijkstra(graphModel.matrix, graphModel.size).dijkstra(start, end) + val result = Dijkstra(graphModel, graphModel.size).dijkstra(start, end) drawEdges(result, Color.Red) } - fun fordBellman(from: V, to: V) { + fun drawFordBellman(from: V, to: V) { val path = FordBellman.findShortestPath(from, to, this.graphModel).second ?: emptyList() drawEdges(path, Color.Cyan) } + fun drawCycles(startVertex: V) { + val findCycle = FindCycle + for (cycle in findCycle.findCycles(graphModel, startVertex)) { + val col = + Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + for (edge in cycle) { + if (edge in graphModel.vertices) { + graphVM[edge]?.color = col + } + } + } + } + fun vertexVmOf(vertex: V): VertexViewModel? { return graphVM[vertex] } @@ -94,10 +115,4 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM vertexVM.color = DefaultColors.primary } } - - fun updateView() { - val keep = graphVM - graphVM = mutableMapOf>() - graphVM = keep - } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index 1dbefd5..d243074 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,13 +1,12 @@ package viewmodel import androidx.compose.ui.graphics.Color -import model.algos.FindCycle import model.algos.StrongConnections import de.tudarmstadt.lt.cw.graph.ArrayBackedGraph import de.tudarmstadt.lt.cw.graph.ArrayBackedGraphCW import de.tudarmstadt.lt.cw.graph.Graph import model.graph.DirectedGraph -import model.graph.edges.Edge +import model.graph.Edge import java.sql.DriverManager import java.sql.SQLException import kotlin.random.Random @@ -47,65 +46,47 @@ class DirectedGraphViewModel( } } - fun chinaWhisperCluster(){ + fun chinaWhisperCluster() { val comparatorItoV = emptyMap().toMutableMap() val comparatorVtoI = emptyMap().toMutableMap() - for (i in graph.vertices){ + for (i in graph.vertices) { comparatorItoV[comparatorItoV.size] = i comparatorVtoI[i] = comparatorVtoI.size } - val cwGraph :Graph = ArrayBackedGraph(comparatorVtoI.size, comparatorVtoI.size) - for (i in comparatorItoV){ + val cwGraph: Graph = ArrayBackedGraph(comparatorVtoI.size, comparatorVtoI.size) + for (i in comparatorItoV) { cwGraph.addNode(i.key) } - for (i in graph.edges){ - cwGraph.addEdge(comparatorVtoI[i.from], comparatorVtoI[i.to],i.weight.toFloat()) + for (i in graph.edges) { + cwGraph.addEdge(comparatorVtoI[i.from], comparatorVtoI[i.to], i.weight.toFloat()) } val cw = ArrayBackedGraphCW(comparatorItoV.size) val findClusters = cw.findClusters(cwGraph) for (k in findClusters.values) { - val col = Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) + val col = + Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) for (j in k) { graphVM[comparatorItoV[j]]?.color = col - println(graphVM[comparatorItoV[j]]?.color) - updateView() } } } - fun showStrongConnections(){ - val k = StrongConnections() - for (i in k.findStrongConnections(graph.matrix)) { + fun drawStrongConnections() { + val strongConnections = StrongConnections() + for (component in strongConnections.findStrongConnections(graphModel)) { val col = Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) - for (j in i) { - if (j in graphModel.vertices) { - graphVM[j]?.color = col - println(graphVM[j]?.color) - updateView() + for (vertex in component) { + if (vertex in graphModel.vertices) { + graphVM[vertex]?.color = col } } } } - fun showFindCycles(startVertex: V) { - val k = FindCycle - for (i in k.findCycles(graph.matrix, startVertex)) { - val col = - Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) - for (j in i) { - if (j in graphModel.vertices) { - graphVM[j]?.color = col - println(graphVM[j]?.color) - updateView() - } - } - } - } - fun saveSQLite() { var parameterCreate = "( Vertexes String," var parameterInput = "( Vertexes," diff --git a/src/main/kotlin/viewmodel/EdgeViewModel.kt b/src/main/kotlin/viewmodel/EdgeViewModel.kt index b89b4ee..9e819e8 100644 --- a/src/main/kotlin/viewmodel/EdgeViewModel.kt +++ b/src/main/kotlin/viewmodel/EdgeViewModel.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel -import model.graph.edges.Edge +import model.graph.Edge class EdgeViewModel( edge: Edge, diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 717b7f2..6a8fd6f 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -1,14 +1,12 @@ package viewmodel import androidx.compose.ui.graphics.Color -import model.algos.FindCycle import model.algos.Prim import model.algos.findBridges import model.graph.UndirectedGraph -import model.graph.edges.Edge +import model.graph.Edge import java.sql.DriverManager import java.sql.SQLException -import kotlin.random.Random class UndirectedGraphViewModel( name: String, @@ -51,29 +49,14 @@ class UndirectedGraphViewModel( } } - fun findMst() { + fun drawMst() { if (size == 0) return val startVertex = graphModel.vertices.first() val result = Prim.findMst(graphModel as UndirectedGraph, startVertex) drawEdges(result, Color.Magenta) } - fun showFindCycles(startVertex: V) { - val k = FindCycle - for (i in k.findCycles(graph.matrix, startVertex)) { - val col = - Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) - for (j in i) { - if (j in graphModel.vertices) { - graphVM[j]?.color = col - println(graphVM[j]?.color) - updateView() - } - } - } - } - - fun findBridges() { + fun drawBridges() { val result = findBridges(graphModel as UndirectedGraph) drawEdges(result, Color.Yellow) } diff --git a/src/test/kotlin/algos/FindCycleTest.kt b/src/test/kotlin/algos/FindCycleTest.kt index 1b0455d..9c6749c 100644 --- a/src/test/kotlin/algos/FindCycleTest.kt +++ b/src/test/kotlin/algos/FindCycleTest.kt @@ -1,8 +1,9 @@ package algos import model.algos.FindCycle +import model.graph.DirectedGraph import model.graph.UndirectedGraph -import model.graph.edges.Edge +import model.graph.Edge import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertNotNull @@ -10,48 +11,21 @@ import kotlin.test.assertNotNull internal class FindCycleTest { @Test - fun basic() { - val graph = UndirectedGraph() - for (i in 1..4) { + fun `3 vertices directed cycle`() { + val graph = DirectedGraph() + for (i in 1..3) { graph.addVertex(i) } graph.run { - this.addEdge(1, 3) this.addEdge(1, 2) - this.addEdge(3, 2) - this.addEdge(3, 4) + this.addEdge(2, 3) + this.addEdge(3, 1) } - val pathActual = FindCycle.findCycles(graph.matrix, 2) + val pathActual = FindCycle.findCycles(graph, 2).elementAt(0) assertNotNull(pathActual) - val pathExpected = listOf( - Edge(2, 1), - Edge(1, 3), - Edge(3, 2) - ) - } - - @Test - fun pentagramm() { - val graph = UndirectedGraph() - for (i in 1..5) { - graph.addVertex(i) - } - - for (i in 1..5) { - for (j in 1..5) { - graph.addEdge(i, j) - } - } - - val pathActual = FindCycle.findCycles(graph.matrix, 1) - assertNotNull(pathActual) - val pathExpected = mutableListOf>() - for (i in 1..5) { - for (j in 1..5) { - if (i != j) pathExpected.add(Edge(i, j)) - } - } + val pathExpected = listOf(1, 2, 3) + assertContentEquals(pathExpected, pathActual, "TODO") } } \ No newline at end of file diff --git a/src/test/kotlin/algos/FordBellmanTest.kt b/src/test/kotlin/algos/FordBellmanTest.kt index 5f086dd..9a2f77c 100644 --- a/src/test/kotlin/algos/FordBellmanTest.kt +++ b/src/test/kotlin/algos/FordBellmanTest.kt @@ -2,7 +2,7 @@ package algos import model.algos.FordBellman import model.graph.DirectedGraph -import model.graph.edges.Edge +import model.graph.Edge import kotlin.test.* internal class FordBellmanTest { diff --git a/src/test/kotlin/algos/PrimTest.kt b/src/test/kotlin/algos/PrimTest.kt index 0606217..b96a443 100644 --- a/src/test/kotlin/algos/PrimTest.kt +++ b/src/test/kotlin/algos/PrimTest.kt @@ -2,7 +2,7 @@ package algos import model.algos.Prim import model.graph.UndirectedGraph -import model.graph.edges.Edge +import model.graph.Edge import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertNotNull diff --git a/src/test/kotlin/algos/SearchBridgesTest.kt b/src/test/kotlin/algos/SearchBridgesTest.kt index 8e813bf..f87ebb0 100644 --- a/src/test/kotlin/algos/SearchBridgesTest.kt +++ b/src/test/kotlin/algos/SearchBridgesTest.kt @@ -2,7 +2,7 @@ package algos import model.algos.findBridges import model.graph.UndirectedGraph -import model.graph.edges.Edge +import model.graph.Edge import kotlin.test.Test import kotlin.test.assertTrue From 25e4cb9a86086e352a85fb2efe1ace2c8e544897 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sat, 25 May 2024 23:14:42 +0300 Subject: [PATCH 140/172] Feat: add zooming, vertex adding dialog menu -in vertex adding dialog menu you can choose is coordinates will be random and number of vertices --- src/main/kotlin/model/algos/ForceAtlas2.kt | 8 +- src/main/kotlin/view/common/AddEdgeDialog.kt | 28 +++--- .../kotlin/view/common/AddVertexDialog.kt | 92 ++++++++++++++++++ .../view/common/DirectedAlgorithmDialog.kt | 25 ++--- .../view/screens/DirectedGraphScreen.kt | 93 +++++++++++++++---- .../view/screens/UndirectedGraphScreen.kt | 84 +++++++++++++---- .../viewmodel/AbstractGraphViewModel.kt | 9 +- src/main/kotlin/viewmodel/VertexViewModel.kt | 17 +++- 8 files changed, 276 insertions(+), 80 deletions(-) create mode 100644 src/main/kotlin/view/common/AddVertexDialog.kt diff --git a/src/main/kotlin/model/algos/ForceAtlas2.kt b/src/main/kotlin/model/algos/ForceAtlas2.kt index fcae5bd..b1c4cf4 100644 --- a/src/main/kotlin/model/algos/ForceAtlas2.kt +++ b/src/main/kotlin/model/algos/ForceAtlas2.kt @@ -9,14 +9,14 @@ import kotlin.math.ln import kotlin.math.sign import kotlin.math.sqrt -const val repulsionK: Double = 250.0 +const val repulsionK: Double = 150.0 const val attractionK: Double = 150.0 const val gravityK: Double = 5.0 object ForceAtlas2 { fun forceDrawing(graphVM: AbstractGraphViewModel) { val vertices = graphVM.verticesVM - repeat(100) { + repeat(1000) { val forces = mutableMapOf, Pair>() for (vertex in vertices) { val edges = vertex.edges @@ -50,12 +50,12 @@ object ForceAtlas2 { for (vertex in forces.keys) { val forcesPair = forces[vertex]!! if (!forcesPair.first.isNaN()) { - val newX = (vertex.x + forcesPair.first).coerceIn(20f, width.toFloat() - 70f) + val newX = (vertex.x + forcesPair.first) vertex.x = newX } if (!forcesPair.second.isNaN()) { val newY = - (vertex.y + forcesPair.second).coerceIn(20f, height.toFloat() - 200f) + (vertex.y + forcesPair.second) vertex.y = newY } } diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index e16c3fa..0459730 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -32,11 +32,11 @@ fun AddEdgeDialog( visible = visible, title = "New Edge", onCloseRequest = onClose, - state = rememberDialogState(height = 600.dp, width = 880.dp) + state = rememberDialogState(height = 420.dp, width = 580.dp) ) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } - var checkedState by remember { mutableStateOf(true) } + var notWeighted by remember { mutableStateOf(true) } var weight by remember { mutableStateOf("1") } Column(modifier = Modifier.padding(30.dp, 24.dp)) { val textWidth = 90.dp @@ -49,8 +49,7 @@ fun AddEdgeDialog( ) TextField( modifier = Modifier - .weight(1f) - .width(115.dp) + .fillMaxWidth() .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), colors = TextFieldDefaults.textFieldColors( @@ -73,8 +72,7 @@ fun AddEdgeDialog( ) TextField( modifier = Modifier - .weight(1f) - .width(115.dp) + .fillMaxWidth() .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), colors = TextFieldDefaults.textFieldColors( @@ -88,9 +86,9 @@ fun AddEdgeDialog( ) Spacer(modifier = Modifier.width(rightPadding)) } - Spacer(modifier = Modifier.height(36.dp)) + Spacer(modifier = Modifier.height(20.dp)) Row { - if (!checkedState) { + if (!notWeighted) { Text( text = localisation("weight"), style = defaultStyle, @@ -98,7 +96,7 @@ fun AddEdgeDialog( .width(textWidth + 30.dp) ) TextField( - enabled = !checkedState, + enabled = !notWeighted, modifier = Modifier .weight(1f) .width(115.dp) @@ -126,10 +124,10 @@ fun AddEdgeDialog( } Checkbox( modifier = Modifier.align(Alignment.CenterVertically), - checked = checkedState, + checked = notWeighted, onCheckedChange = { - checkedState = it; - weight = if (checkedState) "1" else "" + notWeighted = it; + weight = if (notWeighted) "1" else "" } ) Text( @@ -139,7 +137,7 @@ fun AddEdgeDialog( ) Spacer(modifier = Modifier.width(rightPadding)) } - Spacer(modifier = Modifier.height(36.dp)) + Spacer(modifier = Modifier.height(20.dp)) Row { val onClick = { if (weight == "") weight = "1" @@ -148,9 +146,7 @@ fun AddEdgeDialog( } DefaultButton(onClick, "add_edge") - } - Spacer(modifier = Modifier.height(36.dp)) - Row { + Spacer(modifier = Modifier.width(30.dp)) DefaultButton(onClose, "back", Color.Red) } } diff --git a/src/main/kotlin/view/common/AddVertexDialog.kt b/src/main/kotlin/view/common/AddVertexDialog.kt new file mode 100644 index 0000000..ad5e349 --- /dev/null +++ b/src/main/kotlin/view/common/AddVertexDialog.kt @@ -0,0 +1,92 @@ +package view.common + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.rememberDialogState +import localisation.localisation +import viewmodel.AbstractGraphViewModel + +@Composable +fun AddVertexDialog( + _visible: Boolean, + onClose: () -> Unit, + graphVM: AbstractGraphViewModel, +) { + var visible by mutableStateOf(_visible) + var centerCoordinates by remember { mutableStateOf(true) } + DialogWindow( + visible = visible, + title = "New Vertices", + onCloseRequest = onClose, + state = rememberDialogState(height = 320.dp, width = 570.dp) + ) { + var verticesNumber by remember { mutableStateOf("1") } + val textWidth = 130.dp + Column(modifier = Modifier.padding(30.dp, 24.dp)) { + Row(modifier = Modifier.fillMaxWidth()) { + Text( + text = localisation("number"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth), + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + textStyle = defaultStyle, + value = verticesNumber, + onValueChange = { newValue -> + if (newValue.length < 6) { + verticesNumber = newValue.filter { it.isDigit() } + } + }, + ) + } + Spacer(modifier = Modifier.height(20.dp)) + Row { + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = centerCoordinates, + onCheckedChange = { centerCoordinates = it } + ) + Text( + text = localisation("center_coordinates"), + style = defaultStyle, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + Spacer(modifier = Modifier.height(20.dp)) + Row { + val onClick = { + if (verticesNumber == "") verticesNumber = "1" + repeat(verticesNumber.toInt()) { + graphVM.addVertex(graphVM.size.toString(), centerCoordinates) + } + graphVM.updateView() + visible = false + + } + DefaultButton(onClick, "add_edge") + Spacer(modifier = Modifier.width(30.dp)) + DefaultButton(onClose, "back", Color.Red) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt index 3f93090..8b6b60d 100644 --- a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt +++ b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt @@ -29,24 +29,21 @@ fun DirectedAlgorithmDialog( visible = visible, title = title, onCloseRequest = onCloseRequest, - state = rememberDialogState(height = 600.dp, width = 880.dp) + state = rememberDialogState(height = 380.dp, width = 580.dp) ) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } - Column { - Spacer(modifier = Modifier.height(24.dp)) + val textWidth = 90.dp + Column(modifier = Modifier.padding(30.dp, 24.dp)) { Row { - Spacer(modifier = Modifier.width(30.dp)) Text( text = localisation("from"), style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth) ) - Spacer(modifier = Modifier.width(26.dp)) TextField( modifier = Modifier - .weight(1f) - .width(115.dp) + .fillMaxWidth() .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), colors = TextFieldDefaults.textFieldColors( @@ -58,21 +55,17 @@ fun DirectedAlgorithmDialog( value = source, onValueChange = { newValue -> source = newValue }, ) - Spacer(modifier = Modifier.width(200.dp)) } Spacer(modifier = Modifier.height(36.dp)) Row { - Spacer(modifier = Modifier.width(30.dp)) Text( text = localisation("to"), style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically).width(textWidth) ) - Spacer(modifier = Modifier.width(62.dp)) TextField( modifier = Modifier - .weight(1f) - .width(115.dp) + .fillMaxWidth() .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)), colors = TextFieldDefaults.textFieldColors( @@ -88,7 +81,6 @@ fun DirectedAlgorithmDialog( } Spacer(modifier = Modifier.height(36.dp)) Row { - Spacer(modifier = Modifier.width(30.dp)) val dijkstra = { graphVM.drawDijkstra(source, destination) } val fordBellman = { graphVM.drawFordBellman(source, destination) } val onClick = if (action == "Dijkstra") { @@ -99,9 +91,6 @@ fun DirectedAlgorithmDialog( {} } DefaultButton(onClick, "start") - } - Spacer(modifier = Modifier.height(36.dp)) - Row { Spacer(modifier = Modifier.width(30.dp)) DefaultButton(onCloseRequest, "back", Color.Red) } diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 5ec3c2f..959169e 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -1,9 +1,18 @@ package view.screens + +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* +import androidx.compose.material.Text import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController @@ -11,33 +20,69 @@ import model.algos.ForceAtlas2 import view.common.* import view.views.DirectedGraphView import viewmodel.MainScreenViewModel +import kotlin.math.exp +import kotlin.math.sign - +@OptIn(ExperimentalComposeUiApi::class) @Composable fun DirectedGraphScreen( navController: NavController, mainScreenViewModel: MainScreenViewModel, graphId: Int ) { - val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) - Box(modifier = Modifier.fillMaxSize()) { + var scale by remember { mutableStateOf(1f) } + var rotation by remember { mutableStateOf(0f) } + var offset by remember { mutableStateOf(Offset.Zero) } + val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> + scale *= zoomChange + rotation += rotationChange + offset += offsetChange + } + + fun scale(delta: Int) { + scale = (scale * exp(delta * 0.2f)).coerceIn(0.01f, 1.75f) + } + + Box( + modifier = Modifier + .fillMaxSize() + .transformable(state = state) + .onPointerEvent(PointerEventType.Scroll) { + val change = it.changes.first() + val delta = change.scrollDelta.y.toInt().sign + scale(delta) + } + .focusable() + .graphicsLayer( + scaleX = scale, + scaleY = scale, + rotationZ = rotation, + translationX = offset.x, + translationY = offset.y + ) + ) { DirectedGraphView(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { + Text("directed") + + var isOpenedVertexMenu by remember { mutableStateOf(false) } + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + var isOpenedDijkstraMenu by remember { mutableStateOf(false) } + var isOpenedFordBellmanMenu by remember { mutableStateOf(false) } + // To MainScreen DefaultShortButton({ navController.popBackStack() }, "home") Spacer(modifier = Modifier.height(10.dp)) // Add vertex Button - DefaultShortButton({ graphVM.addVertex(graphVM.size.toString()) }, "add_vertex") + DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex") Spacer(modifier = Modifier.height(10.dp)) // Add edge Button - var isOpenedEdgeMenu by remember { mutableStateOf(false) } - val onCloseEdge = { isOpenedEdgeMenu = false } DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") Spacer(modifier = Modifier.height(10.dp)) @@ -57,36 +102,46 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) // Dijkstra Button - var isDijkstraMenu by remember { mutableStateOf(false) } - val onCloseDijkstra = { isDijkstraMenu = !isDijkstraMenu } - DefaultShortButton(onCloseDijkstra, "dijkstra") + DefaultShortButton({ isOpenedDijkstraMenu = !isOpenedDijkstraMenu }, "dijkstra") Spacer(modifier = Modifier.height(10.dp)) // FordBellman Button - var isFordBellmanMenu by remember { mutableStateOf(false) } - val onCloseFB = { isFordBellmanMenu = !isFordBellmanMenu } - DefaultShortButton(onCloseFB, "ford_bellman") + DefaultShortButton({ isOpenedFordBellmanMenu = !isOpenedFordBellmanMenu }, "ford_bellman") Spacer(modifier = Modifier.height(10.dp)) + // Cycles Button DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles") Spacer(modifier = Modifier.height(10.dp)) - AddEdgeDialog(isOpenedEdgeMenu, onCloseEdge, graphVM, isDirected = true) + // Add edge Dialog + AddEdgeDialog( + isOpenedEdgeMenu, + { isOpenedEdgeMenu = !isOpenedEdgeMenu }, + graphVM, + isDirected = true + ) + + // Add vertex Dialog + AddVertexDialog( + isOpenedVertexMenu, + { isOpenedVertexMenu = false }, + graphVM, + ) - // Dijkstra dialog window + // Dijkstra Dialog DirectedAlgorithmDialog( - isDijkstraMenu, + isOpenedDijkstraMenu, "Dijkstra Algorithm", - onCloseDijkstra, + { isOpenedDijkstraMenu = false }, graphVM, "Dijkstra" ) - //Ford-Bellman dialog window + // Ford-Bellman Dialog DirectedAlgorithmDialog( - isFordBellmanMenu, + isOpenedFordBellmanMenu, "Ford Bellman Algorithm", - onCloseFB, + { isOpenedFordBellmanMenu = false }, graphVM, "FordBellman" ) diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 34cb3ce..62048a6 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -1,20 +1,32 @@ package view.screens +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import model.algos.ForceAtlas2 import view.common.AddEdgeDialog +import view.common.AddVertexDialog import view.common.DefaultShortButton import view.common.DirectedAlgorithmDialog import view.views.UndirectedGraphView import viewmodel.MainScreenViewModel +import kotlin.math.exp +import kotlin.math.sign +@OptIn(ExperimentalComposeUiApi::class) @Composable fun UndirectedGraphScreen( navController: NavController, @@ -23,22 +35,55 @@ fun UndirectedGraphScreen( ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) - Box(modifier = Modifier.fillMaxSize()) { + var scale by remember { mutableStateOf(1f) } + var rotation by remember { mutableStateOf(0f) } + var offset by remember { mutableStateOf(Offset.Zero) } + val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> + scale *= zoomChange + rotation += rotationChange + offset += offsetChange + } + + fun scale(delta: Int) { + scale = (scale * exp(delta * 0.2f)).coerceIn(0.01f, 1.75f) + } + + Box(modifier = Modifier + .fillMaxSize() + .transformable(state = state) + .onPointerEvent(PointerEventType.Scroll) { + val change = it.changes.first() + val delta = change.scrollDelta.y.toInt().sign + scale(delta) + } + .focusable() + .graphicsLayer( + scaleX = scale, + scaleY = scale, + rotationZ = rotation, + translationX = offset.x, + translationY = offset.y + )) { UndirectedGraphView(graphVM) } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { - Text(text = "Undirected") + Text("Undirected") + + var isOpenedVertexMenu by remember { mutableStateOf(false) } + var isOpenedEdgeMenu by remember { mutableStateOf(false) } + var isDijkstraMenu by remember { mutableStateOf(false) } + var isFordBellmanMenu by remember { mutableStateOf(false) } + // To MainScreen DefaultShortButton({ navController.popBackStack() }, "home") Spacer(modifier = Modifier.height(10.dp)) - // Add vertex - DefaultShortButton({ graphVM.addVertex(graphVM.size.toString()) }, "add_vertex") + // Add vertex Button + DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex") Spacer(modifier = Modifier.height(10.dp)) // Add edge button - var isOpenedEdgeMenu by remember { mutableStateOf(false) } DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") Spacer(modifier = Modifier.height(16.dp)) @@ -46,22 +91,20 @@ fun UndirectedGraphScreen( DefaultShortButton({ graphVM.saveSQLite() }, "save") Spacer(modifier = Modifier.height(10.dp)) + // Visualization Button DefaultShortButton({ ForceAtlas2.forceDrawing(graphVM) }, "visualize", Color(0xffFFCB32)) Spacer(modifier = Modifier.height(10.dp)) + // Reset colors Button DefaultShortButton({ graphVM.resetColors() }, "reset", Color.LightGray) Spacer(modifier = Modifier.height(10.dp)) // Dijkstra Button - var isDijkstraMenu by remember { mutableStateOf(false) } - val onCloseDijkstra = { isDijkstraMenu = !isDijkstraMenu } - DefaultShortButton(onCloseDijkstra, "dijkstra") + DefaultShortButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra") Spacer(modifier = Modifier.height(10.dp)) // FordBellman Button - var isFordBellmanMenu by remember { mutableStateOf(false) } - val onCloseFB = { isFordBellmanMenu = !isFordBellmanMenu } - DefaultShortButton(onCloseFB, "ford_bellman") + DefaultShortButton({ isFordBellmanMenu = !isFordBellmanMenu }, "ford_bellman") Spacer(modifier = Modifier.height(10.dp)) DefaultShortButton(onClick = { graphVM.drawMst() }, "find_mst") @@ -73,23 +116,30 @@ fun UndirectedGraphScreen( DefaultShortButton(onClick = { graphVM.drawBridges() }, "find_bridges") Spacer(modifier = Modifier.height(10.dp)) - val onClose = { isOpenedEdgeMenu = false } - AddEdgeDialog(isOpenedEdgeMenu, onClose, graphVM) + // Add vertex Dialog + AddVertexDialog( + isOpenedVertexMenu, + { isOpenedVertexMenu = false }, + graphVM, + ) + + // Add edge Dialog + AddEdgeDialog(isOpenedEdgeMenu, { isOpenedEdgeMenu = false }, graphVM) - // Dijkstra dialog window + // Dijkstra Dialog DirectedAlgorithmDialog( isDijkstraMenu, "Dijkstra Algorithm", - onCloseDijkstra, + { isDijkstraMenu = false }, graphVM, "Dijkstra" ) - //Ford-Bellman dialog window + // Ford-Bellman Dialog DirectedAlgorithmDialog( isFordBellmanMenu, "Ford Bellman Algorithm", - onCloseFB, + { isFordBellmanMenu = false }, graphVM, "FordBellman" ) diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt index 27b5369..02eb7e4 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt @@ -11,6 +11,7 @@ import model.algos.FordBellman import model.graph.Graph import model.graph.Edge import view.common.DefaultColors +import javax.swing.text.StyledEditorKit.BoldAction import kotlin.random.Random abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { @@ -100,11 +101,13 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM return graphVM[vertex]?.edges?.toList() ?: emptyList() } - fun addVertex(vertex: V) { + fun addVertex(vertex: V, centerCoordinates: Boolean = false) { size += 1 - graphVM.putIfAbsent(vertex, VertexViewModel(vertex)) + graphVM.putIfAbsent( + vertex, + VertexViewModel(vertex, centerCoordinates = centerCoordinates), + ) graphModel.addVertex(vertex) - updateView() } fun resetColors() { diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/VertexViewModel.kt index 8fa5ac3..12075e8 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/VertexViewModel.kt @@ -10,19 +10,30 @@ import width import view.common.DefaultColors import kotlin.random.Random -class VertexViewModel(_vertex: V, _edges: MutableList> = mutableListOf()) : +class VertexViewModel( + _vertex: V, + _edges: MutableList> = mutableListOf(), + centerCoordinates: Boolean = false +) : ViewModel() { val vertex: V = _vertex var edges = mutableStateListOf>() + var x by mutableStateOf(0f) + var y by mutableStateOf(0f) init { for (edge in _edges) { edges.add(edge) } + if (centerCoordinates) { + x = Random.nextInt(width / 2 - 300, width / 2 + 300).toFloat() + y = Random.nextInt(height / 2 - 300, height / 2 + 300).toFloat() + } else { + x = Random.nextInt(0, 30000).toFloat() + y = Random.nextInt(0, 30000).toFloat() + } } - var x by mutableStateOf(Random.nextInt(100, width - 100).toFloat()) - var y by mutableStateOf(Random.nextInt(100, height - 100).toFloat()) val vertexSize = 60f var color by mutableStateOf(DefaultColors.primary) val degree From 5770cdf79aa1b81b06205ade27d0e2edd995daec Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 26 May 2024 02:43:41 +0300 Subject: [PATCH 141/172] Feat: implemented running visualization as a coroutine and canceling it --- src/main/kotlin/model/algos/Dijkstra.kt | 3 -- src/main/kotlin/model/algos/ForceAtlas2.kt | 10 ++++-- .../view/screens/DirectedGraphScreen.kt | 36 ++++++++++++++----- .../view/screens/UndirectedGraphScreen.kt | 22 ++++++++++-- .../kotlin/viewmodel/MainScreenViewModel.kt | 36 ++++++++++--------- 5 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/model/algos/Dijkstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt index ec00371..a32d28a 100644 --- a/src/main/kotlin/model/algos/Dijkstra.kt +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -18,9 +18,7 @@ class Dijkstra(val graph: Graph, private val totalNodes: Int) { vertexValues[start] = 0 while (visitedSet.size != totalNodes) { - println(vertexValues) for (i in pathMap) { - println(i) } if (prioraQueue.isEmpty()) { return pathMap[end]!! @@ -33,7 +31,6 @@ class Dijkstra(val graph: Graph, private val totalNodes: Int) { visitedSet.add(ux) refreshSearch(ux) } - println(vertexValues) } return pathMap[end]!! } diff --git a/src/main/kotlin/model/algos/ForceAtlas2.kt b/src/main/kotlin/model/algos/ForceAtlas2.kt index b1c4cf4..e05f244 100644 --- a/src/main/kotlin/model/algos/ForceAtlas2.kt +++ b/src/main/kotlin/model/algos/ForceAtlas2.kt @@ -1,6 +1,9 @@ package model.algos import height +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield import kotlinx.serialization.internal.throwMissingFieldException import viewmodel.AbstractGraphViewModel import viewmodel.VertexViewModel @@ -10,13 +13,14 @@ import kotlin.math.sign import kotlin.math.sqrt const val repulsionK: Double = 150.0 -const val attractionK: Double = 150.0 +const val attractionK: Double = 250.0 const val gravityK: Double = 5.0 object ForceAtlas2 { - fun forceDrawing(graphVM: AbstractGraphViewModel) { + suspend fun forceDrawing(graphVM: AbstractGraphViewModel) { val vertices = graphVM.verticesVM - repeat(1000) { + while (true) { + yield() val forces = mutableMapOf, Pair>() for (vertex in vertices) { val edges = vertex.edges diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 959169e..70e0eb6 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -16,6 +16,10 @@ import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import model.algos.ForceAtlas2 import view.common.* import view.views.DirectedGraphView @@ -73,6 +77,7 @@ fun DirectedGraphScreen( var isOpenedEdgeMenu by remember { mutableStateOf(false) } var isOpenedDijkstraMenu by remember { mutableStateOf(false) } var isOpenedFordBellmanMenu by remember { mutableStateOf(false) } + var isVisualizationRunning by remember { mutableStateOf(false) } // To MainScreen DefaultShortButton({ navController.popBackStack() }, "home") @@ -86,10 +91,25 @@ fun DirectedGraphScreen( DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") Spacer(modifier = Modifier.height(10.dp)) + // Save button DefaultShortButton({ graphVM.saveSQLite() }, "save") Spacer(modifier = Modifier.height(16.dp)) - DefaultShortButton({ ForceAtlas2.forceDrawing(graphVM) }, "visualize", Color(0xffFFCB32)) + // Visualization Button + val scope = rememberCoroutineScope { Dispatchers.Default } + DefaultShortButton( + { + isVisualizationRunning = !isVisualizationRunning + if (isVisualizationRunning) { + scope.launch { + ForceAtlas2.forceDrawing(graphVM) + } + } else { + scope.coroutineContext.cancelChildren() + } + }, "visualize", + if (isVisualizationRunning) Color.Red else Color(0xffFFCB32) + ) Spacer(modifier = Modifier.height(10.dp)) DefaultShortButton({ graphVM.resetColors() }, "reset", Color.LightGray) @@ -113,6 +133,13 @@ fun DirectedGraphScreen( DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles") Spacer(modifier = Modifier.height(10.dp)) + // Add vertex Dialog + AddVertexDialog( + isOpenedVertexMenu && isVisualizationRunning.not(), + { isOpenedVertexMenu = false }, + graphVM, + ) + // Add edge Dialog AddEdgeDialog( isOpenedEdgeMenu, @@ -121,13 +148,6 @@ fun DirectedGraphScreen( isDirected = true ) - // Add vertex Dialog - AddVertexDialog( - isOpenedVertexMenu, - { isOpenedVertexMenu = false }, - graphVM, - ) - // Dijkstra Dialog DirectedAlgorithmDialog( isOpenedDijkstraMenu, diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 62048a6..61cd85f 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -16,6 +16,10 @@ import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import model.algos.ForceAtlas2 import view.common.AddEdgeDialog import view.common.AddVertexDialog @@ -74,6 +78,7 @@ fun UndirectedGraphScreen( var isOpenedEdgeMenu by remember { mutableStateOf(false) } var isDijkstraMenu by remember { mutableStateOf(false) } var isFordBellmanMenu by remember { mutableStateOf(false) } + var isVisualizationRunning by remember { mutableStateOf(false) } // To MainScreen DefaultShortButton({ navController.popBackStack() }, "home") @@ -92,7 +97,20 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) // Visualization Button - DefaultShortButton({ ForceAtlas2.forceDrawing(graphVM) }, "visualize", Color(0xffFFCB32)) + val scope = rememberCoroutineScope { Dispatchers.Default } + DefaultShortButton( + { + isVisualizationRunning = !isVisualizationRunning + if (isVisualizationRunning) { + scope.launch { + ForceAtlas2.forceDrawing(graphVM) + } + } else { + scope.coroutineContext.cancelChildren() + } + }, "visualize", + if (isVisualizationRunning) Color.Red else Color(0xffFFCB32) + ) Spacer(modifier = Modifier.height(10.dp)) // Reset colors Button @@ -118,7 +136,7 @@ fun UndirectedGraphScreen( // Add vertex Dialog AddVertexDialog( - isOpenedVertexMenu, + isOpenedVertexMenu && isVisualizationRunning.not(), { isOpenedVertexMenu = false }, graphVM, ) diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 3d8d5ba..5ff6511 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel import java.sql.DriverManager import java.sql.SQLException -enum class initType{ +enum class initType { SQLite, CSV, Neo4j, @@ -24,6 +24,7 @@ class MainScreenViewModel : ViewModel() { graphVM.inType = initType graphs.undirectedGraphs.add(graphVM) } + "directed" -> { graphs.typeList.add(ViewModelType.Directed) val graphVM = DirectedGraphViewModel(name) @@ -34,10 +35,10 @@ class MainScreenViewModel : ViewModel() { } } - fun initModel(index: Int){ - if(graphs.typeList[index] == ViewModelType.Directed) { + fun initModel(index: Int) { + if (graphs.typeList[index] == ViewModelType.Directed) { val graph = graphs.getDirected(index) - if(graph.initedGraph) return + if (graph.initedGraph) return else graph.initedGraph = true if (graph.inType == initType.SQLite) { val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") @@ -47,7 +48,8 @@ class MainScreenViewModel : ViewModel() { val resEdges = getGraphs.executeQuery() while (resVertex.next()) { var vertexName = resVertex.getString("Vertexes") - if(vertexName.length > 1) vertexName = vertexName.slice(1..vertexName.length - 1) + if (vertexName.length > 1) vertexName = + vertexName.slice(1..vertexName.length - 1) graph.addVertex(vertexName) } while (resEdges.next()) { @@ -56,16 +58,16 @@ class MainScreenViewModel : ViewModel() { var to = resEdges.getString("Vertexes") to = to.slice(1.. 1) vertexName = vertexName.slice(1..vertexName.length - 1) + if (vertexName.length > 1) vertexName = + vertexName.slice(1..vertexName.length - 1) graph.addVertex(vertexName) } while (resEdges.next()) { @@ -83,8 +86,7 @@ class MainScreenViewModel : ViewModel() { val weight = resEdges.getString("V$i") var to = resEdges.getString("Vertexes") to = to.slice(1.. { return undirectedGraphs[findGraph(index)] } From dfb36508d38f4fc3a77860b0bd22542a239110ce Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 28 May 2024 05:05:20 -0400 Subject: [PATCH 142/172] test: add Dijkstra algo tests --- src/main/kotlin/model/algos/Dijkstra.kt | 4 +- src/test/kotlin/algos/DijkstraTest.kt | 74 +++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/algos/DijkstraTest.kt diff --git a/src/main/kotlin/model/algos/Dijkstra.kt b/src/main/kotlin/model/algos/Dijkstra.kt index a32d28a..9345178 100644 --- a/src/main/kotlin/model/algos/Dijkstra.kt +++ b/src/main/kotlin/model/algos/Dijkstra.kt @@ -18,8 +18,6 @@ class Dijkstra(val graph: Graph, private val totalNodes: Int) { vertexValues[start] = 0 while (visitedSet.size != totalNodes) { - for (i in pathMap) { - } if (prioraQueue.isEmpty()) { return pathMap[end]!! } @@ -39,7 +37,7 @@ class Dijkstra(val graph: Graph, private val totalNodes: Int) { var newRange = -1 for (edge in graph.edgesOf(currentVertex)) { if (!visitedSet.contains(edge.to)) { - newRange = vertexValues[currentVertex]!! + edge.weight + newRange = vertexValues[edge.from]!! + edge.weight if (newRange < vertexValues[edge.to]!!) { vertexValues[edge.to] = newRange val k = pathMap[edge.from]?.toMutableList() diff --git a/src/test/kotlin/algos/DijkstraTest.kt b/src/test/kotlin/algos/DijkstraTest.kt new file mode 100644 index 0000000..ad98b06 --- /dev/null +++ b/src/test/kotlin/algos/DijkstraTest.kt @@ -0,0 +1,74 @@ +package algos + +import Dijkstra +import model.graph.DirectedGraph +import model.graph.Edge +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class DijkstraTest { + + @Test + fun `basic find without negative cycle and possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 4, 20) + this.addEdge(1, 2, 2) + this.addEdge(2, 3, 3) + this.addEdge(3, 4, 1) + } + val result = Dijkstra(graph, 4).dijkstra(1, 4) + val shortestLengthExpected = 6 + var shortestLengthActual = 0 + for (i in result){ + shortestLengthActual += i.weight + } + assertNotNull(shortestLengthActual) + println("$shortestLengthActual, $shortestLengthExpected") + assertEquals( + shortestLengthExpected, shortestLengthActual, + "Dijkstra must return weight of the shortest path" + ) + val pathExpected = listOf( + Edge(1, 2, 2), + Edge(2, 3, 3), + Edge(3, 4, 1) + ) + assertContentEquals( + pathExpected, result, + "Dijkstra must return shortest path when it is possible to reach destination" + ) + } + + @Test + fun `not possible to reach destination`() { + val graph = DirectedGraph() + for (i in 1..6) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 2, 8) + this.addEdge(2, 3, 2) + this.addEdge(4, 5, 3) + this.addEdge(5, 6, 5) + this.addEdge(6, 4, 9) + this.addEdge(4, 6, 20) + } + + val result = Dijkstra(graph, 4).dijkstra(1, 4) + + val pathExpected = emptyList>() + val pathActual = result + assertContentEquals( + pathExpected, pathActual, + "FordBellman must return null as shortest path if it is not possible to reach destination" + ) + } +} \ No newline at end of file From 35de38feefd2c26e90af246c5d760812a894da5a Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 28 May 2024 05:25:41 -0400 Subject: [PATCH 143/172] test: Strong Connections algo test --- src/test/kotlin/algos/DijkstraTest.kt | 6 +- .../kotlin/algos/StrongConnectionsTest.kt | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/algos/StrongConnectionsTest.kt diff --git a/src/test/kotlin/algos/DijkstraTest.kt b/src/test/kotlin/algos/DijkstraTest.kt index ad98b06..77075a9 100644 --- a/src/test/kotlin/algos/DijkstraTest.kt +++ b/src/test/kotlin/algos/DijkstraTest.kt @@ -11,7 +11,7 @@ import kotlin.test.assertNotNull internal class DijkstraTest { @Test - fun `basic find without negative cycle and possible to reach destination`() { + fun `dijkstra basic find and possible to reach destination`() { val graph = DirectedGraph() for (i in 1..4) { graph.addVertex(i) @@ -47,7 +47,7 @@ internal class DijkstraTest { } @Test - fun `not possible to reach destination`() { + fun `dijkstra not possible to reach destination`() { val graph = DirectedGraph() for (i in 1..6) { graph.addVertex(i) @@ -68,7 +68,7 @@ internal class DijkstraTest { val pathActual = result assertContentEquals( pathExpected, pathActual, - "FordBellman must return null as shortest path if it is not possible to reach destination" + "Dijkstra must return empty list as shortest path if it is not possible to reach destination" ) } } \ No newline at end of file diff --git a/src/test/kotlin/algos/StrongConnectionsTest.kt b/src/test/kotlin/algos/StrongConnectionsTest.kt new file mode 100644 index 0000000..4d6aa68 --- /dev/null +++ b/src/test/kotlin/algos/StrongConnectionsTest.kt @@ -0,0 +1,71 @@ +package algos + +import model.algos.StrongConnections +import model.graph.DirectedGraph +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class StrongConnectionsTest { + + @Test + fun `strong connections unconnected find`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + val resultActual = StrongConnections().findStrongConnections(graph) + val resultExpected = listOf(listOf(1), listOf(2), listOf(3), listOf(4)) + assertNotNull(resultActual) + assertEquals( + resultExpected, resultActual, + "Unconnected vertices should be in different strong connections" + ) + } + + @Test + fun `strong connections cylce`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + graph.run { + this.addEdge(1, 2, 8) + this.addEdge(2, 3, 2) + this.addEdge(3, 4, -3) + this.addEdge(4, 1, 3) + } + val resultActual = StrongConnections().findStrongConnections(graph) + val resultExpected = listOf(listOf(1, 2, 3, 4)) + assertNotNull(resultActual) + assertEquals( + resultExpected, resultActual, + "Cycle is a strong connection" + ) + } + + @Test + fun `strong connections cycles joined with a bridge`() { + val graph = DirectedGraph() + for (i in 1..6) { + graph.addVertex(i) + } + graph.run { + this.addEdge(1, 2, 52) + this.addEdge(2, 3, 52) + this.addEdge(3, 1, -52) + this.addEdge(4, 5, 52) + this.addEdge(5, 6, -52) + this.addEdge(6, 4, 52) + this.addEdge(1, 6, 52) + + } + val resultActual = StrongConnections().findStrongConnections(graph) + val resultExpected = listOf(listOf(1, 2, 3), listOf(4, 5, 6)) + assertNotNull(resultActual) + assertEquals( + resultExpected, resultActual, + "Cycles joined with a bridge is a 2 strong connections" + ) + } +} \ No newline at end of file From c294810bbd3415fba22a19f51626f6c342f04f38 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 28 May 2024 09:26:45 -0400 Subject: [PATCH 144/172] ref: SQLite graph saving now in Graph class --- src/main/kotlin/model/graph/Graph.kt | 100 ++++++++++++++++++ .../viewmodel/DirectedGraphViewModel.kt | 98 +---------------- .../viewmodel/UndirectedGraphViewModel.kt | 97 +---------------- 3 files changed, 104 insertions(+), 191 deletions(-) diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 1e3b4f5..d7de572 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -1,7 +1,12 @@ package model.graph +import java.sql.DriverManager +import java.sql.SQLException + abstract class Graph() { protected val graph = mutableMapOf>>() + private val DB_DRIVER = "jdbc:sqlite" + val entries get() = graph.entries var isWeighted = false @@ -34,6 +39,101 @@ abstract class Graph() { return graph[vertex]?.size ?: 0 } + fun saveSQLite(name: String, type: String, bdName: String){ + var parameterCreate = "( Vertexes String," + var parameterInput = "( Vertexes," + var create = ("CREATE TABLE $name ") + val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") + val insertIndex = ("INSERT INTO BEBRA_KILLER (name, type) VALUES('$name', '$type');") + for (i in graph.entries) { + parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " + parameterInput = "$parameterInput V${i.key.toString()}," + } + parameterCreate = parameterCreate.slice(0..parameterCreate.length - 3) + parameterCreate = "$parameterCreate )" + parameterInput = parameterInput.slice(0..parameterInput.length - 2) + parameterInput = "$parameterInput )" + create = create + parameterCreate + ";" + val connection = DriverManager.getConnection("$DB_DRIVER:$bdName.db") + ?: throw SQLException("Cannot connect to database") + val delTable = "DROP TABLE $name" + val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='$name';" + connection.createStatement().also { stmt -> + try { + stmt.execute(delTable) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(delIndexRec) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(create) + stmt.execute(createIndex) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + connection.createStatement().also { stmt -> + try { + stmt.execute(insertIndex) + } catch (ex: Exception) { + println("Unsuccessful") + println(ex) + } finally { + stmt.close() + } + } + + var request = "INSERT INTO $name $parameterInput VALUES " + for (i in graph.entries) { + var record = "( 'V${i.key}', " + val recList = emptyMap().toMutableMap() + for (j in graph.entries) { + recList[j.key] = "NULL" + } + for (j in i.value) { + recList[j.to] = j.weight.toString() + } + for (j in recList) { + record = "$record ${j.value}, " + } + record = record.slice(0..record.length - 3) + record = "$record )," + request = "$request $record" + } + request = request.slice(0..request.length - 2) + connection.createStatement().also { stmt -> + try { + stmt.execute(request) + } catch (ex: Exception) { + println("Unsuccessful") + println(ex) + } finally { + stmt.close() + } + } + + } + abstract fun addEdge(from: V, to: V, weight: Int = 1) fun edgesOf(from: V): MutableList> { diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt index d243074..5559b0a 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt @@ -1,21 +1,18 @@ package viewmodel import androidx.compose.ui.graphics.Color -import model.algos.StrongConnections import de.tudarmstadt.lt.cw.graph.ArrayBackedGraph import de.tudarmstadt.lt.cw.graph.ArrayBackedGraphCW import de.tudarmstadt.lt.cw.graph.Graph +import model.algos.StrongConnections import model.graph.DirectedGraph import model.graph.Edge -import java.sql.DriverManager -import java.sql.SQLException import kotlin.random.Random class DirectedGraphViewModel( name: String, val graph: DirectedGraph = DirectedGraph() ) : AbstractGraphViewModel(name, graph) { - private val DB_DRIVER = "jdbc:sqlite" var inType = initType.Internal var initedGraph = false @@ -88,97 +85,6 @@ class DirectedGraphViewModel( } fun saveSQLite() { - var parameterCreate = "( Vertexes String," - var parameterInput = "( Vertexes," - var create = ("CREATE TABLE $name ") - val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") - val insertIndex = ("INSERT INTO BEBRA_KILLER (name, type) VALUES('$name', 'Directed');") - for (i in graph.entries) { - parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " - parameterInput = "$parameterInput V${i.key.toString()}," - } - parameterCreate = parameterCreate.slice(0..parameterCreate.length - 3) - parameterCreate = "$parameterCreate )" - parameterInput = parameterInput.slice(0..parameterInput.length - 2) - parameterInput = "$parameterInput )" - create = create + parameterCreate + ";" - val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") - ?: throw SQLException("Cannot connect to database") - val delTable = "DROP TABLE $name" - val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='$name';" - connection.createStatement().also { stmt -> - try { - stmt.execute(delTable) - println("Tables created or already exists") - } catch (ex: Exception) { - println("Cannot create table in database") - println(ex) - } finally { - stmt.close() - } - } - connection.createStatement().also { stmt -> - try { - stmt.execute(delIndexRec) - println("Tables created or already exists") - } catch (ex: Exception) { - println("Cannot create table in database") - println(ex) - } finally { - stmt.close() - } - } - connection.createStatement().also { stmt -> - try { - stmt.execute(create) - stmt.execute(createIndex) - println("Tables created or already exists") - } catch (ex: Exception) { - println("Cannot create table in database") - println(ex) - } finally { - stmt.close() - } - } - connection.createStatement().also { stmt -> - try { - stmt.execute(insertIndex) - } catch (ex: Exception) { - println("Unsuccessful") - println(ex) - } finally { - stmt.close() - } - } - - var request = "INSERT INTO $name $parameterInput VALUES " - for (i in graph.entries) { - var record = "( 'V${i.key}', " - val recList = emptyMap().toMutableMap() - for (j in graph.entries) { - recList[j.key] = "NULL" - } - for (j in i.value) { - recList[j.to] = j.weight.toString() - } - for (j in recList) { - record = "$record ${j.value}, " - } - record = record.slice(0..record.length - 3) - record = "$record )," - request = "$request $record" - } - request = request.slice(0..request.length - 2) - connection.createStatement().also { stmt -> - try { - stmt.execute(request) - } catch (ex: Exception) { - println("Unsuccessful") - println(ex) - } finally { - stmt.close() - } - } - println(request) + graph.saveSQLite(name, "Directed", "storage") } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt index 6a8fd6f..07fe00f 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt @@ -3,10 +3,8 @@ package viewmodel import androidx.compose.ui.graphics.Color import model.algos.Prim import model.algos.findBridges -import model.graph.UndirectedGraph import model.graph.Edge -import java.sql.DriverManager -import java.sql.SQLException +import model.graph.UndirectedGraph class UndirectedGraphViewModel( name: String, @@ -62,97 +60,6 @@ class UndirectedGraphViewModel( } fun saveSQLite() { - var parameterCreate = "( Vertexes String," - var parameterInput = "( Vertexes," - var create = ("CREATE TABLE $name ") - val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") - val insertIndex = ("INSERT INTO BEBRA_KILLER (name, type) VALUES('$name', 'Undirected');") - for (i in graph.entries) { - parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " - parameterInput = "$parameterInput V${i.key.toString()}," - } - parameterCreate = parameterCreate.slice(0..parameterCreate.length - 3) - parameterCreate = "$parameterCreate )" - parameterInput = parameterInput.slice(0..parameterInput.length - 2) - parameterInput = "$parameterInput )" - create = create + parameterCreate + ";" - val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") - ?: throw SQLException("Cannot connect to database") - val delTable = "DROP TABLE $name" - val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='$name';" - connection.createStatement().also { stmt -> - try { - stmt.execute(delTable) - println("Table deleted") - } catch (ex: Exception) { - println("Cannot delete table in database") - println(ex) - } finally { - stmt.close() - } - } - connection.createStatement().also { stmt -> - try { - stmt.execute(delIndexRec) - println("Table deleted") - } catch (ex: Exception) { - println("Cannot delete table in database") - println(ex) - } finally { - stmt.close() - } - } - connection.createStatement().also { stmt -> - try { - stmt.execute(create) - stmt.execute(createIndex) - println("Tables created or already exists") - } catch (ex: Exception) { - println("Cannot create table in database") - println(ex) - } finally { - stmt.close() - } - } - connection.createStatement().also { stmt -> - try { - stmt.execute(insertIndex) - } catch (ex: Exception) { - println("Unsuccessful") - println(ex) - } finally { - stmt.close() - } - } - - var request = "INSERT INTO $name $parameterInput VALUES " - for (i in graph.entries) { - var record = "( 'V${i.key}', " - val recList = emptyMap().toMutableMap() - for (j in graph.entries) { - recList[j.key] = "NULL" - } - for (j in i.value) { - recList[j.to] = j.weight.toString() - } - for (j in recList) { - record = "$record ${j.value}, " - } - record = record.slice(0..record.length - 3) - record = "$record )," - request = "$request $record" - } - request = request.slice(0..request.length - 2) - connection.createStatement().also { stmt -> - try { - stmt.execute(request) - } catch (ex: Exception) { - println("Unsuccessful") - println(ex) - } finally { - stmt.close() - } - } - println(request) + graph.saveSQLite(name, "Undirected", "storage") } } \ No newline at end of file From 0a11420dc297b11badefad794be027994c66fa42 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 28 May 2024 11:20:12 -0400 Subject: [PATCH 145/172] test: add integrate test w. SQLite & dijkstra algo --- src/main/kotlin/view/screens/MainScreen.kt | 3 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 4 +- src/test/kotlin/SQLiteIntegrationTest.kt | 55 +++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/SQLiteIntegrationTest.kt diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 768699f..81c612c 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -40,9 +40,10 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView val selectedOptionTextDropDown = remember { mutableStateOf(optionsDropDown[0]) } if(!mainScreenViewModel.inited) { - mainScreenViewModel.graphInit() + mainScreenViewModel.graphInit("storage") mainScreenViewModel.inited = true } + println("TYPE LIST IS ${mainScreenViewModel.graphs.typeList}, ${mainScreenViewModel.graphs.directedGraphs.toList()}") Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { // Search tab diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 5ff6511..048847f 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -95,9 +95,9 @@ class MainScreenViewModel : ViewModel() { } } - fun graphInit() { + fun graphInit(source: String) { val DB_DRIVER = "jdbc:sqlite" - val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") ?: throw SQLException("Cannot connect to database") val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") diff --git a/src/test/kotlin/SQLiteIntegrationTest.kt b/src/test/kotlin/SQLiteIntegrationTest.kt new file mode 100644 index 0000000..f86ddf7 --- /dev/null +++ b/src/test/kotlin/SQLiteIntegrationTest.kt @@ -0,0 +1,55 @@ + +import model.graph.DirectedGraph +import model.graph.Edge +import viewmodel.MainScreenViewModel +import java.io.File +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class SQLiteIntegrationTest { + + @Test + fun `SQLite integrable test`() { + val graph = DirectedGraph() + for (i in 1..4) { + graph.addVertex(i) + } + + graph.run { + this.addEdge(1, 4, 20) + this.addEdge(1, 2, 2) + this.addEdge(2, 3, 3) + this.addEdge(3, 4, 1) + } + graph.saveSQLite("TEST_DEFAULT", "Directed", "test") + + val graphVM = MainScreenViewModel() + graphVM.graphInit("test") + graphVM.initModel(0) + val loadedGraph = graphVM.graphs.getDirected(0).graph + val result = Dijkstra(loadedGraph, 4).dijkstra("1", "4") + val shortestLengthExpected = 6 + var shortestLengthActual = 0 + for (i in result){ + shortestLengthActual += i.weight + } + assertNotNull(shortestLengthActual) + println("$shortestLengthActual, $shortestLengthExpected") + assertEquals( + shortestLengthExpected, shortestLengthActual, + "Dijkstra must return weight of the shortest path" + ) + val pathExpected = listOf( + Edge("1", "2", 2), + Edge("2", "3", 3), + Edge("3", "4", 1) + ) + assertContentEquals( + pathExpected, result, + "Dijkstra must return shortest path when it is possible to reach destination" + ) + File("test.db").delete() + } +} \ No newline at end of file From 3f45d6dd994b091a52a8548d04b677c5206f20d3 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 28 May 2024 18:26:32 +0300 Subject: [PATCH 146/172] Fix: zooming now by mouse position, not center --- src/main/kotlin/model/algos/ForceAtlas2.kt | 8 +- src/main/kotlin/view/common/AddEdgeDialog.kt | 12 +- .../kotlin/view/common/AddVertexDialog.kt | 2 +- .../view/common/DirectedAlgorithmDialog.kt | 2 +- .../kotlin/view/{views => graph}/GraphView.kt | 12 +- .../view/graph/edge/DirectedEdgeView.kt | 107 ++++++++++++++++++ .../edge/UndirectedEdgeView.kt | 29 +++-- .../view/graph/vertex/DirectedVertexView.kt | 59 ++++++++++ .../view/graph/vertex/UndirectedVertexView.kt | 56 +++++++++ .../view/screens/DirectedGraphScreen.kt | 69 +++++------ src/main/kotlin/view/screens/MainScreen.kt | 22 ++-- .../view/screens/UndirectedGraphScreen.kt | 72 ++++++------ .../view/views/edge/DirectedEdgeView.kt | 99 ---------------- .../view/views/vertex/DirectedVertexView.kt | 53 --------- .../view/views/vertex/UndirectedVertexView.kt | 60 ---------- .../kotlin/viewmodel/MainScreenViewModel.kt | 54 ++++----- .../{ => graph}/AbstractGraphViewModel.kt | 17 ++- .../{ => graph}/DirectedGraphViewModel.kt | 6 +- .../viewmodel/{ => graph}/EdgeViewModel.kt | 3 +- .../{ => graph}/UndirectedGraphViewModel.kt | 5 +- .../viewmodel/{ => graph}/VertexViewModel.kt | 9 +- .../viewmodel/io/neo4j/Neo4jRepository.kt | 4 + src/test/kotlin/io/Neo4jTest.kt | 4 + 23 files changed, 410 insertions(+), 354 deletions(-) rename src/main/kotlin/view/{views => graph}/GraphView.kt (59%) create mode 100644 src/main/kotlin/view/graph/edge/DirectedEdgeView.kt rename src/main/kotlin/view/{views => graph}/edge/UndirectedEdgeView.kt (50%) create mode 100644 src/main/kotlin/view/graph/vertex/DirectedVertexView.kt create mode 100644 src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt delete mode 100644 src/main/kotlin/view/views/edge/DirectedEdgeView.kt delete mode 100644 src/main/kotlin/view/views/vertex/DirectedVertexView.kt delete mode 100644 src/main/kotlin/view/views/vertex/UndirectedVertexView.kt rename src/main/kotlin/viewmodel/{ => graph}/AbstractGraphViewModel.kt (86%) rename src/main/kotlin/viewmodel/{ => graph}/DirectedGraphViewModel.kt (98%) rename src/main/kotlin/viewmodel/{ => graph}/EdgeViewModel.kt (96%) rename src/main/kotlin/viewmodel/{ => graph}/UndirectedGraphViewModel.kt (98%) rename src/main/kotlin/viewmodel/{ => graph}/VertexViewModel.kt (79%) create mode 100644 src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt create mode 100644 src/test/kotlin/io/Neo4jTest.kt diff --git a/src/main/kotlin/model/algos/ForceAtlas2.kt b/src/main/kotlin/model/algos/ForceAtlas2.kt index e05f244..acd6673 100644 --- a/src/main/kotlin/model/algos/ForceAtlas2.kt +++ b/src/main/kotlin/model/algos/ForceAtlas2.kt @@ -1,14 +1,10 @@ package model.algos import height -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import kotlinx.coroutines.yield -import kotlinx.serialization.internal.throwMissingFieldException -import viewmodel.AbstractGraphViewModel -import viewmodel.VertexViewModel +import viewmodel.graph.AbstractGraphViewModel +import viewmodel.graph.VertexViewModel import width -import kotlin.math.ln import kotlin.math.sign import kotlin.math.sqrt diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index 0459730..4fcf185 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.rememberDialogState import localisation.localisation -import viewmodel.AbstractGraphViewModel +import viewmodel.graph.AbstractGraphViewModel @Composable fun AddEdgeDialog( @@ -32,13 +32,13 @@ fun AddEdgeDialog( visible = visible, title = "New Edge", onCloseRequest = onClose, - state = rememberDialogState(height = 420.dp, width = 580.dp) + state = rememberDialogState(height = 520.dp, width = 580.dp) ) { var source by remember { mutableStateOf("") } var destination by remember { mutableStateOf("") } var notWeighted by remember { mutableStateOf(true) } var weight by remember { mutableStateOf("1") } - Column(modifier = Modifier.padding(30.dp, 24.dp)) { + Column(modifier = Modifier.padding(30.dp, 24.dp).fillMaxSize()) { val textWidth = 90.dp val rightPadding = 200.dp Row { @@ -98,8 +98,7 @@ fun AddEdgeDialog( TextField( enabled = !notWeighted, modifier = Modifier - .weight(1f) - .width(115.dp) + .fillMaxWidth() .border( 3.dp, color = Color.Black, @@ -122,6 +121,9 @@ fun AddEdgeDialog( ) Spacer(modifier = Modifier.width(20.dp)) } + } + Spacer(modifier = Modifier.weight(1f)) + Row { Checkbox( modifier = Modifier.align(Alignment.CenterVertically), checked = notWeighted, diff --git a/src/main/kotlin/view/common/AddVertexDialog.kt b/src/main/kotlin/view/common/AddVertexDialog.kt index ad5e349..d193780 100644 --- a/src/main/kotlin/view/common/AddVertexDialog.kt +++ b/src/main/kotlin/view/common/AddVertexDialog.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.rememberDialogState import localisation.localisation -import viewmodel.AbstractGraphViewModel +import viewmodel.graph.AbstractGraphViewModel @Composable fun AddVertexDialog( diff --git a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt index 8b6b60d..35c205c 100644 --- a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt +++ b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.rememberDialogState import localisation.localisation -import viewmodel.AbstractGraphViewModel +import viewmodel.graph.AbstractGraphViewModel @Composable diff --git a/src/main/kotlin/view/views/GraphView.kt b/src/main/kotlin/view/graph/GraphView.kt similarity index 59% rename from src/main/kotlin/view/views/GraphView.kt rename to src/main/kotlin/view/graph/GraphView.kt index da5b32e..65b1d27 100644 --- a/src/main/kotlin/view/views/GraphView.kt +++ b/src/main/kotlin/view/graph/GraphView.kt @@ -1,12 +1,10 @@ -package view.views +package view.graph import androidx.compose.runtime.Composable -import view.views.edge.DirectedEdgeView -import view.views.edge.UndirectedEdgeView -import view.views.vertex.DirectedVertexView -import view.views.vertex.UndirectedVertexView -import viewmodel.DirectedGraphViewModel -import viewmodel.UndirectedGraphViewModel +import view.graph.vertex.DirectedVertexView +import view.graph.vertex.UndirectedVertexView +import viewmodel.graph.DirectedGraphViewModel +import viewmodel.graph.UndirectedGraphViewModel @Composable fun UndirectedGraphView(graphVM: UndirectedGraphViewModel) { diff --git a/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt b/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt new file mode 100644 index 0000000..e9ba0d2 --- /dev/null +++ b/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt @@ -0,0 +1,107 @@ +package view.graph.edge + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import viewmodel.graph.DirectedGraphViewModel +import viewmodel.graph.EdgeViewModel +import kotlin.math.atan2 + +@Composable +fun DirectedEdgeView( + graphVM: DirectedGraphViewModel, + edgeVM: EdgeViewModel, + isWeighted: Boolean, +) { + + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + val vertexSizeZoomed = edgeVM.fromVM.vertexSize * graphVM.zoom + val first = edgeVM.fromVM + val second = edgeVM.toVM + drawLine( + start = Offset( + first.offsetX + vertexSizeZoomed / 2, + first.offsetY + vertexSizeZoomed / 2 + ), + end = Offset( + second.offsetX + vertexSizeZoomed / 2, + second.offsetY + vertexSizeZoomed / 2 + ), + strokeWidth = 6f * graphVM.zoom, + color = edgeVM.color, + ) + rotate( + degrees = ((57.2958 * (atan2( + ((first.offsetY - second.offsetY).toDouble()), + ((first.offsetX - second.offsetX).toDouble()) + ))).toFloat()), + pivot = Offset( + second.offsetX + vertexSizeZoomed / 2, + second.offsetY + vertexSizeZoomed / 2 + ) + ) { + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 16f * graphVM.zoom), + topLeft = Offset( + second.offsetX + vertexSizeZoomed / 2 + 65 * graphVM.zoom, + second.offsetY + vertexSizeZoomed / 2 - 8f * graphVM.zoom + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 14f * graphVM.zoom), + topLeft = Offset( + second.offsetX + vertexSizeZoomed / 2 + 60 * graphVM.zoom, + second.offsetY + vertexSizeZoomed / 2 - 7f * graphVM.zoom + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 12f * graphVM.zoom), + topLeft = Offset( + second.offsetX + vertexSizeZoomed / 2 + 55 * graphVM.zoom, + second.offsetY + vertexSizeZoomed / 2 - 6f * graphVM.zoom + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 10f * graphVM.zoom), + topLeft = Offset( + second.offsetX + vertexSizeZoomed / 2 + 50 * graphVM.zoom, + second.offsetY + vertexSizeZoomed / 2 - 5f * graphVM.zoom + ), + ) + drawRect( + color = edgeVM.color, + size = Size(5f * graphVM.zoom, 8f * graphVM.zoom), + topLeft = Offset( + second.offsetX + vertexSizeZoomed / 2 + 45 * graphVM.zoom, + second.offsetY + vertexSizeZoomed / 2 - 4f * graphVM.zoom + ), + ) + } + if (isWeighted) { + drawText( + textMeasurer, edgeVM.weight.toString(), + topLeft = Offset( + (first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom, + (first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom + ), + style = TextStyle(background = Color.White, fontSize = 20.sp) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt b/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt similarity index 50% rename from src/main/kotlin/view/views/edge/UndirectedEdgeView.kt rename to src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt index 3f60e40..3a6670b 100644 --- a/src/main/kotlin/view/views/edge/UndirectedEdgeView.kt +++ b/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt @@ -1,4 +1,4 @@ -package view.views.edge +package view.graph.edge import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.* @@ -11,29 +11,40 @@ import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -import viewmodel.EdgeViewModel +import viewmodel.graph.EdgeViewModel +import viewmodel.graph.UndirectedGraphViewModel @Composable -fun UndirectedEdgeView(edgeVM: EdgeViewModel, isWeighted: Boolean) { +fun UndirectedEdgeView( + graphVM: UndirectedGraphViewModel, + edgeVM: EdgeViewModel, + isWeighted: Boolean, +) { val textMeasurer = rememberTextMeasurer() Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { + val vertexSizeZoomed = edgeVM.fromVM.vertexSize * graphVM.zoom + val first = edgeVM.fromVM + val second = edgeVM.toVM drawLine( start = Offset( - edgeVM.fromX + edgeVM.vertexSize / 2, - edgeVM.fromY + edgeVM.vertexSize / 2 + first.offsetX + vertexSizeZoomed / 2, + first.offsetY + vertexSizeZoomed / 2 ), - end = Offset(edgeVM.toX + edgeVM.vertexSize / 2, edgeVM.toY + edgeVM.vertexSize / 2), - strokeWidth = 5f, + end = Offset( + second.offsetX + vertexSizeZoomed / 2, + second.offsetY + vertexSizeZoomed / 2 + ), + strokeWidth = 5f * graphVM.zoom, color = edgeVM.color, ) if (isWeighted) drawText( textMeasurer, edgeVM.weight.toString(), topLeft = Offset( - (edgeVM.fromX + edgeVM.vertexSize + edgeVM.toX) / 2 - edgeVM.weight.toString().length * 5.5f, - (edgeVM.fromY + edgeVM.vertexSize + edgeVM.toY) / 2 - 9 + (first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom, + (first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom ), style = TextStyle(background = Color.White, fontSize = 20.sp) ) diff --git a/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt b/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt new file mode 100644 index 0000000..cf66130 --- /dev/null +++ b/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt @@ -0,0 +1,59 @@ +package view.graph.vertex + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.graph.edge.DirectedEdgeView +import viewmodel.graph.DirectedGraphViewModel +import viewmodel.graph.VertexViewModel + +@Composable +fun DirectedVertexView( + vertexVM: VertexViewModel, + graphVM: DirectedGraphViewModel, +) { + val vertex = vertexVM.vertex + + Box( + modifier = Modifier + .offset( + vertexVM.offsetX.dp, + vertexVM.offsetY.dp + ) + .clip(shape = CircleShape) + .size((vertexVM.vertexSize * graphVM.zoom).dp) + .background(vertexVM.color) + .border((5 * graphVM.zoom).dp, Color.Black, CircleShape) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + vertexVM.x += dragAmount.x / graphVM.zoom + vertexVM.y += dragAmount.y / graphVM.zoom + } + + } + ) { + Text( + text = "$vertex", + fontSize = (graphVM.zoom * 28).sp, + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + .offset(y = (-vertexVM.vertexSize / 10).dp), + ) + } + + for (edgeVM in vertexVM.edges) { + DirectedEdgeView(graphVM, edgeVM, graphVM.isWeighted) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt new file mode 100644 index 0000000..a8b884d --- /dev/null +++ b/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt @@ -0,0 +1,56 @@ +package view.graph.vertex + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import view.graph.edge.UndirectedEdgeView +import viewmodel.graph.UndirectedGraphViewModel +import viewmodel.graph.VertexViewModel + +@Composable +fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { + val vertex = vertexVM.vertex + + Box( + modifier = Modifier + .offset( + vertexVM.offsetX.dp, + vertexVM.offsetY.dp + ) + .clip(shape = CircleShape) + .size((vertexVM.vertexSize * graphVM.zoom).dp) + .background(vertexVM.color) + .border((5 * graphVM.zoom).dp, Color.Black, CircleShape) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + vertexVM.x += dragAmount.x / graphVM.zoom + vertexVM.y += dragAmount.y / graphVM.zoom + } + + } + ) { + Text( + text = "$vertex", + fontSize = (graphVM.zoom * 28).sp, + modifier = Modifier + .fillMaxSize() + .wrapContentSize() + .offset(y = (-vertexVM.vertexSize / 10).dp), + ) + } + + for (edgeVM in vertexVM.edges) { + UndirectedEdgeView(graphVM, edgeVM, graphVM.isWeighted) + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 70e0eb6..62378a0 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -1,33 +1,32 @@ package view.screens -import androidx.compose.foundation.focusable +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import model.algos.ForceAtlas2 import view.common.* -import view.views.DirectedGraphView +import view.graph.DirectedGraphView import viewmodel.MainScreenViewModel -import kotlin.math.exp -import kotlin.math.sign -@OptIn(ExperimentalComposeUiApi::class) +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun DirectedGraphScreen( navController: NavController, @@ -36,36 +35,38 @@ fun DirectedGraphScreen( ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) - var scale by remember { mutableStateOf(1f) } - var rotation by remember { mutableStateOf(0f) } - var offset by remember { mutableStateOf(Offset.Zero) } - val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> - scale *= zoomChange - rotation += rotationChange - offset += offsetChange - } - - fun scale(delta: Int) { - scale = (scale * exp(delta * 0.2f)).coerceIn(0.01f, 1.75f) - } - Box( modifier = Modifier .fillMaxSize() - .transformable(state = state) .onPointerEvent(PointerEventType.Scroll) { - val change = it.changes.first() - val delta = change.scrollDelta.y.toInt().sign - scale(delta) + if (it.changes.first().scrollDelta.y > 0) { + graphVM.zoom = (graphVM.zoom - graphVM.zoom / 8).coerceIn(0.01f, 15f) + } else { + graphVM.zoom = (graphVM.zoom + graphVM.zoom / 8).coerceIn(0.01f, 15f) + + val awtEvent = it.awtEventOrNull + if (awtEvent != null) { + val xPosition = awtEvent.x.toFloat() + val yPosition = awtEvent.y.toFloat() + val pointerVector = + (Offset( + xPosition, + yPosition + ) - (graphVM.canvasSize / 2f)) * (1 / graphVM.zoom) + graphVM.center += pointerVector * 0.15f + } + } + }.pointerInput(Unit) { + detectDragGestures( + matcher = PointerMatcher.Primary + ) { + graphVM.center -= it * (1 / graphVM.zoom) + } + }.pointerHoverIcon(PointerIcon.Hand) + .onSizeChanged { + graphVM.canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) } - .focusable() - .graphicsLayer( - scaleX = scale, - scaleY = scale, - rotationZ = rotation, - translationX = offset.x, - translationY = offset.y - ) + .clipToBounds() ) { DirectedGraphView(graphVM) } diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 768699f..0aff663 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -27,8 +27,9 @@ import view.common.DefaultColors import view.common.bigStyle import view.common.bounceClick import view.common.defaultStyle +import viewmodel.GraphType import viewmodel.MainScreenViewModel -import viewmodel.initType +import viewmodel.SaveType @Composable fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { @@ -39,7 +40,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView val expandedDropDown = remember { mutableStateOf(false) } val selectedOptionTextDropDown = remember { mutableStateOf(optionsDropDown[0]) } - if(!mainScreenViewModel.inited) { + if (!mainScreenViewModel.inited) { mainScreenViewModel.graphInit() mainScreenViewModel.inited = true } @@ -175,7 +176,11 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView ), onClick = { if (graphName != "") { - mainScreenViewModel.addGraph(graphName, selectedOptionTextDropDown.value, initType.Internal) + mainScreenViewModel.addGraph( + graphName, + selectedOptionTextDropDown.value, + SaveType.Internal + ) graphName = "" dialogState.value = false } @@ -257,16 +262,19 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( onClick = { - if(mainScreenViewModel.graphs.typeList[index] == MainScreenViewModel.ViewModelType.Directed){ + if (mainScreenViewModel.graphs.typeList[index] == GraphType.Directed) { mainScreenViewModel.initModel(index) } - if(mainScreenViewModel.graphs.typeList[index] == MainScreenViewModel.ViewModelType.Undirected){ + if (mainScreenViewModel.graphs.typeList[index] == GraphType.Undirected) { mainScreenViewModel.initModel(index) } navController.navigate( when (mainScreenViewModel.graphs.typeList[index]) { - MainScreenViewModel.ViewModelType.Undirected -> {"${Screen.UndirectedGraphScreen.route}/$index"} - MainScreenViewModel.ViewModelType.Directed -> "${Screen.DirectedGraphScreen.route}/$index" + GraphType.Undirected -> { + "${Screen.UndirectedGraphScreen.route}/$index" + } + + GraphType.Directed -> "${Screen.DirectedGraphScreen.route}/$index" } ) diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 61cd85f..f109757 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -1,23 +1,23 @@ package view.screens -import androidx.compose.foundation.focusable -import androidx.compose.foundation.gestures.rememberTransformableState -import androidx.compose.foundation.gestures.transformable +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.PointerMatcher +import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.navigation.NavController import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import model.algos.ForceAtlas2 @@ -25,12 +25,10 @@ import view.common.AddEdgeDialog import view.common.AddVertexDialog import view.common.DefaultShortButton import view.common.DirectedAlgorithmDialog -import view.views.UndirectedGraphView +import view.graph.UndirectedGraphView import viewmodel.MainScreenViewModel -import kotlin.math.exp -import kotlin.math.sign -@OptIn(ExperimentalComposeUiApi::class) +@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun UndirectedGraphScreen( navController: NavController, @@ -39,35 +37,39 @@ fun UndirectedGraphScreen( ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) - var scale by remember { mutableStateOf(1f) } - var rotation by remember { mutableStateOf(0f) } - var offset by remember { mutableStateOf(Offset.Zero) } - val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> - scale *= zoomChange - rotation += rotationChange - offset += offsetChange - } - - fun scale(delta: Int) { - scale = (scale * exp(delta * 0.2f)).coerceIn(0.01f, 1.75f) - } Box(modifier = Modifier .fillMaxSize() - .transformable(state = state) .onPointerEvent(PointerEventType.Scroll) { - val change = it.changes.first() - val delta = change.scrollDelta.y.toInt().sign - scale(delta) + if (it.changes.first().scrollDelta.y > 0) { + graphVM.zoom = (graphVM.zoom - graphVM.zoom / 8).coerceIn(0.01f, 15f) + } else { + graphVM.zoom = (graphVM.zoom + graphVM.zoom / 8).coerceIn(0.01f, 15f) + + val awtEvent = it.awtEventOrNull + if (awtEvent != null) { + val xPosition = awtEvent.x.toFloat() + val yPosition = awtEvent.y.toFloat() + val pointerVector = + (Offset( + xPosition, + yPosition + ) - (graphVM.canvasSize / 2f)) * (1 / graphVM.zoom) + graphVM.center += pointerVector * 0.15f + } + } + }.pointerInput(Unit) { + detectDragGestures( + matcher = PointerMatcher.Primary + ) { + graphVM.center -= it * (1 / graphVM.zoom) + } + }.pointerHoverIcon(PointerIcon.Hand) + .onSizeChanged { + graphVM.canvasSize = Offset(it.width.toFloat(), it.height.toFloat()) } - .focusable() - .graphicsLayer( - scaleX = scale, - scaleY = scale, - rotationZ = rotation, - translationX = offset.x, - translationY = offset.y - )) { + .clipToBounds() + ) { UndirectedGraphView(graphVM) } diff --git a/src/main/kotlin/view/views/edge/DirectedEdgeView.kt b/src/main/kotlin/view/views/edge/DirectedEdgeView.kt deleted file mode 100644 index ce8c2d3..0000000 --- a/src/main/kotlin/view/views/edge/DirectedEdgeView.kt +++ /dev/null @@ -1,99 +0,0 @@ -package view.views.edge - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.rotate -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.drawText -import androidx.compose.ui.text.rememberTextMeasurer -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex -import viewmodel.EdgeViewModel -import kotlin.math.atan2 - -@Composable -fun DirectedEdgeView(edgeVM: EdgeViewModel, isWeighted: Boolean) { - - val textMeasurer = rememberTextMeasurer() - - Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) { - drawLine( - start = Offset( - edgeVM.fromX + edgeVM.vertexSize / 2, - edgeVM.fromY + edgeVM.vertexSize / 2 - ), - end = Offset( - edgeVM.toX + edgeVM.vertexSize / 2, - edgeVM.toY + edgeVM.vertexSize / 2 - ), - strokeWidth = 6f, - color = edgeVM.color, - ) - rotate( - degrees = ((57.2958 * (atan2( - ((edgeVM.fromY - edgeVM.toY).toDouble()), - ((edgeVM.fromX - edgeVM.toX).toDouble()) - ))).toFloat()), - pivot = Offset( - edgeVM.toX + edgeVM.vertexSize / 2, - edgeVM.toY + edgeVM.vertexSize / 2 - ) - ) { - drawRect( - color = edgeVM.color, - size = Size(5f, 16f), - topLeft = Offset( - edgeVM.toX + edgeVM.vertexSize / 2 + 65, - edgeVM.toY + edgeVM.vertexSize / 2 - 8f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 14f), - topLeft = Offset( - edgeVM.toX + edgeVM.vertexSize / 2 + 60, - edgeVM.toY + edgeVM.vertexSize / 2 - 7f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 12f), - topLeft = Offset( - edgeVM.toX + edgeVM.vertexSize / 2 + 55, - edgeVM.toY + edgeVM.vertexSize / 2 - 6f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 10f), - topLeft = Offset( - edgeVM.toX + edgeVM.vertexSize / 2 + 50, - edgeVM.toY + edgeVM.vertexSize / 2 - 5f - ), - ) - drawRect( - color = edgeVM.color, - size = Size(5f, 8f), - topLeft = Offset( - edgeVM.toX + edgeVM.vertexSize / 2 + 45, - edgeVM.toY + edgeVM.vertexSize / 2 - 4f - ), - ) - } - if (isWeighted) { - drawText( - textMeasurer, edgeVM.weight.toString(), - topLeft = Offset( - (edgeVM.fromX + edgeVM.vertexSize + edgeVM.toX) / 2 - edgeVM.weight.toString().length * 5.5f, - (edgeVM.fromY + edgeVM.vertexSize + edgeVM.toY) / 2 - 9 - ), - style = TextStyle(background = Color.White, fontSize = 20.sp) - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt b/src/main/kotlin/view/views/vertex/DirectedVertexView.kt deleted file mode 100644 index 91ca093..0000000 --- a/src/main/kotlin/view/views/vertex/DirectedVertexView.kt +++ /dev/null @@ -1,53 +0,0 @@ -package view.views.vertex - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import view.common.DefaultColors -import view.views.edge.DirectedEdgeView -import viewmodel.DirectedGraphViewModel -import viewmodel.VertexViewModel -import kotlin.math.roundToInt - -@Composable -fun DirectedVertexView(vertexVM: VertexViewModel, graphVM: DirectedGraphViewModel) { - val vertex = vertexVM.vertex - Box(modifier = Modifier - .offset { IntOffset(vertexVM.x.roundToInt(), vertexVM.y.roundToInt()) } - .clip(shape = CircleShape) - .size(vertexVM.vertexSize.dp) - .background(vertexVM.color) - .border(5.dp, Color.Black, CircleShape) - .pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() - vertexVM.x += dragAmount.x - vertexVM.y += dragAmount.y - } - - } - ) { - Text( - text = "$vertex", - fontSize = 28.sp, - modifier = Modifier - .fillMaxSize() - .wrapContentSize(), - ) - } - - for (edgeVM in vertexVM.edges) { - DirectedEdgeView(edgeVM, graphVM.isWeighted) - } -} \ No newline at end of file diff --git a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt deleted file mode 100644 index 3cfa81d..0000000 --- a/src/main/kotlin/view/views/vertex/UndirectedVertexView.kt +++ /dev/null @@ -1,60 +0,0 @@ -package view.views.vertex - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.drawText -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex -import view.common.DefaultColors -import view.views.edge.DirectedEdgeView -import view.views.edge.UndirectedEdgeView -import viewmodel.UndirectedGraphViewModel -import viewmodel.VertexViewModel -import kotlin.math.roundToInt - -@Composable -fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { - val vertex = vertexVM.vertex - - Box(modifier = Modifier - .offset { IntOffset(vertexVM.x.roundToInt(), vertexVM.y.roundToInt()) } - .clip(shape = CircleShape) - .size(vertexVM.vertexSize.dp) - .background(vertexVM.color) - .border(5.dp, Color.Black, CircleShape) - .pointerInput(Unit) { - detectDragGestures { change, dragAmount -> - change.consume() - vertexVM.x += dragAmount.x - vertexVM.y += dragAmount.y - } - - } - ) { - Text( - text = "$vertex", - fontSize = 28.sp, - modifier = Modifier - .fillMaxSize() - .wrapContentSize(), - ) - } - - for (edgeVM in vertexVM.edges) { - UndirectedEdgeView(edgeVM, graphVM.isWeighted) - } -} \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 5ff6511..e6a1f80 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -2,33 +2,40 @@ package viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel +import viewmodel.graph.DirectedGraphViewModel +import viewmodel.graph.UndirectedGraphViewModel import java.sql.DriverManager import java.sql.SQLException -enum class initType { +enum class SaveType { SQLite, CSV, Neo4j, Internal } +enum class GraphType() { + Undirected, + Directed, +} + class MainScreenViewModel : ViewModel() { val graphs = GraphStorage() internal var inited = false private val DB_DRIVER = "jdbc:sqlite" - fun addGraph(name: String, type: String, initType: initType) { + fun addGraph(name: String, type: String, saveType: SaveType) { when (type) { "undirected" -> { - graphs.typeList.add(ViewModelType.Undirected) + graphs.typeList.add(GraphType.Undirected) val graphVM = UndirectedGraphViewModel(name) - graphVM.inType = initType + graphVM.saveType = saveType graphs.undirectedGraphs.add(graphVM) } "directed" -> { - graphs.typeList.add(ViewModelType.Directed) + graphs.typeList.add(GraphType.Directed) val graphVM = DirectedGraphViewModel(name) - graphVM.inType = initType + graphVM.saveType = saveType graphs.directedGraphs.add(graphVM) } @@ -36,11 +43,11 @@ class MainScreenViewModel : ViewModel() { } fun initModel(index: Int) { - if (graphs.typeList[index] == ViewModelType.Directed) { + if (graphs.typeList[index] == GraphType.Directed) { val graph = graphs.getDirected(index) if (graph.initedGraph) return else graph.initedGraph = true - if (graph.inType == initType.SQLite) { + if (graph.saveType == SaveType.SQLite) { val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } @@ -65,11 +72,11 @@ class MainScreenViewModel : ViewModel() { } } } - if (graphs.typeList[index] == ViewModelType.Undirected) { + if (graphs.typeList[index] == GraphType.Undirected) { val graph = graphs.getUndirected(index) if (graph.initedGraph) return else graph.initedGraph = true - if (graph.inType == initType.SQLite) { + if (graph.saveType == SaveType.SQLite) { val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } @@ -116,27 +123,22 @@ class MainScreenViewModel : ViewModel() { val resSet = getGraphs.executeQuery() while (resSet.next()) { if (resSet.getString("type") == "Directed") { - addGraph(resSet.getString("name"), "directed", initType.SQLite) + addGraph(resSet.getString("name"), "directed", SaveType.SQLite) } else if (resSet.getString("type") == "Undirected") { - addGraph(resSet.getString("name"), "undirected", initType.SQLite) + addGraph(resSet.getString("name"), "undirected", SaveType.SQLite) } } connection.close() } - enum class ViewModelType() { - Undirected, - Directed, - } - inner class GraphStorage() { fun getName(index: Int): String { when (graphs.typeList[index]) { - ViewModelType.Undirected -> { + GraphType.Undirected -> { return graphs.undirectedGraphs[findGraph(index)].name } - ViewModelType.Directed -> { + GraphType.Directed -> { return graphs.directedGraphs[findGraph(index)].name } } @@ -145,12 +147,12 @@ class MainScreenViewModel : ViewModel() { internal fun findGraph(index: Int): Int { var indexAr = 0 when (graphs.typeList[index]) { - ViewModelType.Undirected -> { - for (i in 0..index) if (graphs.typeList[i] == ViewModelType.Undirected) indexAr += 1 + GraphType.Undirected -> { + for (i in 0..index) if (graphs.typeList[i] == GraphType.Undirected) indexAr += 1 } - ViewModelType.Directed -> { - for (i in 0..index) if (graphs.typeList[i] == ViewModelType.Directed) indexAr += 1 + GraphType.Directed -> { + for (i in 0..index) if (graphs.typeList[i] == GraphType.Directed) indexAr += 1 } } return indexAr - 1 @@ -175,12 +177,12 @@ class MainScreenViewModel : ViewModel() { } } when (graphs.typeList[index]) { - ViewModelType.Undirected -> { + GraphType.Undirected -> { graphs.undirectedGraphs.removeAt(findGraph(index)) graphs.typeList.removeAt(index) } - ViewModelType.Directed -> { + GraphType.Directed -> { graphs.directedGraphs.removeAt(findGraph(index)) graphs.typeList.removeAt(index) } @@ -198,6 +200,6 @@ class MainScreenViewModel : ViewModel() { var undirectedGraphs = mutableStateListOf>() var directedGraphs = mutableStateListOf>() - var typeList = mutableStateListOf() + var typeList = mutableStateListOf() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt similarity index 86% rename from src/main/kotlin/viewmodel/AbstractGraphViewModel.kt rename to src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt index 02eb7e4..66db5b3 100644 --- a/src/main/kotlin/viewmodel/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt @@ -1,17 +1,21 @@ -package viewmodel +package viewmodel.graph import Dijkstra import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel +import height import model.algos.FindCycle import model.algos.FordBellman import model.graph.Graph import model.graph.Edge import view.common.DefaultColors -import javax.swing.text.StyledEditorKit.BoldAction +import viewmodel.GraphType +import viewmodel.SaveType +import width import kotlin.random.Random abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewModel() { @@ -37,10 +41,15 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM } return result.toList() } + var saveType = SaveType.SQLite + abstract val graphType: GraphType + var zoom by mutableStateOf(1f) + var canvasSize by mutableStateOf(Offset(400f, 400f)) + var center by mutableStateOf(Offset((width / 2).toFloat(), (height / 2).toFloat())) init { for (vertex in graphModel.entries) { - graphVM[vertex.key] = VertexViewModel(vertex.key) + graphVM[vertex.key] = VertexViewModel(vertex.key, graphVM = this) } for (vertex in graphModel.entries) { for (edge in vertex.value) { @@ -105,7 +114,7 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM size += 1 graphVM.putIfAbsent( vertex, - VertexViewModel(vertex, centerCoordinates = centerCoordinates), + VertexViewModel(vertex, graphVM = this, centerCoordinates = centerCoordinates), ) graphModel.addVertex(vertex) } diff --git a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt similarity index 98% rename from src/main/kotlin/viewmodel/DirectedGraphViewModel.kt rename to src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt index d243074..4ec7444 100644 --- a/src/main/kotlin/viewmodel/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt @@ -1,4 +1,4 @@ -package viewmodel +package viewmodel.graph import androidx.compose.ui.graphics.Color import model.algos.StrongConnections @@ -7,6 +7,7 @@ import de.tudarmstadt.lt.cw.graph.ArrayBackedGraphCW import de.tudarmstadt.lt.cw.graph.Graph import model.graph.DirectedGraph import model.graph.Edge +import viewmodel.GraphType import java.sql.DriverManager import java.sql.SQLException import kotlin.random.Random @@ -16,9 +17,8 @@ class DirectedGraphViewModel( val graph: DirectedGraph = DirectedGraph() ) : AbstractGraphViewModel(name, graph) { private val DB_DRIVER = "jdbc:sqlite" - - var inType = initType.Internal var initedGraph = false + override val graphType = GraphType.Directed override fun addEdge(from: V, to: V, weight: Int) { val source: VertexViewModel diff --git a/src/main/kotlin/viewmodel/EdgeViewModel.kt b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt similarity index 96% rename from src/main/kotlin/viewmodel/EdgeViewModel.kt rename to src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index 9e819e8..2a60329 100644 --- a/src/main/kotlin/viewmodel/EdgeViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -1,4 +1,4 @@ -package viewmodel +package viewmodel.graph import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -15,6 +15,7 @@ class EdgeViewModel( ViewModel() { val fromVM = vertexFromVM val toVM = vertexToVM + val fromX get() = fromVM.x val fromY diff --git a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt similarity index 98% rename from src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt rename to src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt index 6a8fd6f..9027db1 100644 --- a/src/main/kotlin/viewmodel/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt @@ -1,10 +1,11 @@ -package viewmodel +package viewmodel.graph import androidx.compose.ui.graphics.Color import model.algos.Prim import model.algos.findBridges import model.graph.UndirectedGraph import model.graph.Edge +import viewmodel.GraphType import java.sql.DriverManager import java.sql.SQLException @@ -13,8 +14,8 @@ class UndirectedGraphViewModel( val graph: UndirectedGraph = UndirectedGraph() ) : AbstractGraphViewModel(name, graph) { private val DB_DRIVER = "jdbc:sqlite" - var inType = initType.Internal var initedGraph = false + override val graphType = GraphType.Undirected override fun addEdge(from: V, to: V, weight: Int) { val source: VertexViewModel diff --git a/src/main/kotlin/viewmodel/VertexViewModel.kt b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt similarity index 79% rename from src/main/kotlin/viewmodel/VertexViewModel.kt rename to src/main/kotlin/viewmodel/graph/VertexViewModel.kt index 12075e8..9e70e50 100644 --- a/src/main/kotlin/viewmodel/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -1,4 +1,4 @@ -package viewmodel +package viewmodel.graph import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -13,6 +13,7 @@ import kotlin.random.Random class VertexViewModel( _vertex: V, _edges: MutableList> = mutableListOf(), + graphVM: AbstractGraphViewModel, centerCoordinates: Boolean = false ) : ViewModel() { @@ -20,6 +21,7 @@ class VertexViewModel( var edges = mutableStateListOf>() var x by mutableStateOf(0f) var y by mutableStateOf(0f) + val graphVM = graphVM init { for (edge in _edges) { @@ -34,6 +36,11 @@ class VertexViewModel( } } + val offsetX + get() = (graphVM.canvasSize.x / 2) + ((x - graphVM.center.x) * graphVM.zoom) + val offsetY + get() = (graphVM.canvasSize.y / 2) + ((y - graphVM.center.y) * graphVM.zoom) + val vertexSize = 60f var color by mutableStateOf(DefaultColors.primary) val degree diff --git a/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt b/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt new file mode 100644 index 0000000..552439b --- /dev/null +++ b/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt @@ -0,0 +1,4 @@ +package viewmodel.io.neo4j + +class Neo4jRepositoryr { +} \ No newline at end of file diff --git a/src/test/kotlin/io/Neo4jTest.kt b/src/test/kotlin/io/Neo4jTest.kt new file mode 100644 index 0000000..dfdafe4 --- /dev/null +++ b/src/test/kotlin/io/Neo4jTest.kt @@ -0,0 +1,4 @@ +package io + +class Neo4jTest { +} \ No newline at end of file From 577d0f313d5c01ad0650037ac13a6cf54fef9361 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 28 May 2024 11:33:07 -0400 Subject: [PATCH 147/172] fix: SQLite graph initialization source bug fix --- src/main/kotlin/view/screens/MainScreen.kt | 4 ++-- src/main/kotlin/viewmodel/MainScreenViewModel.kt | 4 ++-- src/test/kotlin/SQLiteIntegrationTest.kt | 3 +-- src/test/kotlin/algos/DijkstraTest.kt | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index 81c612c..1e3d5ed 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -259,10 +259,10 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Button( onClick = { if(mainScreenViewModel.graphs.typeList[index] == MainScreenViewModel.ViewModelType.Directed){ - mainScreenViewModel.initModel(index) + mainScreenViewModel.initModel(index, "storage") } if(mainScreenViewModel.graphs.typeList[index] == MainScreenViewModel.ViewModelType.Undirected){ - mainScreenViewModel.initModel(index) + mainScreenViewModel.initModel(index, "storage") } navController.navigate( when (mainScreenViewModel.graphs.typeList[index]) { diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 048847f..3ea9b48 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -35,13 +35,13 @@ class MainScreenViewModel : ViewModel() { } } - fun initModel(index: Int) { + fun initModel(index: Int, source: String) { if (graphs.typeList[index] == ViewModelType.Directed) { val graph = graphs.getDirected(index) if (graph.initedGraph) return else graph.initedGraph = true if (graph.inType == initType.SQLite) { - val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } val resVertex = getVertex.executeQuery() diff --git a/src/test/kotlin/SQLiteIntegrationTest.kt b/src/test/kotlin/SQLiteIntegrationTest.kt index f86ddf7..155c87e 100644 --- a/src/test/kotlin/SQLiteIntegrationTest.kt +++ b/src/test/kotlin/SQLiteIntegrationTest.kt @@ -27,7 +27,7 @@ internal class SQLiteIntegrationTest { val graphVM = MainScreenViewModel() graphVM.graphInit("test") - graphVM.initModel(0) + graphVM.initModel(0, "test") val loadedGraph = graphVM.graphs.getDirected(0).graph val result = Dijkstra(loadedGraph, 4).dijkstra("1", "4") val shortestLengthExpected = 6 @@ -36,7 +36,6 @@ internal class SQLiteIntegrationTest { shortestLengthActual += i.weight } assertNotNull(shortestLengthActual) - println("$shortestLengthActual, $shortestLengthExpected") assertEquals( shortestLengthExpected, shortestLengthActual, "Dijkstra must return weight of the shortest path" diff --git a/src/test/kotlin/algos/DijkstraTest.kt b/src/test/kotlin/algos/DijkstraTest.kt index 77075a9..a1bca6a 100644 --- a/src/test/kotlin/algos/DijkstraTest.kt +++ b/src/test/kotlin/algos/DijkstraTest.kt @@ -30,7 +30,6 @@ internal class DijkstraTest { shortestLengthActual += i.weight } assertNotNull(shortestLengthActual) - println("$shortestLengthActual, $shortestLengthExpected") assertEquals( shortestLengthExpected, shortestLengthActual, "Dijkstra must return weight of the shortest path" From 501a8af6d635a6a60d3d0c0c259c2384e25c68fc Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Tue, 28 May 2024 15:54:08 -0400 Subject: [PATCH 148/172] upd: update localisation & text view improvement --- src/main/kotlin/settings.json | 2 +- src/main/kotlin/view/common/AddEdgeDialog.kt | 4 +- .../kotlin/view/common/AddVertexDialog.kt | 6 +- src/main/kotlin/view/common/DefaultButton.kt | 6 +- .../kotlin/view/common/DefaultShortButton.kt | 5 +- .../view/common/DirectedAlgorithmDialog.kt | 4 +- src/main/kotlin/view/common/styling.kt | 4 ++ .../view/screens/DirectedGraphScreen.kt | 54 ++++++++++++----- .../view/screens/UndirectedGraphScreen.kt | 56 +++++++++++------ src/main/resources/localisation/cn-CN.json | 60 +++++++++++++++++++ src/main/resources/localisation/en-US.json | 60 +++++++++++++++++++ src/main/resources/localisation/ru-RU.json | 60 +++++++++++++++++++ 12 files changed, 275 insertions(+), 46 deletions(-) diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json index 6e3de82..c38add4 100644 --- a/src/main/kotlin/settings.json +++ b/src/main/kotlin/settings.json @@ -1 +1 @@ -{"language":"en-US"} \ No newline at end of file +{"language":"cn-CN"} \ No newline at end of file diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index 4fcf185..88ccdb3 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -147,9 +147,9 @@ fun AddEdgeDialog( visible = false } - DefaultButton(onClick, "add_edge") + DefaultButton(onClick, "add_edge", defaultStyle) Spacer(modifier = Modifier.width(30.dp)) - DefaultButton(onClose, "back", Color.Red) + DefaultButton(onClose, "back", defaultStyle, Color.Red) } } } diff --git a/src/main/kotlin/view/common/AddVertexDialog.kt b/src/main/kotlin/view/common/AddVertexDialog.kt index d193780..bb5190f 100644 --- a/src/main/kotlin/view/common/AddVertexDialog.kt +++ b/src/main/kotlin/view/common/AddVertexDialog.kt @@ -29,7 +29,7 @@ fun AddVertexDialog( visible = visible, title = "New Vertices", onCloseRequest = onClose, - state = rememberDialogState(height = 320.dp, width = 570.dp) + state = rememberDialogState(height = 420.dp, width = 800.dp) ) { var verticesNumber by remember { mutableStateOf("1") } val textWidth = 130.dp @@ -83,9 +83,9 @@ fun AddVertexDialog( visible = false } - DefaultButton(onClick, "add_edge") + DefaultButton(onClick, "add_edge", defaultStyle) Spacer(modifier = Modifier.width(30.dp)) - DefaultButton(onClose, "back", Color.Red) + DefaultButton(onClose, "back", defaultStyle, Color.Red) } } } diff --git a/src/main/kotlin/view/common/DefaultButton.kt b/src/main/kotlin/view/common/DefaultButton.kt index 51392c7..705bfd1 100644 --- a/src/main/kotlin/view/common/DefaultButton.kt +++ b/src/main/kotlin/view/common/DefaultButton.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import localisation.localisation @@ -17,9 +18,10 @@ import localisation.localisation fun DefaultButton( onClick: () -> Unit, localisationCode: String, + style: TextStyle, color: Color = DefaultColors.primary, width: androidx.compose.ui.unit.Dp = 240.dp, - height: androidx.compose.ui.unit.Dp = 80.dp + height: androidx.compose.ui.unit.Dp = 80.dp, ) { Button( onClick = onClick, @@ -29,6 +31,6 @@ fun DefaultButton( .size(width, height), colors = ButtonDefaults.buttonColors(backgroundColor = color) ) { - Text(localisation(localisationCode), style = defaultStyle) + Text(localisation(localisationCode), style = style) } } \ No newline at end of file diff --git a/src/main/kotlin/view/common/DefaultShortButton.kt b/src/main/kotlin/view/common/DefaultShortButton.kt index 4e801b5..ef6726a 100644 --- a/src/main/kotlin/view/common/DefaultShortButton.kt +++ b/src/main/kotlin/view/common/DefaultShortButton.kt @@ -2,13 +2,16 @@ package view.common import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp @Composable fun DefaultShortButton( onClick: () -> Unit, localisationCode: String, + style: TextStyle, color: Color = DefaultColors.primary, + ) { - DefaultButton(onClick, localisationCode, color, 220.dp, 70.dp) + DefaultButton(onClick, localisationCode, style, color, 220.dp, 70.dp, ) } \ No newline at end of file diff --git a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt index 35c205c..a1dace1 100644 --- a/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt +++ b/src/main/kotlin/view/common/DirectedAlgorithmDialog.kt @@ -90,9 +90,9 @@ fun DirectedAlgorithmDialog( } else { {} } - DefaultButton(onClick, "start") + DefaultButton(onClick, "start", defaultStyle) Spacer(modifier = Modifier.width(30.dp)) - DefaultButton(onCloseRequest, "back", Color.Red) + DefaultButton(onCloseRequest, "back", defaultStyle, Color.Red) } } } diff --git a/src/main/kotlin/view/common/styling.kt b/src/main/kotlin/view/common/styling.kt index 1d01901..3d6aef2 100644 --- a/src/main/kotlin/view/common/styling.kt +++ b/src/main/kotlin/view/common/styling.kt @@ -2,9 +2,13 @@ package view.common import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp val defaultStyle = TextStyle(fontSize = 28.sp) +val microDickSize = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Center) +val smallDickSize = TextStyle(fontSize = 22.sp, textAlign = TextAlign.Center) +val mediumDickSize = TextStyle(fontSize = 26.sp, textAlign = TextAlign.Center) val bigStyle = TextStyle(fontSize = 50.sp) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 62378a0..cc9a81d 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -3,9 +3,8 @@ package view.screens import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher -import androidx.compose.foundation.gestures.* +import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* -import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -21,6 +20,7 @@ import androidx.navigation.NavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch +import localisation.getLocalisation import model.algos.ForceAtlas2 import view.common.* import view.graph.DirectedGraphView @@ -34,7 +34,7 @@ fun DirectedGraphScreen( graphId: Int ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) - + val language = getLocalisation() Box( modifier = Modifier .fillMaxSize() @@ -72,8 +72,6 @@ fun DirectedGraphScreen( } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { - Text("directed") - var isOpenedVertexMenu by remember { mutableStateOf(false) } var isOpenedEdgeMenu by remember { mutableStateOf(false) } var isOpenedDijkstraMenu by remember { mutableStateOf(false) } @@ -81,19 +79,22 @@ fun DirectedGraphScreen( var isVisualizationRunning by remember { mutableStateOf(false) } // To MainScreen - DefaultShortButton({ navController.popBackStack() }, "home") + DefaultShortButton({ navController.popBackStack() }, "home", defaultStyle) Spacer(modifier = Modifier.height(10.dp)) // Add vertex Button - DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex") + DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // Add edge Button - DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") + DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge", defaultStyle) Spacer(modifier = Modifier.height(10.dp)) // Save button - DefaultShortButton({ graphVM.saveSQLite() }, "save") + DefaultShortButton({ graphVM.saveSQLite() }, "save", defaultStyle) Spacer(modifier = Modifier.height(16.dp)) // Visualization Button @@ -108,30 +109,51 @@ fun DirectedGraphScreen( } else { scope.coroutineContext.cancelChildren() } - }, "visualize", + }, "visualize", defaultStyle, if (isVisualizationRunning) Color.Red else Color(0xffFFCB32) ) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton({ graphVM.resetColors() }, "reset", Color.LightGray) + DefaultShortButton({ graphVM.resetColors() }, "reset", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + else -> defaultStyle }, Color.LightGray) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton({ graphVM.drawStrongConnections() }, "find_strong_connections") + DefaultShortButton({ graphVM.drawStrongConnections() }, "find_strong_connections", when(language) { + ("en-US") -> smallDickSize + ("ru-RU") -> microDickSize + ("cn-CN") -> microDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton({ graphVM.chinaWhisperCluster() }, "find_clusters") + DefaultShortButton({ graphVM.chinaWhisperCluster() }, "find_clusters", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // Dijkstra Button - DefaultShortButton({ isOpenedDijkstraMenu = !isOpenedDijkstraMenu }, "dijkstra") + DefaultShortButton({ isOpenedDijkstraMenu = !isOpenedDijkstraMenu }, "dijkstra", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + ("cn-CN") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // FordBellman Button - DefaultShortButton({ isOpenedFordBellmanMenu = !isOpenedFordBellmanMenu }, "ford_bellman") + DefaultShortButton({ isOpenedFordBellmanMenu = !isOpenedFordBellmanMenu }, "ford_bellman", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microDickSize + ("cn-CN") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // Cycles Button - DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles") + DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> mediumDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // Add vertex Dialog diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index f109757..17c2585 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.* -import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -20,11 +19,9 @@ import androidx.navigation.NavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch +import localisation.getLocalisation import model.algos.ForceAtlas2 -import view.common.AddEdgeDialog -import view.common.AddVertexDialog -import view.common.DefaultShortButton -import view.common.DirectedAlgorithmDialog +import view.common.* import view.graph.UndirectedGraphView import viewmodel.MainScreenViewModel @@ -36,7 +33,7 @@ fun UndirectedGraphScreen( graphId: Int ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) - + val language = getLocalisation() Box(modifier = Modifier .fillMaxSize() @@ -74,8 +71,6 @@ fun UndirectedGraphScreen( } Column(modifier = Modifier.zIndex(1f).padding(16.dp).width(300.dp)) { - Text("Undirected") - var isOpenedVertexMenu by remember { mutableStateOf(false) } var isOpenedEdgeMenu by remember { mutableStateOf(false) } var isDijkstraMenu by remember { mutableStateOf(false) } @@ -83,19 +78,22 @@ fun UndirectedGraphScreen( var isVisualizationRunning by remember { mutableStateOf(false) } // To MainScreen - DefaultShortButton({ navController.popBackStack() }, "home") + DefaultShortButton({ navController.popBackStack() }, "home", defaultStyle) Spacer(modifier = Modifier.height(10.dp)) // Add vertex Button - DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex") + DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // Add edge button - DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge") + DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge", defaultStyle) Spacer(modifier = Modifier.height(16.dp)) // Save button - DefaultShortButton({ graphVM.saveSQLite() }, "save") + DefaultShortButton({ graphVM.saveSQLite() }, "save", defaultStyle) Spacer(modifier = Modifier.height(10.dp)) // Visualization Button @@ -110,30 +108,50 @@ fun UndirectedGraphScreen( } else { scope.coroutineContext.cancelChildren() } - }, "visualize", + }, "visualize", defaultStyle, if (isVisualizationRunning) Color.Red else Color(0xffFFCB32) ) Spacer(modifier = Modifier.height(10.dp)) // Reset colors Button - DefaultShortButton({ graphVM.resetColors() }, "reset", Color.LightGray) + DefaultShortButton({ graphVM.resetColors() }, "reset", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + else -> defaultStyle }, Color.LightGray) Spacer(modifier = Modifier.height(10.dp)) // Dijkstra Button - DefaultShortButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra") + DefaultShortButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + ("cn-CN") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // FordBellman Button - DefaultShortButton({ isFordBellmanMenu = !isFordBellmanMenu }, "ford_bellman") + DefaultShortButton({ isFordBellmanMenu = !isFordBellmanMenu }, "ford_bellman", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microDickSize + ("cn-CN") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.drawMst() }, "find_mst") + DefaultShortButton(onClick = { graphVM.drawMst() }, "find_mst", when(language) { + ("en-US") -> smallDickSize + ("ru-RU") -> microDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles") + DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> mediumDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.drawBridges() }, "find_bridges") + DefaultShortButton(onClick = { graphVM.drawBridges() }, "find_bridges", when(language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallDickSize + else -> defaultStyle }) Spacer(modifier = Modifier.height(10.dp)) // Add vertex Dialog diff --git a/src/main/resources/localisation/cn-CN.json b/src/main/resources/localisation/cn-CN.json index 44439e6..ad936f3 100644 --- a/src/main/resources/localisation/cn-CN.json +++ b/src/main/resources/localisation/cn-CN.json @@ -39,6 +39,66 @@ { "code": "enter_new_graph_name", "localisation": "创建新图表" + }, + { + "code": "save", + "localisation": "储蓄" + }, + { + "code": "visualize", + "localisation": "图形布局" + }, + { + "code": "reset", + "localisation": "重置颜色" + }, + { + "code": "find_strong_connections", + "localisation": "强连通性的组成部分" + }, + { + "code": "find_clusters", + "localisation": "查找集群" + }, + { + "code": "dijkstra", + "localisation": "Dijkstra的算法" + }, + { + "code": "ford_bellman", + "localisation": "福特-贝尔曼算法" + }, + { + "code": "find_cycles", + "localisation": "查找周期" + }, + { + "code": "find_mst", + "localisation": "最小生成树" + }, + { + "code": "find_bridges", + "localisation": "寻找桥梁" + }, + { + "code": "center_coordinates", + "localisation": "中心坐标" + }, + { + "code": "number", + "localisation": "数量" + }, + { + "code": "unweighted", + "localisation": "未加权" + }, + { + "code": "start", + "localisation": "开始" + }, + { + "code": "end", + "localisation": "结局" } ] } \ No newline at end of file diff --git a/src/main/resources/localisation/en-US.json b/src/main/resources/localisation/en-US.json index ea8d952..e80e563 100644 --- a/src/main/resources/localisation/en-US.json +++ b/src/main/resources/localisation/en-US.json @@ -39,6 +39,66 @@ { "code": "enter_new_graph_name", "localisation": "Create new graph" + }, + { + "code": "save", + "localisation": "Save" + }, + { + "code": "visualize", + "localisation": "Visualize" + }, + { + "code": "reset", + "localisation": "Reset" + }, + { + "code": "find_strong_connections", + "localisation": "Strong Connections" + }, + { + "code": "find_clusters", + "localisation": "Find Clusters" + }, + { + "code": "dijkstra", + "localisation": "Dijkstra Path" + }, + { + "code": "ford_bellman", + "localisation": "Ford Bellman" + }, + { + "code": "find_cycles", + "localisation": "Find Cycles" + }, + { + "code": "find_mst", + "localisation": "Minimal Spanning Tree" + }, + { + "code": "find_bridges", + "localisation": "Find Bridges" + }, + { + "code": "center_coordinates", + "localisation": "Center Coordinates" + }, + { + "code": "number", + "localisation": "Amount" + }, + { + "code": "unweighted", + "localisation": "Unweighted" + }, + { + "code": "start", + "localisation": "Start" + }, + { + "code": "end", + "localisation": "End" } ] } \ No newline at end of file diff --git a/src/main/resources/localisation/ru-RU.json b/src/main/resources/localisation/ru-RU.json index 5311dc7..912b0ec 100644 --- a/src/main/resources/localisation/ru-RU.json +++ b/src/main/resources/localisation/ru-RU.json @@ -39,6 +39,66 @@ { "code": "enter_new_graph_name", "localisation": "Создание нового графа" + }, + { + "code": "save", + "localisation": "Сохранить" + }, + { + "code": "visualize", + "localisation": "Раскладка" + }, + { + "code": "reset", + "localisation": "Сбросить цвета" + }, + { + "code": "find_strong_connections", + "localisation": "Компоненты Сильной Связности" + }, + { + "code": "find_clusters", + "localisation": "Найти Кластера" + }, + { + "code": "dijkstra", + "localisation": "Алгоритм Дейкстры" + }, + { + "code": "ford_bellman", + "localisation": "Алгоритм Форда Беллмана" + }, + { + "code": "find_cycles", + "localisation": "Найти циклы" + }, + { + "code": "find_mst", + "localisation": "Минимальное остовное дерево" + }, + { + "code": "find_bridges", + "localisation": "Найти мосты" + }, + { + "code": "center_coordinates", + "localisation": "Центрировать координаты" + }, + { + "code": "number", + "localisation": "Количество" + }, + { + "code": "unweighted", + "localisation": "Невзвешенный" + }, + { + "code": "start", + "localisation": "Начало" + }, + { + "code": "end", + "localisation": "Конец" } ] } \ No newline at end of file From c7c3c14cccedfdc54078c39ad8eb690b5c573d00 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 28 May 2024 23:20:04 +0300 Subject: [PATCH 149/172] feat: pagerank implemented --- .../kotlin/model/algos/BetweenesCentrality.kt | 119 ++++-------------- 1 file changed, 23 insertions(+), 96 deletions(-) diff --git a/src/main/kotlin/model/algos/BetweenesCentrality.kt b/src/main/kotlin/model/algos/BetweenesCentrality.kt index ba7e4c3..299f0c1 100644 --- a/src/main/kotlin/model/algos/BetweenesCentrality.kt +++ b/src/main/kotlin/model/algos/BetweenesCentrality.kt @@ -1,105 +1,32 @@ package model.algos -import model.graph.UndirectedGraph +import model.graph.Graph -//object BetweenesCentrality { -// fun betweennessCentrality(graph: UndirectedGraph): Map { -// val centrality = mutableMapOf() -// for (v in graph.vertices) { -// centrality[v] = 0.0 -// } -// -// for (s in graph.vertices) { -// val stack = Stack() -// val predecessors = mutableMapOf>() -// val shortestPaths = mutableMapOf() -// val distance = mutableMapOf() -// val dependency = mutableMapOf() -// -// for (v in graph.vertices) { -// predecessors[v] = mutableListOf() -// shortestPaths[v] = 0 -// distance[v] = -1 -// dependency[v] = 0.0 -// } -// -// shortestPaths[s] = 1 -// distance[s] = 0 -// val queue: Queue = LinkedList() -// queue.add(s) -// -// while (queue.isNotEmpty()) { -// val v = queue.poll() -// stack.push(v) -// for (edge in graph.edgesOf(v)) { -// val w = edge.to -// if (distance[w]!! < 0) { -// queue.add(w) -// distance[w] = distance[v]!! + 1 -// } -// if (distance[w] == distance[v]!! + 1) { -// shortestPaths[w] = shortestPaths[w]!! + shortestPaths[v]!! -// predecessors[w]!!.add(v) -// } -// } -// } -// -// while (stack.isNotEmpty()) { -// val w = stack.pop() -// for (v in predecessors[w]!!) { -// dependency[v] = dependency[v]!! + (shortestPaths[v]!!.toDouble() / shortestPaths[w]!!) * (1 + dependency[w]!!) -// } -// if (w != s) { -// centrality[w] = centrality[w]!! + dependency[w]!! -// } -// } -// } -// -// return centrality -// } -//} - -/* -* to view the centrality value for each vertex use: -* `val centrality = graph.betweennessCentrality() -* centrality.forEach { (vertex, value) -> -* println("Vertex: $vertex, Betweenness Centrality: $value") }` -* */ object BetweenesCentrality { - fun hits(iterations: Int = 100, graph: UndirectedGraph): Pair, Map> { - val authority = mutableMapOf() - val hub = mutableMapOf() - - // Инициализация всех авторитетов и хабов значением 1.0 - for (v in graph.vertices) { - authority[v] = 1.0 - hub[v] = 1.0 - } - - for (i in 0 until iterations) { - val newAuthority = mutableMapOf() - val newHub = mutableMapOf() - - // Обновление авторитетов - for (v in graph.vertices) { - newAuthority[v] = graph.edgesOf(v).sumOf { edge -> hub[edge.to] ?: 0.0 } + fun pagerank(graph: Graph, topN: Int): List> { + val ranks = mutableMapOf() + val dampingFactor = 0.8 + val vertices = graph.vertices + + vertices.forEach { vertex -> ranks[vertex] = 1.0/ vertices.size } + repeat(100) { + val newRanks = mutableMapOf() + vertices.forEach { vertex -> + var rankSum = 0.0 + vertices.forEach { neighbor -> + val edges = graph.matrix[neighbor] + if (neighbor != vertex && edges != null) { + if (edges.any { it.to == vertex }) { + rankSum += ranks[neighbor]?.div(edges.size) ?: 0.0 + } + } + } + newRanks[vertex] = (1 - dampingFactor) / vertices.size + dampingFactor * rankSum } - - // Обновление хабов - for (v in graph.vertices) { - newHub[v] = graph.edgesOf(v).sumOf { edge -> newAuthority[edge.to] ?: 0.0 } - } - - // Нормализация - val normAuthority = Math.sqrt(newAuthority.values.sumOf { it * it }) - val normHub = Math.sqrt(newHub.values.sumOf { it * it }) - - for (v in graph.vertices) { - authority[v] = newAuthority[v]!! / normAuthority - hub[v] = newHub[v]!! / normHub + newRanks.forEach { (vertex, value) -> + ranks[vertex] = value } } - - return Pair(authority, hub) + return ranks.entries.sortedByDescending { it.value }.take(topN).map { it.toPair() } } } \ No newline at end of file From 34db6f7254c29c032b32af8cd7f0892a308f57b0 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Tue, 28 May 2024 23:41:01 +0300 Subject: [PATCH 150/172] feat: implemented algo for undirected graph - delete HITS algorithm - for digraphs pagerank --- .../kotlin/model/algos/BetweenesCentrality.kt | 67 ++++++++++++++++++- .../kotlin/algos/BetweennesCentralityTest.kt | 10 ++- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/model/algos/BetweenesCentrality.kt b/src/main/kotlin/model/algos/BetweenesCentrality.kt index 299f0c1..85f0ad4 100644 --- a/src/main/kotlin/model/algos/BetweenesCentrality.kt +++ b/src/main/kotlin/model/algos/BetweenesCentrality.kt @@ -1,9 +1,13 @@ package model.algos -import model.graph.Graph +import model.graph.DirectedGraph +import model.graph.UndirectedGraph +import java.util.LinkedList +import java.util.Queue +import java.util.Stack object BetweenesCentrality { - fun pagerank(graph: Graph, topN: Int): List> { + fun pagerank(graph: DirectedGraph, top: Int): Map { val ranks = mutableMapOf() val dampingFactor = 0.8 val vertices = graph.vertices @@ -27,6 +31,63 @@ object BetweenesCentrality { ranks[vertex] = value } } - return ranks.entries.sortedByDescending { it.value }.take(topN).map { it.toPair() } + return ranks.entries.sortedByDescending { it.value }.take(top).associate { it.toPair() } + } +} + +object BetweenesCentralityUndirected { + fun compute(graph: UndirectedGraph, top: Int): Map { + val centrality = mutableMapOf() + for (v in graph.vertices) { + centrality[v] = 0.0 + } + + for (s in graph.vertices) { + val stack = Stack() + val predecessors = mutableMapOf>() + val shortestPaths = mutableMapOf() + val distance = mutableMapOf() + val dependency = mutableMapOf() + + for (v in graph.vertices) { + predecessors[v] = mutableListOf() + shortestPaths[v] = 0 + distance[v] = -1 + dependency[v] = 0.0 + } + + shortestPaths[s] = 1 + distance[s] = 0 + val queue: Queue = LinkedList() + queue.add(s) + + while (queue.isNotEmpty()) { + val v = queue.poll() + stack.push(v) + for (edge in graph.edgesOf(v)) { + val w = edge.to + if (distance[w]!! < 0) { + queue.add(w) + distance[w] = distance[v]!! + 1 + } + if (distance[w] == distance[v]!! + 1) { + shortestPaths[w] = shortestPaths[w]!! + shortestPaths[v]!! + predecessors[w]!!.add(v) + } + } + } + + while (stack.isNotEmpty()) { + val w = stack.pop() + for (v in predecessors[w]!!) { + dependency[v] = dependency[v]!! + (shortestPaths[v]!!.toDouble() / shortestPaths[w]!!) * (1 + dependency[w]!!) + } + if (w != s) { + centrality[w] = centrality[w]!! + dependency[w]!! + } + } + } + + return centrality.entries.sortedByDescending { it.value }.take(top).associate { it.toPair() } } } \ No newline at end of file diff --git a/src/test/kotlin/algos/BetweennesCentralityTest.kt b/src/test/kotlin/algos/BetweennesCentralityTest.kt index f8c5b20..432cf49 100644 --- a/src/test/kotlin/algos/BetweennesCentralityTest.kt +++ b/src/test/kotlin/algos/BetweennesCentralityTest.kt @@ -1,6 +1,7 @@ package algos import model.algos.BetweenesCentrality +import model.algos.BetweenesCentralityUndirected import model.graph.UndirectedGraph import kotlin.test.Test import kotlin.test.assertNotNull @@ -12,8 +13,8 @@ class BetweennesCentralityTest { for (i in 0..9) { graph.addVertex(i) } - graph.addEdge(1, 2, 1) - graph.addEdge(1, 3, 1) + graph.addEdge(1, 2) + graph.addEdge(1, 3) graph.addEdge(1, 4) graph.addEdge(2, 3) graph.addEdge(2, 4) @@ -28,10 +29,7 @@ class BetweennesCentralityTest { graph.addEdge(7, 8) graph.addEdge(7, 9) graph.addEdge(8, 9) - val (centrality, v) = BetweenesCentrality.hits(100, graph) - for ((vertex, value) in centrality) { - println("Vertex: $vertex, Betweenness Centrality: $value") - } + val centrality = BetweenesCentralityUndirected.compute(graph, 9) for ((vertex, value) in centrality) { println("Vertex: $vertex, Betweenness Centrality: $value") } From f836ac1b7ffd4a2ed6024cf2b339902977436463 Mon Sep 17 00:00:00 2001 From: Aleksei Dmitrievstev <93659834+admitrievtsev@users.noreply.github.com> Date: Tue, 28 May 2024 23:50:27 +0300 Subject: [PATCH 151/172] fix: set en-US as default lang --- src/main/kotlin/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json index c38add4..5fdb268 100644 --- a/src/main/kotlin/settings.json +++ b/src/main/kotlin/settings.json @@ -1 +1 @@ -{"language":"cn-CN"} \ No newline at end of file +{"language":"en-US"} From 398ce39f8ff9d84a0328e72a56123b1b74f58670 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Wed, 29 May 2024 00:03:46 +0300 Subject: [PATCH 152/172] change: add matrix method --- src/main/kotlin/model/graph/Graph.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 78c8c77..f53904c 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -4,7 +4,8 @@ import model.graph.edges.Edge abstract class Graph() { protected val graph = mutableMapOf>>() - val matrix get() = graph + val matrix + get() = graph val entries get() = graph.entries From f04470e0cdd768888aa61ae186b8ee54fdfb5f31 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 29 May 2024 00:21:38 +0300 Subject: [PATCH 153/172] Implemented Neo4jRepository saving and loading graphs -add 1 local test (don't know how to test in remotely) -not integrated in gui --- build.gradle.kts | 8 + .../viewmodel/io/neo4j/Neo4jRepository.kt | 145 +++++++++++++++++- src/test/kotlin/io/Neo4jTest.kt | 51 +++++- 3 files changed, 202 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bcd4ab7..f651fed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,6 +29,14 @@ dependencies { implementation("org.jetbrains.androidx.navigation:navigation-compose:$nav_version") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("com.github.uhh-lt:chinese-whispers:-SNAPSHOT") + + //neo4j + implementation("org.neo4j.driver", "neo4j-java-driver", "5.6.0") + + // logging + implementation("io.github.microutils", "kotlin-logging-jvm", "2.0.6") + implementation("org.slf4j", "slf4j-simple", "1.7.29") + testImplementation(kotlin("test")) } diff --git a/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt b/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt index 552439b..0a3b08e 100644 --- a/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt +++ b/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt @@ -1,4 +1,147 @@ package viewmodel.io.neo4j -class Neo4jRepositoryr { + +import mu.KotlinLogging +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.GraphDatabase +import org.neo4j.driver.TransactionContext +import viewmodel.DirectedGraphViewModel +import viewmodel.UndirectedGraphViewModel +import viewmodel.graph.AbstractGraphViewModel +import java.io.Closeable + +// penguin-carlo-ceramic-invite-wheel-2163 + +private val logger = KotlinLogging.logger { } + +class Neo4jRepository(uri: String, user: String, password: String) : Closeable { + val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) + val session = driver.session() + + fun saveGraph(graphVM: AbstractGraphViewModel) { + val graphName = graphVM.name + val graphType = graphVM.graphType + // remove old graph if exist + session.executeWrite { tx -> + removeGraph(tx, graphVM) + } + // create graph + session.executeWrite { tx -> + tx.run("CREATE (graph: Graph {name: '$graphName', type: '$graphType'});") + } + session.executeWrite { tx -> + tx.run("CREATE CONSTRAINT uniqueGraphName IF NOT EXISTS FOR (graph: Graph) REQUIRE (graph.value) IS UNIQUE;") + tx.run("CREATE INDEX IF NOT EXISTS FOR (graph: Graph) ON (graph.name);") + } + // create vertices in a graph + session.executeWrite { tx -> + for (vertexVM in graphVM.verticesVM) { + tx.run( + "CREATE (v: $graphName {value : \$vertexValue});", + mapOf("vertexValue" to vertexVM.vertex.toString()) + ) + } + } + session.executeWrite { tx -> + tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (n: $graphName) REQUIRE (n.value) IS UNIQUE;") + tx.run("CREATE INDEX IF NOT EXISTS FOR (n: $graphName) ON (n.value);") + } + // create edges in a graph + session.executeWrite { tx -> + for (edgeVM in graphVM.edgesVM) { + tx.run( + "MATCH (v1:$graphName) WHERE v1.value = \$vertex1 \n" + + "MATCH (v2: $graphName) WHERE v2.value = \$vertex2 \n" + + "CREATE (v1)-[:Edge {weight: \$edgeWeight}]->(v2)", + mapOf( + "vertex1" to edgeVM.from.toString(), + "vertex2" to edgeVM.to.toString(), + "edgeWeight" to edgeVM.weight.toString() + ) + ) + } + } + } + + fun removeGraph(graphVM: AbstractGraphViewModel) { + session.executeWrite { tx -> + removeGraph(tx, graphVM) + } + } + + private fun removeGraph( + tx: TransactionContext, + graphVM: AbstractGraphViewModel, + ) { + val graphName = graphVM.name + val check = tx.run( + "MATCH (graph: Graph) WHERE graph.name = '$graphName' RETURN graph.name as name;" + ).list() + + if (check.size == 0) return + if (check.size > 1) { + logger.error { "More than 1 graph with same name and type exist in neo4j" } + return + } + tx.run( + "OPTIONAL MATCH (n: $graphName)" + + "OPTIONAL MATCH (graph: Graph) WHERE graph.name = '$graphName'" + + " DETACH DELETE n, graph;" + ) + } + + fun getAllGraphs(): List> { + val graphs = mutableListOf>() + val names = session.executeRead { tx -> + val result = tx.run("MATCH (graph: Graph) RETURN graph.name as name") + return@executeRead result.list() { it.asMap()["name"].toString() } + } + for (name in names) { + graphs.add(getGraph(name)) + } + return graphs + } + + fun getGraph(graphName: String): AbstractGraphViewModel { + val graphData = session.executeRead() { tx -> + val result = + tx.run("MATCH (graph: Graph) WHERE graph.name = '$graphName' RETURN graph.name as name, graph.type as type") + return@executeRead result.single().asMap() + } + val graph: AbstractGraphViewModel + if (graphData["type"] == "Undirected") { + graph = UndirectedGraphViewModel(graphData["name"].toString()) + } else { + graph = DirectedGraphViewModel(graphData["name"].toString()) + } + val vertices = session.executeRead() { tx -> + val result = tx.run("MATCH (v: $graphName) RETURN v.value as value") + return@executeRead result.list() { it.asMap()["value"].toString() } + } + for (vertex in vertices) { + graph.addVertex(vertex) + } + session.executeRead { tx -> + for (vertex in vertices) { + val destinations = + tx.run("MATCH (v: $graphName {value: '$vertex'})-[:Edge]->(n) RETURN n.value as destination") + .list() { it.asMap()["destination"].toString() } + for (destination in destinations) { + graph.addEdge(vertex, destination) + } + } + } + return graph + } + + fun clearDB() { + session.executeWrite { tx -> + tx.run("MATCH (n) DETACH DELETE n") + } + } + + override fun close() { + session.close() + driver.close() + } } \ No newline at end of file diff --git a/src/test/kotlin/io/Neo4jTest.kt b/src/test/kotlin/io/Neo4jTest.kt index dfdafe4..caaf7c9 100644 --- a/src/test/kotlin/io/Neo4jTest.kt +++ b/src/test/kotlin/io/Neo4jTest.kt @@ -1,4 +1,53 @@ package io -class Neo4jTest { +import model.graph.DirectedGraph +import org.junit.Test +import view.graph.UndirectedGraphView +import view.screens.UndirectedGraphScreen +import viewmodel.DirectedGraphViewModel +import viewmodel.UndirectedGraphViewModel +import viewmodel.io.neo4j.Neo4jRepository +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +internal class Neo4jTest { + + @Test + fun `check`() { + val rep = + Neo4jRepository( + "bolt://localhost:7687", + "neo4j", + "penguin-carlo-ceramic-invite-wheel-2163" + ) + rep.clearDB() + val graph1 = UndirectedGraphViewModel("name1") + val graph2 = DirectedGraphViewModel("name2") + graph1.run { + this.addVertex("1") + this.addVertex("2") + this.addVertex("3") + this.addEdge("1", "2") + this.addEdge("2", "3") + } + graph2.run { + this.addVertex("14") + this.addVertex("15") + this.addVertex("16") + this.addEdge("14", "15") + this.addEdge("15", "16") + this.addEdge("16", "15") + } + rep.saveGraph(graph1) + rep.saveGraph(graph2) + graph1.addVertex("4") + rep.saveGraph(graph1) + val graphsSaved = rep.getAllGraphs() + val graph1Saved = graphsSaved.find { it.name == "name1" } + val graph2Saved = graphsSaved.find { it.name == "name2" } + assertEquals(graph1.model.vertices, graph1Saved?.model?.vertices) + assertEquals(graph1.model.edges, graph1Saved?.model?.edges) + assertEquals(graph2.model.vertices, graph2Saved?.model?.vertices) + assertEquals(graph2.model.edges, graph2Saved?.model?.edges) + } } \ No newline at end of file From 8c7917032ea770176e1303f07a9decd979f7d71e Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Wed, 29 May 2024 00:29:05 +0300 Subject: [PATCH 154/172] change: delete method `matrix` --- src/main/kotlin/model/algos/BetweenesCentrality.kt | 2 +- src/main/kotlin/model/graph/Graph.kt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/model/algos/BetweenesCentrality.kt b/src/main/kotlin/model/algos/BetweenesCentrality.kt index 85f0ad4..dc4671b 100644 --- a/src/main/kotlin/model/algos/BetweenesCentrality.kt +++ b/src/main/kotlin/model/algos/BetweenesCentrality.kt @@ -18,7 +18,7 @@ object BetweenesCentrality { vertices.forEach { vertex -> var rankSum = 0.0 vertices.forEach { neighbor -> - val edges = graph.matrix[neighbor] + val edges = graph.edgesOf(neighbor) if (neighbor != vertex && edges != null) { if (edges.any { it.to == vertex }) { rankSum += ranks[neighbor]?.div(edges.size) ?: 0.0 diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index f53904c..f4f537d 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -4,8 +4,6 @@ import model.graph.edges.Edge abstract class Graph() { protected val graph = mutableMapOf>>() - val matrix - get() = graph val entries get() = graph.entries From 71e1f6ce827e2e1c573f32a7d3bbb5f5acfc400f Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Tue, 28 May 2024 22:45:46 +0300 Subject: [PATCH 155/172] Fix: fix device pixels dependencies (was not correct view on macos) --- src/main/kotlin/Main.kt | 48 +++++++++++-------- .../view/graph/edge/DirectedEdgeView.kt | 41 ++++++++-------- .../view/graph/edge/UndirectedEdgeView.kt | 13 ++--- .../view/screens/DirectedGraphScreen.kt | 12 ++--- .../kotlin/viewmodel/graph/EdgeViewModel.kt | 12 +---- 5 files changed, 64 insertions(+), 62 deletions(-) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index c55356e..6fa3d01 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,34 +1,44 @@ import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.WindowState -import androidx.compose.ui.window.application +import androidx.compose.ui.window.* import java.awt.Dimension +import java.awt.GraphicsEnvironment -val width = 1920 -val height = 1080 +val width = try { + GraphicsEnvironment.getLocalGraphicsEnvironment().maximumWindowBounds.width +} catch (e: Exception) { + 100 +} +val height = try { + GraphicsEnvironment.getLocalGraphicsEnvironment().maximumWindowBounds.height +} catch (e: Exception) { + 100 +} -fun main() = application { - val state = WindowState( - width = width.dp, - height = height.dp, - position = WindowPosition(alignment = Alignment.Center), - ) - Window( - state = state, - onCloseRequest = ::exitApplication, - title = "Graph Visualizer", - ) { - window.minimumSize = Dimension(100, 100) - App() +fun main() { + application { + val state = WindowState( + width = width.dp, + height = height.dp, + position = WindowPosition(alignment = Alignment.Center), + ) + Window( + state = state, + onCloseRequest = ::exitApplication, + title = "Graph Visualizer", + ) { + window.minimumSize = Dimension(100, 100) + App() + } } } @Composable fun App() { + MaterialTheme() { Navigation() } diff --git a/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt b/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt index 1660d49..c602265 100644 --- a/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt +++ b/src/main/kotlin/view/graph/edge/DirectedEdgeView.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import viewmodel.DirectedGraphViewModel @@ -32,64 +33,64 @@ fun DirectedEdgeView( val second = edgeVM.toVM drawLine( start = Offset( - first.offsetX + vertexSizeZoomed / 2, - first.offsetY + vertexSizeZoomed / 2 + (first.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (first.offsetY + vertexSizeZoomed / 2).dp.toPx() ), end = Offset( - second.offsetX + vertexSizeZoomed / 2, - second.offsetY + vertexSizeZoomed / 2 + (second.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2).dp.toPx() ), strokeWidth = 6f * graphVM.zoom, color = edgeVM.color, ) rotate( degrees = ((57.2958 * (atan2( - ((first.offsetY - second.offsetY).toDouble()), - ((first.offsetX - second.offsetX).toDouble()) + ((first.offsetY - second.offsetY).toDouble()).dp.toPx(), + ((first.offsetX - second.offsetX).toDouble()).dp.toPx() ))).toFloat()), pivot = Offset( - second.offsetX + vertexSizeZoomed / 2, - second.offsetY + vertexSizeZoomed / 2 + (second.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2).dp.toPx() ) ) { drawRect( color = edgeVM.color, size = Size(5f * graphVM.zoom, 16f * graphVM.zoom), topLeft = Offset( - second.offsetX + vertexSizeZoomed / 2 + 65 * graphVM.zoom, - second.offsetY + vertexSizeZoomed / 2 - 8f * graphVM.zoom + (second.offsetX + vertexSizeZoomed / 2 + 65 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 8f * graphVM.zoom).dp.toPx() ), ) drawRect( color = edgeVM.color, size = Size(5f * graphVM.zoom, 14f * graphVM.zoom), topLeft = Offset( - second.offsetX + vertexSizeZoomed / 2 + 60 * graphVM.zoom, - second.offsetY + vertexSizeZoomed / 2 - 7f * graphVM.zoom + (second.offsetX + vertexSizeZoomed / 2 + 60 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 7f * graphVM.zoom).dp.toPx() ), ) drawRect( color = edgeVM.color, size = Size(5f * graphVM.zoom, 12f * graphVM.zoom), topLeft = Offset( - second.offsetX + vertexSizeZoomed / 2 + 55 * graphVM.zoom, - second.offsetY + vertexSizeZoomed / 2 - 6f * graphVM.zoom + (second.offsetX + vertexSizeZoomed / 2 + 55 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 6f * graphVM.zoom).dp.toPx() ), ) drawRect( color = edgeVM.color, size = Size(5f * graphVM.zoom, 10f * graphVM.zoom), topLeft = Offset( - second.offsetX + vertexSizeZoomed / 2 + 50 * graphVM.zoom, - second.offsetY + vertexSizeZoomed / 2 - 5f * graphVM.zoom + (second.offsetX + vertexSizeZoomed / 2 + 50 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 5f * graphVM.zoom).dp.toPx() ), ) drawRect( color = edgeVM.color, size = Size(5f * graphVM.zoom, 8f * graphVM.zoom), topLeft = Offset( - second.offsetX + vertexSizeZoomed / 2 + 45 * graphVM.zoom, - second.offsetY + vertexSizeZoomed / 2 - 4f * graphVM.zoom + (second.offsetX + vertexSizeZoomed / 2 + 45 * graphVM.zoom).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2 - 4f * graphVM.zoom).dp.toPx() ), ) } @@ -97,8 +98,8 @@ fun DirectedEdgeView( drawText( textMeasurer, edgeVM.weight.toString(), topLeft = Offset( - (first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom, - (first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom + ((first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom).dp.toPx(), + ((first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom).dp.toPx() ), style = TextStyle(background = Color.White, fontSize = 20.sp) ) diff --git a/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt b/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt index c4cbb92..e7b9042 100644 --- a/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt +++ b/src/main/kotlin/view/graph/edge/UndirectedEdgeView.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import viewmodel.UndirectedGraphViewModel @@ -29,12 +30,12 @@ fun UndirectedEdgeView( val second = edgeVM.toVM drawLine( start = Offset( - first.offsetX + vertexSizeZoomed / 2, - first.offsetY + vertexSizeZoomed / 2 + (first.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (first.offsetY + vertexSizeZoomed / 2).dp.toPx() ), end = Offset( - second.offsetX + vertexSizeZoomed / 2, - second.offsetY + vertexSizeZoomed / 2 + (second.offsetX + vertexSizeZoomed / 2).dp.toPx(), + (second.offsetY + vertexSizeZoomed / 2).dp.toPx() ), strokeWidth = 5f * graphVM.zoom, color = edgeVM.color, @@ -43,8 +44,8 @@ fun UndirectedEdgeView( drawText( textMeasurer, edgeVM.weight.toString(), topLeft = Offset( - (first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom, - (first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom + ((first.offsetX + vertexSizeZoomed + second.offsetX) / 2 - edgeVM.weight.toString().length * 5.5f * graphVM.zoom).dp.toPx(), + ((first.offsetY + vertexSizeZoomed + second.offsetY) / 2 - 9 * graphVM.zoom).dp.toPx() ), style = TextStyle(background = Color.White, fontSize = 20.sp) ) diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 62378a0..0364800 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -40,9 +40,9 @@ fun DirectedGraphScreen( .fillMaxSize() .onPointerEvent(PointerEventType.Scroll) { if (it.changes.first().scrollDelta.y > 0) { - graphVM.zoom = (graphVM.zoom - graphVM.zoom / 8).coerceIn(0.01f, 15f) + graphVM.zoom = (graphVM.zoom - graphVM.zoom / 8).coerceIn(0.01f, 15f).dp.toPx() } else { - graphVM.zoom = (graphVM.zoom + graphVM.zoom / 8).coerceIn(0.01f, 15f) + graphVM.zoom = (graphVM.zoom + graphVM.zoom / 8).coerceIn(0.01f, 15f).dp.toPx() val awtEvent = it.awtEventOrNull if (awtEvent != null) { @@ -50,17 +50,17 @@ fun DirectedGraphScreen( val yPosition = awtEvent.y.toFloat() val pointerVector = (Offset( - xPosition, - yPosition + xPosition.dp.toPx(), + yPosition.dp.toPx() ) - (graphVM.canvasSize / 2f)) * (1 / graphVM.zoom) - graphVM.center += pointerVector * 0.15f + graphVM.center += pointerVector * (0.15f.dp.toPx()) } } }.pointerInput(Unit) { detectDragGestures( matcher = PointerMatcher.Primary ) { - graphVM.center -= it * (1 / graphVM.zoom) + graphVM.center -= it * (1 / graphVM.zoom).dp.toPx() } }.pointerHoverIcon(PointerIcon.Hand) .onSizeChanged { diff --git a/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt index 2a60329..bab4290 100644 --- a/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/EdgeViewModel.kt @@ -15,17 +15,7 @@ class EdgeViewModel( ViewModel() { val fromVM = vertexFromVM val toVM = vertexToVM - - val fromX - get() = fromVM.x - val fromY - get() = fromVM.y - val toX - get() = toVM.x - val toY - get() = toVM.y - val vertexSize - get() = fromVM.vertexSize + val weight by mutableStateOf(edge.weight) val from by mutableStateOf(edge.from) val to by mutableStateOf(edge.to) From 1fc5538aff1003d04259c9708b8f943075f7739a Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 29 May 2024 03:22:49 +0300 Subject: [PATCH 156/172] Feat: add betweenness-centrality visualization --- .../kotlin/model/algos/BetweenesCentrality.kt | 10 +- src/main/kotlin/settings.json | 2 +- src/main/kotlin/view/common/DefaultButton.kt | 2 +- .../kotlin/view/common/DefaultShortButton.kt | 6 +- src/main/kotlin/view/common/styling.kt | 6 +- .../view/graph/vertex/DirectedVertexView.kt | 18 ++++ .../view/graph/vertex/UndirectedVertexView.kt | 24 +++++ .../view/screens/DirectedGraphScreen.kt | 92 ++++++++++++------- .../view/screens/UndirectedGraphScreen.kt | 90 +++++++++++------- .../viewmodel/graph/AbstractGraphViewModel.kt | 5 + .../viewmodel/graph/DirectedGraphViewModel.kt | 19 +++- .../graph/UndirectedGraphViewModel.kt | 17 ++++ .../kotlin/viewmodel/graph/VertexViewModel.kt | 3 +- src/main/resources/localisation/en-US.json | 6 +- src/main/resources/localisation/ru-RU.json | 6 +- .../kotlin/algos/BetweennesCentralityTest.kt | 7 +- 16 files changed, 230 insertions(+), 83 deletions(-) diff --git a/src/main/kotlin/model/algos/BetweenesCentrality.kt b/src/main/kotlin/model/algos/BetweenesCentrality.kt index dc4671b..51e2029 100644 --- a/src/main/kotlin/model/algos/BetweenesCentrality.kt +++ b/src/main/kotlin/model/algos/BetweenesCentrality.kt @@ -6,13 +6,13 @@ import java.util.LinkedList import java.util.Queue import java.util.Stack -object BetweenesCentrality { +object BetweenesCentralityDirected { fun pagerank(graph: DirectedGraph, top: Int): Map { val ranks = mutableMapOf() val dampingFactor = 0.8 val vertices = graph.vertices - vertices.forEach { vertex -> ranks[vertex] = 1.0/ vertices.size } + vertices.forEach { vertex -> ranks[vertex] = 1.0 / vertices.size } repeat(100) { val newRanks = mutableMapOf() vertices.forEach { vertex -> @@ -80,7 +80,8 @@ object BetweenesCentralityUndirected { while (stack.isNotEmpty()) { val w = stack.pop() for (v in predecessors[w]!!) { - dependency[v] = dependency[v]!! + (shortestPaths[v]!!.toDouble() / shortestPaths[w]!!) * (1 + dependency[w]!!) + dependency[v] = + dependency[v]!! + (shortestPaths[v]!!.toDouble() / shortestPaths[w]!!) * (1 + dependency[w]!!) } if (w != s) { centrality[w] = centrality[w]!! + dependency[w]!! @@ -88,6 +89,7 @@ object BetweenesCentralityUndirected { } } - return centrality.entries.sortedByDescending { it.value }.take(top).associate { it.toPair() } + return centrality.entries.sortedByDescending { it.value }.take(top) + .associate { it.toPair() } } } \ No newline at end of file diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json index 5fdb268..6e3de82 100644 --- a/src/main/kotlin/settings.json +++ b/src/main/kotlin/settings.json @@ -1 +1 @@ -{"language":"en-US"} +{"language":"en-US"} \ No newline at end of file diff --git a/src/main/kotlin/view/common/DefaultButton.kt b/src/main/kotlin/view/common/DefaultButton.kt index 705bfd1..7d572d7 100644 --- a/src/main/kotlin/view/common/DefaultButton.kt +++ b/src/main/kotlin/view/common/DefaultButton.kt @@ -18,7 +18,7 @@ import localisation.localisation fun DefaultButton( onClick: () -> Unit, localisationCode: String, - style: TextStyle, + style: TextStyle = defaultStyle, color: Color = DefaultColors.primary, width: androidx.compose.ui.unit.Dp = 240.dp, height: androidx.compose.ui.unit.Dp = 80.dp, diff --git a/src/main/kotlin/view/common/DefaultShortButton.kt b/src/main/kotlin/view/common/DefaultShortButton.kt index ef6726a..08e9c0e 100644 --- a/src/main/kotlin/view/common/DefaultShortButton.kt +++ b/src/main/kotlin/view/common/DefaultShortButton.kt @@ -9,9 +9,9 @@ import androidx.compose.ui.unit.dp fun DefaultShortButton( onClick: () -> Unit, localisationCode: String, - style: TextStyle, + style: TextStyle = defaultStyle, color: Color = DefaultColors.primary, -) { - DefaultButton(onClick, localisationCode, style, color, 220.dp, 70.dp, ) + ) { + DefaultButton(onClick, localisationCode, style, color, 220.dp, 70.dp) } \ No newline at end of file diff --git a/src/main/kotlin/view/common/styling.kt b/src/main/kotlin/view/common/styling.kt index 3d6aef2..2b9a57d 100644 --- a/src/main/kotlin/view/common/styling.kt +++ b/src/main/kotlin/view/common/styling.kt @@ -6,9 +6,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp val defaultStyle = TextStyle(fontSize = 28.sp) -val microDickSize = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Center) -val smallDickSize = TextStyle(fontSize = 22.sp, textAlign = TextAlign.Center) -val mediumDickSize = TextStyle(fontSize = 26.sp, textAlign = TextAlign.Center) +val microSize = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Center) +val smallSize = TextStyle(fontSize = 22.sp, textAlign = TextAlign.Center) +val mediumSize = TextStyle(fontSize = 26.sp, textAlign = TextAlign.Center) val bigStyle = TextStyle(fontSize = 50.sp) diff --git a/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt b/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt index aeeb728..64af669 100644 --- a/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt +++ b/src/main/kotlin/view/graph/vertex/DirectedVertexView.kt @@ -13,9 +13,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import view.graph.edge.DirectedEdgeView import viewmodel.DirectedGraphViewModel import viewmodel.graph.VertexViewModel +import kotlin.math.min @Composable fun DirectedVertexView( @@ -52,6 +54,22 @@ fun DirectedVertexView( .offset(y = (-vertexVM.vertexSize / 10).dp), ) } + if (graphVM.visibleCentrality) { + Box(modifier = Modifier.zIndex(-3f)) { + var centrality = vertexVM.centrality.toString() + centrality = centrality.substring(0, min(4, centrality.length)) + Text( + centrality, fontSize = (28 * graphVM.zoom).sp, + color = Color.LightGray, + modifier = Modifier + .wrapContentSize() + .offset( + x = (vertexVM.offsetX).dp, + y = (vertexVM.offsetY - vertexVM.vertexSize * 0.7 * graphVM.zoom).dp + ) + ) + } + } for (edgeVM in vertexVM.edges) { DirectedEdgeView(graphVM, edgeVM, graphVM.isWeighted) diff --git a/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt b/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt index 371ecb3..522af96 100644 --- a/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt +++ b/src/main/kotlin/view/graph/vertex/UndirectedVertexView.kt @@ -13,9 +13,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import view.graph.edge.UndirectedEdgeView import viewmodel.UndirectedGraphViewModel import viewmodel.graph.VertexViewModel +import kotlin.math.min @Composable fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGraphViewModel) { @@ -48,6 +50,28 @@ fun UndirectedVertexView(vertexVM: VertexViewModel, graphVM: UndirectedGr .wrapContentSize() .offset(y = (-vertexVM.vertexSize / 10).dp), ) + if (graphVM.visibleCentrality) { + Text( + "${vertexVM.centrality}", fontSize = (28 * graphVM.zoom).sp, + modifier = Modifier.wrapContentSize().offset(y = (-vertexVM.vertexSize - 10).dp) + ) + } + } + if (graphVM.visibleCentrality) { + Box(modifier = Modifier.zIndex(-3f)) { + var centrality = vertexVM.centrality.toString() + centrality = centrality.substring(0, min(4, centrality.length)) + Text( + centrality, fontSize = (28 * graphVM.zoom).sp, + color = Color.LightGray, + modifier = Modifier + .wrapContentSize() + .offset( + x = (vertexVM.offsetX + vertexVM.vertexSize / 7 * graphVM.zoom).dp, + y = (vertexVM.offsetY - vertexVM.vertexSize * 0.7 * graphVM.zoom).dp + ) + ) + } } for (edgeVM in vertexVM.edges) { diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index e985dec..0b4f816 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -83,10 +83,13 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) // Add vertex Button - DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + { isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // Add edge Button @@ -114,46 +117,73 @@ fun DirectedGraphScreen( ) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton({ graphVM.resetColors() }, "reset", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - else -> defaultStyle }, Color.LightGray) + DefaultShortButton( + { graphVM.resetColors() }, "reset", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + else -> defaultStyle + }, Color.LightGray + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + { graphVM.drawBetweennessCentrality() }, + "betweenness_centrality", + microSize + ) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton({ graphVM.drawStrongConnections() }, "find_strong_connections", when(language) { - ("en-US") -> smallDickSize - ("ru-RU") -> microDickSize - ("cn-CN") -> microDickSize - else -> defaultStyle }) + DefaultShortButton( + { graphVM.chinaWhisperCluster() }, "find_clusters", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton({ graphVM.chinaWhisperCluster() }, "find_clusters", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + { graphVM.drawStrongConnections() }, "find_strong_connections", when (language) { + ("en-US") -> smallSize + ("ru-RU") -> microSize + ("cn-CN") -> microSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // Dijkstra Button - DefaultShortButton({ isOpenedDijkstraMenu = !isOpenedDijkstraMenu }, "dijkstra", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - ("cn-CN") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + { isOpenedDijkstraMenu = !isOpenedDijkstraMenu }, "dijkstra", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + ("cn-CN") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // FordBellman Button - DefaultShortButton({ isOpenedFordBellmanMenu = !isOpenedFordBellmanMenu }, "ford_bellman", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> microDickSize - ("cn-CN") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + { isOpenedFordBellmanMenu = !isOpenedFordBellmanMenu }, + "ford_bellman", + when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microSize + ("cn-CN") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // Cycles Button - DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> mediumDickSize - else -> defaultStyle }) + DefaultShortButton( + onClick = { graphVM.drawCycles("1") }, "find_cycles", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> mediumSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // Add vertex Dialog diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 17c2585..05d5744 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -82,10 +82,13 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) // Add vertex Button - DefaultShortButton({ isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + { isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // Add edge button @@ -108,50 +111,75 @@ fun UndirectedGraphScreen( } else { scope.coroutineContext.cancelChildren() } - }, "visualize", defaultStyle, + }, "visualize", defaultStyle, if (isVisualizationRunning) Color.Red else Color(0xffFFCB32) ) Spacer(modifier = Modifier.height(10.dp)) // Reset colors Button - DefaultShortButton({ graphVM.resetColors() }, "reset", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - else -> defaultStyle }, Color.LightGray) + DefaultShortButton( + { graphVM.resetColors() }, "reset", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + else -> defaultStyle + }, Color.LightGray + ) + Spacer(modifier = Modifier.height(10.dp)) + + DefaultShortButton( + { graphVM.drawBetweennessCentrality() }, + "betweenness_centrality", + microSize + ) Spacer(modifier = Modifier.height(10.dp)) // Dijkstra Button - DefaultShortButton({ isDijkstraMenu = !isDijkstraMenu }, "dijkstra", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - ("cn-CN") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + { isDijkstraMenu = !isDijkstraMenu }, "dijkstra", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + ("cn-CN") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // FordBellman Button - DefaultShortButton({ isFordBellmanMenu = !isFordBellmanMenu }, "ford_bellman", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> microDickSize - ("cn-CN") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + { isFordBellmanMenu = !isFordBellmanMenu }, "ford_bellman", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> microSize + ("cn-CN") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.drawMst() }, "find_mst", when(language) { - ("en-US") -> smallDickSize - ("ru-RU") -> microDickSize - else -> defaultStyle }) + DefaultShortButton( + onClick = { graphVM.drawMst() }, "find_mst", when (language) { + ("en-US") -> smallSize + ("ru-RU") -> microSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.drawCycles("1") }, "find_cycles", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> mediumDickSize - else -> defaultStyle }) + DefaultShortButton( + onClick = { graphVM.drawCycles("1") }, "find_cycles", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> mediumSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) - DefaultShortButton(onClick = { graphVM.drawBridges() }, "find_bridges", when(language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallDickSize - else -> defaultStyle }) + DefaultShortButton( + onClick = { graphVM.drawBridges() }, "find_bridges", when (language) { + ("en-US") -> defaultStyle + ("ru-RU") -> smallSize + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // Add vertex Dialog diff --git a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt index 66db5b3..3b5bc4f 100644 --- a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt @@ -27,6 +27,7 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM get() = graphModel.isWeighted val negativeWeights get() = graphModel.negativeWeights + var visibleCentrality by mutableStateOf(false) val model get() = graphModel val verticesVM @@ -41,6 +42,7 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM } return result.toList() } + var saveType = SaveType.SQLite abstract val graphType: GraphType var zoom by mutableStateOf(1f) @@ -78,6 +80,8 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM graphVM = keep } + abstract fun drawBetweennessCentrality() + fun drawDijkstra(start: V, end: V) { if (this.negativeWeights) return val result = Dijkstra(graphModel, graphModel.size).dijkstra(start, end) @@ -126,5 +130,6 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM for (vertexVM in verticesVM) { vertexVM.color = DefaultColors.primary } + visibleCentrality = false } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt index fb009ca..8547edf 100644 --- a/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt @@ -5,13 +5,17 @@ import model.algos.StrongConnections import de.tudarmstadt.lt.cw.graph.ArrayBackedGraph import de.tudarmstadt.lt.cw.graph.ArrayBackedGraphCW import de.tudarmstadt.lt.cw.graph.Graph +import model.algos.BetweenesCentralityDirected import model.graph.DirectedGraph import model.graph.Edge +import mu.KotlinLogging import viewmodel.graph.AbstractGraphViewModel import viewmodel.graph.EdgeViewModel import viewmodel.graph.VertexViewModel import kotlin.random.Random +private val logger = KotlinLogging.logger { } + class DirectedGraphViewModel( name: String, val graph: DirectedGraph = DirectedGraph() @@ -75,14 +79,25 @@ class DirectedGraphViewModel( } } + override fun drawBetweennessCentrality() { + val result = BetweenesCentralityDirected.pagerank(graphModel as DirectedGraph, size) + for (vertexVM in verticesVM) { + vertexVM.centrality = (result[vertexVM.vertex] ?: run { + logger.error { "Can't find centrality value for vertex in graph" } + 0.0 + }) * 100 + } + this.visibleCentrality = true + } + fun drawStrongConnections() { val strongConnections = StrongConnections() for (component in strongConnections.findStrongConnections(graphModel)) { - val col = + val color = Color(Random.nextInt(30, 230), Random.nextInt(30, 230), Random.nextInt(30, 230)) for (vertex in component) { if (vertex in graphModel.vertices) { - graphVM[vertex]?.color = col + graphVM[vertex]?.color = color } } } diff --git a/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt index d53f0df..b488663 100644 --- a/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt @@ -1,15 +1,21 @@ package viewmodel import androidx.compose.ui.graphics.Color +import model.algos.BetweenesCentralityDirected +import model.algos.BetweenesCentralityUndirected import model.algos.Prim import model.algos.findBridges +import model.graph.DirectedGraph import model.graph.Edge import model.graph.UndirectedGraph +import mu.KotlinLogging import viewmodel.GraphType import viewmodel.graph.AbstractGraphViewModel import viewmodel.graph.EdgeViewModel import viewmodel.graph.VertexViewModel +private val logger = KotlinLogging.logger { } + class UndirectedGraphViewModel( name: String, val graph: UndirectedGraph = UndirectedGraph() @@ -58,6 +64,17 @@ class UndirectedGraphViewModel( drawEdges(result, Color.Magenta) } + override fun drawBetweennessCentrality() { + val result = BetweenesCentralityUndirected.compute(graphModel as UndirectedGraph, size) + for (vertexVM in verticesVM) { + vertexVM.centrality = (result[vertexVM.vertex] ?: run { + logger.error { "Can't find centrality value for vertex in graph" } + 0.0 + }) + } + this.visibleCentrality = true + } + fun drawBridges() { val result = findBridges(graphModel as UndirectedGraph) drawEdges(result, Color.Yellow) diff --git a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index 9e70e50..4ccd3d7 100644 --- a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -41,7 +41,8 @@ class VertexViewModel( val offsetY get() = (graphVM.canvasSize.y / 2) + ((y - graphVM.center.y) * graphVM.zoom) - val vertexSize = 60f + var vertexSize by mutableStateOf(60f) + var centrality by mutableStateOf(0.0) var color by mutableStateOf(DefaultColors.primary) val degree get() = edges.size diff --git a/src/main/resources/localisation/en-US.json b/src/main/resources/localisation/en-US.json index e80e563..cfdfa06 100644 --- a/src/main/resources/localisation/en-US.json +++ b/src/main/resources/localisation/en-US.json @@ -22,7 +22,7 @@ }, { "code": "open_edge", - "localisation":"Add edge" + "localisation": "Add edge" }, { "code": "add_edge", @@ -64,6 +64,10 @@ "code": "dijkstra", "localisation": "Dijkstra Path" }, + { + "code": "betweenness_centrality", + "localisation": "Betweenness Centrality" + }, { "code": "ford_bellman", "localisation": "Ford Bellman" diff --git a/src/main/resources/localisation/ru-RU.json b/src/main/resources/localisation/ru-RU.json index 912b0ec..83230e2 100644 --- a/src/main/resources/localisation/ru-RU.json +++ b/src/main/resources/localisation/ru-RU.json @@ -40,6 +40,10 @@ "code": "enter_new_graph_name", "localisation": "Создание нового графа" }, + { + "code": "betweenness_centrality", + "localisation": "Ключевые вершины" + }, { "code": "save", "localisation": "Сохранить" @@ -58,7 +62,7 @@ }, { "code": "find_clusters", - "localisation": "Найти Кластера" + "localisation": "Найти Кластеры" }, { "code": "dijkstra", diff --git a/src/test/kotlin/algos/BetweennesCentralityTest.kt b/src/test/kotlin/algos/BetweennesCentralityTest.kt index 432cf49..7422dbb 100644 --- a/src/test/kotlin/algos/BetweennesCentralityTest.kt +++ b/src/test/kotlin/algos/BetweennesCentralityTest.kt @@ -1,6 +1,5 @@ package algos -import model.algos.BetweenesCentrality import model.algos.BetweenesCentralityUndirected import model.graph.UndirectedGraph import kotlin.test.Test @@ -10,9 +9,9 @@ class BetweennesCentralityTest { @Test fun basic() { val graph = UndirectedGraph() - for (i in 0..9) { - graph.addVertex(i) - } + for (i in 0..9) { + graph.addVertex(i) + } graph.addEdge(1, 2) graph.addEdge(1, 3) graph.addEdge(1, 4) From e9d2130217787045b2c4f9d54cff9320cc5ef66b Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Wed, 29 May 2024 03:42:52 +0300 Subject: [PATCH 157/172] ref: remove extra check for null --- src/main/kotlin/model/algos/BetweenesCentrality.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/model/algos/BetweenesCentrality.kt b/src/main/kotlin/model/algos/BetweenesCentrality.kt index 51e2029..5b80219 100644 --- a/src/main/kotlin/model/algos/BetweenesCentrality.kt +++ b/src/main/kotlin/model/algos/BetweenesCentrality.kt @@ -15,18 +15,18 @@ object BetweenesCentralityDirected { vertices.forEach { vertex -> ranks[vertex] = 1.0 / vertices.size } repeat(100) { val newRanks = mutableMapOf() - vertices.forEach { vertex -> + vertices.forEach(fun(vertex: V) { var rankSum = 0.0 vertices.forEach { neighbor -> val edges = graph.edgesOf(neighbor) - if (neighbor != vertex && edges != null) { + if (neighbor != vertex) { if (edges.any { it.to == vertex }) { rankSum += ranks[neighbor]?.div(edges.size) ?: 0.0 } } } newRanks[vertex] = (1 - dampingFactor) / vertices.size + dampingFactor * rankSum - } + }) newRanks.forEach { (vertex, value) -> ranks[vertex] = value } From d35c3921e920bcb8520a2d41bde26f16ab7e40d7 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Wed, 29 May 2024 10:27:08 +0300 Subject: [PATCH 158/172] Feat: restructured io, integrated neo4j 1. Add saving neo4j input data to settings 2. Moved sqlite functions to special class --- src/main/kotlin/Navigation.kt | 50 ++-- .../localisation/logs/localisationError.log | 50 ++++ src/main/kotlin/model/graph/Graph.kt | 4 +- src/main/kotlin/settings.json | 2 +- .../view/screens/DirectedGraphScreen.kt | 8 +- src/main/kotlin/view/screens/MainScreen.kt | 40 +-- .../kotlin/view/screens/SettingsScreen.kt | 243 +++++++++++++++- .../view/screens/UndirectedGraphScreen.kt | 9 +- .../kotlin/viewmodel/MainScreenViewModel.kt | 261 ++++++------------ .../viewmodel/graph/AbstractGraphViewModel.kt | 3 +- .../viewmodel/graph/DirectedGraphViewModel.kt | 2 - .../graph/UndirectedGraphViewModel.kt | 2 - .../io/{neo4j => }/Neo4jRepository.kt | 35 ++- .../kotlin/viewmodel/io/SQLiteRepository.kt | 113 ++++++++ src/main/resources/localisation/en-US.json | 14 +- src/main/resources/localisation/ru-RU.json | 12 + src/test/kotlin/SQLiteIntegrationTest.kt | 15 +- src/test/kotlin/io/Neo4jTest.kt | 2 +- 18 files changed, 595 insertions(+), 270 deletions(-) rename src/main/kotlin/viewmodel/io/{neo4j => }/Neo4jRepository.kt (84%) create mode 100644 src/main/kotlin/viewmodel/io/SQLiteRepository.kt diff --git a/src/main/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt index fa2a4e1..8fa116e 100644 --- a/src/main/kotlin/Navigation.kt +++ b/src/main/kotlin/Navigation.kt @@ -1,4 +1,3 @@ - import androidx.compose.runtime.Composable import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -6,37 +5,50 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import view.screens.* +import viewmodel.DirectedGraphViewModel import viewmodel.MainScreenViewModel +import viewmodel.UndirectedGraphViewModel @Composable fun Navigation() { val navController = rememberNavController() val mainScreenViewModel = MainScreenViewModel() - NavHost(navController = navController, - startDestination = Screen.MainScreen.route) { - composable(route = Screen.MainScreen.route){ + NavHost( + navController = navController, + startDestination = Screen.MainScreen.route + ) { + composable(route = Screen.MainScreen.route) { MainScreen(navController = navController, mainScreenViewModel) } + composable( - route = "${Screen.UndirectedGraphScreen.route}/{graphId}", - arguments = listOf(navArgument("graphId") { type = NavType.IntType }) - ){ navBackStackEntry -> - val graphId = navBackStackEntry.arguments?.getInt("graphId") - graphId?.let{ - UndirectedGraphScreen(navController, mainScreenViewModel, graphId) - } + route = "${Screen.UndirectedGraphScreen.route}/{graphName}", + arguments = listOf(navArgument("graphName") { type = NavType.StringType }) + ) { navBackStackEntry -> + + val graphName = navBackStackEntry.arguments?.getString("graphName") + ?: throw IllegalArgumentException("graphName must be provided when navigate to UndirectedGraphScreen") + val graphVM = + mainScreenViewModel.getGraph(graphName) as? UndirectedGraphViewModel + ?: throw IllegalStateException("Can't find graph with given in navigation name") + + UndirectedGraphScreen(mainScreenViewModel, navController, graphVM) } + composable( - route = "${Screen.DirectedGraphScreen.route}/{graphId}", - arguments = listOf(navArgument("graphId") { type = NavType.IntType }) - ){ navBackStackEntry -> - val graphId = navBackStackEntry.arguments?.getInt("graphId") - graphId?.let{ - DirectedGraphScreen(navController, mainScreenViewModel, graphId) - } + route = "${Screen.DirectedGraphScreen.route}/{graphName}", + arguments = listOf(navArgument("graphName") { type = NavType.StringType }) + ) { navBackStackEntry -> + + val graphName = navBackStackEntry.arguments?.getString("graphName") + ?: throw IllegalArgumentException("graphName must be provided when navigate to DirectedGraphScreen") + val graphVM = mainScreenViewModel.getGraph(graphName) as? DirectedGraphViewModel + ?: throw IllegalStateException("Can't find graph with given in navigation name") + + DirectedGraphScreen(mainScreenViewModel, navController, graphVM) } - composable(route = Screen.SettingsScreen.route){ + composable(route = Screen.SettingsScreen.route) { SettingsScreen(navController = navController) } } diff --git a/src/main/kotlin/localisation/logs/localisationError.log b/src/main/kotlin/localisation/logs/localisationError.log index 8aa04b4..d35f640 100644 --- a/src/main/kotlin/localisation/logs/localisationError.log +++ b/src/main/kotlin/localisation/logs/localisationError.log @@ -39,3 +39,53 @@ That's a Krishtall Да здравствует Петербург, да здравствует 52 Да здравствует Петербург, да здравствует 52 (Ау; YEEI, а) Да здравствует 52 (Ау), YEEI, long live (Это второй) +FILE OF LOC NOT FOUND 2024-05-29T06:30:21.693068917 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:21.828792866 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:21.831859741 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:21.838886296 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:21.840370747 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.217342716 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.217648276 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.218234550 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.218734043 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.222135802 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.244915831 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.245091339 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.245433958 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.245763073 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.945423905 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.945631212 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.945993158 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.946277639 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.969892566 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.970111996 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.970528894 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:30:23.970892442 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:12.236345206 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:12.368647425 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:12.371719765 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:12.378468643 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:12.379810360 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:13.154090285 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:13.154399012 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:33.073103573 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:33.212062760 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:33.215975500 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:33.223497923 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:33.225075831 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:34.143768231 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:34:34.144145846 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:35:16.641525630 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:35:16.772786023 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:35:16.775649132 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:35:16.782362013 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:35:16.783730339 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:35:18.073092712 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:35:18.073416526 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:37:51.958874791 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:37:52.095255530 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:37:52.098815501 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:37:52.105568046 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:37:52.106973853 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:37:53.078412673 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ +FILE OF LOC NOT FOUND 2024-05-29T06:37:53.078780871 -- EXCEPTION IS kotlinx.serialization.MissingFieldException: Field 'bd' is required for type with serial name 'view.screens.SettingsJSON', but it was missing at path: $ diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index d7de572..553081c 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -39,10 +39,10 @@ abstract class Graph() { return graph[vertex]?.size ?: 0 } - fun saveSQLite(name: String, type: String, bdName: String){ + fun saveSQLite(name: String, type: String, bdName: String) { var parameterCreate = "( Vertexes String," var parameterInput = "( Vertexes," - var create = ("CREATE TABLE $name ") + var create = ("CREATE TABLE $name") val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") val insertIndex = ("INSERT INTO BEBRA_KILLER (name, type) VALUES('$name', '$type');") for (i in graph.entries) { diff --git a/src/main/kotlin/settings.json b/src/main/kotlin/settings.json index 6e3de82..8ec643a 100644 --- a/src/main/kotlin/settings.json +++ b/src/main/kotlin/settings.json @@ -1 +1 @@ -{"language":"en-US"} \ No newline at end of file +{"language":"en-US","bd":"neo4j","neo4jUri":"bolt://localhost:7687","neo4jUser":"neo4j","neo4jPassword":"penguin-carlo-ceramic-invite-wheel-2163"} \ No newline at end of file diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 62378a0..70d91a4 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -24,16 +24,16 @@ import kotlinx.coroutines.launch import model.algos.ForceAtlas2 import view.common.* import view.graph.DirectedGraphView +import viewmodel.DirectedGraphViewModel import viewmodel.MainScreenViewModel @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun DirectedGraphScreen( - navController: NavController, mainScreenViewModel: MainScreenViewModel, - graphId: Int + navController: NavController, + graphVM: DirectedGraphViewModel ) { - val graphVM by mutableStateOf(mainScreenViewModel.graphs.getDirected(graphId)) Box( modifier = Modifier @@ -93,7 +93,7 @@ fun DirectedGraphScreen( Spacer(modifier = Modifier.height(10.dp)) // Save button - DefaultShortButton({ graphVM.saveSQLite() }, "save") + DefaultShortButton({ mainScreenViewModel.saveGraph(graphVM.name) }, "save") Spacer(modifier = Modifier.height(16.dp)) // Visualization Button diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index b7bf1ea..a416b07 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -29,7 +29,6 @@ import view.common.bounceClick import view.common.defaultStyle import viewmodel.GraphType import viewmodel.MainScreenViewModel -import viewmodel.SaveType @Composable fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { @@ -40,9 +39,9 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView val expandedDropDown = remember { mutableStateOf(false) } val selectedOptionTextDropDown = remember { mutableStateOf(optionsDropDown[0]) } + if (!mainScreenViewModel.inited) { - mainScreenViewModel.graphInit("storage") - mainScreenViewModel.inited = true + mainScreenViewModel.initGraphList("storage") } Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { @@ -179,8 +178,8 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView mainScreenViewModel.addGraph( graphName, selectedOptionTextDropDown.value, - SaveType.Internal ) + mainScreenViewModel.saveGraph(graphName) graphName = "" dialogState.value = false } @@ -254,29 +253,20 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView Spacer(modifier = Modifier.height(30.dp)) LazyColumn(modifier = Modifier.fillMaxWidth()) { - itemsIndexed(mainScreenViewModel.graphs.typeList) { index, _ -> - if (!mainScreenViewModel.graphs.getName(index) - .startsWith(search) - ) return@itemsIndexed + itemsIndexed(mainScreenViewModel.graphsNames) { index, name -> + if (!name.startsWith(search)) return@itemsIndexed // To GraphScreen + val graphVM = mainScreenViewModel.getGraph(name) Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( onClick = { - if (mainScreenViewModel.graphs.typeList[index] == GraphType.Directed) { - mainScreenViewModel.initModel(index, "storage") - } - if (mainScreenViewModel.graphs.typeList[index] == GraphType.Undirected) { - mainScreenViewModel.initModel(index, "storage") - } - navController.navigate( - when (mainScreenViewModel.graphs.typeList[index]) { - GraphType.Undirected -> { - "${Screen.UndirectedGraphScreen.route}/$index" - } - - GraphType.Directed -> "${Screen.DirectedGraphScreen.route}/$index" - - } + mainScreenViewModel.initGraph(name, "storage") + if (graphVM.graphType == GraphType.Undirected) { + navController.navigate( + "${Screen.UndirectedGraphScreen.route}/$name" + ) + } else navController.navigate( + "${Screen.DirectedGraphScreen.route}/$name" ) }, modifier = Modifier @@ -292,7 +282,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) ) { Text( - text = mainScreenViewModel.graphs.getName(index), + text = name, style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp)) ) @@ -302,7 +292,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView // Remove Graph IconButton( - onClick = { mainScreenViewModel.graphs.removeGraph(index) }, + onClick = { mainScreenViewModel.removeGraph(name) }, modifier = Modifier .padding(horizontal = 10.dp) .size(100.dp) diff --git a/src/main/kotlin/view/screens/SettingsScreen.kt b/src/main/kotlin/view/screens/SettingsScreen.kt index 20eb3d2..5203722 100644 --- a/src/main/kotlin/view/screens/SettingsScreen.kt +++ b/src/main/kotlin/view/screens/SettingsScreen.kt @@ -1,14 +1,14 @@ package view.screens +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -18,6 +18,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import localisation.localisation import view.common.DefaultColors +import view.common.bigStyle import view.common.bounceClick import view.common.defaultStyle import java.io.File @@ -25,20 +26,42 @@ import java.io.File const val pathToSettings = "src/main/kotlin/settings.json" @Serializable -class SettingsJSON(var language: String) +class SettingsJSON( + var language: String, + var bd: String, + var neo4jUri: String = "dsfds", + var neo4jUser: String = "dfsds", + var neo4jPassword: String = "dsfds", +) + enum class SettingType { - LANGUAGE + LANGUAGE, + BD, + NEO4JURI, + NEO4JUSER, + NEO4JPASSWORD, } fun resetSettings() { - File(pathToSettings).writeText(Json.encodeToString(SettingsJSON("en-US"))) + File(pathToSettings).writeText( + Json.encodeToString( + SettingsJSON( + "en-US", + "sqlite" + ) + ) + ) } -fun makeSetting(name: SettingType, value: String) { +fun makeSetting(type: SettingType, value: String) { try { val data = Json.decodeFromString(File(pathToSettings).readText()) - when (name) { + when (type) { SettingType.LANGUAGE -> data.language = value + SettingType.BD -> data.bd = value + SettingType.NEO4JURI -> data.neo4jUri = value + SettingType.NEO4JUSER -> data.neo4jUser = value + SettingType.NEO4JPASSWORD -> data.neo4jPassword = value } File(pathToSettings).writeText(Json.encodeToString(data)) } catch (exception: Exception) { @@ -47,12 +70,41 @@ fun makeSetting(name: SettingType, value: String) { } } +fun getSetting(type: SettingType): String { + try { + val data = Json.decodeFromString(File(pathToSettings).readText()) + return when (type) { + SettingType.LANGUAGE -> data.language + SettingType.BD -> data.bd + SettingType.NEO4JURI -> data.neo4jUri + SettingType.NEO4JUSER -> data.neo4jUser + SettingType.NEO4JPASSWORD -> data.neo4jPassword + } + } catch (e: Exception) { + resetSettings() + return getSetting(type) + } +} + @Composable fun SettingsScreen(navController: NavController) { var language by mutableStateOf(getLocalisation()) + Column(modifier = Modifier.padding(20.dp, 10.dp)) { + Row { + Language(navController) + Spacer(Modifier.width(10.dp)) + Saving() + Spacer(Modifier.width(10.dp)) + } + } +} + +@Composable +fun Language(navController: NavController) { Column { + var language by mutableStateOf(getSetting(SettingType.LANGUAGE)) Text( - text = localisation("settings"), + text = localisation("language"), fontSize = 28.sp, modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp) ) @@ -64,7 +116,8 @@ fun SettingsScreen(navController: NavController) { modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) .border(width = if (language == "cn-CN") 5.dp else 3.dp, color = Color.Black) - .bounceClick(), + .bounceClick() + .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( backgroundColor = if (language == "cn-CN") DefaultColors.primarySelected else DefaultColors.primary @@ -80,7 +133,8 @@ fun SettingsScreen(navController: NavController) { modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) .border(width = if (language == "ru-RU") 5.dp else 3.dp, color = Color.Black) - .bounceClick(), + .bounceClick() + .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( backgroundColor = if (language == "ru-RU") DefaultColors.primarySelected else DefaultColors.primary @@ -96,7 +150,8 @@ fun SettingsScreen(navController: NavController) { modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) .border(width = if (language == "en-US") 5.dp else 3.dp, color = Color.Black) - .bounceClick(), + .bounceClick() + .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( backgroundColor = if (language == "en-US") DefaultColors.primarySelected else DefaultColors.primary @@ -109,10 +164,168 @@ fun SettingsScreen(navController: NavController) { modifier = Modifier .padding(16.dp) .border(width = 3.dp, color = Color.Black) - .bounceClick(), + .bounceClick() + .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) ) { Text(localisation("back"), style = defaultStyle, color = Color.White) } } +} + +@Composable +fun Saving() { + var saving by mutableStateOf(getSetting(SettingType.BD)) + Column { + Text( + text = localisation("saving"), + fontSize = 28.sp, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp) + ) + Button( + onClick = { + makeSetting(SettingType.BD, "sqlite") + saving = "sqlite" + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = if (saving == "sqlite") 5.dp else 3.dp, color = Color.Black) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFF17f908) + ) + ) { + Text("SQLite", style = defaultStyle) + } + Button( + onClick = { + makeSetting(SettingType.BD, "neo4j") + saving = "neo4j" + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = if (saving == "neo4j") 5.dp else 3.dp, color = Color.Black) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFF0c97ff) + ) + ) { + Text("Neo4j", style = defaultStyle) + } + Button( + onClick = { + makeSetting(SettingType.BD, "local_file") + saving = "local_file" + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = if (saving == "local_file") 5.dp else 3.dp, color = Color.Black) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color.Yellow + ) + ) { + Text("Local file", style = defaultStyle) + } + } + Spacer(modifier = Modifier.width(30.dp)) + var uri by remember { mutableStateOf(getSetting(SettingType.NEO4JURI)) } + var user by remember { mutableStateOf(getSetting(SettingType.NEO4JUSER)) } + var password by remember { mutableStateOf(getSetting(SettingType.NEO4JPASSWORD)) } + if (saving == "neo4j") { + Column(modifier = Modifier.fillMaxSize()) { + Text( + text = localisation("neo4j_data"), + fontSize = 28.sp, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + TextField( + modifier = Modifier + .width(1000.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)) + .size(400.dp, 80.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + placeholder = { + Text( + "Enter URI: for example, bolt://localhost:7687", + fontSize = 32.sp + ) + }, + textStyle = TextStyle(fontSize = 32.sp), + shape = RoundedCornerShape(25.dp), + value = uri, + onValueChange = { newValue -> uri = newValue }, + ) + Spacer(modifier = Modifier.height(40.dp)) + TextField( + modifier = Modifier + .width(1000.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)) + .size(300.dp, 80.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(25.dp), + placeholder = { + Text( + "Enter username:", + fontSize = 32.sp + ) + }, + textStyle = TextStyle(fontSize = 32.sp), + value = user, + onValueChange = { newValue -> user = newValue }, + ) + Spacer(modifier = Modifier.height(40.dp)) + TextField( + modifier = Modifier + .width(1000.dp) + .border(4.dp, color = Color.Black, shape = RoundedCornerShape(25.dp)) + .size(400.dp, 80.dp), + + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + placeholder = { + Text( + "Enter password", + fontSize = 32.sp + ) + }, + shape = RoundedCornerShape(25.dp), + textStyle = TextStyle(fontSize = 32.sp), + value = password, + onValueChange = { newValue -> password = newValue }, + ) + Spacer(modifier = Modifier.height(40.dp)) + Button( + onClick = { + makeSetting(SettingType.NEO4JURI, uri) + makeSetting(SettingType.NEO4JUSER, user) + makeSetting(SettingType.NEO4JPASSWORD, password) + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 20.dp) + .border(width = 3.dp, color = Color.Black) + .bounceClick() + .size(200.dp, 80.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xFF0c97ff) + ) + ) { + Text("Connect", style = defaultStyle) + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index f109757..eab9e28 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -27,16 +27,15 @@ import view.common.DefaultShortButton import view.common.DirectedAlgorithmDialog import view.graph.UndirectedGraphView import viewmodel.MainScreenViewModel +import viewmodel.UndirectedGraphViewModel @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) @Composable fun UndirectedGraphScreen( - navController: NavController, mainScreenViewModel: MainScreenViewModel, - graphId: Int + navController: NavController, + graphVM: UndirectedGraphViewModel, ) { - val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) - Box(modifier = Modifier .fillMaxSize() @@ -95,7 +94,7 @@ fun UndirectedGraphScreen( Spacer(modifier = Modifier.height(16.dp)) // Save button - DefaultShortButton({ graphVM.saveSQLite() }, "save") + DefaultShortButton({ mainScreenViewModel.saveGraph(graphVM.name) }, "save") Spacer(modifier = Modifier.height(10.dp)) // Visualization Button diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 8f41d70..6fe8905 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -1,208 +1,125 @@ package viewmodel +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import java.sql.DriverManager -import java.sql.SQLException - -enum class SaveType { - SQLite, - CSV, - Neo4j, - Internal -} +import mu.KotlinLogging +import view.screens.SettingType +import view.screens.getSetting +import viewmodel.graph.AbstractGraphViewModel +import viewmodel.io.Neo4jRepository +import viewmodel.io.SQLiteRepository +import kotlin.math.log enum class GraphType() { Undirected, Directed, } -class MainScreenViewModel : ViewModel() { - val graphs = GraphStorage() - internal var inited = false - private val DB_DRIVER = "jdbc:sqlite" - fun addGraph(name: String, type: String, saveType: SaveType) { - when (type) { - "undirected" -> { - graphs.typeList.add(GraphType.Undirected) - val graphVM = UndirectedGraphViewModel(name) - graphVM.saveType = saveType - graphs.undirectedGraphs.add(graphVM) - } +private val logger = KotlinLogging.logger { } - "directed" -> { - graphs.typeList.add(GraphType.Directed) - val graphVM = DirectedGraphViewModel(name) - graphVM.saveType = saveType - graphs.directedGraphs.add(graphVM) - } +class MainScreenViewModel(val saveType: String = getSetting(SettingType.BD)) : ViewModel() { + val graphs by mutableStateOf(mutableMapOf>()) + val graphsNames = mutableStateListOf() + internal var inited = false + fun addGraph(name: String, type: String) { + if (graphsNames.contains(name)) { + return } - } + val graphVM: AbstractGraphViewModel + when (type) { + "Undirected" -> { + graphVM = UndirectedGraphViewModel(name) - fun initModel(index: Int, source: String) { - if (graphs.typeList[index] == GraphType.Directed) { - val graph = graphs.getDirected(index) - if (graph.initedGraph) return - else graph.initedGraph = true - if (graph.saveType == SaveType.SQLite) { - val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") - val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } - val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } - val resVertex = getVertex.executeQuery() - val resEdges = getGraphs.executeQuery() - while (resVertex.next()) { - var vertexName = resVertex.getString("Vertexes") - if (vertexName.length > 1) vertexName = - vertexName.slice(1..vertexName.length - 1) - graph.addVertex(vertexName) - } - while (resEdges.next()) { - for (i in graph.graph.vertices) { - val weight = resEdges.getString("V$i") - var to = resEdges.getString("Vertexes") - to = to.slice(1.. 1) vertexName = - vertexName.slice(1..vertexName.length - 1) - graph.addVertex(vertexName) - } - while (resEdges.next()) { - for (i in graph.graph.vertices) { - val weight = resEdges.getString("V$i") - var to = resEdges.getString("Vertexes") - to = to.slice(1.. { + graphVM = DirectedGraphViewModel(name) } + } + graphs[name] = graphVM + graphsNames.add(name) } - fun graphInit(source: String) { - val DB_DRIVER = "jdbc:sqlite" - val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") - ?: throw SQLException("Cannot connect to database") - val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") - - connection.createStatement().also { stmt -> - try { - stmt.execute(createIndex) - println("Tables created or already exists") - } catch (ex: Exception) { - println("Cannot create table in database") - println(ex) - } finally { - stmt.close() + fun saveGraph(name: String) { + try { + + val graphVM = getGraph(name) + if (saveType == "sqlite") { + graphVM.model.saveSQLite(name, graphVM.graphType.toString(), "storage") + } else if (saveType == "neo4j") { + val rep = Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + rep.saveGraph(graphVM) } + } catch (e: Exception) { + logger.error { "Can't save graph: $name" } } - val getGraphs by lazy { connection.prepareStatement("SELECT * FROM BEBRA_KILLER") } - val resSet = getGraphs.executeQuery() - while (resSet.next()) { - if (resSet.getString("type") == "Directed") { - addGraph(resSet.getString("name"), "directed", SaveType.SQLite) - } else if (resSet.getString("type") == "Undirected") { - addGraph(resSet.getString("name"), "undirected", SaveType.SQLite) - } - } - connection.close() } - enum class graphType() { - Undirected, - Directed, + fun getGraph(name: String): AbstractGraphViewModel { + return graphs[name] + ?: throw IllegalStateException("Can't find graph with name $name") } - inner class GraphStorage() { - fun getName(index: Int): String { - when (graphs.typeList[index]) { - GraphType.Undirected -> { - return graphs.undirectedGraphs[findGraph(index)].name - } - - GraphType.Directed -> { - return graphs.directedGraphs[findGraph(index)].name - } - } + fun initGraph(name: String, sourceSQLite: String) { + val graphVM = getGraph(name) + if (graphVM.isInited) return + if (saveType == "sqlite") { + SQLiteRepository.initGraph(graphVM, sourceSQLite) + } else if (saveType == "neo4j") { + val rep = Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + graphs[name] = rep.getGraph(name) } + graphVM.isInited = true - internal fun findGraph(index: Int): Int { - var indexAr = 0 - when (graphs.typeList[index]) { - GraphType.Undirected -> { - for (i in 0..index) if (graphs.typeList[i] == GraphType.Undirected) indexAr += 1 - } + } - GraphType.Directed -> { - for (i in 0..index) if (graphs.typeList[i] == GraphType.Directed) indexAr += 1 - } + fun initGraphList(sourceSQLite: String) { + if (saveType == "sqlite") { + SQLiteRepository.initGraphList("storage", this) + } else if (saveType == "neo4j") { + val rep = try { + Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + } catch (e: Exception) { + logger.info { "Could not start a neo4j session in repository with given data" } + return } - return indexAr - 1 + rep.initGraphList(this) } + inited = true + } - fun removeGraph(index: Int) { - val DB_DRIVER = "jdbc:sqlite" - val delTable = "DROP TABLE ${getName(index)}" - val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='${getName(index)}';" - val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") - ?: throw SQLException("Cannot connect to database") - connection.createStatement().also { stmt -> - try { - stmt.execute(delTable) - stmt.execute(delIndexRec) - println("Tables created or already exists") - } catch (ex: Exception) { - println("Cannot create table in database") - println(ex) - } finally { - stmt.close() - } - } - when (graphs.typeList[index]) { - GraphType.Undirected -> { - graphs.undirectedGraphs.removeAt(findGraph(index)) - graphs.typeList.removeAt(index) - } - - GraphType.Directed -> { - graphs.directedGraphs.removeAt(findGraph(index)) - graphs.typeList.removeAt(index) - } + fun removeGraph(name: String) { + if (saveType == "sqlite") { + try { + SQLiteRepository.removeGraph(name) + } catch (e: Exception) { } + } else if (saveType == "neo4j") { + val rep = Neo4jRepository( + getSetting(SettingType.NEO4JURI), + getSetting(SettingType.NEO4JUSER), + getSetting(SettingType.NEO4JPASSWORD) + ) + rep.removeGraph(name) } - - fun getUndirected(index: Int): UndirectedGraphViewModel { - return undirectedGraphs[findGraph(index)] - } - - fun getDirected(index: Int): DirectedGraphViewModel { - return directedGraphs[findGraph(index)] - } - - var undirectedGraphs = mutableStateListOf>() - var directedGraphs = mutableStateListOf>() - var typeList = mutableStateListOf() + graphs.remove(name) + graphsNames.remove(name) } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt index 66db5b3..b2aed0a 100644 --- a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt @@ -14,7 +14,6 @@ import model.graph.Graph import model.graph.Edge import view.common.DefaultColors import viewmodel.GraphType -import viewmodel.SaveType import width import kotlin.random.Random @@ -27,6 +26,7 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM get() = graphModel.isWeighted val negativeWeights get() = graphModel.negativeWeights + var isInited = false val model get() = graphModel val verticesVM @@ -41,7 +41,6 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM } return result.toList() } - var saveType = SaveType.SQLite abstract val graphType: GraphType var zoom by mutableStateOf(1f) var canvasSize by mutableStateOf(Offset(400f, 400f)) diff --git a/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt index fb009ca..148053d 100644 --- a/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/DirectedGraphViewModel.kt @@ -17,8 +17,6 @@ class DirectedGraphViewModel( val graph: DirectedGraph = DirectedGraph() ) : AbstractGraphViewModel(name, graph) { - private val DB_DRIVER = "jdbc:sqlite" - var initedGraph = false override val graphType = GraphType.Directed override fun addEdge(from: V, to: V, weight: Int) { diff --git a/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt index d53f0df..9df9919 100644 --- a/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/UndirectedGraphViewModel.kt @@ -14,8 +14,6 @@ class UndirectedGraphViewModel( name: String, val graph: UndirectedGraph = UndirectedGraph() ) : AbstractGraphViewModel(name, graph) { - private val DB_DRIVER = "jdbc:sqlite" - var initedGraph = false override val graphType = GraphType.Undirected override fun addEdge(from: V, to: V, weight: Int) { diff --git a/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt b/src/main/kotlin/viewmodel/io/Neo4jRepository.kt similarity index 84% rename from src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt rename to src/main/kotlin/viewmodel/io/Neo4jRepository.kt index 0a3b08e..12c9abb 100644 --- a/src/main/kotlin/viewmodel/io/neo4j/Neo4jRepository.kt +++ b/src/main/kotlin/viewmodel/io/Neo4jRepository.kt @@ -1,4 +1,4 @@ -package viewmodel.io.neo4j +package viewmodel.io import mu.KotlinLogging @@ -6,6 +6,7 @@ import org.neo4j.driver.AuthTokens import org.neo4j.driver.GraphDatabase import org.neo4j.driver.TransactionContext import viewmodel.DirectedGraphViewModel +import viewmodel.MainScreenViewModel import viewmodel.UndirectedGraphViewModel import viewmodel.graph.AbstractGraphViewModel import java.io.Closeable @@ -15,6 +16,7 @@ import java.io.Closeable private val logger = KotlinLogging.logger { } class Neo4jRepository(uri: String, user: String, password: String) : Closeable { + val driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password)) val session = driver.session() @@ -23,7 +25,7 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl val graphType = graphVM.graphType // remove old graph if exist session.executeWrite { tx -> - removeGraph(tx, graphVM) + removeGraph(tx, graphName) } // create graph session.executeWrite { tx -> @@ -61,21 +63,21 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl ) } } + println("fds") } - fun removeGraph(graphVM: AbstractGraphViewModel) { + fun removeGraph(name: String) { session.executeWrite { tx -> - removeGraph(tx, graphVM) + removeGraph(tx, name) } } private fun removeGraph( tx: TransactionContext, - graphVM: AbstractGraphViewModel, + name: String, ) { - val graphName = graphVM.name val check = tx.run( - "MATCH (graph: Graph) WHERE graph.name = '$graphName' RETURN graph.name as name;" + "MATCH (graph: Graph) WHERE graph.name = '$name' RETURN graph.name as name;" ).list() if (check.size == 0) return @@ -84,9 +86,9 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl return } tx.run( - "OPTIONAL MATCH (n: $graphName)" + - "OPTIONAL MATCH (graph: Graph) WHERE graph.name = '$graphName'" + - " DETACH DELETE n, graph;" + "OPTIONAL MATCH (n: $name)" + + "OPTIONAL MATCH (graph: Graph) WHERE graph.name = '$name'" + + "DETACH DELETE n, graph;" ) } @@ -106,7 +108,7 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl val graphData = session.executeRead() { tx -> val result = tx.run("MATCH (graph: Graph) WHERE graph.name = '$graphName' RETURN graph.name as name, graph.type as type") - return@executeRead result.single().asMap() + return@executeRead result.list().first().asMap() } val graph: AbstractGraphViewModel if (graphData["type"] == "Undirected") { @@ -134,6 +136,17 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl return graph } + fun initGraphList(mainScreenViewModel: MainScreenViewModel) { + val graphs = session.executeRead { tx -> + val result = + tx.run("MATCH (graph: Graph) RETURN graph.name as name, graph.type as type") + return@executeRead result.list() { it.asMap() } + } + for (graph in graphs) { + mainScreenViewModel.addGraph(graph["name"].toString(), graph["type"].toString()) + } + } + fun clearDB() { session.executeWrite { tx -> tx.run("MATCH (n) DETACH DELETE n") diff --git a/src/main/kotlin/viewmodel/io/SQLiteRepository.kt b/src/main/kotlin/viewmodel/io/SQLiteRepository.kt new file mode 100644 index 0000000..8e66284 --- /dev/null +++ b/src/main/kotlin/viewmodel/io/SQLiteRepository.kt @@ -0,0 +1,113 @@ +package viewmodel.io + +import viewmodel.DirectedGraphViewModel +import viewmodel.GraphType +import viewmodel.MainScreenViewModel +import viewmodel.UndirectedGraphViewModel +import viewmodel.graph.AbstractGraphViewModel +import java.sql.DriverManager +import java.sql.SQLException + +object SQLiteRepository { + private val DB_DRIVER = "jdbc:sqlite" + fun initGraphList(source: String, mainScreenVM: MainScreenViewModel) { + val DB_DRIVER = "jdbc:sqlite" + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") + ?: throw SQLException("Cannot connect to database") + val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") + + connection.createStatement().also { stmt -> + try { + stmt.execute(createIndex) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM BEBRA_KILLER") } + val resSet = getGraphs.executeQuery() + while (resSet.next()) { + if (resSet.getString("type") == "Directed") { + mainScreenVM.addGraph(resSet.getString("name"), "directed") + } else if (resSet.getString("type") == "Undirected") { + mainScreenVM.addGraph(resSet.getString("name"), "undirected") + } + } + connection.close() + } + + fun initGraph(graphVM: AbstractGraphViewModel, source: String) { + if (graphVM.graphType == GraphType.Directed) { + val graphVM = graphVM as DirectedGraphViewModel + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graphVM.name}") } + val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graphVM.name}") } + val resVertex = getVertex.executeQuery() + val resEdges = getGraphs.executeQuery() + while (resVertex.next()) { + var vertexName = resVertex.getString("Vertexes") + if (vertexName.length > 1) vertexName = + vertexName.slice(1..vertexName.length - 1) + graphVM.addVertex(vertexName) + } + while (resEdges.next()) { + for (i in graphVM.graph.vertices) { + val weight = resEdges.getString("V$i") + var to = resEdges.getString("Vertexes") + to = to.slice(1.. + val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } + val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } + val resVertex = getVertex.executeQuery() + val resEdges = getGraphs.executeQuery() + while (resVertex.next()) { + var vertexName = resVertex.getString("Vertexes") + if (vertexName.length > 1) vertexName = + vertexName.slice(1..vertexName.length - 1) + graph.addVertex(vertexName) + } + while (resEdges.next()) { + for (i in graph.graph.vertices) { + val weight = resEdges.getString("V$i") + var to = resEdges.getString("Vertexes") + to = to.slice(1.. + try { + stmt.execute(delTable) + stmt.execute(delIndexRec) + println("Tables created or already exists") + } catch (ex: Exception) { + println("Cannot create table in database") + println(ex) + } finally { + stmt.close() + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/localisation/en-US.json b/src/main/resources/localisation/en-US.json index ea8d952..0afd86a 100644 --- a/src/main/resources/localisation/en-US.json +++ b/src/main/resources/localisation/en-US.json @@ -20,9 +20,21 @@ "code": "add_vertex", "localisation": "Add vertex" }, + { + "code": "saving", + "localisation": "Saving" + }, + { + "code": "language", + "localisation": "Language" + }, + { + "code": "neo4j_data", + "localisation": "Neo4j data" + }, { "code": "open_edge", - "localisation":"Add edge" + "localisation": "Add edge" }, { "code": "add_edge", diff --git a/src/main/resources/localisation/ru-RU.json b/src/main/resources/localisation/ru-RU.json index 5311dc7..24c9c39 100644 --- a/src/main/resources/localisation/ru-RU.json +++ b/src/main/resources/localisation/ru-RU.json @@ -16,6 +16,18 @@ "code": "home", "localisation": "На главную" }, + { + "code": "saving", + "localisation": "Сохранение" + }, + { + "code": "neo4j_data", + "localisation": "Данные для Neo4j" + }, + { + "code": "language", + "localisation": "Язык" + }, { "code": "add_vertex", "localisation": "Добавить вершину" diff --git a/src/test/kotlin/SQLiteIntegrationTest.kt b/src/test/kotlin/SQLiteIntegrationTest.kt index 155c87e..06bc179 100644 --- a/src/test/kotlin/SQLiteIntegrationTest.kt +++ b/src/test/kotlin/SQLiteIntegrationTest.kt @@ -1,4 +1,3 @@ - import model.graph.DirectedGraph import model.graph.Edge import viewmodel.MainScreenViewModel @@ -23,16 +22,16 @@ internal class SQLiteIntegrationTest { this.addEdge(2, 3, 3) this.addEdge(3, 4, 1) } - graph.saveSQLite("TEST_DEFAULT", "Directed", "test") + graph.saveSQLite("someName", "Directed", "test") - val graphVM = MainScreenViewModel() - graphVM.graphInit("test") - graphVM.initModel(0, "test") - val loadedGraph = graphVM.graphs.getDirected(0).graph - val result = Dijkstra(loadedGraph, 4).dijkstra("1", "4") + val mainScreenVM = MainScreenViewModel("sqlite") + mainScreenVM.initGraphList("test") + mainScreenVM.initGraph("someName", "test") + val loadedGraph = mainScreenVM.getGraph("someName") + val result = Dijkstra(loadedGraph.model, 4).dijkstra("1", "4") val shortestLengthExpected = 6 var shortestLengthActual = 0 - for (i in result){ + for (i in result) { shortestLengthActual += i.weight } assertNotNull(shortestLengthActual) diff --git a/src/test/kotlin/io/Neo4jTest.kt b/src/test/kotlin/io/Neo4jTest.kt index caaf7c9..71855c8 100644 --- a/src/test/kotlin/io/Neo4jTest.kt +++ b/src/test/kotlin/io/Neo4jTest.kt @@ -6,7 +6,7 @@ import view.graph.UndirectedGraphView import view.screens.UndirectedGraphScreen import viewmodel.DirectedGraphViewModel import viewmodel.UndirectedGraphViewModel -import viewmodel.io.neo4j.Neo4jRepository +import viewmodel.io.Neo4jRepository import kotlin.test.assertContentEquals import kotlin.test.assertEquals From 7b963e1ea139a632ff9a71c6fa2a7cc68be81219 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 29 May 2024 05:57:47 -0400 Subject: [PATCH 159/172] add: gui improve, add graph icons --- src/main/kotlin/view/screens/MainScreen.kt | 37 ++++++++++++------ .../view/screens/UndirectedGraphScreen.kt | 3 +- src/main/resources/directed.png | Bin 0 -> 14279 bytes src/main/resources/undirected.png | Bin 0 -> 5984 bytes 4 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/directed.png create mode 100644 src/main/resources/undirected.png diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index b7bf1ea..2a1e158 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -1,9 +1,6 @@ package view.screens -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -16,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.loadImageBitmap import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -30,6 +28,7 @@ import view.common.defaultStyle import viewmodel.GraphType import viewmodel.MainScreenViewModel import viewmodel.SaveType +import java.io.File @Composable fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { @@ -289,13 +288,29 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView color = Color.Black, shape = RoundedCornerShape(45.dp) ), - colors = ButtonDefaults.buttonColors(backgroundColor = DefaultColors.primary) - ) { - Text( - text = mainScreenViewModel.graphs.getName(index), - style = bigStyle, - modifier = Modifier.clip(RoundedCornerShape(45.dp)) - ) + colors = ButtonDefaults.buttonColors(backgroundColor = if (mainScreenViewModel.graphs.typeList[index] == GraphType.Undirected) DefaultColors.primary + else Color.Cyan + )) { + Row{ + Column (modifier = Modifier.align(Alignment.CenterVertically)){ + Image( + bitmap = if (mainScreenViewModel.graphs.typeList[index] == GraphType.Directed) loadImageBitmap(File("src/main/resources/directed.png").inputStream()) + else loadImageBitmap(File("src/main/resources/undirected.png").inputStream()), + contentDescription = "Type", + + modifier = Modifier + .padding(15.dp) + .align(Alignment.End), + ) + } + Column (modifier = Modifier.align(Alignment.CenterVertically)){ + Text( + text = mainScreenViewModel.graphs.getName(index), + style = bigStyle, + modifier = Modifier.clip(RoundedCornerShape(45.dp)) + ) + } + } } Spacer(modifier = Modifier.width(10.dp)) diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index 05d5744..4add5ea 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -34,10 +34,10 @@ fun UndirectedGraphScreen( ) { val graphVM by mutableStateOf(mainScreenViewModel.graphs.getUndirected(graphId)) val language = getLocalisation() - Box(modifier = Modifier .fillMaxSize() .onPointerEvent(PointerEventType.Scroll) { + if (it.changes.first().scrollDelta.y > 0) { graphVM.zoom = (graphVM.zoom - graphVM.zoom / 8).coerceIn(0.01f, 15f) } else { @@ -77,6 +77,7 @@ fun UndirectedGraphScreen( var isFordBellmanMenu by remember { mutableStateOf(false) } var isVisualizationRunning by remember { mutableStateOf(false) } + // To MainScreen DefaultShortButton({ navController.popBackStack() }, "home", defaultStyle) Spacer(modifier = Modifier.height(10.dp)) diff --git a/src/main/resources/directed.png b/src/main/resources/directed.png new file mode 100644 index 0000000000000000000000000000000000000000..46744ca52b306ccc87a94d2923e6baa60a3eb9d8 GIT binary patch literal 14279 zcmeHuc{tSX`|sFlN;0w)8e=Re&DgRwmh59GEvR8+vP5DqgRw8!Dm9i+Qj#TMP+2mz zB2A^p*vi@vk}!5rIS+ll-|PI&_4}Q3u5-?xr|WVV@427*x!2coKllCas`YVGzMWz_ zArJ^3*39S>1j3QD`2*bson?D?o%&ff-$hKV`XU>w(T3Kp4 z6TQ#`7vcq1G{wsYP(vUXJ&F&(+0&JbxZrxx!&?VATib|4c(~{w?bNK4tb7bz-95~L z{ajB6A3x(9?CGrKg4EMRU?|#vftM?pfS`C?_9kglbdXzowZZ4j$BIbA775u?2YGCB zL&RAtYlI=u&lRDCRz^81DXAbdw9syT3T9)N-ByfDkwmKA_aMq z2^5q!N%~(Lj9f|1ejYw#5280>lOy2*(Vwh?1fu@c!OQ30Y`sbU*a=unkwWlMR7NXp z>a<1Z;{0zqAAi5gTbsK$E4p5G^>XzllK`#qziEBkiDV+lo%mm{{`c4aVFO@WE31F+ z@t?}#<@N6^NMz#xpvFH2`A?}yXM%iO6;HX6i2i=guEqgCOzBN+e6$VyTnS{N-x(tD z^1nP~{V$ObDr)E>2zlU}BTC9jYG^OC$`Lg+ge&0!L0w(#AOaz8=i%)_3?v==?|8Wy z5y-AO$jwD4B{h_a)){4GZDl2GRSg9tRc$4uzob?~7Z10f|BUpAvbL(`e;@_nXN0d=o8cGDz1+@!DRNXYxHI-G=|F!;vp9jb~!sY*C zKAX9?87Tz){o@R0 zcY^mtR{*|>$W1*JH&N=ZD;57oFK>PK-yzz+$N@}le*8B;gFpTa@vh#0wI2ZRRa$2O z1fop98XY@B$(kMv`WS}A56_*@Yb2cQHvfbD^XB|W`R-zv`pK(=?HtOtuJRsKE;RDl z6=y84%{5J`OJ4K1%fVz#DH)+?{ZH2}4EI_itoJG*pYl&#Zk(9>nGM{;)OEEfcIRBWBQ>M9!&`TLGdBm}Y? z58;5EkKl;D5#8}po6HcwiWwfxm!5>iJYv1OQC>sN#DbA1= zKR%%J9Trs`R>Pj&=$&->7NW;T)zlxEUTbo7ze`^CZ=Z1cHcNA9T=bpP?P=E>3uaa~ zJ6ErDwj7n6#ij(l^~JYO(2jl3`%+%|FU=OepyI;yIv|8KdSq<*Mj~hj`4yccN?dKToaeLw0@*o-O5H`C;nj# znwj$tafU43b>IL3Q!Y#VBdliFg4dz9{8y#Jp1z^u>e z-u^MXb~qRF+PZRkppNPpad_{u&^Xr85pDxf8!2b%Y3RN5F{{usD36v&bEKS^?O-@Y0aXL^W6 zdR6x5`kUC^6BXLbcaw`g0}YU?o`}2L+h}PYthqScoB+p@gniDP?7g^$+M-BPT+flF za6-1TzZzAL2A{AT|1|f-~n!&b8CUDs6&8t8QuZl6qu?~Ak6>Uin| z?^Snb&(?JL_Eby~fA&SpG!j!XsO`IQYG!ZENDu`s`kG3n3-6&9`wcqnmE_>G>?&X> zA33IYu~6y$-7E>Ei3MSg+C2vQ-i^b*>|p$MT~(4&KA-*%^dD)bbNQH37$z;l$m4Mgy)=!}u!ltbA2TdmfE>_^q=v^B+F(s2TlRTAz zt5!m?I<^5rCdgy1&1@vcnR>Xt>fbH=+OEy2HP~4D)S>&*DFN7gi=@2Pw7l%=RWQU0 zZV+lx&XiE2st()y;qRoW<6}LK-s$Xa4lsOzA-nE3Zto=JRCb`RebXNmVa*ZtH9O-a zd*wvOKfX(t2sCTWd0wNhWxh;}pPsi0$XuP>cI2`+E4h^7nOth)KOH37AJfakB~<*F zxmNpZ}3Jl{eCH`-jEXx#db@H{u|B zK<2paqVjuIjwuUCOnsM}TuA-u>wVL7ATz;JoP`t|9+s~DwWIRpq?CT->%&c>SNa|} zHFyi9j$OE?kj`LWxum*GnAa`(9`3_M3Uum6Liir^46q_udX9W=PSH@+z0+C%Ak(3?yKKwz;>i*~~MN^wYz+FO^Urd{c}m zq57}c2^urCn!OcfYd1yPKPS|dtc}ONDx>j2=}!x8fc;>xjGC#cv^>hGH}o{Wd=~El zoi}Wt&+Piwn&~xzi-SQ~->~2t9-eD=h!TIBDRg>YzA42yul{ zS1zg#k+1IY_+3XW|3DWwW4YT1c<1OTaroT4OfOtvp-;Vp_X-|mYykPd1-5=6$9iJX zlRp?})A#_b!Vh+E?O{Ksr2}ZYQrlCC9;lGtEy%%nIhizL3L<93VKQZ4j5_J}cBZGhBvfdAZVwczZmf)3d!Tq~HZW{Km z6Ar|d@a{nS)GiHzvugwjbK+{NIr?rdeD;R=(30N-_s85^5)=A7k?%N1XZuu`+wW%O<;HYNjL5;zlh8@)by6!-^k|$7V;G4bhxz~pH6JaZ& zlBYcNyp{40s~C2O{RT=&p{6y4e-Rqeyv&3?st8{KLHApINao%X>hMuwyb3~R*!k9&;d26 zf^8ROJ>hE$dF}Is4TE zmcOf47lO})0GSU?&ZMA+hYrHQS-m}$eVbZgoWh&p$z8c0d)u3E8Oqymcf+c@%8XfxXeb8amH~W-^o=_~H$Ry1$K6>5N^;SftNW8&5Iu^($SU zZU82GSjwn;Pu!9feAi7`9sL5e(wnk_%^~N{${kCDy!>&td6Z#9Nz8ZIbL{Os9@)u~ zy!E$2@B=9`)AfpnQxV3IPPSPjJ#ZWdiJF!B8>T`=$IO~M!eKA2-)>M3CuSPqRRfhMNcNh0%c!>wC}%MohSm8 zy9%7hTn{oEv>x0a&)|E<&-W*y=_?Gra4Pt8Aq-Ab)LVR#w;nXh$5nAQ^m8;=(IU19 zRf6hcwQEO&;D!ai?CL<~$Te%NoCZ-k_k8H;vt40c#xpmsXkF|^z4hKjUH9G#=QiQs z@ znYZ1mCG?H-S@gy#@Z#c-xXH=|!3zD4M?5LFI(7Meu1!Eah&5~*uc{^l)VLU zx3lgB(OTlp3qoOuZCZhn`P)oi;4@SfhS4KWco;3IVf4gX8zg4@rU*3PomOB3wrsPSW-O`8YYbEpZ=I4j64tWV z;9Na(2pgERcy%6MnoJg*ttUkG9CsjoZO;}08r;!}XM9ZAAJ!Ti^FZ_D;QQ&vg-~j7 zw+Y3J>Ce!OI=H!a;f_t{2}#)v;>Vo*Ho#Ci!*9?5MU!+4LEB;J^?4`KXZ?WKA)9IQ z@ml(yh!)HDQkcA@l^Qcs+cBK*@zy@mnN_vc$RAv+s6Djbde^U#+H|br*h*GAKffs8 z`PQ)StFM06;Xhw>NxS`TNCBp__blH7P0oE#*yRsMjH4{bYyJ;?*3+?c;&Bu03zrIO z5AF97R^(yMzbRspYER)QzpQ@d0RMCaA9v^Fh^Dz~Vbr@d4zFkiho=16wH_NCq#dIn zLHv$NQji@yKF)qF%~Iw+y}BWKG*!XY9JLo>O27tyKzZtd< zToap`OiEaUqSD!Opm6`CDvD(J%l7Bdu1nL<1Qma~LWSGn&7-@ALYluhR@{UfQ=1sL zB2#}#70N%k2ZSo~F&u10OYC3&BH`yeN`&A15Ma3ng=dotO2 z)a;6MwM$6xu~WgvK}v;e`bwnW!nF-OXP47Es40Dh^Z_%6A8jb*lmNrniHi>zlXE1z z!)bDCUcdV?>!l@`i<4Up+-aGb~{TNx39xj zD%GpBTmpwtw`VQrX_hSCd^%gH>yxrEH}o1Pup>NPv6HGcrt8Z-hLv>U&oVj|o);?& z$>FP9L}@p|s+<}WCwo$K?HXFsQoKzm65ul8sWlO9A{}E|df)mfeptPRH#|6zl0QIR zo!NKK+y86aR2w1s0RXk1DzOYD9i4=S;L?=v`6B-ed*!S*1^@-`&~7r3QsLw2Hy`Z8eS* z&qin8)Zt~BGC7&In|Uu6C=Jji@kK3=j?>3-e_sTp2-#r{A5~qLwntDuX*yk>U+Z% zyY97@9@yh;;Y!KU8)YZuO;N)*Pa8x^Xif|?yFjV&h2oiMQ}3?EBCa-mctlX8IO_B- z^L?@XQ zig1Vj;Q8UC`9a;9zE{>K>&x^dXR|{Te~9CR9a~F_$?F4o&HkvXWEs1+y3Rju=Gng! zf)A&Xd7HQna(>nCj$xd?6m)VID)G6!Jcg=R@MQ6B#H8iSfHIR&GnyZju5j~FYw}{O zzQ^^)0oB#i9+5MM^v{{*bGQ30Lpa+`Y?#GGr2bZ1%4BsY582 zl`wCLjveGg{Eyd+o74<(n{LsBu(F9Z56fZOUazH=d3?ix$?=%&Yf3w(UUXGp{y03c z7nyqs4>KfDE7rVp>jJx;m8tae^B9%d3P|edT+FB|dml3AJMv)P&QyQtCpfkjRwH#W ze;#i@*(;`?d4&57$2p+RJ8Pv4lE3JyXSmoG$X^qze_v}yV5{EcAi(R*G4$P zalk5VMx1}@L#*Mi=R~*P^a+|2rd)wc*bh1e%syXRD8I*2(~s1N9LoLO)EkD#6+Ru> z(N})CkDC?-n&MeO!?BpbMxy+Tb?@oW<5Nv}u{cvU3agQP5YwR0#1#uo;wmo;5{hSB zk*zlxjAfV5lc2{ss^6QIS{y=`mWeMH-(Mk-RL$dhUh~Ya4vdWEYea}?+lK1a*q$7| zm^f*l;(chcb*whjZqOa@GlnMJxxRDUm^Ig#^2f56kX(hinY+TgGgOmo8z!A;Zlaz) z{EcK7ONyP(5ivQRgR$F=m3%}pZJNr@tz4F^!0ckp?H7P6A@$u#7X^f z$&lXm>41_f_Z!P|wKTZ== z`sa@=pNQvr)OcPD?O}28o9f{~yIN>gtL*OtOV*s%$%TYLryqj1_m~hqSPQ|8H^L<4 zCx7DZNhj~FxnXKIW^xEyoRjQ$d%Yol0w-TXT&)@XiW7dEGs@^+pNUB+W1#&0QFQ{FEs zsUc&VajE~uUjMEYuh2pT1s&VqEC*j55L-A~51*C=tRE`disspIYpsBuX=+T_YqG-m zNC2Tq;G?c%>1XjRO8e7W}2UGbu+8O)8B{gy_V%EkS~br74BHM%6{<)nsoJ993!lvZ(dXIH}=Zc#B|Y5 z4LFm&Fxk&^H^1hf&IfzNdc--URvbfrIiO^Jh&!Ih(7Y;Ht?sfi^*#N7avZ~|ZY7Qb z_dvVHi*z2y?$N0th5o%u6Lbyl0ShYT45}rT{#qwfB8!soPX#^%xt|BF;a#54fnZbaI3_@ zCob)SOKCccKOqM<#0TGhl$&y?@!lg^qQlu+U8np=26gEXCt06rKBt8ZdeBxg>6zPE zQMNFIp!2;6Um`XFIZ4NbkLog2jG9I%rKW3MM`>JU&8?M;H91JpL=YO|mvigr@wH$H z@41pC|7WAUVat0P3cT+##z3Z=##aloPQKA%EPYI9EZ^~XvMM+#Z^~1vS32g+oo_W2 zIHinv|HJ`y@56rAPndtN2l%ZC8x9o%6vz{v{qh-BL*$CKq~_CN+5b z5$aCN6xfCxyEXOhX%{}=2K&Vs7~+Os+wVGhe~M@s8fX}JO8a5JnS#!evYW&WWfvsyK@8#_rOvJ>1A~yI5 zXz^dAr!%Z6UuOmdetho_`~dr0G3Jv}H&Yo*Tvo*mtsbiDu{-g$lH7A|q9ODg{zEei ze*U!G0s^D7lQU1bttQ?gI2nNNBU}|`%ZB-L_L{>|hB-%$6l3_JVS6T{<5~63 za6SG_A*OGu4CN%?6G5|z!yoo>ObHbV^$=D(opQG~d20N# z>r`&pYr??`@#vDpPt5Puqd7H)!kVKP%hd1VK`=N7mpp;C8dI~iGL;xcKIpSJpfaY+ zu8%S2A7;WS=SAE7{nK(}jYhEHzP!?5I<$!xXK*pVL2o=z!Fd0u*GQb<3Ah6Bh$PL} zb;X)?R8Akm1=zO?+RQ-}Bz1_c5S@oHWhdb-Qvl>W7A%>(;S$_6$lzel zK?jArvOgUL5B^*MOm48|n!zOltwiqt=ZzZon}ib9oHaJ0pj%wOWzd%S^!7mXfP5ia zl`qxVeM+&-m2)D8E(PwD2Ua9Z9(Ib04-`*Z;nzQ@9ROPUf_z$D&00KZbfVHX6 zBpp)Q?@W3}wQ+CNdFx(=|9+HWg74W^&9)?qh(y9*>{0U3Snuc0W%uamnTNk-SeZKU zp!{vHtqbcR{)4tL+s9)dpQSx1zf$r`lg21rtk7Qu#|zckPDv7X)42p2bvI#3Bu5{P+sEi2 zW!Gi2J;*p-?T%Yq`7UBoM-RcB&$(WR5^Tf^(Zh4sLc?CSTsfeD+5r`lhwHAa=$;Ez+`(7odA*2zm zNIsX}AM-I``1;D}#lDX^R$T>frsZ3Z)% z@@t{wc)L8+Ufd(9ZmG1mz4-0~0ZMJO>z!A!zS`PdnsdZ)uqO_ZWdfy zvB=2c?|+o@q_jX~d3bojMtC#h`Eo;|mSG6{r>$)FpZ$TL5WE_ZlP_>W!t#Lb>#{L9 zgJqNjvuU_x*qmMi<(H7-bm86oWK3#FF=tQNZSio%Dtz3RPv$pg2O5Q$Mz<$kj* zi$t{*g&V{3e}ZOBA1Bsc+BdMrcQM#KEhd68iTX0aelysg%aaZ64|keJF-=?~!^ksW zmLZwuO%%)QF06pwui9x~9YZ(O9yY}BC9*ZS&HKW-M~1sEy6&bb0#92CAcS$dsQ7mN zNremXh^w=s$>Q|8_9?QxmEC@=qt8;>GPE$>1{9}KC@fc`*)RkX9_rgpdnhgvxeLtG zh{CU;n1Lr2(nC$g(-{-S6sK`03=9%(7flJmst+?oPRQSm_b$=YZ<@X|iZ1%-D!kpa z)V_o#JDF@jLqls`Pjp-Dk--$WqBn|X_?jua0GXsGN0`%Aqru`CRocw;fLD0Cxxylf z?N6kP>jWJPfqM*cI(7;Q4vvFkKbT=o^UrNF7B=Cw3Gi;4+3v72M3 zi*{1&VQ_HwG2@>YAimuTZ|Ppz@#e-g^C<~N$CL*mZ^bX;`1a=HK4{{nM_qi~YM>ip zO!=aJ(x@FYt|JZ(hQDKVRJWb^rO9o8*{RT_)n?KkPdA~zPOH#%&#x6(%1tryohcne6HfO)~@QJi%?9GbO8(Im?A~&{+bPZPgU-$*luqJQi|Q z0!QJlNw#55S|?mgkMA=JmZWMTMuY=8Ljo(xGrie6u+;)Aj@a$1FU%AtEV8fH`(yHeh3Q4bYkt9m!shI5_r@n9k`HischX(&V$+V-qqFrq$huXC&T|0CivA2vUJapY& zYVwKwPU=z6{H_Knhi?}~fZmSDb?F;xBlh+S}zX;wg zfY_SXxm;lKhSX_Z@Ggz7*cGe#9VeC3f5>t6khgyY(|_q0KC0 zQp_INL+487U@~)AH#IRMkyv~28gz+rm_BpAS8b>Ra~jJ3V4#Ex=z4T>@bIx@hxx_q zcqKDar~D0hW=M$H&#}H{WyW$ic7s{f3T8PFw`Y7w#uQ9Fd0i{s)LW{Vt-3qP8bPhC z7^#8hjQid^PObWW*S~={n@T>=kCsd&hct1O-*p87H6J;cchYe9SLc^EAM`V9dB^6i z(I=TQbH1e9^4If@*ubjkc>w{qe&uJLgsowKm|IOGL+$zi-7W{bo^aRGD=(FYu?z^G z077ga0s|u)6wEz`D)^Qal9Hzu*hoTV@?Fhb7XRao{@0&!ONXQ4&xB4F+UuEy;EX)UXVde?X6t>)ijlgh#3&ft6>k%^vT7DTT9C@w zX%M7fybuT`1(FC%4{> zP~nTj)1LmHD(As$?&SJYY!o2iE%!7CBp?S%8lm%)HN9v ziI?6D&}$i?$3L9*%hp&vyiko@+W{CaZ84TttAS{JWfyh?ZctY%9g^!Rdl>ezzw^8@ zR)S}5oHm4-wP(6vO| zx!q9yt-pZy$!ma?b{u)R#olE%H85n0QP*vwNYJxwVopy;z*By6W*1+1Rs6jN9@smC zoIKwj5R|d<$NQ+(K8a{a3DD*%_z5x+FW=;BJtKG0LpgNtS{=b-#kTI;IGgflWk^$v z`)o&FVBxsh{>{WPpInaL3qSRliS(V4Mwc*12DqOLwj6e)VtRfz@Wdr(q*bNghs_@8 znSD(l)?D2DP<)RgJ})<*OGN5UGV6ghgInfv635kso6*%V_Z{f<9_~G_XbKM|;zpN0 z47=0n|7^pqfIw9L%hJmOt)E4TKs4priNR`o%zA6DMU?WUJ*jhKh5>%vAseZ+nxYWb z%$=J_@tC}B$V)vloYYUAF{RLmaX&wyl6$fAY_E)JVC&ovj?4C{|9}=()r=$Dx!TB4 zVe(HEe#n3V@p4Cky-uVmW6%SiKU-$Y3cbD@_Ny{)`=@}Q>%bm#(HEtOa70_pMYFX* z=Cq;^>q(oneB$5;)KPje3h=N5Joc&SgOSuL@bxW%!|eMmhgFMo%)aJg+20jj85%)b ze)`ep{*Ms*ifY)^wzEljjVX(Wel!tG20+DT_{k zh`mMBZ8FQu&aB^vMKz7?$nU=^7mM)x@g<4QrX+Y}^!^QqADxby&~kW_hr3ATw}k2wz~EWBDtm=W&0(QvM;kX?A(T&GFTFT1VYTxm&iTUzj-r&lU z+nl;RJg1np=+lob14cJp;dK9O@~<<`!+(Dfo^ll|@B<67zeB*AMkAbWxzotfAEaCy zk&_t!E(ZQuWT(0ZfG86h&Bk@<0yslez>2baiUnqr_s!R3b(*SLUAN?doPN3hk-`+r z8iOLf6_iwe0^o?#vsGGT#aoLCRd~noEeKUIUaygSqpQnpzhZ#hcv;nIv4j7M&zbEB zD?_(G3u)VXohOd}bXFBtbw0O%GUk4?y=wEDm-*J3A>W6~S;q~rN6MFjq=hy|T zD9ERrzxm`aeHlr(K6vi#wij8p17Y{UeDENP15wp^=NpZDF?MBH;MD2DDa;9G#M5a% z&=A}dU|>4*U}{*5*?G#Vmn_vX$%hjF=rdq&h~M&4ksh?+E5)k7x1Mvp%on!}6bPdC z6&Aj~$)Ie8P1IZZ%gq~h$PSoCpj1i~+E!Opt4FFDgHF-H+&@A2^1|UFxB+F>^CVQ< zh3!BkE&jx|JQ@FvyfbUO5ehN`oHmBfdLhZ`yaJNH7rz@IdpWNfws4!=&T)K>h?Hx_W z{=)oF-QI%;P0u?3ECXk8ytvRcRjg)|H?|}D+%Jhs_ufCAdN+R*bDAmW6(4c`uN88{ zfAa=gC4BuZo+Ia1w|b|?Vs}fY?C+ai854WpkV^okPK6~<{G*j_5EAMlTDu{$<4`(cfRN=RL`tS}U`MZHkmjTn&!cVnp z;``&2`xpAhkXu%Olw3NbpSPBt(97b9m%%?@b!JO6alN7c*|hElo`f@^$li`iKJUZk zj1(u|e#c2}YhT_1t?i=&n-Q703-*{-z%wc9+3KAt{qav?LD%y1%BSZKZ=nxl(NkTY z(=#FFyn7q3ye<|%NWm8FK;qOS5vTA8?|uKo+lj(YR8KFH|E?&RsLO71?a{iZ{dw}2 z+f*!@66aO@=r2}0crSxP9GgZa>i*O+7G}+@$83+pXP(*A|CILIay2RTeR&cE)4hd8 z5cO1En1|lVIbxJu^~0HGmuvCg>KlEwKjMNY$n8Tge;Ix?8hn`0UNyb!@^|a5J7)xR zF5ZmwSDc*tJ|pMO-h570gmm2vjWk=>DNr_pju!nK5R$UxH;B6Gbx3l7IC2u1L`_@7 z^h-hcA-T7$BF#Ga1RiT+!g~Q?Jm7)!D}P1?w+Zo1v5sGBlx^aAQSrWrXHUh@9<1Ih zCpN<6=qthcmDe5hkaGO9OHtgkYNL_2HyRI~rckga5Ew{>Q#uE-QWEJ1Cg@MRG7kJr zMkfuo&#}UuPmZ=)1Xtv3reWP*y9QjWLM1(@OgD3-U~aM@A2UB!>qm>=kd92E*_vQK znhGkJ?d}{eW`5Hl^BBqx-k<+ie#Ik-+quH1F3%LZcDmnNlrpP0)WqO=aa_WZ zSLRcoMV<;KJQWbquK=S)NR7z;rUSg(3k>8a1^F?2Y+FLbI&25a_pw(sgEGqpY9S}g zb~`TY^h3E3S)Ef%Ua$DDimlfrAg}$)QAtkyC;?!L9L-e`v+^712^ot}tm2dX;~I5a zwoq!gLX6_RTV(`n8XqTO_OL(}uyJ6X{}d2%3uvm(3+X!+>2lWaD`=2FG%?^V>6suj zpArtr5^!)qrgOG2n{k2%wekQ1RRdm^8>q@n<$^cc790<}D)f6k8Cb-yAxdE2veTw& z^E)6m_|8epJxZv^{VG5qw;Lh}CkYm>EP|i^mAoJpJUF(w%m)Td+R3$PhCM8fFR+iX zl^me|npYMydtUu@4dg|G00I(u2)v*TDS~Z&;)X&V?Bw8t6dHj4rtrUhG{3f+< TU~y;~0{&r*j~kU5I7R1c#0Yh@recwsq&Onfva#@2$7i&01Oc&-uUI_t|IfByHKW zZoxc*c@PL>!Fo3rPY47mR(>_qz#UO7p$Yuaa8x~4NfvSG7A(;1_Er~}0LfE&u?2XOy__4n6*5CHnt!{fV*Kh-5T_`3*xsB1WA z##e{@DVo1s$YY^BS$u9-2!rJs4ytLQw1!7=4q?$kxgp!R+@Nof+VV|h0B37$1FQsN zV}rqBY^{T>aW=NL0EoOuFnAIM^9Aa`Wd^c^e+IR|l5Fh$00q;DNeiX@k69(secq<}~h_k{H zZEURQwiu!n!4}VC*)Z`qJf8TC-i6BuQwIGTJrks76Nq#gjY+q{Fl=yOs@q_#?1&f| z$b@c#XA=plu{gpv`pqGM;Hjep{o8z$XHl6gB)3357)RmP!?KOF|LapwAn-XsNHm5r z@yRHLGU6;I>g#skpLpP3v)~KchyWJI^q++L3yjZYhepssSZn-2Z~bpF4gFVk@oC}z z+$thtHH&FOWLsegOdBg(q7BB%&W_Hq!ZV0C43>?twV~m@dF9Wn!edAnn{S!j2vFLt7p)%Z)wqW>|QKfn9C$_m=C?CIH zMBw84RmS3gE(`%LsemtnprnIZeRNu0?p zNADvSgm@-7!&=js)Rf*aD&+N*;;J|(q-i|`;)c+Mc*5bL#ZV~V`X9X{j0+sjrZjd7 zCyNgWH8m_2Hj}E|%i!@(3=PjMhQ2W!^*S}V;fRQrX;MxY&M1e+I~!(@16RrHQc-Bi4&yBQ6K#ZRv|xCu zma^$PUl;?F2vnIGG%GX3Au~&sn4-I{tNq|Goc^evt2}ZO z42N)UxIcYfJf$HuwOoQRfM$sVCA!F@CTEHoJGC`Sh6GtibW%$-rK0G!(74va`!%J0 zmh-2X=mt~Z_F@;N0}Q7-fq2wnCfJa4cm+6F!A01)*?1O zynm|u!=R>{iJGq5Z&w>aylBd2K~v+S>&5^Q6OTbF6n4<{xM+-1)U-ZyJtnkdp(4i| zxIN)wx{k657XYc%kZN{8``?@Y*sLp7(ONPfDMFe9T)h>oDK$>=(dfuc2$8w#Wl;%Nlkvyn1O^P(p=y{zuctlDA@ zbn2bV>@HIuwv#(fn|@8j&wE+$&eSzO8Wl~a=; zd1ZS`mrv~bJ%yqb5tQ&o9w}dWT{1%@0Z*xsyK$RKpP7^eQFg}dr>5tw%Dhar($^ln zRQ}fcUi7oV=#=zNs89FNecRHreFtbk*T~*-6K&^Zdx8V>vxUbB!!M?5NPk8bm_2A( z-sBTh5M>)f*%_%{NTm-kXX%&Q3x8E9yYGJUU=Ir}$6T+QIYR(eIZR*n+P#);#3=79 zwNSw+oNG_AST1rLVT_z@4d9&yE>k_%vK<-aude+H!}U84-3h^|@C~v@Z+Ym7^^qKO z`pK-!7p*}NoTH+qi7ioO$D5?>4eZJYi8}L_1X;EjlPxC_9&{`t0p$+$_Fj%V{gN5@ z_F9?>P9mu~R=_Zm+Yvr3H^c!KoUYna2|JlZ43kdD8yN1rq%zx#!IIk(`g{z31THw{ zBfPA`{T?w)UNzESxGstQ;R3q3oJ@F`Yls3akkeAVR)jN$7$zML+Ej2Y61@{sgDS;ds}REW(3Qdy^=4ik5TJ-%2Y7 zj%N?H`XGKc(stHXFf|-@u}@`1@QtcWP(CVMJ&n^v$9-~hWAMoS<*A&#E|V>ct~-w& zJt{?CX^iZQw}$S2NAH57T0EQEctVO{Wv1XZ64kP)c-j8IFRRVqDrL`p+0w_$D_ea= zM}B(O^W^f-{Qg02eVIF~i`5X>C^`0;BkMOq4SN5OD$lEXufrgBk6L!!(JL!|_k=p= z9hQ-;=HfuFIkIQ9-i{R6;?&k3CM&3vdXwk<_DwYqHjlk&8#3mY+okbbj*j+d;`p>w zh5g1PRdKcY1yWr}IZJmzi}aW0wtYYXh}12Yt7f)>y_Rv)gV~1US2Gz( z^qWIdvhLPY1yX6=k%iA`d5mGwme06lR~mgAeaBy~Q|c-8^O>+;685HF8mA#qwJ+%{ z-Hp8EJL4RS-CnXtVWhaByZ2h$DJL!6fyP&jwf`qvMLb?K8*pPbc2KH4xV^%#NN>a7RuVADn4bR1Oz@ zJUR=F!*2wI_)c<7Vxtj0F7zYwp8GfV{~Ua2*1{5zq;Kt&F;jHxjP@+n7hK`IQfi8# zo*m5SR7at(61D!4=CEOdk%?uRuBPmFez6lbhopl}A9CNJbnww&?c5XbL5`fgywSxp zyiq$+|H|8B;gXa}{EgYCorldp1{z}}#bN&3>ECNNZUsf_H281s$K4>0-xxjWcBPjW zsd?pGui>$EJ`=xOoFhdblCJ4_HZPqGC|%UGb@E>M%)8zLt3yhCC4H)xa^JC4Mqzoc zMpEL=r6tyDS+zGC`Sy-RUl(5Y^&B`QHHs-RaxAy2Tuh$uCl5UIOxl3d~0pXyeYm1mxb8ogB6w}PlZMUL0DKhWwZHS|@hiHj0XshWV^zGgL__ZC-%qe-it_dlUx=8pzkY%IL#02Hi_EKbyzMi~u$ zs6_a{Jda+OJepWy55DdVez!#IX;`z%CEh2gvD$hLloR*#!J?@Ki*VZWw#sf|+6{RA zV&{>gwTgt(S;G@UpMvUxp%!;B)m^>F5}mQ=TzjX}G5aVRu#1a78mLcqZF<@fV^SH5 zsF4q;V{q;&69BGmu$l}M93KVNvV^T!Q^m`A*b5P2&jul2Qsy!sZGUkhbX9_;n+1cM z`4}ux^jH0Bt5R>oA;@%5-`ZtBz{8;zCvGa7W8m*Q5*O`*rym0qsoXmJrUYuiYEK9! zx;Gqc3hR(Y@ZO2ia1)ZMQ!KD-R&YKjh|!b{`g6vw?zWV|_})ZGM>Yy5Xd1-~`%PtL zO%-PMyaKDVr(AfchWZnm$?0cT^0%qxr&SnnHm9e8b-PkF)D{W~xs}jizs-bmK1z-7NS{)2paE!Hc7xUAy;* zdVbo}-nt!@RwkH)uvP7aW-!nD2I%g2=^6G}wUs-Dp5Z_txfv~I%;XE@MVtcrYuhOs zsQo=xnOVQztA17(o|it~&7!|1f2XA_IkUX-hvAl!AjQLsX9s)a8|0Tc&!VmdffSPk zG51gMRrxz^ykEYX1QcAEdGAP*feqlZ!q~Xs{AfUhJ>>EAa5#g_GgSUc~iajTy?ya+%XGq)B z7hb(;k6L~jdG7=wS*18LrP62k-gQu`&Wzb)lQkzQF11Z12Q3FFVyB8%FNyXy>I<($ zwyA^3q`lu}C&`vx!N5aBF&`a^5OcL$b4|bwq~=lXM`y5aS>0!Nsz%Tm zHYDkU=hum!+Wi0ZIMOMk$Tk9Qp;yt-nYedf&o?(ECeB$*IknGLR&mvfA`HiGT175H;vTA z40K+D=c_nt&B&_yHQmgQ=zl8Z#%Q>$J7Q4e-~370ni9PmW5>+^BIfOHXKTRLltXVl zzg!Wz2wIVPKA~B5L7^disO9ulmKUWdg;+v!{JBHsz@ZW?UQP_y_iEL9-j14J8p3dT z51?2Db_rBpY<3`T;I079+>)+3Te66uBR)ViC{Z`j72ly6g|lZHR{**t%Hx=_nX-bG z)tV&CJ0#jz?^&wCcU%O`I^IKP$)l`cIAi5;8;^!FJ=0H|&g`|*l-64=UxjG|oBMlI zM>adQcLgxNBp&Q@9&M&9q3LHO34IQUEbBeN#;9^3lzO~}$&~Z0U}b>&{lumoaE#cR zemXU?(Nxl1CeZijW`3hiJNqzgB_QK5+0!?`o2EoyW{_F2|at58jiKe3B zh6g4(0 zbfpL$AY`W|AKAm|jFa#AR&$fJwHNZYPrN4c(?qS|x%a&CKk)Ugn_P;1@;mt7rlFFR literal 0 HcmV?d00001 From 5f2fca81483f37a49b8821fd3cc3d5333263ee6a Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev Date: Wed, 29 May 2024 13:15:05 +0300 Subject: [PATCH 160/172] change: small case --- src/main/kotlin/viewmodel/MainScreenViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index d57ed88..c2b8997 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -30,7 +30,7 @@ class MainScreenViewModel(val saveType: String = getSetting(SettingType.BD)) : V } val graphVM: AbstractGraphViewModel when (type) { - "Undirected" -> { + "undirected" -> { graphVM = UndirectedGraphViewModel(name) } From 1589234977b954db23c41aae4c8f412c99d9d5ec Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 29 May 2024 06:22:31 -0400 Subject: [PATCH 161/172] add: added EXAMPLE --- storage.db | Bin 0 -> 28672 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 storage.db diff --git a/storage.db b/storage.db new file mode 100644 index 0000000000000000000000000000000000000000..e83574164eb90a912a4a7c39f897d9f3aae03041 GIT binary patch literal 28672 zcmeI4Ta4UR8OQAzU*$|=iI6lFY~&GxLt z%aM3amYqrM6gF9oE610ASB)FwDtDCnE0sINcQ$D)H^~HK0y2UB(F95_?j9RcKlRp$ zwX07*v-BKaTD`vfe0z~Uv(T7ZY*>qPr{^11d!=>Qik4Q_mY!ewxOH)D6>hFfT9Iv? zdu*}saART8vLYv4anlu+uJCllOIQ4KB}iAobVZ~q4z_?iILL&9TsX*vgM2v1h=ZIs z$clr!xX6o(ytv4Vi@dnVi;KLt$cu}-xX6o(yfEa2AukMhVaN+ZUKsMikQau$Fyw_H zFC2N{$O}haIP$`g7mmDemi-){;$cvA> z_{fWoy!gnAkG%NEi;ukc$cvA>_{d9uyadQgfV>3AOMtut$V-5{1jtK(yaXLDac{{- zN7W0hUTJ-0eZ@q5(fUeXH)KsHivHcCGrJwgzAWj6(K(QPS?BijU+c?0J(4q%>+>_u zwXV;`rMXAX&o|l&e6MY*TGy8iSQ>+sF1HOZdEs-LZwYrm-7u02_^s&7`mR&7>~R{m7^PUYFkbVVt@R(`qsczH+Z z_0sLq<^Jfp!Lmwi;bxR_A)KtzP}o_efjE&>>dzi;OE>@N z6W$vCo+{#=5l`Y+Rx+QS+@7>$A^wOsD-@=VZcp64h}4L?#i=9P)ovVp#2s^bVrwnV zszr{ucCL8x@E~yt2CGz-&)%9+j9{yUlR{( zt?em#rHFYm_irujVq1lcTEuwHG-nR>tGlgxoNlSTO^sN5brr>des#C$CWVN_KflN- zb!PS_QZ;q*liJo(#O(O1ICWneJd7l@)5sNu*q70?8;X0gN&_*LRqBhqS*4!XlU2&6 z?iotjcLE}p88f@Fwy#m@lq6@pvpsTM(=5!~y_tHWZAX#bQRF%~EjPWZs~*%SE$Eh$ zhLLM)nz*Yg?rt2W5WlGvJF`kdu_LQA5TjY8z8J|W^+Y|Zl+V@@Y3IyzmVFubHndBT z!&>*?YF4s4UD-^Ed!n?T$Z<4nrrZ;^9Q8iZZTrfRlXO?JC4aPUQ{N%REmG@}VkDAd zHbilNyl;OgaeG@78lo_iG_|u03sGPSb2DkrX-eBFM!psrn@Rs!(5pwD(6?3&LPuN^ z+Sbw@Dk-%Om6gu{~p2{WaSflnHZ`!TXJK89B*Tj#NjuFfC_#;%i ze2m$m7TL-8e_Z*3QomiFto^8VsrrZNt?J&&w<~AMzbSvLY?QuSvWu@3FBkq;xK-F= ze#<;#{K~jtCm3JOG>+LWH-6-T2dGdU z+)st#aF7bc-~biM1&a#hzR z*Yv;YpVJS@@&6E=W2;I0hDMJ6hYf2`yZBv^9RCjrX4rM{iz_+)A2ux14z3eDun7Uz zs8At1O@#{JDiz9y4^yE$xI%^E@F6M`gAY=nTzHBK<-n6vC>t&lp+W&4phAUki3%0K z`>9YqL{umbo}fZ;SfoNRxJZR^VSx(ezy&Il4d;nafq=)UP$4`PvveI@6qKUJ?Ne^FjS56{C_;C)ob%-|dq%7yn*p&WRa z3T4AtB9t$nL4^w8Au3b=XQ)sp z^57U1io+xoiosDTlnY0wP!3E`p=>xzgkl07q(X&ohzb?72k7~Dnfz~mBcAnmPU>=! i3CILw0x|)afJ{IpAQO-Y$OL2pG69)@OyK_{fqwu8J}j{S literal 0 HcmV?d00001 From 092954b860542455fe481ee2a6cfb65777e435a3 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 29 May 2024 06:51:20 -0400 Subject: [PATCH 162/172] add: examlpe graph --- storage.db | Bin 0 -> 28672 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 storage.db diff --git a/storage.db b/storage.db new file mode 100644 index 0000000000000000000000000000000000000000..e83574164eb90a912a4a7c39f897d9f3aae03041 GIT binary patch literal 28672 zcmeI4Ta4UR8OQAzU*$|=iI6lFY~&GxLt z%aM3amYqrM6gF9oE610ASB)FwDtDCnE0sINcQ$D)H^~HK0y2UB(F95_?j9RcKlRp$ zwX07*v-BKaTD`vfe0z~Uv(T7ZY*>qPr{^11d!=>Qik4Q_mY!ewxOH)D6>hFfT9Iv? zdu*}saART8vLYv4anlu+uJCllOIQ4KB}iAobVZ~q4z_?iILL&9TsX*vgM2v1h=ZIs z$clr!xX6o(ytv4Vi@dnVi;KLt$cu}-xX6o(yfEa2AukMhVaN+ZUKsMikQau$Fyw_H zFC2N{$O}haIP$`g7mmDemi-){;$cvA> z_{fWoy!gnAkG%NEi;ukc$cvA>_{d9uyadQgfV>3AOMtut$V-5{1jtK(yaXLDac{{- zN7W0hUTJ-0eZ@q5(fUeXH)KsHivHcCGrJwgzAWj6(K(QPS?BijU+c?0J(4q%>+>_u zwXV;`rMXAX&o|l&e6MY*TGy8iSQ>+sF1HOZdEs-LZwYrm-7u02_^s&7`mR&7>~R{m7^PUYFkbVVt@R(`qsczH+Z z_0sLq<^Jfp!Lmwi;bxR_A)KtzP}o_efjE&>>dzi;OE>@N z6W$vCo+{#=5l`Y+Rx+QS+@7>$A^wOsD-@=VZcp64h}4L?#i=9P)ovVp#2s^bVrwnV zszr{ucCL8x@E~yt2CGz-&)%9+j9{yUlR{( zt?em#rHFYm_irujVq1lcTEuwHG-nR>tGlgxoNlSTO^sN5brr>des#C$CWVN_KflN- zb!PS_QZ;q*liJo(#O(O1ICWneJd7l@)5sNu*q70?8;X0gN&_*LRqBhqS*4!XlU2&6 z?iotjcLE}p88f@Fwy#m@lq6@pvpsTM(=5!~y_tHWZAX#bQRF%~EjPWZs~*%SE$Eh$ zhLLM)nz*Yg?rt2W5WlGvJF`kdu_LQA5TjY8z8J|W^+Y|Zl+V@@Y3IyzmVFubHndBT z!&>*?YF4s4UD-^Ed!n?T$Z<4nrrZ;^9Q8iZZTrfRlXO?JC4aPUQ{N%REmG@}VkDAd zHbilNyl;OgaeG@78lo_iG_|u03sGPSb2DkrX-eBFM!psrn@Rs!(5pwD(6?3&LPuN^ z+Sbw@Dk-%Om6gu{~p2{WaSflnHZ`!TXJK89B*Tj#NjuFfC_#;%i ze2m$m7TL-8e_Z*3QomiFto^8VsrrZNt?J&&w<~AMzbSvLY?QuSvWu@3FBkq;xK-F= ze#<;#{K~jtCm3JOG>+LWH-6-T2dGdU z+)st#aF7bc-~biM1&a#hzR z*Yv;YpVJS@@&6E=W2;I0hDMJ6hYf2`yZBv^9RCjrX4rM{iz_+)A2ux14z3eDun7Uz zs8At1O@#{JDiz9y4^yE$xI%^E@F6M`gAY=nTzHBK<-n6vC>t&lp+W&4phAUki3%0K z`>9YqL{umbo}fZ;SfoNRxJZR^VSx(ezy&Il4d;nafq=)UP$4`PvveI@6qKUJ?Ne^FjS56{C_;C)ob%-|dq%7yn*p&WRa z3T4AtB9t$nL4^w8Au3b=XQ)sp z^57U1io+xoiosDTlnY0wP!3E`p=>xzgkl07q(X&ohzb?72k7~Dnfz~mBcAnmPU>=! i3CILw0x|)afJ{IpAQO-Y$OL2pG69)@OyK_{fqwu8J}j{S literal 0 HcmV?d00001 From 2f6992b7447178d40b2a96c5dff62e3b786c5e00 Mon Sep 17 00:00:00 2001 From: Alexei Dmitrievtsev Date: Wed, 29 May 2024 07:03:49 -0400 Subject: [PATCH 163/172] fix: refu bug fix --- src/main/kotlin/view/screens/MainScreen.kt | 7 +++---- src/main/kotlin/viewmodel/MainScreenViewModel.kt | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index a9ba56b..b8f7edd 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -27,7 +27,6 @@ import view.common.bounceClick import view.common.defaultStyle import viewmodel.GraphType import viewmodel.MainScreenViewModel -import viewmodel.SaveType import java.io.File @@ -280,13 +279,13 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView color = Color.Black, shape = RoundedCornerShape(45.dp) ), - colors = ButtonDefaults.buttonColors(backgroundColor = if (mainScreenViewModel.graphs.typeList[index] == GraphType.Undirected) DefaultColors.primary + colors = ButtonDefaults.buttonColors(backgroundColor = if (graphVM.graphType == GraphType.Undirected) DefaultColors.primary else Color.Cyan )) { Row{ Column (modifier = Modifier.align(Alignment.CenterVertically)){ Image( - bitmap = if (mainScreenViewModel.graphs.typeList[index] == GraphType.Directed) loadImageBitmap(File("src/main/resources/directed.png").inputStream()) + bitmap = if (graphVM.graphType == GraphType.Directed) loadImageBitmap(File("src/main/resources/directed.png").inputStream()) else loadImageBitmap(File("src/main/resources/undirected.png").inputStream()), contentDescription = "Type", @@ -297,7 +296,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView } Column (modifier = Modifier.align(Alignment.CenterVertically)){ Text( - text = mainScreenViewModel.graphs.getName(index), + text = name, style = bigStyle, modifier = Modifier.clip(RoundedCornerShape(45.dp)) ) diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index c2b8997..60b3f9c 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -10,7 +10,6 @@ import view.screens.getSetting import viewmodel.graph.AbstractGraphViewModel import viewmodel.io.Neo4jRepository import viewmodel.io.SQLiteRepository -import kotlin.math.log enum class GraphType() { Undirected, From 5d9a1b5deb5413aebac97737f0079b9737b6f0a7 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 30 May 2024 00:15:38 +0300 Subject: [PATCH 164/172] Fix: fix db bugs, make theme depends on what saving you chosen 1. Fix bugs in db's: fix queires (names with spaces, digits etc) 2. Fix styling, sizes of dialogs, sizes of localisation fonts 3. Implemented theme changing depending on saving type you chosen --- src/main/kotlin/Main.kt | 1 - src/main/kotlin/Navigation.kt | 2 +- src/main/kotlin/model/graph/Graph.kt | 20 ++-- src/main/kotlin/view/common/AddEdgeDialog.kt | 11 ++- .../kotlin/view/common/AddVertexDialog.kt | 13 +-- src/main/kotlin/view/common/DefaultButton.kt | 2 +- .../kotlin/view/common/DefaultShortButton.kt | 2 +- src/main/kotlin/view/common/styling.kt | 40 ++++++-- .../view/screens/DirectedGraphScreen.kt | 40 ++++---- src/main/kotlin/view/screens/MainScreen.kt | 51 +++++----- .../kotlin/view/screens/SettingsScreen.kt | 88 ++++++++++++------ .../view/screens/UndirectedGraphScreen.kt | 36 ++++--- .../kotlin/viewmodel/MainScreenViewModel.kt | 34 ++++--- .../viewmodel/graph/AbstractGraphViewModel.kt | 2 +- .../kotlin/viewmodel/graph/VertexViewModel.kt | 2 +- .../kotlin/viewmodel/io/Neo4jRepository.kt | 26 +++--- .../kotlin/viewmodel/io/SQLiteRepository.kt | 34 ++++--- src/main/resources/localisation/cn-CN.json | 4 - src/main/resources/localisation/en-US.json | 10 +- src/main/resources/localisation/ru-RU.json | 8 +- src/test/kotlin/SQLiteIntegrationTest.kt | 13 +-- storage.db | Bin 28672 -> 0 bytes 22 files changed, 263 insertions(+), 176 deletions(-) delete mode 100644 storage.db diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 6fa3d01..f23f5a6 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -38,7 +38,6 @@ fun main() { @Composable fun App() { - MaterialTheme() { Navigation() } diff --git a/src/main/kotlin/Navigation.kt b/src/main/kotlin/Navigation.kt index 8fa116e..9ada410 100644 --- a/src/main/kotlin/Navigation.kt +++ b/src/main/kotlin/Navigation.kt @@ -49,7 +49,7 @@ fun Navigation() { } composable(route = Screen.SettingsScreen.route) { - SettingsScreen(navController = navController) + SettingsScreen(navController = navController, mainScreenViewModel) } } } diff --git a/src/main/kotlin/model/graph/Graph.kt b/src/main/kotlin/model/graph/Graph.kt index 553081c..b690dcd 100644 --- a/src/main/kotlin/model/graph/Graph.kt +++ b/src/main/kotlin/model/graph/Graph.kt @@ -42,9 +42,9 @@ abstract class Graph() { fun saveSQLite(name: String, type: String, bdName: String) { var parameterCreate = "( Vertexes String," var parameterInput = "( Vertexes," - var create = ("CREATE TABLE $name") - val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") - val insertIndex = ("INSERT INTO BEBRA_KILLER (name, type) VALUES('$name', '$type');") + var create = ("CREATE TABLE '$name'") + val createIndex = ("CREATE TABLE IF NOT EXISTS Graphs (name TEXT, type TEXT);") + val insertIndex = ("INSERT INTO Graphs (name, type) VALUES('$name', '$type');") for (i in graph.entries) { parameterCreate = "$parameterCreate V${i.key.toString()} INTEGER, " parameterInput = "$parameterInput V${i.key.toString()}," @@ -56,14 +56,13 @@ abstract class Graph() { create = create + parameterCreate + ";" val connection = DriverManager.getConnection("$DB_DRIVER:$bdName.db") ?: throw SQLException("Cannot connect to database") - val delTable = "DROP TABLE $name" - val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='$name';" + val delTable = "DROP TABLE IF EXISTS '$name';" + val delIndexRec = "DELETE FROM Graphs WHERE name='$name';" connection.createStatement().also { stmt -> try { stmt.execute(delTable) - println("Tables created or already exists") } catch (ex: Exception) { - println("Cannot create table in database") + println("Can't delete old table of graph") println(ex) } finally { stmt.close() @@ -72,9 +71,8 @@ abstract class Graph() { connection.createStatement().also { stmt -> try { stmt.execute(delIndexRec) - println("Tables created or already exists") } catch (ex: Exception) { - println("Cannot create table in database") + println("Can't delete graph entry from Graphs") println(ex) } finally { stmt.close() @@ -82,8 +80,8 @@ abstract class Graph() { } connection.createStatement().also { stmt -> try { - stmt.execute(create) stmt.execute(createIndex) + stmt.execute(create) println("Tables created or already exists") } catch (ex: Exception) { println("Cannot create table in database") @@ -103,7 +101,7 @@ abstract class Graph() { } } - var request = "INSERT INTO $name $parameterInput VALUES " + var request = "INSERT INTO '$name' $parameterInput VALUES " for (i in graph.entries) { var record = "( 'V${i.key}', " val recList = emptyMap().toMutableMap() diff --git a/src/main/kotlin/view/common/AddEdgeDialog.kt b/src/main/kotlin/view/common/AddEdgeDialog.kt index 88ccdb3..bb6b173 100644 --- a/src/main/kotlin/view/common/AddEdgeDialog.kt +++ b/src/main/kotlin/view/common/AddEdgeDialog.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.rememberDialogState +import localisation.getLocalisation import localisation.localisation import viewmodel.graph.AbstractGraphViewModel @@ -38,6 +39,7 @@ fun AddEdgeDialog( var destination by remember { mutableStateOf("") } var notWeighted by remember { mutableStateOf(true) } var weight by remember { mutableStateOf("1") } + val language = getLocalisation() Column(modifier = Modifier.padding(30.dp, 24.dp).fillMaxSize()) { val textWidth = 90.dp val rightPadding = 200.dp @@ -147,10 +149,15 @@ fun AddEdgeDialog( visible = false } - DefaultButton(onClick, "add_edge", defaultStyle) + DefaultButton( + onClick, "add", when (language) { + "ru-RU" -> smallStyle + else -> defaultStyle + } + ) Spacer(modifier = Modifier.width(30.dp)) DefaultButton(onClose, "back", defaultStyle, Color.Red) } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/view/common/AddVertexDialog.kt b/src/main/kotlin/view/common/AddVertexDialog.kt index bb5190f..5eb0d4a 100644 --- a/src/main/kotlin/view/common/AddVertexDialog.kt +++ b/src/main/kotlin/view/common/AddVertexDialog.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.rememberDialogState +import localisation.getLocalisation import localisation.localisation import viewmodel.graph.AbstractGraphViewModel @@ -29,16 +30,16 @@ fun AddVertexDialog( visible = visible, title = "New Vertices", onCloseRequest = onClose, - state = rememberDialogState(height = 420.dp, width = 800.dp) + state = rememberDialogState(height = 340.dp, width = 560.dp) ) { var verticesNumber by remember { mutableStateOf("1") } - val textWidth = 130.dp + val language = getLocalisation() Column(modifier = Modifier.padding(30.dp, 24.dp)) { Row(modifier = Modifier.fillMaxWidth()) { Text( text = localisation("number"), style = defaultStyle, - modifier = Modifier.align(Alignment.CenterVertically).width(textWidth), + modifier = Modifier.align(Alignment.CenterVertically).width(180.dp), ) TextField( modifier = Modifier @@ -59,7 +60,7 @@ fun AddVertexDialog( }, ) } - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(30.dp)) Row { Checkbox( modifier = Modifier.align(Alignment.CenterVertically), @@ -72,7 +73,7 @@ fun AddVertexDialog( modifier = Modifier.align(Alignment.CenterVertically) ) } - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(30.dp)) Row { val onClick = { if (verticesNumber == "") verticesNumber = "1" @@ -83,7 +84,7 @@ fun AddVertexDialog( visible = false } - DefaultButton(onClick, "add_edge", defaultStyle) + DefaultButton(onClick, "add", defaultStyle) Spacer(modifier = Modifier.width(30.dp)) DefaultButton(onClose, "back", defaultStyle, Color.Red) } diff --git a/src/main/kotlin/view/common/DefaultButton.kt b/src/main/kotlin/view/common/DefaultButton.kt index 7d572d7..c83960c 100644 --- a/src/main/kotlin/view/common/DefaultButton.kt +++ b/src/main/kotlin/view/common/DefaultButton.kt @@ -19,7 +19,7 @@ fun DefaultButton( onClick: () -> Unit, localisationCode: String, style: TextStyle = defaultStyle, - color: Color = DefaultColors.primary, + color: Color = DefaultColors.primaryBright, width: androidx.compose.ui.unit.Dp = 240.dp, height: androidx.compose.ui.unit.Dp = 80.dp, ) { diff --git a/src/main/kotlin/view/common/DefaultShortButton.kt b/src/main/kotlin/view/common/DefaultShortButton.kt index 08e9c0e..dfda6f4 100644 --- a/src/main/kotlin/view/common/DefaultShortButton.kt +++ b/src/main/kotlin/view/common/DefaultShortButton.kt @@ -10,7 +10,7 @@ fun DefaultShortButton( onClick: () -> Unit, localisationCode: String, style: TextStyle = defaultStyle, - color: Color = DefaultColors.primary, + color: Color = DefaultColors.primaryBright, ) { DefaultButton(onClick, localisationCode, style, color, 220.dp, 70.dp) diff --git a/src/main/kotlin/view/common/styling.kt b/src/main/kotlin/view/common/styling.kt index 2b9a57d..b277944 100644 --- a/src/main/kotlin/view/common/styling.kt +++ b/src/main/kotlin/view/common/styling.kt @@ -1,21 +1,47 @@ package view.common +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp +import view.screens.SettingType +import view.screens.getSetting val defaultStyle = TextStyle(fontSize = 28.sp) -val microSize = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Center) -val smallSize = TextStyle(fontSize = 22.sp, textAlign = TextAlign.Center) -val mediumSize = TextStyle(fontSize = 26.sp, textAlign = TextAlign.Center) +val microStyle = TextStyle(fontSize = 18.sp, textAlign = TextAlign.Center) +val smallStyle = TextStyle(fontSize = 22.sp, textAlign = TextAlign.Center) +val mediumStyle = TextStyle(fontSize = 26.sp, textAlign = TextAlign.Center) val bigStyle = TextStyle(fontSize = 50.sp) object DefaultColors { - val primary = Color(0xff, 0xf1, 0x4a) - val primarySelected = Color(0xcf, 0xc0, 0x07) - val darkGreen = Color(0x00, 0x64, 0x00) - val simpleGreen = Color(0x00, 0xe4, 0x00) + val greenBright = Color(0xff00E400) + + val yellowBright = Color(0xffFFF14A) + val yellowDark = Color(0xffCFC007) + val blueBright = Color(0xff00BDFF) + val blueDark = Color(0xff076FBE) + val pinkBright = Color(0xffFFD3D3) + val pinkDark = Color(0xffFCABAB) val background = Color.White + + var primaryBright by mutableStateOf( + when (getSetting(SettingType.BD)) { + "sqlite" -> pinkBright + "neo4j" -> blueBright + "local" -> yellowBright + else -> throw IllegalStateException("BD Setting is invalid") + } + ) + var primaryDark by mutableStateOf( + when (getSetting(SettingType.BD)) { + "sqlite" -> pinkDark + "neo4j" -> blueDark + "local" -> yellowDark + else -> throw IllegalStateException("BD Setting is invalid") + } + ) } \ No newline at end of file diff --git a/src/main/kotlin/view/screens/DirectedGraphScreen.kt b/src/main/kotlin/view/screens/DirectedGraphScreen.kt index 922f903..8c1ead9 100644 --- a/src/main/kotlin/view/screens/DirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/DirectedGraphScreen.kt @@ -85,19 +85,27 @@ fun DirectedGraphScreen( // Add vertex Button DefaultShortButton( { isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when (language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallSize + ("ru-RU") -> microStyle else -> defaultStyle } ) Spacer(modifier = Modifier.height(10.dp)) // Add edge Button - DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge", defaultStyle) + DefaultShortButton( + { isOpenedEdgeMenu = !isOpenedEdgeMenu }, "add_edge", when (language) { + ("ru-RU") -> smallStyle + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(10.dp)) // Save button - DefaultShortButton({ mainScreenViewModel.saveGraph(graphVM.name) }, "save") + DefaultShortButton( + { mainScreenViewModel.saveGraph(graphVM.name) }, + "save", + color = DefaultColors.greenBright + ) Spacer(modifier = Modifier.height(16.dp)) // Visualization Button @@ -113,14 +121,14 @@ fun DirectedGraphScreen( scope.coroutineContext.cancelChildren() } }, "visualize", defaultStyle, - if (isVisualizationRunning) Color.Red else Color(0xffFFCB32) + if (isVisualizationRunning) Color.Red else Color(0xffFFB300) ) Spacer(modifier = Modifier.height(10.dp)) DefaultShortButton( { graphVM.resetColors() }, "reset", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> smallSize + ("ru-RU") -> smallStyle else -> defaultStyle }, Color.LightGray ) @@ -129,14 +137,14 @@ fun DirectedGraphScreen( DefaultShortButton( { graphVM.drawBetweennessCentrality() }, "betweenness_centrality", - microSize + microStyle ) Spacer(modifier = Modifier.height(10.dp)) DefaultShortButton( { graphVM.chinaWhisperCluster() }, "find_clusters", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> smallSize + ("ru-RU") -> smallStyle else -> defaultStyle } ) @@ -144,9 +152,9 @@ fun DirectedGraphScreen( DefaultShortButton( { graphVM.drawStrongConnections() }, "find_strong_connections", when (language) { - ("en-US") -> smallSize - ("ru-RU") -> microSize - ("cn-CN") -> microSize + ("en-US") -> smallStyle + ("ru-RU") -> microStyle + ("cn-CN") -> microStyle else -> defaultStyle } ) @@ -156,8 +164,8 @@ fun DirectedGraphScreen( DefaultShortButton( { isOpenedDijkstraMenu = !isOpenedDijkstraMenu }, "dijkstra", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> smallSize - ("cn-CN") -> smallSize + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle else -> defaultStyle } ) @@ -169,8 +177,8 @@ fun DirectedGraphScreen( "ford_bellman", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> microSize - ("cn-CN") -> smallSize + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle else -> defaultStyle } ) @@ -180,7 +188,7 @@ fun DirectedGraphScreen( DefaultShortButton( onClick = { graphVM.drawCycles("1") }, "find_cycles", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> mediumSize + ("ru-RU") -> mediumStyle else -> defaultStyle } ) diff --git a/src/main/kotlin/view/screens/MainScreen.kt b/src/main/kotlin/view/screens/MainScreen.kt index b8f7edd..aa519d5 100644 --- a/src/main/kotlin/view/screens/MainScreen.kt +++ b/src/main/kotlin/view/screens/MainScreen.kt @@ -35,13 +35,11 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView var search by remember { mutableStateOf("") } var graphName by remember { mutableStateOf("") } val dialogState = remember { mutableStateOf(false) } - val optionsDropDown = listOf("undirected", "directed") val expandedDropDown = remember { mutableStateOf(false) } - val selectedOptionTextDropDown = remember { mutableStateOf(optionsDropDown[0]) } - + var selectedOptionTextDropDown = remember { GraphType.Undirected } if (!mainScreenViewModel.inited) { - mainScreenViewModel.initGraphList("storage") + mainScreenViewModel.initGraphList() } Column(modifier = Modifier.fillMaxSize().background(DefaultColors.background).padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth().height(100.dp)) { @@ -56,7 +54,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView .fillMaxHeight() .border( width = 4.dp, - color = DefaultColors.primary, + color = DefaultColors.primaryBright, shape = RoundedCornerShape(45.dp) ), shape = RoundedCornerShape(45.dp), @@ -85,7 +83,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView .size(100.dp) .clip(shape = RoundedCornerShape(45.dp)) .clickable { } - .background(DefaultColors.primary) + .background(DefaultColors.primaryBright) .border( width = 5.dp, color = Color.Black, @@ -108,7 +106,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView .size(100.dp) .clip(shape = RoundedCornerShape(45.dp)) .clickable { } - .background(DefaultColors.primary) + .background(DefaultColors.primaryBright) .border( width = 5.dp, color = Color.Black, @@ -170,14 +168,14 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView .width(300.dp) .height(60.dp), shape = RoundedCornerShape(25.dp), - colors = if (graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.simpleGreen) else ButtonDefaults.buttonColors( - backgroundColor = DefaultColors.darkGreen + colors = if (graphName != "") ButtonDefaults.buttonColors(backgroundColor = DefaultColors.pinkBright) else ButtonDefaults.buttonColors( + backgroundColor = DefaultColors.pinkDark ), onClick = { if (graphName != "") { mainScreenViewModel.addGraph( graphName, - selectedOptionTextDropDown.value, + selectedOptionTextDropDown, ) mainScreenViewModel.saveGraph(graphName) graphName = "" @@ -222,7 +220,7 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView .clickable { expandedDropDown.value = !expandedDropDown.value }, ) { Text( - text = selectedOptionTextDropDown.value, + text = selectedOptionTextDropDown.toString(), fontSize = 20.sp, modifier = Modifier.padding(start = 20.dp) ) @@ -234,33 +232,31 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView expanded = expandedDropDown.value, onDismissRequest = { expandedDropDown.value = false } ) { - optionsDropDown.forEach { selectionOption -> + GraphType.entries.forEach { selectedOption -> DropdownMenuItem( onClick = { - selectedOptionTextDropDown.value = selectionOption + selectedOptionTextDropDown = selectedOption expandedDropDown.value = false } ) { - Text(text = localisation(selectionOption)) + Text(text = localisation(selectedOption.toString().lowercase())) } } } } - - } Spacer(modifier = Modifier.height(30.dp)) LazyColumn(modifier = Modifier.fillMaxWidth()) { - itemsIndexed(mainScreenViewModel.graphsNames) { index, name -> + itemsIndexed(mainScreenViewModel.graphNames) { _, name -> if (!name.startsWith(search)) return@itemsIndexed // To GraphScreen val graphVM = mainScreenViewModel.getGraph(name) Row(modifier = Modifier.padding(vertical = 15.dp)) { Button( onClick = { - mainScreenViewModel.initGraph(name, "storage") + mainScreenViewModel.loadGraph(name, "storage") if (graphVM.graphType == GraphType.Undirected) { navController.navigate( "${Screen.UndirectedGraphScreen.route}/$name" @@ -279,22 +275,25 @@ fun MainScreen(navController: NavController, mainScreenViewModel: MainScreenView color = Color.Black, shape = RoundedCornerShape(45.dp) ), - colors = ButtonDefaults.buttonColors(backgroundColor = if (graphVM.graphType == GraphType.Undirected) DefaultColors.primary - else Color.Cyan - )) { - Row{ - Column (modifier = Modifier.align(Alignment.CenterVertically)){ + colors = ButtonDefaults.buttonColors( + backgroundColor = DefaultColors.primaryBright + ) + ) { + Row { + Column(modifier = Modifier.align(Alignment.CenterVertically)) { Image( - bitmap = if (graphVM.graphType == GraphType.Directed) loadImageBitmap(File("src/main/resources/directed.png").inputStream()) + bitmap = if (graphVM.graphType == GraphType.Directed) loadImageBitmap( + File("src/main/resources/directed.png").inputStream() + ) else loadImageBitmap(File("src/main/resources/undirected.png").inputStream()), contentDescription = "Type", modifier = Modifier .padding(15.dp) .align(Alignment.End), - ) + ) } - Column (modifier = Modifier.align(Alignment.CenterVertically)){ + Column(modifier = Modifier.align(Alignment.CenterVertically)) { Text( text = name, style = bigStyle, diff --git a/src/main/kotlin/view/screens/SettingsScreen.kt b/src/main/kotlin/view/screens/SettingsScreen.kt index 8cff542..05f3bc3 100644 --- a/src/main/kotlin/view/screens/SettingsScreen.kt +++ b/src/main/kotlin/view/screens/SettingsScreen.kt @@ -1,6 +1,5 @@ package view.screens -import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -13,25 +12,26 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import kotlinx.serialization.Serializable -import localisation.getLocalisation import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import localisation.localisation import view.common.DefaultColors -import view.common.bigStyle import view.common.bounceClick import view.common.defaultStyle +import viewmodel.MainScreenViewModel import java.io.File const val pathToSettings = "src/main/kotlin/settings.json" +const val boldLine = 8 +const val defaultLine = 3 @Serializable class SettingsJSON( var language: String, var bd: String, - var neo4jUri: String = "dsfds", - var neo4jUser: String = "dfsds", - var neo4jPassword: String = "dsfds", + var neo4jUri: String = "", + var neo4jUser: String = "", + var neo4jPassword: String = "", ) enum class SettingType { @@ -87,13 +87,12 @@ fun getSetting(type: SettingType): String { } @Composable -fun SettingsScreen(navController: NavController) { - var language by mutableStateOf(getLocalisation()) +fun SettingsScreen(navController: NavController, mainScreenViewModel: MainScreenViewModel) { Column(modifier = Modifier.padding(20.dp, 10.dp)) { Row(modifier = Modifier.fillMaxSize()) { Language(navController) Spacer(Modifier.width(10.dp)) - Saving() + Saving(mainScreenViewModel) Spacer(Modifier.width(10.dp)) } } @@ -115,12 +114,15 @@ fun Language(navController: NavController) { }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = if (language == "cn-CN") 5.dp else 3.dp, color = Color.Black) + .border( + width = if (language == "cn-CN") boldLine.dp else defaultLine.dp, + color = Color.Black + ) .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( backgroundColor = - if (language == "cn-CN") DefaultColors.primarySelected else DefaultColors.primary + if (language == "cn-CN") DefaultColors.primaryBright else DefaultColors.primaryDark ) ) { Text("汉语", style = defaultStyle) @@ -132,12 +134,15 @@ fun Language(navController: NavController) { }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = if (language == "ru-RU") 5.dp else 3.dp, color = Color.Black) + .border( + width = if (language == "ru-RU") boldLine.dp else defaultLine.dp, + color = Color.Black + ) .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( backgroundColor = - if (language == "ru-RU") DefaultColors.primarySelected else DefaultColors.primary + if (language == "ru-RU") DefaultColors.primaryBright else DefaultColors.primaryDark ) ) { Text("Русский", style = defaultStyle) @@ -149,12 +154,15 @@ fun Language(navController: NavController) { }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = if (language == "en-US") 5.dp else 3.dp, color = Color.Black) + .border( + width = if (language == "en-US") boldLine.dp else defaultLine.dp, + color = Color.Black + ) .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( backgroundColor = - if (language == "en-US") DefaultColors.primarySelected else DefaultColors.primary + if (language == "en-US") DefaultColors.primaryBright else DefaultColors.primaryDark ) ) { Text("English", style = defaultStyle) @@ -163,7 +171,7 @@ fun Language(navController: NavController) { onClick = { navController.navigate(Screen.MainScreen.route) }, modifier = Modifier .padding(16.dp) - .border(width = 3.dp, color = Color.Black) + .border(width = defaultLine.dp, color = Color.Black) .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red) @@ -174,8 +182,7 @@ fun Language(navController: NavController) { } @Composable -fun Saving() { - var saving by mutableStateOf(getSetting(SettingType.BD)) +fun Saving(mainScreenViewModel: MainScreenViewModel) { Column { Text( text = localisation("saving"), @@ -185,15 +192,22 @@ fun Saving() { Button( onClick = { makeSetting(SettingType.BD, "sqlite") - saving = "sqlite" + mainScreenViewModel.saveType = "sqlite" + mainScreenViewModel.inited = false + DefaultColors.primaryBright = DefaultColors.pinkBright + DefaultColors.primaryDark = DefaultColors.pinkDark }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = if (saving == "sqlite") 5.dp else 3.dp, color = Color.Black) + .border( + width = if (mainScreenViewModel.saveType == "sqlite") boldLine.dp else defaultLine.dp, + color = Color.Black + ) .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = Color(0xFF17f908) + backgroundColor = if (mainScreenViewModel.saveType == "sqlite") + DefaultColors.pinkBright else DefaultColors.pinkDark ) ) { Text("SQLite", style = defaultStyle) @@ -201,31 +215,45 @@ fun Saving() { Button( onClick = { makeSetting(SettingType.BD, "neo4j") - saving = "neo4j" + mainScreenViewModel.saveType = "neo4j" + mainScreenViewModel.inited = false + DefaultColors.primaryBright = DefaultColors.blueBright + DefaultColors.primaryDark = DefaultColors.blueDark }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = if (saving == "neo4j") 5.dp else 3.dp, color = Color.Black) + .border( + width = if (mainScreenViewModel.saveType == "neo4j") boldLine.dp else defaultLine.dp, + color = Color.Black + ) .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = Color(0xFF0c97ff) + backgroundColor = if (mainScreenViewModel.saveType == "neo4j") + DefaultColors.blueBright else DefaultColors.blueDark ) ) { Text("Neo4j", style = defaultStyle) } Button( onClick = { - makeSetting(SettingType.BD, "local_file") - saving = "local_file" + makeSetting(SettingType.BD, "local") + mainScreenViewModel.saveType = "local" + mainScreenViewModel.inited = false + DefaultColors.primaryBright = DefaultColors.yellowBright + DefaultColors.primaryDark = DefaultColors.yellowDark }, modifier = Modifier .padding(horizontal = 16.dp, vertical = 20.dp) - .border(width = if (saving == "local_file") 5.dp else 3.dp, color = Color.Black) + .border( + width = if (mainScreenViewModel.saveType == "local") boldLine.dp else defaultLine.dp, + color = Color.Black + ) .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = Color.Yellow + backgroundColor = if (mainScreenViewModel.saveType == "local") + DefaultColors.yellowBright else DefaultColors.yellowDark ) ) { Text("Local file", style = defaultStyle) @@ -235,7 +263,7 @@ fun Saving() { var uri by remember { mutableStateOf(getSetting(SettingType.NEO4JURI)) } var user by remember { mutableStateOf(getSetting(SettingType.NEO4JUSER)) } var password by remember { mutableStateOf(getSetting(SettingType.NEO4JPASSWORD)) } - if (saving == "neo4j") { + if (mainScreenViewModel.saveType == "neo4j") { Column(modifier = Modifier.fillMaxSize()) { Text( text = localisation("neo4j_data"), @@ -321,7 +349,7 @@ fun Saving() { .bounceClick() .size(200.dp, 80.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = Color(0xFF0c97ff) + backgroundColor = DefaultColors.blueBright ) ) { Text("Connect", style = defaultStyle) diff --git a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt index feb5ff0..6327ee5 100644 --- a/src/main/kotlin/view/screens/UndirectedGraphScreen.kt +++ b/src/main/kotlin/view/screens/UndirectedGraphScreen.kt @@ -85,19 +85,27 @@ fun UndirectedGraphScreen( // Add vertex Button DefaultShortButton( { isOpenedVertexMenu = !isOpenedVertexMenu }, "add_vertex", when (language) { - ("en-US") -> defaultStyle - ("ru-RU") -> smallSize + ("ru-RU") -> microStyle else -> defaultStyle } ) Spacer(modifier = Modifier.height(10.dp)) // Add edge button - DefaultShortButton({ isOpenedEdgeMenu = !isOpenedEdgeMenu }, "open_edge", defaultStyle) + DefaultShortButton( + { isOpenedEdgeMenu = !isOpenedEdgeMenu }, "add_edge", when (language) { + ("ru-RU") -> smallStyle + else -> defaultStyle + } + ) Spacer(modifier = Modifier.height(16.dp)) // Save button - DefaultShortButton({ mainScreenViewModel.saveGraph(graphVM.name) }, "save") + DefaultShortButton( + { mainScreenViewModel.saveGraph(graphVM.name) }, + "save", + color = DefaultColors.greenBright + ) Spacer(modifier = Modifier.height(10.dp)) // Visualization Button @@ -113,7 +121,7 @@ fun UndirectedGraphScreen( scope.coroutineContext.cancelChildren() } }, "visualize", defaultStyle, - if (isVisualizationRunning) Color.Red else Color(0xffFFCB32) + if (isVisualizationRunning) Color.Red else Color(0xffFFB300) ) Spacer(modifier = Modifier.height(10.dp)) @@ -121,7 +129,7 @@ fun UndirectedGraphScreen( DefaultShortButton( { graphVM.resetColors() }, "reset", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> smallSize + ("ru-RU") -> smallStyle else -> defaultStyle }, Color.LightGray ) @@ -130,7 +138,7 @@ fun UndirectedGraphScreen( DefaultShortButton( { graphVM.drawBetweennessCentrality() }, "betweenness_centrality", - microSize + microStyle ) Spacer(modifier = Modifier.height(10.dp)) @@ -138,8 +146,8 @@ fun UndirectedGraphScreen( DefaultShortButton( { isDijkstraMenu = !isDijkstraMenu }, "dijkstra", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> smallSize - ("cn-CN") -> smallSize + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle else -> defaultStyle } ) @@ -149,8 +157,8 @@ fun UndirectedGraphScreen( DefaultShortButton( { isFordBellmanMenu = !isFordBellmanMenu }, "ford_bellman", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> microSize - ("cn-CN") -> smallSize + ("ru-RU") -> microStyle + ("cn-CN") -> smallStyle else -> defaultStyle } ) @@ -158,8 +166,8 @@ fun UndirectedGraphScreen( DefaultShortButton( onClick = { graphVM.drawMst() }, "find_mst", when (language) { - ("en-US") -> smallSize - ("ru-RU") -> microSize + ("en-US") -> smallStyle + ("ru-RU") -> microStyle else -> defaultStyle } ) @@ -168,7 +176,7 @@ fun UndirectedGraphScreen( DefaultShortButton( onClick = { graphVM.drawBridges() }, "find_bridges", when (language) { ("en-US") -> defaultStyle - ("ru-RU") -> smallSize + ("ru-RU") -> smallStyle else -> defaultStyle } ) diff --git a/src/main/kotlin/viewmodel/MainScreenViewModel.kt b/src/main/kotlin/viewmodel/MainScreenViewModel.kt index 60b3f9c..1b02507 100644 --- a/src/main/kotlin/viewmodel/MainScreenViewModel.kt +++ b/src/main/kotlin/viewmodel/MainScreenViewModel.kt @@ -3,6 +3,7 @@ package viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import mu.KotlinLogging import view.screens.SettingType @@ -18,33 +19,33 @@ enum class GraphType() { private val logger = KotlinLogging.logger { } -class MainScreenViewModel(val saveType: String = getSetting(SettingType.BD)) : ViewModel() { +class MainScreenViewModel() : ViewModel() { + var saveType by mutableStateOf(getSetting(SettingType.BD)) val graphs by mutableStateOf(mutableMapOf>()) - val graphsNames = mutableStateListOf() + val graphNames = mutableStateListOf() internal var inited = false - fun addGraph(name: String, type: String) { - if (graphsNames.contains(name)) { + fun addGraph(name: String, type: GraphType) { + if (graphNames.contains(name)) { return } val graphVM: AbstractGraphViewModel when (type) { - "undirected" -> { + GraphType.Undirected -> { graphVM = UndirectedGraphViewModel(name) } - else -> { + GraphType.Directed -> { graphVM = DirectedGraphViewModel(name) } } graphs[name] = graphVM - graphsNames.add(name) + graphNames.add(name) } fun saveGraph(name: String, bdName: String = "storage") { try { - val graphVM = getGraph(name) if (saveType == "sqlite") { graphVM.model.saveSQLite(name, graphVM.graphType.toString(), bdName) @@ -66,11 +67,11 @@ class MainScreenViewModel(val saveType: String = getSetting(SettingType.BD)) : V ?: throw IllegalStateException("Can't find graph with name $name") } - fun initGraph(name: String, sourceSQLite: String) { + fun loadGraph(name: String, bdName: String) { val graphVM = getGraph(name) if (graphVM.isInited) return if (saveType == "sqlite") { - SQLiteRepository.initGraph(graphVM, sourceSQLite) + SQLiteRepository.loadGraph(graphVM, bdName) } else if (saveType == "neo4j") { val rep = Neo4jRepository( getSetting(SettingType.NEO4JURI), @@ -83,9 +84,11 @@ class MainScreenViewModel(val saveType: String = getSetting(SettingType.BD)) : V } - fun initGraphList(sourceSQLite: String) { + fun initGraphList(bdName: String = "storage") { + saveType = getSetting(SettingType.BD) + clear() if (saveType == "sqlite") { - SQLiteRepository.initGraphList("storage", this) + SQLiteRepository.initGraphList(bdName, this) } else if (saveType == "neo4j") { val rep = try { Neo4jRepository( @@ -118,6 +121,11 @@ class MainScreenViewModel(val saveType: String = getSetting(SettingType.BD)) : V rep.removeGraph(name) } graphs.remove(name) - graphsNames.remove(name) + graphNames.remove(name) + } + + fun clear() { + graphs.clear() + graphNames.clear() } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt index f4c2bea..1081f90 100644 --- a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt @@ -126,7 +126,7 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM edgeVM.color = Color.Black } for (vertexVM in verticesVM) { - vertexVM.color = DefaultColors.primary + vertexVM.color = DefaultColors.primaryBright } visibleCentrality = false } diff --git a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index 4ccd3d7..6019086 100644 --- a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -43,7 +43,7 @@ class VertexViewModel( var vertexSize by mutableStateOf(60f) var centrality by mutableStateOf(0.0) - var color by mutableStateOf(DefaultColors.primary) + var color by mutableStateOf(DefaultColors.primaryBright) val degree get() = edges.size } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/io/Neo4jRepository.kt b/src/main/kotlin/viewmodel/io/Neo4jRepository.kt index 12c9abb..0c8e5dd 100644 --- a/src/main/kotlin/viewmodel/io/Neo4jRepository.kt +++ b/src/main/kotlin/viewmodel/io/Neo4jRepository.kt @@ -6,6 +6,7 @@ import org.neo4j.driver.AuthTokens import org.neo4j.driver.GraphDatabase import org.neo4j.driver.TransactionContext import viewmodel.DirectedGraphViewModel +import viewmodel.GraphType import viewmodel.MainScreenViewModel import viewmodel.UndirectedGraphViewModel import viewmodel.graph.AbstractGraphViewModel @@ -39,21 +40,21 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl session.executeWrite { tx -> for (vertexVM in graphVM.verticesVM) { tx.run( - "CREATE (v: $graphName {value : \$vertexValue});", + "CREATE (v: `$graphName` {value : \$vertexValue});", mapOf("vertexValue" to vertexVM.vertex.toString()) ) } } session.executeWrite { tx -> - tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (n: $graphName) REQUIRE (n.value) IS UNIQUE;") - tx.run("CREATE INDEX IF NOT EXISTS FOR (n: $graphName) ON (n.value);") + tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (n: `$graphName`) REQUIRE (n.value) IS UNIQUE;") + tx.run("CREATE INDEX IF NOT EXISTS FOR (n: `$graphName`) ON (n.value);") } // create edges in a graph session.executeWrite { tx -> for (edgeVM in graphVM.edgesVM) { tx.run( - "MATCH (v1:$graphName) WHERE v1.value = \$vertex1 \n" + - "MATCH (v2: $graphName) WHERE v2.value = \$vertex2 \n" + + "MATCH (v1: `$graphName`) WHERE v1.value = \$vertex1 \n" + + "MATCH (v2: `$graphName`) WHERE v2.value = \$vertex2 \n" + "CREATE (v1)-[:Edge {weight: \$edgeWeight}]->(v2)", mapOf( "vertex1" to edgeVM.from.toString(), @@ -63,7 +64,6 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl ) } } - println("fds") } fun removeGraph(name: String) { @@ -86,7 +86,7 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl return } tx.run( - "OPTIONAL MATCH (n: $name)" + + "OPTIONAL MATCH (n: `$name`)" + "OPTIONAL MATCH (graph: Graph) WHERE graph.name = '$name'" + "DETACH DELETE n, graph;" ) @@ -113,11 +113,11 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl val graph: AbstractGraphViewModel if (graphData["type"] == "Undirected") { graph = UndirectedGraphViewModel(graphData["name"].toString()) - } else { + } else if (graphData["type"] == "Directed") { graph = DirectedGraphViewModel(graphData["name"].toString()) - } + } else throw IllegalArgumentException("graph type in db isn't correct") val vertices = session.executeRead() { tx -> - val result = tx.run("MATCH (v: $graphName) RETURN v.value as value") + val result = tx.run("MATCH (v: `$graphName`) RETURN v.value as value") return@executeRead result.list() { it.asMap()["value"].toString() } } for (vertex in vertices) { @@ -126,7 +126,7 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl session.executeRead { tx -> for (vertex in vertices) { val destinations = - tx.run("MATCH (v: $graphName {value: '$vertex'})-[:Edge]->(n) RETURN n.value as destination") + tx.run("MATCH (v: `$graphName` {value: '$vertex'})-[:Edge]->(n) RETURN n.value as destination") .list() { it.asMap()["destination"].toString() } for (destination in destinations) { graph.addEdge(vertex, destination) @@ -143,7 +143,9 @@ class Neo4jRepository(uri: String, user: String, password: String) : Closeabl return@executeRead result.list() { it.asMap() } } for (graph in graphs) { - mainScreenViewModel.addGraph(graph["name"].toString(), graph["type"].toString()) + val graphType = + if (graph["type"] == "Undirected") GraphType.Undirected else GraphType.Directed + mainScreenViewModel.addGraph(graph["name"].toString(), graphType) } } diff --git a/src/main/kotlin/viewmodel/io/SQLiteRepository.kt b/src/main/kotlin/viewmodel/io/SQLiteRepository.kt index 8e66284..644ab04 100644 --- a/src/main/kotlin/viewmodel/io/SQLiteRepository.kt +++ b/src/main/kotlin/viewmodel/io/SQLiteRepository.kt @@ -14,7 +14,7 @@ object SQLiteRepository { val DB_DRIVER = "jdbc:sqlite" val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") ?: throw SQLException("Cannot connect to database") - val createIndex = ("CREATE TABLE BEBRA_KILLER (name TEXT, type TEXT);") + val createIndex = ("CREATE TABLE IF NOT EXISTS Graphs (name TEXT, type TEXT);") connection.createStatement().also { stmt -> try { @@ -27,19 +27,19 @@ object SQLiteRepository { stmt.close() } } - val getGraphs by lazy { connection.prepareStatement("SELECT * FROM BEBRA_KILLER") } + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM Graphs") } val resSet = getGraphs.executeQuery() while (resSet.next()) { if (resSet.getString("type") == "Directed") { - mainScreenVM.addGraph(resSet.getString("name"), "directed") + mainScreenVM.addGraph(resSet.getString("name"), GraphType.Directed) } else if (resSet.getString("type") == "Undirected") { - mainScreenVM.addGraph(resSet.getString("name"), "undirected") + mainScreenVM.addGraph(resSet.getString("name"), GraphType.Undirected) } } connection.close() } - fun initGraph(graphVM: AbstractGraphViewModel, source: String) { + fun loadGraph(graphVM: AbstractGraphViewModel, source: String) { if (graphVM.graphType == GraphType.Directed) { val graphVM = graphVM as DirectedGraphViewModel val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") @@ -67,9 +67,9 @@ object SQLiteRepository { } if (graphVM.graphType == GraphType.Undirected) { val graph = graphVM as UndirectedGraphViewModel - val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") - val getGraphs by lazy { connection.prepareStatement("SELECT * FROM ${graph.name}") } - val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM ${graph.name}") } + val connection = DriverManager.getConnection("$DB_DRIVER:$source.db") + val getGraphs by lazy { connection.prepareStatement("SELECT * FROM '${graph.name}'") } + val getVertex by lazy { connection.prepareStatement("SELECT Vertexes FROM '${graph.name}'") } val resVertex = getVertex.executeQuery() val resEdges = getGraphs.executeQuery() while (resVertex.next()) { @@ -93,18 +93,24 @@ object SQLiteRepository { fun removeGraph(name: String) { val DB_DRIVER = "jdbc:sqlite" - val delTable = "DROP TABLE $name" - val delIndexRec = "DELETE FROM BEBRA_KILLER WHERE name='$name';" + val delTable = "DROP TABLE '$name'" + val delIndexRec = "DELETE FROM Graphs WHERE name='$name';" val connection = DriverManager.getConnection("$DB_DRIVER:storage.db") ?: throw SQLException("Cannot connect to database") connection.createStatement().also { stmt -> try { stmt.execute(delTable) + } catch (e: Exception) { + println("Can't remove table with name $name in sqlite: no such table") + println(e) + } finally { + stmt.close() + } + try { stmt.execute(delIndexRec) - println("Tables created or already exists") - } catch (ex: Exception) { - println("Cannot create table in database") - println(ex) + } catch (e: Exception) { + println("Can't graph entry with name $name in sqlite: no such entry") + println(e) } finally { stmt.close() } diff --git a/src/main/resources/localisation/cn-CN.json b/src/main/resources/localisation/cn-CN.json index ad936f3..9157ba3 100644 --- a/src/main/resources/localisation/cn-CN.json +++ b/src/main/resources/localisation/cn-CN.json @@ -20,10 +20,6 @@ "code": "add_vertex", "localisation": "添加" }, - { - "code": "open_edge", - "localisation":"开放边缘" - }, { "code": "add_edge", "localisation": "添加边缘" diff --git a/src/main/resources/localisation/en-US.json b/src/main/resources/localisation/en-US.json index 00ed1a4..e2e718a 100644 --- a/src/main/resources/localisation/en-US.json +++ b/src/main/resources/localisation/en-US.json @@ -32,13 +32,9 @@ "code": "neo4j_data", "localisation": "Neo4j data" }, - { - "code": "open_edge", - "localisation": "Add edge" - }, { "code": "add_edge", - "localisation": "Add" + "localisation": "Add edge" }, { "code": "add", @@ -115,6 +111,10 @@ { "code": "end", "localisation": "End" + }, + { + "code": "weight", + "localisation": "weight" } ] } \ No newline at end of file diff --git a/src/main/resources/localisation/ru-RU.json b/src/main/resources/localisation/ru-RU.json index 094528a..859a713 100644 --- a/src/main/resources/localisation/ru-RU.json +++ b/src/main/resources/localisation/ru-RU.json @@ -36,10 +36,6 @@ "code": "add_edge", "localisation": "Добавить ребро" }, - { - "code": "open_edge", - "localisation": "Добавить" - }, { "code": "add", "localisation": "Добавить" @@ -115,6 +111,10 @@ { "code": "end", "localisation": "Конец" + }, + { + "code": "weight", + "localisation": "вес" } ] } \ No newline at end of file diff --git a/src/test/kotlin/SQLiteIntegrationTest.kt b/src/test/kotlin/SQLiteIntegrationTest.kt index 5901ce7..461e25b 100644 --- a/src/test/kotlin/SQLiteIntegrationTest.kt +++ b/src/test/kotlin/SQLiteIntegrationTest.kt @@ -1,5 +1,5 @@ -import model.graph.DirectedGraph import model.graph.Edge +import viewmodel.GraphType import viewmodel.MainScreenViewModel import java.io.File import kotlin.test.Test @@ -11,22 +11,23 @@ internal class SQLiteIntegrationTest { @Test fun `SQLite integrable test`() { - val mainScreenVM = MainScreenViewModel("sqlite") - mainScreenVM.addGraph("someName", "Directed") - mainScreenVM.saveGraph("someName", "test") + val mainScreenVM = MainScreenViewModel() + mainScreenVM.addGraph("someName", GraphType.Directed) val graph = mainScreenVM.getGraph("someName") for (i in 1..4) { graph.addVertex("$i") } - graph.run { this.addEdge("1", "4", 20) this.addEdge("1", "2", 2) this.addEdge("2", "3", 3) this.addEdge("3", "4", 1) } + mainScreenVM.saveGraph("someName", "test") + mainScreenVM.initGraphList("test") - mainScreenVM.initGraph("someName", "test") + mainScreenVM.saveType = "sqlite" + mainScreenVM.loadGraph("someName", "test") val loadedGraph = mainScreenVM.getGraph("someName") val result = Dijkstra(loadedGraph.model, 4).dijkstra("1", "4") val shortestLengthExpected = 6 diff --git a/storage.db b/storage.db deleted file mode 100644 index e83574164eb90a912a4a7c39f897d9f3aae03041..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI4Ta4UR8OQAzU*$|=iI6lFY~&GxLt z%aM3amYqrM6gF9oE610ASB)FwDtDCnE0sINcQ$D)H^~HK0y2UB(F95_?j9RcKlRp$ zwX07*v-BKaTD`vfe0z~Uv(T7ZY*>qPr{^11d!=>Qik4Q_mY!ewxOH)D6>hFfT9Iv? zdu*}saART8vLYv4anlu+uJCllOIQ4KB}iAobVZ~q4z_?iILL&9TsX*vgM2v1h=ZIs z$clr!xX6o(ytv4Vi@dnVi;KLt$cu}-xX6o(yfEa2AukMhVaN+ZUKsMikQau$Fyw_H zFC2N{$O}haIP$`g7mmDemi-){;$cvA> z_{fWoy!gnAkG%NEi;ukc$cvA>_{d9uyadQgfV>3AOMtut$V-5{1jtK(yaXLDac{{- zN7W0hUTJ-0eZ@q5(fUeXH)KsHivHcCGrJwgzAWj6(K(QPS?BijU+c?0J(4q%>+>_u zwXV;`rMXAX&o|l&e6MY*TGy8iSQ>+sF1HOZdEs-LZwYrm-7u02_^s&7`mR&7>~R{m7^PUYFkbVVt@R(`qsczH+Z z_0sLq<^Jfp!Lmwi;bxR_A)KtzP}o_efjE&>>dzi;OE>@N z6W$vCo+{#=5l`Y+Rx+QS+@7>$A^wOsD-@=VZcp64h}4L?#i=9P)ovVp#2s^bVrwnV zszr{ucCL8x@E~yt2CGz-&)%9+j9{yUlR{( zt?em#rHFYm_irujVq1lcTEuwHG-nR>tGlgxoNlSTO^sN5brr>des#C$CWVN_KflN- zb!PS_QZ;q*liJo(#O(O1ICWneJd7l@)5sNu*q70?8;X0gN&_*LRqBhqS*4!XlU2&6 z?iotjcLE}p88f@Fwy#m@lq6@pvpsTM(=5!~y_tHWZAX#bQRF%~EjPWZs~*%SE$Eh$ zhLLM)nz*Yg?rt2W5WlGvJF`kdu_LQA5TjY8z8J|W^+Y|Zl+V@@Y3IyzmVFubHndBT z!&>*?YF4s4UD-^Ed!n?T$Z<4nrrZ;^9Q8iZZTrfRlXO?JC4aPUQ{N%REmG@}VkDAd zHbilNyl;OgaeG@78lo_iG_|u03sGPSb2DkrX-eBFM!psrn@Rs!(5pwD(6?3&LPuN^ z+Sbw@Dk-%Om6gu{~p2{WaSflnHZ`!TXJK89B*Tj#NjuFfC_#;%i ze2m$m7TL-8e_Z*3QomiFto^8VsrrZNt?J&&w<~AMzbSvLY?QuSvWu@3FBkq;xK-F= ze#<;#{K~jtCm3JOG>+LWH-6-T2dGdU z+)st#aF7bc-~biM1&a#hzR z*Yv;YpVJS@@&6E=W2;I0hDMJ6hYf2`yZBv^9RCjrX4rM{iz_+)A2ux14z3eDun7Uz zs8At1O@#{JDiz9y4^yE$xI%^E@F6M`gAY=nTzHBK<-n6vC>t&lp+W&4phAUki3%0K z`>9YqL{umbo}fZ;SfoNRxJZR^VSx(ezy&Il4d;nafq=)UP$4`PvveI@6qKUJ?Ne^FjS56{C_;C)ob%-|dq%7yn*p&WRa z3T4AtB9t$nL4^w8Au3b=XQ)sp z^57U1io+xoiosDTlnY0wP!3E`p=>xzgkl07q(X&ohzb?72k7~Dnfz~mBcAnmPU>=! i3CILw0x|)afJ{IpAQO-Y$OL2pG69)@OyK_{fqwu8J}j{S From fe20c71ceef11a773b07eaf959508b15853e452d Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:29:36 +0300 Subject: [PATCH 165/172] feat: create README.md --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9519bca..32c1082 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,50 @@ -You can run it by +# Graph Visualizer +## About +Application for graph analysis using embedded algorithms +## Quick start +### Pre-requisites +- Kotlin 1.9.23 +- Gradle 8.5 +- JDK 21 +#### Start +Clone repository +``` +git clone git@github.com:odiumuniverse/GraphVisualizer.git //SSH +``` +``` +git clone https://github.com/odiumuniverse/GraphVisualizer.git //HTTPS +``` +Go to the application folder +``` +cd GraphVisualizer +``` +Now, run it by ``` ./gradlew run ``` +### Import and Exports +You can import and export graphs using SQLite and Neo4j +- When using Neo4j you have to enter your data (we do not use your data) +- SQLite is a local database, you don't need to do anything, just select it in the application settings + +## Features +#### You can use basic algorithms +- Searching for clusters +- Betweenness centrality +- Graph layout +#### There are also other algorithms +- Finding bridges +- Finding cycles +- Finding the shortest path using Ford-Bellman and Dijkstra algorithms +- Finding strong connectivity components +- Constructing a minimal island tree + +## Licence +The app is distributed under [Unlicence](https://unlicense.org/), meaning we are putting this project into the public domain +## Contributing + We do not support contributing, so please write to the authors with your suggestions +## Authors +- [Aleksey Dmitrievtsev](https://github.com/admitrievtsev) +- [Gleb Nasretdinov](https://github.com/Ycyken) +- [Azamat Ishbaev](https://github.com/odiumuniverse) + From 71132a416372c45bff766e00e8d61494568fc741 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:34:30 +0300 Subject: [PATCH 166/172] feat: update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 32c1082..137537c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Graph Visualizer ## About Application for graph analysis using embedded algorithms + + +https://github.com/odiumuniverse/GraphVisualizer/assets/143432879/9789b03c-3728-4bea-9389-9c795d12b611 + + ## Quick start ### Pre-requisites - Kotlin 1.9.23 @@ -8,6 +13,7 @@ Application for graph analysis using embedded algorithms - JDK 21 #### Start Clone repository + ``` git clone git@github.com:odiumuniverse/GraphVisualizer.git //SSH ``` From c5e03632f3f59d9bfdfe676ec40ecfdf1aae5b0b Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:44:44 +0300 Subject: [PATCH 167/172] change: remove video form README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 137537c..4012871 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,6 @@ ## About Application for graph analysis using embedded algorithms - -https://github.com/odiumuniverse/GraphVisualizer/assets/143432879/9789b03c-3728-4bea-9389-9c795d12b611 - - ## Quick start ### Pre-requisites - Kotlin 1.9.23 From e2c4eae0e52dddb30714a488db82848c061e7353 Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Fri, 14 Jun 2024 12:27:14 +0300 Subject: [PATCH 168/172] Update README.md Co-authored-by: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4012871..ad67fc2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ Application for graph analysis using embedded algorithms ## Quick start ### Pre-requisites - Kotlin 1.9.23 -- Gradle 8.5 - JDK 21 #### Start Clone repository From ca521cb1d4a442035bb0c7833efb0cc14235228c Mon Sep 17 00:00:00 2001 From: Azamat Ishbaev <143432879+odiumuniverse@users.noreply.github.com> Date: Fri, 14 Jun 2024 12:30:09 +0300 Subject: [PATCH 169/172] Update README.md Co-authored-by: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> --- README.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/README.md b/README.md index ad67fc2..8f6a830 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,7 @@ Application for graph analysis using embedded algorithms - Kotlin 1.9.23 - JDK 21 #### Start -Clone repository - -``` -git clone git@github.com:odiumuniverse/GraphVisualizer.git //SSH -``` -``` -git clone https://github.com/odiumuniverse/GraphVisualizer.git //HTTPS -``` -Go to the application folder -``` -cd GraphVisualizer -``` -Now, run it by +Run it by ``` ./gradlew run ``` From cb573aa33d986784d6f3d81df8832b3dafbe43dd Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov <135718038+Ycyken@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:36:22 +0300 Subject: [PATCH 170/172] Update README.md --- README.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8f6a830..d13bf86 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # Graph Visualizer ## About -Application for graph analysis using embedded algorithms +Application for creating graphs and visualisation graph algorithms. + +[dsfds.webm](https://github.com/spbu-coding-2023/graphs-graphs-8/assets/135718038/5fbf7226-122b-412e-b1c4-bb05ad998f0e) + ## Quick start -### Pre-requisites +### Requirements - Kotlin 1.9.23 - JDK 21 #### Start @@ -11,22 +14,22 @@ Run it by ``` ./gradlew run ``` -### Import and Exports -You can import and export graphs using SQLite and Neo4j -- When using Neo4j you have to enter your data (we do not use your data) -- SQLite is a local database, you don't need to do anything, just select it in the application settings +### Saving graphs +You can save graphs graphs using SQLite and Neo4j +- When using Neo4j you have to enter uri, username and password to establish a connection. +- SQLite is a local database, you don't need to do anything, just select it in the application settings. ## Features #### You can use basic algorithms -- Searching for clusters -- Betweenness centrality -- Graph layout +- Searching for clusters. +- Betweenness centrality. +- Graph force-directed drawing algorithm. #### There are also other algorithms -- Finding bridges -- Finding cycles -- Finding the shortest path using Ford-Bellman and Dijkstra algorithms -- Finding strong connectivity components -- Constructing a minimal island tree +- Find bridges. +- Find cycles. +- Find the shortest path using Ford-Bellman and Dijkstra algorithms. +- Find strongly connected components. +- Constructing a minimal spanning tree. ## Licence The app is distributed under [Unlicence](https://unlicense.org/), meaning we are putting this project into the public domain @@ -36,4 +39,3 @@ The app is distributed under [Unlicence](https://unlicense.org/), meaning we are - [Aleksey Dmitrievtsev](https://github.com/admitrievtsev) - [Gleb Nasretdinov](https://github.com/Ycyken) - [Azamat Ishbaev](https://github.com/odiumuniverse) - From d8c8046d9cf67b4215da34eb60f77ad7b830e1c1 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Thu, 4 Jul 2024 00:18:01 +0300 Subject: [PATCH 171/172] Feat: graphs now creating with centre coordinate vertices --- src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt | 2 +- src/main/kotlin/viewmodel/graph/VertexViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt index 1081f90..57be5bb 100644 --- a/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/AbstractGraphViewModel.kt @@ -112,7 +112,7 @@ abstract class AbstractGraphViewModel(_name: String, graph: Graph) : ViewM return graphVM[vertex]?.edges?.toList() ?: emptyList() } - fun addVertex(vertex: V, centerCoordinates: Boolean = false) { + fun addVertex(vertex: V, centerCoordinates: Boolean = true) { size += 1 graphVM.putIfAbsent( vertex, diff --git a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt index 6019086..eac230e 100644 --- a/src/main/kotlin/viewmodel/graph/VertexViewModel.kt +++ b/src/main/kotlin/viewmodel/graph/VertexViewModel.kt @@ -14,7 +14,7 @@ class VertexViewModel( _vertex: V, _edges: MutableList> = mutableListOf(), graphVM: AbstractGraphViewModel, - centerCoordinates: Boolean = false + centerCoordinates: Boolean = true ) : ViewModel() { val vertex: V = _vertex From 71792883fa2b8f876dc432cba2cc3994f69826b6 Mon Sep 17 00:00:00 2001 From: Gleb Nasretdinov Date: Sun, 11 May 2025 00:30:53 +0300 Subject: [PATCH 172/172] CI: add pull request run on all workflows Signed-off-by: Gleb Nasretdinov --- .github/workflows/coverage.yml | 2 +- .github/workflows/gradle-build.yml | 3 ++- .github/workflows/gradle-test.yml | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index adf9d60..bbd1fcc 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,7 +1,7 @@ name: Measure coverage on: - pull_request: + [push, pull_request] jobs: build: diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml index d55daa5..9210217 100644 --- a/.github/workflows/gradle-build.yml +++ b/.github/workflows/gradle-build.yml @@ -1,7 +1,8 @@ name: Build on: - push: + [push, pull_request] + jobs: build: runs-on: ${{ matrix.os }} diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 2c8155e..dad9e7c 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -1,7 +1,8 @@ name: Test on: - push: + [push, pull_request] + jobs: test: runs-on: ${{ matrix.os }}