diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 568f717..22f0633 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("jvm") version "2.1.10" + kotlin("plugin.serialization") version "2.1.10" // ← добавь эту строку id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" id("org.jetbrains.compose") version "1.7.1" } @@ -28,6 +29,7 @@ dependencies { implementation("org.jetbrains.exposed:exposed-dao:0.61.0") implementation("org.jetbrains.exposed:exposed-jdbc:0.61.0") implementation("com.github.JetBrains-Research:louvain:main-SNAPSHOT") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") } tasks.test { @@ -41,4 +43,4 @@ compose.desktop { application { mainClass = "MainKt" } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/model/algo/findBridges.kt b/app/src/main/kotlin/model/algo/findBridges.kt index 3c3bde3..31249ca 100644 --- a/app/src/main/kotlin/model/algo/findBridges.kt +++ b/app/src/main/kotlin/model/algo/findBridges.kt @@ -41,9 +41,9 @@ fun findBridges(graph: Graph): Set { ret[adjacentVertex] ?: throw IllegalStateException(), ) if ((ret[adjacentVertex] ?: throw IllegalStateException()) > ( - timeIn[current] - ?: throw IllegalStateException() - ) + timeIn[current] + ?: throw IllegalStateException() + ) ) { briges.add( graph.getEdge(current, adjacentVertex) ?: throw IllegalStateException("No such vertex in graph"), diff --git a/app/src/main/kotlin/model/algo/findCrucialVertex.kt b/app/src/main/kotlin/model/algo/findCrucialVertex.kt new file mode 100644 index 0000000..04aca8a --- /dev/null +++ b/app/src/main/kotlin/model/algo/findCrucialVertex.kt @@ -0,0 +1,79 @@ +package model.algo + +import model.graph.Graph +import model.graph.Vertex +import java.util.LinkedList +import java.util.Queue + + +fun Graph.computeBetweennessCentrality(): Map { + val centrality = mutableMapOf() + vertices.forEach { v -> centrality[v] = 0.0 } + + for (s in vertices) { + val stack = mutableListOf() + val predecessors = mutableMapOf>() + val sigma = mutableMapOf() + val distance = mutableMapOf() + + vertices.forEach { w -> predecessors[w] = mutableListOf() } + vertices.forEach { w -> sigma[w] = 0 } + vertices.forEach { w -> distance[w] = -1 } + + sigma[s] = 1 + distance[s] = 0 + + val queue: Queue = LinkedList() + queue.add(s) + + + while (queue.isNotEmpty()) { + val v = queue.poll() + stack.add(v) + + val neighbors = if (isDirected) { + edges.filter { it.vertices.first == v }.map { it.vertices.second } + } else { + edges.filter { it.vertices.first == v || it.vertices.second == v } + .map { if (it.vertices.first == v) it.vertices.second else it.vertices.first } + } + + for (w in neighbors) { + distance[w]?.let { + if (it < 0) { + queue.add(w) + distance[w] = distance[v]!! + 1 + } + } + if (distance[w] == distance[v]!! + 1) { + sigma[w] = sigma[w]!! + sigma[v]!! + predecessors[w]!!.add(v) + } + } + } + + val delta = mutableMapOf() + vertices.forEach { w -> delta[w] = 0.0 } + + while (stack.isNotEmpty()) { + val w = stack.removeAt(stack.size - 1) + for (v in predecessors[w] ?: emptyList()) { + delta[v] = delta[v]!! + (sigma[v]!!.toDouble() / sigma[w]!!.toDouble()) * (1 + delta[w]!!) + } + if (w != s) { + centrality[w] = centrality[w]!! + delta[w]!! + } + } + } + + return centrality +} + + +fun Graph.findCrucialVertices(): List { + val centrality = computeBetweennessCentrality() + if (centrality.isEmpty()) return emptyList() + + val maxCentrality = centrality.maxOfOrNull { it.value } ?: return emptyList() + return centrality.filter { it.value == maxCentrality }.keys.toList() +} \ No newline at end of file diff --git a/app/src/main/kotlin/model/algo/findCycles.kt b/app/src/main/kotlin/model/algo/findCycles.kt new file mode 100644 index 0000000..677693a --- /dev/null +++ b/app/src/main/kotlin/model/algo/findCycles.kt @@ -0,0 +1,43 @@ +package model.algo + +import model.graph.Graph +import model.graph.Vertex + +fun Graph.findCyclesStartingFrom(startVertexId: Int): List> { + val cycles = mutableListOf>() + val startVertex = getVertex(startVertexId) ?: return emptyList() + + fun dfs( + current: Vertex, + path: MutableList, + recStack: MutableSet + ) { + path.add(current) + recStack.add(current) + + val neighbors = if (isDirected) { + edges.filter { it.vertices.first == current }.map { it.vertices.second } + } else { + edges + .filter { it.vertices.first == current || it.vertices.second == current } + .map { if (it.vertices.first == current) it.vertices.second else it.vertices.first } + } + + for (neighbor in neighbors) { + if (neighbor == startVertex && path.size > 2) { + // Цикл завершён: начинается и заканчивается в startVertex + cycles.add((path + neighbor).toList()) + } else if (neighbor !in recStack) { + dfs(neighbor, path, recStack) + } + } + + // Backtracking + path.removeAt(path.size - 1) + recStack.remove(current) + } + + dfs(startVertex, mutableListOf(), mutableSetOf()) + + return cycles +} \ No newline at end of file diff --git a/app/src/main/kotlin/model/io/json/GraphJson.kt b/app/src/main/kotlin/model/io/json/GraphJson.kt new file mode 100644 index 0000000..e2819a6 --- /dev/null +++ b/app/src/main/kotlin/model/io/json/GraphJson.kt @@ -0,0 +1,25 @@ +package model.io.json + +import kotlinx.serialization.Serializable + + +@Serializable +data class GraphJson( + val isDirected:Boolean, + val vertices :List, + val edges: List, +) +@Serializable +data class VertexData( + val id: Int, + val label: String, +) +@Serializable +data class EdgeData( + val id: Long, + val from: Int, + val to: Int, + val weight: Long, +) + + diff --git a/app/src/main/kotlin/model/io/json/GraphRepository.kt b/app/src/main/kotlin/model/io/json/GraphRepository.kt new file mode 100644 index 0000000..f728288 --- /dev/null +++ b/app/src/main/kotlin/model/io/json/GraphRepository.kt @@ -0,0 +1,54 @@ +package model.io.json + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import model.graph.Graph +import java.io.File + +class JsonRepository { + val json = Json { prettyPrint = true } + + fun writeToDisk(graph: Graph, filePath: String) { + + val graphJson = GraphJson( + isDirected = graph.isDirected, + vertices = graph.vertices.map { vertex -> + VertexData(id = vertex.id, label = vertex.label) + }, + edges = graph.edges.map { edge -> + EdgeData( + id = edge.id, + from = edge.vertices.first.id, + to = edge.vertices.second.id, + weight = edge.weight + ) + } + ) + + + val jsonString = json.encodeToString(graphJson) + File(filePath).writeText(jsonString) + } + + fun readFromDisk(filePath: String): Graph { + + val jsonString = File(filePath).readText() + val graphJson = json.decodeFromString(jsonString) + + + val graph = Graph(isDirected = graphJson.isDirected) + + + graphJson.vertices.forEach { vertexData -> + graph.addVertex(vertexData.id, vertexData.label) + } + + + graphJson.edges.forEach { edgeData -> + graph.addEdge(edgeData.from, edgeData.to, edgeData.weight) + } + + return graph + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/dialogs/CycleDetectionView.kt b/app/src/main/kotlin/view/dialogs/CycleDetectionView.kt new file mode 100644 index 0000000..9facd94 --- /dev/null +++ b/app/src/main/kotlin/view/dialogs/CycleDetectionView.kt @@ -0,0 +1,93 @@ +package view.dialogs + +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.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import viewmodel.graph.GraphViewModel + +@Composable +fun CycleDetectionDialog( + graphViewModel: GraphViewModel, + onDismiss: () -> Unit, + onVertexSelected: (Int) -> Unit +) { + var selectedVertexId by remember { mutableStateOf(null) } + + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier + .width(400.dp) + .wrapContentHeight(), + shape = MaterialTheme.shapes.medium, + elevation = 8.dp + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Find Cycles for Vertex", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + + LazyColumn( + modifier = Modifier + .height(200.dp) + .fillMaxWidth() + ) { + items(graphViewModel.vertices.toList()) { vertexViewModel -> + Card( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + .clickable { + selectedVertexId = vertexViewModel.origin.id + }, + backgroundColor = if (selectedVertexId == vertexViewModel.origin.id) + MaterialTheme.colors.primary.copy(alpha = 0.1f) + else MaterialTheme.colors.surface, + elevation = 2.dp + ) { + Text( + text = "Vertex ${vertexViewModel.origin.id}: ${vertexViewModel.origin.label}", + modifier = Modifier.padding(16.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Button(onClick = onDismiss) { + Text("Cancel") + } + + Button( + onClick = { + selectedVertexId?.let { vertexId -> + onVertexSelected(vertexId) + } + onDismiss() + }, + enabled = selectedVertexId != null + ) { + Text("Find Cycles") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/dialogs/CycleListDialog.kt b/app/src/main/kotlin/view/dialogs/CycleListDialog.kt new file mode 100644 index 0000000..b1e32c9 --- /dev/null +++ b/app/src/main/kotlin/view/dialogs/CycleListDialog.kt @@ -0,0 +1,73 @@ +package view.dialogs + +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.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import model.graph.Vertex +import viewmodel.graph.GraphViewModel +import androidx.compose.ui.window.Dialog + +@Composable +fun CycleListDialog( + cycles: List>, + graphViewModel: GraphViewModel, + onDismiss: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier + .width(400.dp) + .wrapContentHeight(), + shape = MaterialTheme.shapes.medium, + elevation = 8.dp + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Select a Cycle", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + LazyColumn( + modifier = Modifier + .height(300.dp) + .fillMaxWidth() + ) { + items(cycles) { cycle -> + Card( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + .clickable { + graphViewModel.highlightCycle(cycle) + onDismiss() + }, + backgroundColor = MaterialTheme.colors.surface, + elevation = 2.dp + ) { + Text( + text = cycle.joinToString(" -> ") { it.label }, + modifier = Modifier.padding(16.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = onDismiss) { + Text("Close") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/io/JsonSaveView.kt b/app/src/main/kotlin/view/io/JsonSaveView.kt new file mode 100644 index 0000000..2978048 --- /dev/null +++ b/app/src/main/kotlin/view/io/JsonSaveView.kt @@ -0,0 +1,124 @@ +package view.io + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +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.Dialog +import model.graph.Graph +import model.io.json.JsonRepository +import java.io.File +import javax.swing.JFileChooser +import javax.swing.filechooser.FileNameExtensionFilter + +@Composable +fun jsonSaveView( + graph: Graph, + onDismiss: () -> Unit, +) { + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + var successMessage by remember { mutableStateOf(null) } + + Dialog( + onDismissRequest = onDismiss, + ) { + Surface( + modifier = Modifier + .width(400.dp) + .wrapContentHeight(), + shape = RoundedCornerShape(8.dp), + elevation = 8.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Save Graph to JSON", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.padding(16.dp)) + Text("Saving JSON file...") + } else if (errorMessage != null) { + Text( + text = errorMessage!!, + color = Color.Red, + modifier = Modifier.padding(vertical = 8.dp) + ) + } else if (successMessage != null) { + Text( + text = successMessage!!, + color = Color.Green, + modifier = Modifier.padding(vertical = 8.dp) + ) + } else { + Text( + text = "Select location to save JSON file", + modifier = Modifier.padding(bottom = 16.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Gray), + ) { + Text("Cancel") + } + + Button( + onClick = { + isLoading = true + errorMessage = null + successMessage = null + + try { + val file = selectSaveJsonFile() + if (file != null) { + val repository = JsonRepository() + repository.writeToDisk(graph, file.absolutePath) + isLoading = false + successMessage = "Graph saved successfully to: ${file.name}" + } else { + isLoading = false + } + } catch (e: Exception) { + isLoading = false + errorMessage = "Error saving JSON: ${e.message}" + } + }, + enabled = !isLoading, + ) { + Text("Save JSON File") + } + } + } + } + } +} + +fun selectSaveJsonFile(): File? { + val fileChooser = JFileChooser().apply { + fileFilter = FileNameExtensionFilter("JSON files", "json") + dialogTitle = "Save Graph as JSON" + selectedFile = File("graph.json") + } + + return when (fileChooser.showSaveDialog(null)) { + JFileChooser.APPROVE_OPTION -> fileChooser.selectedFile + else -> null + } +} diff --git a/app/src/main/kotlin/view/io/jsonView.kt b/app/src/main/kotlin/view/io/jsonView.kt new file mode 100644 index 0000000..fa1a389 --- /dev/null +++ b/app/src/main/kotlin/view/io/jsonView.kt @@ -0,0 +1,114 @@ +package view.io + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +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.Dialog +import java.io.File +import javax.swing.JFileChooser +import javax.swing.filechooser.FileNameExtensionFilter +import model.graph.Graph +import model.io.json.JsonRepository + +@Composable +fun jsonView( + onDismiss: () -> Unit, + onGraphChosen: (Graph) -> Unit, +) { + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + + Dialog( + onDismissRequest = onDismiss, + ) { + Surface( + modifier = Modifier + .width(400.dp) + .wrapContentHeight(), + shape = RoundedCornerShape(8.dp), + elevation = 8.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Load Graph from JSON", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 16.dp) + ) + + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.padding(16.dp)) + Text("Loading JSON file...") + } else if (errorMessage != null) { + Text( + text = errorMessage!!, + color = Color.Red, + modifier = Modifier.padding(vertical = 8.dp) + ) + } else { + Text( + text = "Select JSON file to load graph", + modifier = Modifier.padding(bottom = 16.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Gray), + ) { + Text("Cancel") + } + + Button( + onClick = { + isLoading = true + errorMessage = null + + + try { + val file = selectJsonFile() + if (file != null) { + val repository = JsonRepository() + val graph = repository.readFromDisk(file.absolutePath) + onGraphChosen(graph) + } + } catch (e: Exception) { + errorMessage = "Error loading JSON: ${e.message}" + } finally { + isLoading = false + } + }, + enabled = !isLoading, + ) { + Text("Load JSON File") + } + } + } + } + } +} + +fun selectJsonFile(): File? { + val fileChooser = JFileChooser().apply { + fileFilter = FileNameExtensionFilter("JSON files", "json") + dialogTitle = "Select JSON Graph File" + } + + return when (fileChooser.showOpenDialog(null)) { + JFileChooser.APPROVE_OPTION -> fileChooser.selectedFile + else -> null + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/view/screens/HelloScreenView.kt b/app/src/main/kotlin/view/screens/HelloScreenView.kt index 124442f..f2ea585 100644 --- a/app/src/main/kotlin/view/screens/HelloScreenView.kt +++ b/app/src/main/kotlin/view/screens/HelloScreenView.kt @@ -60,6 +60,13 @@ fun helloScreen(viewModel: HelloScreenViewModel = remember { HelloScreenViewMode modifier = Modifier.fillMaxWidth(0.5f), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { + OutlinedButton( + onClick = { viewModel.selectStorage(Storage.JSON) }, + colors = ButtonDefaults.buttonColors(backgroundColor = ColorTheme.TranslucentButtonColor), + modifier = Modifier.clip(RoundedCornerShape(percent = 25)).weight(0.25f), + ) { + Text("JSON") + } OutlinedButton( onClick = { viewModel.selectStorage(Storage.SQLite) }, colors = ButtonDefaults.buttonColors(backgroundColor = ColorTheme.TranslucentButtonColor), @@ -80,7 +87,7 @@ fun helloScreen(viewModel: HelloScreenViewModel = remember { HelloScreenViewMode OutlinedButton( onClick = { viewModel.setIsRandom(true) }, colors = ButtonDefaults.buttonColors(backgroundColor = ColorTheme.TranslucentButtonColor), - modifier = Modifier.clip(RoundedCornerShape(percent = 25)).weight(0.28f), + modifier = Modifier.clip(RoundedCornerShape(percent = 25)).weight(0.34f), ) { Text("Random Graph") } @@ -89,7 +96,14 @@ fun helloScreen(viewModel: HelloScreenViewModel = remember { HelloScreenViewMode when (storage) { Storage.JSON -> { - viewModel.selectStorage(null) + jsonView( + onDismiss = { viewModel.selectStorage(null) }, + onGraphChosen = { g-> + viewModel.selectGraph(g) + navigator?.push(MainScreenNav(g, ForceAtlas2())) + viewModel.selectStorage(null) + }, + ) } Storage.SQLite -> { diff --git a/app/src/main/kotlin/view/screens/MainScreenView.kt b/app/src/main/kotlin/view/screens/MainScreenView.kt index 4ea4def..c3f94a2 100644 --- a/app/src/main/kotlin/view/screens/MainScreenView.kt +++ b/app/src/main/kotlin/view/screens/MainScreenView.kt @@ -18,11 +18,17 @@ import androidx.compose.ui.unit.sp import cafe.adriel.voyager.navigator.LocalNavigator import view.dialogs.FordBellmanDialog import view.dialogs.exceptionView +import view.io.jsonSaveView import view.graph.GraphView import view.io.neo4jView import view.io.sqliteSaveView import viewmodel.colors.ColorTheme import viewmodel.screens.MainScreenViewModel +import view.dialogs.CycleDetectionDialog +import view.dialogs.CycleListDialog +import model.graph.Vertex +import model.algo.findCyclesStartingFrom +import model.algo.findCrucialVertices @Composable fun MainScreen(viewModel: MainScreenViewModel) { @@ -32,11 +38,16 @@ fun MainScreen(viewModel: MainScreenViewModel) { val message = remember { viewModel.message } var scale by remember { mutableStateOf(1f) } var expanded by remember { mutableStateOf(false) } + var showCycleDetectionDialog by remember { mutableStateOf(false) } val storage by viewModel.storage val uri = remember { viewModel.uri } val username = remember { viewModel.username } val password = remember { viewModel.password } + + var showCycleListDialog by remember { mutableStateOf(false) } + var cyclesForDialog by remember { mutableStateOf>>(emptyList()) } + Column { Box( modifier = @@ -52,6 +63,9 @@ fun MainScreen(viewModel: MainScreenViewModel) { expanded = expanded, onDismissRequest = { expanded = false }, ) { + DropdownMenuItem(onClick = { viewModel.selectStorage(Storage.JSON) }) { + Text("Save to JSON") + } DropdownMenuItem(onClick = { viewModel.selectStorage(Storage.Neo4j) }) { Text("Save to Neo4j") } @@ -160,6 +174,28 @@ fun MainScreen(viewModel: MainScreenViewModel) { } } Spacer(modifier = Modifier.height(4.dp)) + Button( + onClick = { showCycleDetectionDialog = true }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(backgroundColor = ColorTheme.ButtonColor), + ) { + Text("Find Cycles for Vertex") + } + Button( + onClick = { + try { + viewModel.graphViewModel.findCrucialVertices() + } catch (e: IllegalStateException) { + viewModel.setExceptionDialog(true) + viewModel.setMessage(e.message ?: "An error occurred") + } + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(backgroundColor = ColorTheme.ButtonColor), + ) { + Text("Find Crucial Vertices") + } + Spacer(modifier = Modifier.height(4.dp)) Button( onClick = viewModel::showCommunities, modifier = Modifier.fillMaxWidth(), @@ -242,7 +278,41 @@ fun MainScreen(viewModel: MainScreenViewModel) { ) } + + if (showCycleDetectionDialog) { + CycleDetectionDialog( + graphViewModel = viewModel.graphViewModel, + onDismiss = { showCycleDetectionDialog = false }, + onVertexSelected = { vertexId -> + val cycles = viewModel.graphViewModel.graph.findCyclesStartingFrom(vertexId) + if (cycles.isEmpty()) { + viewModel.setExceptionDialog(true) + viewModel.setMessage("No cycles found for vertex $vertexId") + } else { + cyclesForDialog = cycles + showCycleDetectionDialog = false + showCycleListDialog = true + } + } + ) + } + + + if (showCycleListDialog) { + CycleListDialog( + cycles = cyclesForDialog, + graphViewModel = viewModel.graphViewModel, + onDismiss = { showCycleListDialog = false } + ) + } + when (storage) { + Storage.JSON -> { + jsonSaveView( + graph = viewModel.graphViewModel.graph, + onDismiss = { viewModel.selectStorage(null) }, + ) + } Storage.Neo4j -> { neo4jView( uri, @@ -262,4 +332,4 @@ fun MainScreen(viewModel: MainScreenViewModel) { else -> { } } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/viewmodel/colors/ColorTheme.kt b/app/src/main/kotlin/viewmodel/colors/ColorTheme.kt index 995ca3e..da78f7c 100644 --- a/app/src/main/kotlin/viewmodel/colors/ColorTheme.kt +++ b/app/src/main/kotlin/viewmodel/colors/ColorTheme.kt @@ -17,7 +17,7 @@ object ColorTheme { val TextColor = Color(0, 0, 0) @Stable - val ConfirmColor = Color(120, 255, 140) + val ConfirmColor = Color(124, 255, 120, 255) @Stable val RejectColor = Color(255, 120, 140) diff --git a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt index d73ab74..6acdd86 100644 --- a/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt +++ b/app/src/main/kotlin/viewmodel/graph/GraphViewModel.kt @@ -1,9 +1,12 @@ package viewmodel.graph import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import model.algo.findBridges import model.algo.fordBellman +import model.algo.findCrucialVertices import model.graph.Graph import model.graph.Vertex import viewmodel.colors.ColorTheme @@ -15,27 +18,27 @@ class GraphViewModel( showEdgeWeights: State, showVertexId: State, ) { - internal val _vertices = + internal val verticesMap = graph.vertices.associateWith { vertex -> VertexViewModel(0.dp, 0.dp, ColorTheme.VertexDefaultColor, vertex, showVertexLabels, showVertexId) } - internal val _edges = + internal val edgesMap = graph.edges.associateWith { edge -> val first = - _vertices[edge.vertices.first] + verticesMap[edge.vertices.first] ?: throw IllegalStateException("VertexView for ${edge.vertices.first} not found") val second = - _vertices[edge.vertices.second] + verticesMap[edge.vertices.second] ?: throw IllegalStateException("VertexView for ${edge.vertices.second} not found") EdgeViewModel(first, second, ColorTheme.EdgeDefaultColor, edge, showEdgeWeights, graph.isDirected) } val vertices: Collection - get() = _vertices.values + get() = verticesMap.values val edges: Collection - get() = _edges.values + get() = edgesMap.values fun fordBellman( firstId: Int, @@ -51,31 +54,62 @@ class GraphViewModel( val cycle = result.second val isCycle = result.third - var verticesForColoring = path ?: Vector() - - if (isCycle) { - verticesForColoring = cycle ?: Vector() + val verticesForColoring = if (isCycle) { + cycle ?: Vector() + } else { + path ?: Vector() } - if (verticesForColoring.size == 0) { + if (verticesForColoring.isEmpty()) { throw IllegalStateException("No path from vertex $firstId to vertex $secondId") } verticesForColoring.forEach { vertex -> - _vertices[vertex]?.color = ColorTheme.VertexPickedColor + verticesMap[vertex]?.color = ColorTheme.VertexPickedColor } var i = 0 while (i < verticesForColoring.size - 1) { - _edges[graph.getEdge(verticesForColoring[i], verticesForColoring[i + 1])]?.color = ColorTheme.EdgePickedColor + edgesMap[graph.getEdge(verticesForColoring[i], verticesForColoring[i + 1])]?.color = ColorTheme.EdgePickedColor i++ } } fun findBridges() { - var edgesForColoring = findBridges(graph) + val edgesForColoring = findBridges(graph) edgesForColoring.forEach { edge -> - _edges[edge]?.color = ColorTheme.EdgePickedColor + edgesMap[edge]?.color = ColorTheme.EdgePickedColor + } + } + fun findCrucialVertices() { + val crucialVertices = graph.findCrucialVertices() + if (crucialVertices.isNotEmpty()) { + resetColors() + crucialVertices.forEach { vertex -> + verticesMap[vertex]?.color = ColorTheme.VertexPickedColor + } + } else { + throw IllegalStateException("No crucial vertices found (graph is empty)") } } -} + + + fun highlightCycle(cycle: List) { + resetColors() + + for (i in cycle.indices) { + val currentVertex = cycle[i] + val nextVertex = cycle[(i + 1) % cycle.size] //% нужен для случая, чтобы соединить size-1 вершину с исходной 0 (n%n==0) + val edge = graph.getEdge(currentVertex, nextVertex) + if (edge != null) { + edgesMap[edge]?.color = ColorTheme.EdgePickedColor + } + } + } + + + fun resetColors() { + verticesMap.values.forEach { it.color = ColorTheme.VertexDefaultColor } + edgesMap.values.forEach { it.color = ColorTheme.EdgeDefaultColor } + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/integration/FordBellmanAndBridgeFindIntedrationTest.kt b/app/src/test/kotlin/integration/FordBellmanAndBridgeFindIntedrationTest.kt index fec800c..5338cd7 100644 --- a/app/src/test/kotlin/integration/FordBellmanAndBridgeFindIntedrationTest.kt +++ b/app/src/test/kotlin/integration/FordBellmanAndBridgeFindIntedrationTest.kt @@ -50,25 +50,28 @@ class FordBellmanAndBridgeFindIntedrationTest { graphViewModel.findBridges() - graphViewModel._edges.keys.forEach { edge -> - if (bridges.contains(edge)) { - assertEquals(graphViewModel._edges[edge]?.color, ColorTheme.EdgePickedColor) + + graphViewModel.edges.forEach { edge -> + if (bridges.contains(edge.origin)) { + assertEquals(edge.color, ColorTheme.EdgePickedColor) } else { - assertEquals(graphViewModel._edges[edge]?.color, ColorTheme.EdgeDefaultColor) + assertEquals(edge.color, ColorTheme.EdgeDefaultColor) } } graphViewModel.fordBellman(firstId, secondId) - graphViewModel._vertices.keys.forEach { vertex -> - if (vertex.id == 0 || vertex.id == 1 || vertex.id == 3 || vertex.id == 7) { - assertEquals(graphViewModel._vertices[vertex]?.color, ColorTheme.VertexPickedColor) + + graphViewModel.vertices.forEach { vertex -> + + if (vertex.origin.id == 0 || vertex.origin.id == 1 || vertex.origin.id == 3 || vertex.origin.id == 7) { + assertEquals(vertex.color, ColorTheme.VertexPickedColor) } else { - assertEquals(graphViewModel._vertices[vertex]?.color, ColorTheme.VertexDefaultColor) + assertEquals(vertex.color, ColorTheme.VertexDefaultColor) } } val exception = assertThrows { graphViewModel.fordBellman(firstId, thirdId) } assertEquals(exception.message, "No path from vertex $firstId to vertex $thirdId") } -} +} \ No newline at end of file