Skip to content

Commit 91244c5

Browse files
committed
feat: added simulation and node subscription
1 parent f3b440b commit 91244c5

File tree

7 files changed

+170
-72
lines changed

7 files changed

+170
-72
lines changed

alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,24 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
3333
import androidx.lifecycle.viewmodel.compose.viewModel
3434
import com.apollographql.apollo3.api.Error
3535
import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatus
36-
import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatusViewModel
36+
import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationViewModel
3737

3838
/**
3939
* Application entry point, this will be rendered the same in all the platforms.
4040
*/
4141
@Composable
42-
fun app(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewModel() }) {
43-
val simulationStatus by viewModel.simulationStatus.collectAsStateWithLifecycle()
44-
val time by viewModel.time.collectAsStateWithLifecycle()
42+
fun app(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) {
43+
val status by viewModel.status.collectAsStateWithLifecycle()
4544
val errors by viewModel.errors.collectAsStateWithLifecycle()
45+
val nodes by viewModel.nodes.collectAsStateWithLifecycle()
4646
Scaffold(
47-
topBar = { topBar(simulationStatus) },
47+
topBar = { topBar(status) },
4848
) { innerPadding ->
4949
Column(
5050
modifier = Modifier.padding(innerPadding).padding(horizontal = 8.dp, vertical = 16.dp),
5151
verticalArrangement = Arrangement.spacedBy(16.dp),
5252
) {
53-
controlButton(simulationStatus, viewModel::play, viewModel::pause)
53+
controlButton(status, viewModel::play, viewModel::pause)
5454
OutlinedCard(
5555
modifier = Modifier.fillMaxSize(),
5656
colors = CardDefaults.cardColors(
@@ -59,8 +59,10 @@ fun app(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewM
5959
border = BorderStroke(1.dp, Color.Black),
6060
) {
6161
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
62-
Text("Time: $time")
63-
errorDialog(viewModel::monitor, errors)
62+
errorDialog(viewModel::fetch, errors)
63+
for (node in nodes) {
64+
nodeDrawer(node.id)
65+
}
6466
}
6567
}
6668
}
@@ -102,6 +104,9 @@ fun controlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Uni
102104
}
103105
}
104106

107+
/**
108+
* Display the error dialog, currently used to circumvent the null issue we're facing when subscribing to simulation.
109+
*/
105110
@Composable
106111
fun errorDialog(dismiss: () -> Unit, errors: List<Error>?) {
107112
if (!errors.isNullOrEmpty()) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (C) 2010-2025, Danilo Pianini and contributors
3+
* listed, for each module, in the respective subproject's build.gradle.kts file.
4+
*
5+
* This file is part of Alchemist, and is distributed under the terms of the
6+
* GNU General Public License, with a linking exception,
7+
* as described in the file LICENSE in the Alchemist distribution's top directory.
8+
*/
9+
10+
package it.unibo.alchemist.boundary.composeui
11+
12+
import androidx.compose.material3.Text
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.runtime.getValue
15+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
16+
import androidx.lifecycle.viewmodel.compose.viewModel
17+
import it.unibo.alchemist.boundary.composeui.viewmodels.NodeViewModel
18+
19+
/**
20+
* Display the information of a node, subscribing to its own channel for data.
21+
*/
22+
@Composable
23+
fun nodeDrawer(nodeId: Int) {
24+
val nodeModel: NodeViewModel = viewModel(key = "node-$nodeId") { NodeViewModel(nodeId) }
25+
val nodeInfo by nodeModel.nodeInfo.collectAsStateWithLifecycle()
26+
Text("Node $nodeId: $nodeInfo")
27+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (C) 2010-2025, Danilo Pianini and contributors
3+
* listed, for each module, in the respective subproject's build.gradle.kts file.
4+
*
5+
* This file is part of Alchemist, and is distributed under the terms of the
6+
* GNU General Public License, with a linking exception,
7+
* as described in the file LICENSE in the Alchemist distribution's top directory.
8+
*/
9+
10+
package it.unibo.alchemist.boundary.composeui.viewmodels
11+
12+
import androidx.lifecycle.ViewModel
13+
import androidx.lifecycle.viewModelScope
14+
import com.apollographql.apollo3.api.Error
15+
import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory
16+
import it.unibo.alchemist.boundary.graphql.client.NodeInfoSubscription
17+
import kotlinx.coroutines.flow.MutableStateFlow
18+
import kotlinx.coroutines.flow.asStateFlow
19+
import kotlinx.coroutines.launch
20+
21+
data class Molecule(val name: String)
22+
23+
data class MoleculeConcentration(val concentration: String, val molecule: Molecule)
24+
25+
data class NodeInfo(
26+
val id: Int,
27+
val moleculeCount: Int,
28+
val properties: List<String>,
29+
val contents: List<MoleculeConcentration>,
30+
)
31+
32+
class NodeViewModel(private val nodeId: Int) : ViewModel() {
33+
private val _nodeInfo = MutableStateFlow<NodeInfo?>(null)
34+
val nodeInfo = _nodeInfo.asStateFlow()
35+
36+
private val _errors = MutableStateFlow<List<Error>>(emptyList())
37+
val errors = _errors.asStateFlow()
38+
39+
// TODO: parameterize the host and port and separate client in different file
40+
private val client = GraphQLClientFactory.subscriptionClient(
41+
"127.0.0.1",
42+
3000,
43+
)
44+
45+
private fun load() {
46+
_errors.value = emptyList()
47+
viewModelScope.launch {
48+
client.subscription(NodeInfoSubscription(nodeId))
49+
.toFlow()
50+
.collect { response ->
51+
response.data?.let { data ->
52+
_nodeInfo.value = NodeInfo(
53+
data.environment.nodeById.id,
54+
data.environment.nodeById.moleculeCount,
55+
data.environment.nodeById.properties,
56+
data.environment.nodeById.contents.entries.map {
57+
MoleculeConcentration(
58+
it.concentration,
59+
Molecule(it.molecule.name),
60+
)
61+
},
62+
)
63+
}
64+
}
65+
}
66+
}
67+
68+
init {
69+
load()
70+
}
71+
}

alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt renamed to alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationViewModel.kt

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ import androidx.lifecycle.ViewModel
1313
import androidx.lifecycle.viewModelScope
1414
import com.apollographql.apollo3.api.Error
1515
import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory
16-
import it.unibo.alchemist.boundary.graphql.client.NodesSubscription
1716
import it.unibo.alchemist.boundary.graphql.client.PauseSimulationMutation
1817
import it.unibo.alchemist.boundary.graphql.client.PlaySimulationMutation
19-
import it.unibo.alchemist.boundary.graphql.client.SimulationStatusQuery
20-
import kotlinx.coroutines.delay
18+
import it.unibo.alchemist.boundary.graphql.client.SimulationSubscription
2119
import kotlinx.coroutines.flow.MutableStateFlow
2220
import kotlinx.coroutines.flow.asStateFlow
2321
import kotlinx.coroutines.flow.update
@@ -31,18 +29,15 @@ enum class SimulationStatus {
3129
Terminated,
3230
}
3331

34-
var i = 0
35-
var j = 0
36-
37-
class SimulationStatusViewModel : ViewModel() {
38-
private val _simulationStatus = MutableStateFlow(SimulationStatus.Init)
39-
val simulationStatus = _simulationStatus.asStateFlow()
40-
41-
private val _time = MutableStateFlow(0.0)
42-
val time = _time.asStateFlow()
43-
private var tempTime = 0.0
32+
data class Node(val id: Int, val coordinates: List<Double>)
4433

34+
class SimulationViewModel : ViewModel() {
35+
private val _nodes = MutableStateFlow<List<Node>>(emptyList())
36+
private val _status = MutableStateFlow(SimulationStatus.Init)
4537
private val _errors = MutableStateFlow<List<Error>>(emptyList())
38+
39+
val nodes = _nodes.asStateFlow()
40+
val status = _status.asStateFlow()
4641
val errors = _errors.asStateFlow()
4742

4843
// TODO: parameterize the host and port and separate client in different file
@@ -63,54 +58,39 @@ class SimulationStatusViewModel : ViewModel() {
6358
}
6459
}
6560

66-
fun monitor() {
61+
fun fetch() {
6762
_errors.value = emptyList()
6863
viewModelScope.launch {
69-
client.subscription(NodesSubscription())
64+
client.subscription(SimulationSubscription())
7065
.toFlow()
7166
.collect { response ->
7267
if (response.hasErrors()) {
7368
response.errors?.let { errors ->
7469
_errors.update { errors }
7570
}
7671
}
77-
i++
78-
println("data received $i")
79-
if (i > 99) {
80-
i = 0
81-
j++
82-
println("fetching data $j")
83-
response.data?.let { data ->
84-
_time.value = data.simulation.time
72+
response.data?.let { data ->
73+
_status.update {
74+
when (data.simulation.status) {
75+
"READY" -> SimulationStatus.Ready
76+
"PAUSED" -> SimulationStatus.Paused
77+
"RUNNING" -> SimulationStatus.Running
78+
"TERMINATED" -> SimulationStatus.Terminated
79+
else -> SimulationStatus.Init
80+
}
81+
}
82+
_nodes.value = data.simulation.environment.nodeToPos.entries.map {
83+
Node(
84+
id = it.id,
85+
coordinates = it.position.coordinates,
86+
)
8587
}
8688
}
8789
}
8890
}
8991
}
9092

9193
init {
92-
monitor()
93-
viewModelScope.launch {
94-
while (true) {
95-
client.query(SimulationStatusQuery())
96-
.toFlow()
97-
.collect { response ->
98-
response.data?.let { data ->
99-
_simulationStatus.update {
100-
// True correlation can be achieved only moving
101-
// alchemist-api Status enum class to commonMain
102-
when (data.simulation.status) {
103-
"READY" -> SimulationStatus.Ready
104-
"PAUSED" -> SimulationStatus.Paused
105-
"RUNNING" -> SimulationStatus.Running
106-
"TERMINATED" -> SimulationStatus.Terminated
107-
else -> SimulationStatus.Init
108-
}
109-
}
110-
}
111-
}
112-
delay(50)
113-
}
114-
}
94+
fetch()
11595
}
11696
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
subscription NodeInfo($id: Int!) {
2+
environment {
3+
nodeById(id: $id) {
4+
id
5+
moleculeCount
6+
properties
7+
contents {
8+
entries {
9+
concentration
10+
molecule {
11+
name
12+
}
13+
}
14+
}
15+
}
16+
}
17+
}

alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
subscription SimulationSubscription {
2+
simulation {
3+
status
4+
environment {
5+
nodeToPos {
6+
entries {
7+
id
8+
position {
9+
coordinates
10+
dimensions
11+
}
12+
}
13+
}
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)