2024-06-07 20:20:39 +03:00
|
|
|
@file:OptIn(ExperimentalFoundationApi::class, ExperimentalSplitPaneApi::class)
|
|
|
|
|
|
|
|
package space.kscience.controls.demo.collective
|
|
|
|
|
|
|
|
import androidx.compose.foundation.*
|
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
|
import androidx.compose.foundation.layout.Row
|
|
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
|
import androidx.compose.foundation.layout.padding
|
2024-06-12 11:56:27 +03:00
|
|
|
import androidx.compose.material.Button
|
2024-06-07 20:20:39 +03:00
|
|
|
import androidx.compose.material.Card
|
|
|
|
import androidx.compose.material.Checkbox
|
|
|
|
import androidx.compose.material.Text
|
2024-06-09 20:51:12 +03:00
|
|
|
import androidx.compose.material3.CircularProgressIndicator
|
2024-06-06 16:54:17 +03:00
|
|
|
import androidx.compose.material3.MaterialTheme
|
2024-06-07 20:20:39 +03:00
|
|
|
import androidx.compose.runtime.*
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
import androidx.compose.ui.Modifier
|
2024-06-06 16:54:17 +03:00
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
import androidx.compose.ui.res.painterResource
|
2024-06-07 20:20:39 +03:00
|
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
import androidx.compose.ui.unit.sp
|
2024-06-06 16:54:17 +03:00
|
|
|
import androidx.compose.ui.window.Window
|
|
|
|
import androidx.compose.ui.window.application
|
|
|
|
import io.ktor.client.HttpClient
|
|
|
|
import io.ktor.client.engine.cio.CIO
|
2024-06-12 11:56:27 +03:00
|
|
|
import kotlinx.coroutines.*
|
2024-06-06 16:54:17 +03:00
|
|
|
import kotlinx.coroutines.flow.launchIn
|
|
|
|
import kotlinx.coroutines.flow.onEach
|
2024-06-12 11:56:27 +03:00
|
|
|
import kotlinx.coroutines.flow.sample
|
2024-06-07 20:20:39 +03:00
|
|
|
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
|
|
|
|
import org.jetbrains.compose.splitpane.HorizontalSplitPane
|
|
|
|
import org.jetbrains.compose.splitpane.rememberSplitPaneState
|
2024-06-09 15:09:43 +03:00
|
|
|
import space.kscience.controls.api.PropertyChangedMessage
|
|
|
|
import space.kscience.controls.client.*
|
2024-06-07 20:20:39 +03:00
|
|
|
import space.kscience.controls.compose.conditional
|
2024-06-06 16:54:17 +03:00
|
|
|
import space.kscience.controls.manager.DeviceManager
|
|
|
|
import space.kscience.dataforge.context.Context
|
2024-06-09 15:09:43 +03:00
|
|
|
import space.kscience.dataforge.context.ContextBuilder
|
|
|
|
import space.kscience.dataforge.meta.MetaConverter
|
|
|
|
import space.kscience.dataforge.names.parseAsName
|
|
|
|
import space.kscience.magix.api.MagixEndpoint
|
|
|
|
import space.kscience.magix.api.subscribe
|
|
|
|
import space.kscience.magix.rsocket.rSocketWithWebSockets
|
2024-06-06 16:54:17 +03:00
|
|
|
import space.kscience.maps.compose.MapView
|
|
|
|
import space.kscience.maps.compose.OpenStreetMapTileProvider
|
2024-06-09 15:09:43 +03:00
|
|
|
import space.kscience.maps.coordinates.Gmc
|
|
|
|
import space.kscience.maps.coordinates.meters
|
2024-06-07 20:20:39 +03:00
|
|
|
import space.kscience.maps.features.*
|
2024-06-06 16:54:17 +03:00
|
|
|
import java.nio.file.Path
|
2024-06-12 11:56:27 +03:00
|
|
|
import kotlin.time.Duration.Companion.milliseconds
|
2024-06-09 21:12:18 +03:00
|
|
|
import kotlin.time.Duration.Companion.seconds
|
2024-06-06 16:54:17 +03:00
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
2024-06-09 15:09:43 +03:00
|
|
|
fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}): Context = remember {
|
|
|
|
Context(name, contextBuilder)
|
2024-06-06 16:54:17 +03:00
|
|
|
}
|
|
|
|
|
2024-06-09 20:51:12 +03:00
|
|
|
private val gmcMetaConverter = MetaConverter.serializable<Gmc>()
|
|
|
|
|
2024-06-06 16:54:17 +03:00
|
|
|
@Composable
|
|
|
|
fun App() {
|
|
|
|
val scope = rememberCoroutineScope()
|
|
|
|
|
2024-06-09 15:09:43 +03:00
|
|
|
val parentContext = rememberContext("Parent") {
|
|
|
|
plugin(DeviceManager)
|
|
|
|
}
|
2024-06-06 16:54:17 +03:00
|
|
|
|
|
|
|
val collectiveModel = remember {
|
2024-06-09 21:12:18 +03:00
|
|
|
generateModel(parentContext, 100, reportInterval = 1.seconds)
|
2024-06-06 16:54:17 +03:00
|
|
|
}
|
|
|
|
|
2024-06-09 15:09:43 +03:00
|
|
|
val roster = remember {
|
|
|
|
collectiveModel.roster
|
|
|
|
}
|
|
|
|
|
|
|
|
val client = remember { CompletableDeferred<MagixEndpoint>() }
|
|
|
|
|
|
|
|
val devices = remember { mutableStateMapOf<CollectiveDeviceId, DeviceClient>() }
|
|
|
|
|
|
|
|
|
|
|
|
LaunchedEffect(collectiveModel) {
|
|
|
|
launchCollectiveMagixServer(collectiveModel)
|
2024-06-09 21:12:18 +03:00
|
|
|
|
2024-06-09 15:09:43 +03:00
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost")
|
|
|
|
|
|
|
|
client.complete(magixClient)
|
2024-06-09 21:12:18 +03:00
|
|
|
|
|
|
|
collectiveModel.roster.forEach { (id, config) ->
|
|
|
|
scope.launch {
|
2024-06-12 11:56:27 +03:00
|
|
|
val deviceClient = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName())
|
|
|
|
devices[id] = deviceClient
|
2024-06-09 21:12:18 +03:00
|
|
|
}
|
2024-06-07 20:20:39 +03:00
|
|
|
}
|
2024-06-06 16:54:17 +03:00
|
|
|
}
|
2024-06-09 15:09:43 +03:00
|
|
|
|
2024-06-06 16:54:17 +03:00
|
|
|
}
|
|
|
|
|
2024-06-09 15:09:43 +03:00
|
|
|
var selectedDeviceId by remember { mutableStateOf<CollectiveDeviceId?>(null) }
|
2024-06-07 20:20:39 +03:00
|
|
|
|
2024-06-09 15:09:43 +03:00
|
|
|
var currentPosition by remember { mutableStateOf<Gmc?>(null) }
|
|
|
|
|
|
|
|
LaunchedEffect(selectedDeviceId, devices) {
|
|
|
|
selectedDeviceId?.let { devices[it] }?.propertyFlow(CollectiveDevice.position)?.collect {
|
|
|
|
currentPosition = it
|
|
|
|
}
|
2024-06-06 16:54:17 +03:00
|
|
|
}
|
|
|
|
|
2024-06-09 15:09:43 +03:00
|
|
|
var showOnlyVisible by remember { mutableStateOf(false) }
|
|
|
|
|
2024-06-12 11:56:27 +03:00
|
|
|
var movementProgram: Job? by remember { mutableStateOf(null) }
|
|
|
|
|
2024-06-07 20:20:39 +03:00
|
|
|
HorizontalSplitPane(
|
|
|
|
splitPaneState = rememberSplitPaneState(0.9f)
|
2024-06-06 16:54:17 +03:00
|
|
|
) {
|
2024-06-07 20:20:39 +03:00
|
|
|
first(400.dp) {
|
|
|
|
MapView(
|
2024-06-09 15:09:43 +03:00
|
|
|
mapTileProvider = remember {
|
|
|
|
OpenStreetMapTileProvider(
|
|
|
|
client = HttpClient(CIO),
|
|
|
|
cacheDirectory = Path.of("mapCache")
|
|
|
|
)
|
|
|
|
},
|
2024-06-07 20:20:39 +03:00
|
|
|
config = ViewConfig()
|
|
|
|
) {
|
|
|
|
collectiveModel.deviceStates.forEach { device ->
|
|
|
|
circle(device.position.value, id = device.id + ".position").color(Color.Red)
|
2024-06-12 11:56:27 +03:00
|
|
|
device.position.valueFlow.sample(50.milliseconds).onEach {
|
|
|
|
val activeDevice = selectedDeviceId?.let { devices[it] }
|
|
|
|
val color = if (selectedDeviceId == device.id) {
|
|
|
|
Color.Magenta
|
|
|
|
} else if (
|
|
|
|
showOnlyVisible &&
|
|
|
|
activeDevice != null &&
|
|
|
|
device.id in activeDevice.request(CollectiveDevice.visibleNeighbors)
|
|
|
|
) {
|
|
|
|
Color.Cyan
|
|
|
|
} else {
|
|
|
|
Color.Red
|
|
|
|
}
|
|
|
|
|
|
|
|
circle(
|
|
|
|
device.position.value,
|
|
|
|
id = device.id + ".position",
|
|
|
|
size = if (selectedDeviceId == device.id) 6.dp else 3.dp
|
|
|
|
)
|
|
|
|
.color(color)
|
|
|
|
.modifyAttribute(ZAttribute, 10f)
|
|
|
|
.modifyAttribute(AlphaAttribute, if (selectedDeviceId == device.id) 1f else 0.5f)
|
2024-06-07 20:20:39 +03:00
|
|
|
.modifyAttribute(AlphaAttribute, 0.5f) // does not work right now
|
|
|
|
}.launchIn(scope)
|
|
|
|
}
|
|
|
|
|
2024-06-09 15:09:43 +03:00
|
|
|
scope.launch {
|
|
|
|
|
|
|
|
client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) ->
|
|
|
|
if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") {
|
|
|
|
val id = magixMessage.sourceEndpoint
|
2024-06-09 20:51:12 +03:00
|
|
|
val position = gmcMetaConverter.read(deviceMessage.value)
|
2024-06-12 11:56:27 +03:00
|
|
|
|
|
|
|
rectangle(
|
|
|
|
position,
|
|
|
|
id = id,
|
|
|
|
).color(Color.Blue).onClick { selectedDeviceId = id }
|
2024-06-07 20:20:39 +03:00
|
|
|
}
|
2024-06-09 15:09:43 +03:00
|
|
|
}.launchIn(scope)
|
2024-06-07 20:20:39 +03:00
|
|
|
|
|
|
|
}
|
|
|
|
}
|
2024-06-06 16:54:17 +03:00
|
|
|
}
|
2024-06-07 20:20:39 +03:00
|
|
|
second(200.dp) {
|
2024-06-09 15:09:43 +03:00
|
|
|
|
|
|
|
Column(
|
|
|
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
|
|
|
) {
|
2024-06-12 11:56:27 +03:00
|
|
|
Button(
|
|
|
|
onClick = {
|
|
|
|
if (movementProgram == null) {
|
|
|
|
//start movement program
|
|
|
|
movementProgram = parentContext.launch {
|
|
|
|
devices.values.forEach { device ->
|
|
|
|
device.moveInCircles(this)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
movementProgram?.cancel()
|
|
|
|
parentContext.launch {
|
|
|
|
devices.values.forEach { device ->
|
|
|
|
device.write(CollectiveDevice.velocity, GmcVelocity.zero)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
movementProgram = null
|
|
|
|
}
|
|
|
|
},
|
|
|
|
modifier = Modifier.fillMaxWidth()
|
|
|
|
) {
|
|
|
|
if (movementProgram == null) {
|
|
|
|
Text("Move")
|
|
|
|
} else {
|
|
|
|
Text("Stop")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-09 20:51:12 +03:00
|
|
|
collectiveModel.roster.forEach { (id, _) ->
|
2024-06-09 15:09:43 +03:00
|
|
|
Card(
|
|
|
|
elevation = 16.dp,
|
|
|
|
modifier = Modifier.padding(8.dp).onClick {
|
|
|
|
selectedDeviceId = id
|
|
|
|
}.conditional(id == selectedDeviceId) {
|
|
|
|
border(2.dp, Color.Blue)
|
2024-06-09 20:51:12 +03:00
|
|
|
},
|
2024-06-09 15:09:43 +03:00
|
|
|
) {
|
|
|
|
Column(
|
|
|
|
modifier = Modifier.padding(8.dp)
|
2024-06-07 20:20:39 +03:00
|
|
|
) {
|
2024-06-09 20:51:12 +03:00
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
if (devices[id] == null) {
|
|
|
|
CircularProgressIndicator()
|
|
|
|
}
|
|
|
|
Text(
|
|
|
|
text = id,
|
|
|
|
fontSize = 16.sp,
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
modifier = Modifier.padding(10.dp).fillMaxWidth(),
|
|
|
|
)
|
|
|
|
}
|
2024-06-09 15:09:43 +03:00
|
|
|
if (id == selectedDeviceId) {
|
|
|
|
roster[id]?.let {
|
|
|
|
Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold)
|
|
|
|
Card(elevation = 16.dp, modifier = Modifier.fillMaxWidth().padding(8.dp)) {
|
|
|
|
Text(it.toString())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
currentPosition?.let { currentPosition ->
|
|
|
|
Text(
|
2024-06-09 20:51:12 +03:00
|
|
|
"Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}"
|
|
|
|
)
|
|
|
|
Text(
|
|
|
|
"Долгота: ${String.format("%.3f", currentPosition.longitude.toDegrees().value)}"
|
2024-06-09 15:09:43 +03:00
|
|
|
)
|
|
|
|
currentPosition.elevation?.let {
|
|
|
|
Text("Высота: ${String.format("%.1f", it.meters)} м")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Row(
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
|
modifier = Modifier.fillMaxWidth()
|
|
|
|
) {
|
|
|
|
Text("Показать только видимые")
|
|
|
|
Checkbox(showOnlyVisible, { showOnlyVisible = it })
|
|
|
|
}
|
|
|
|
}
|
2024-06-07 20:20:39 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-06-09 15:09:43 +03:00
|
|
|
|
2024-06-06 16:54:17 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun main() = application {
|
2024-06-09 15:09:43 +03:00
|
|
|
// System.setProperty(IO_PARALLELISM_PROPERTY_NAME, 300.toString())
|
2024-06-06 16:54:17 +03:00
|
|
|
Window(onCloseRequest = ::exitApplication, title = "Maps-kt demo", icon = painterResource("SPC-logo.png")) {
|
|
|
|
MaterialTheme {
|
|
|
|
App()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|