303 lines
12 KiB
Kotlin
Raw Normal View History

@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 16:31:14 +03:00
import androidx.compose.material.*
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
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
2024-06-12 16:31:14 +03:00
import androidx.compose.ui.input.pointer.isSecondaryPressed
2024-06-06 16:54:17 +03:00
import androidx.compose.ui.res.painterResource
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
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.rememberSplitPaneState
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.client.*
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
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
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.coordinates.meters
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
fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}): Context = remember {
Context(name, contextBuilder)
2024-06-06 16:54:17 +03:00
}
2024-06-12 16:31:14 +03:00
internal val gmcMetaConverter = MetaConverter.serializable<Gmc>()
internal val gmcVelocityMetaConverter = MetaConverter.serializable<GmcVelocity>()
2024-06-09 20:51:12 +03:00
2024-06-06 16:54:17 +03:00
@Composable
fun App() {
val scope = rememberCoroutineScope()
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
}
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
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-06 16:54:17 +03:00
}
2024-06-06 16:54:17 +03:00
}
var selectedDeviceId by remember { mutableStateOf<CollectiveDeviceId?>(null) }
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
}
var showOnlyVisible by remember { mutableStateOf(false) }
2024-06-12 11:56:27 +03:00
var movementProgram: Job? by remember { mutableStateOf(null) }
2024-06-12 16:31:14 +03:00
val trawler: CollectiveDeviceConstructor = remember {
collectiveModel.createTrawler(Gmc.ofDegrees(55.925, 37.50))
}
HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(0.9f)
2024-06-06 16:54:17 +03:00
) {
first(400.dp) {
2024-06-12 16:31:14 +03:00
var clickPoint by remember { mutableStateOf<Gmc?>(null) }
CursorDropdownMenu(clickPoint != null, { clickPoint = null }) {
clickPoint?.let { point ->
TextButton({
trawler.moveTo(point)
clickPoint = null
}) {
Text("Move trawler here")
}
}
}
MapView(
mapTileProvider = remember {
OpenStreetMapTileProvider(
client = HttpClient(CIO),
cacheDirectory = Path.of("mapCache")
)
},
2024-06-12 16:31:14 +03:00
config = ViewConfig(
onClick = { event, point ->
if (event.buttons.isSecondaryPressed) {
clickPoint = point.focus
}
}
)
) {
2024-06-12 16:31:14 +03:00
//draw real positions
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)
.modifyAttribute(AlphaAttribute, 0.5f) // does not work right now
}.launchIn(scope)
}
2024-06-12 16:31:14 +03:00
//draw received data
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 }
}
}.launchIn(scope)
}
2024-06-12 16:31:14 +03:00
// draw trawler
trawler.position.valueFlow.onEach {
circle(it, id = "trawler").color(Color.Black)
}.launchIn(scope)
}
2024-06-06 16:54:17 +03:00
}
second(200.dp) {
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, _) ->
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
},
) {
Column(
modifier = Modifier.padding(8.dp)
) {
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(),
)
}
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)}"
)
currentPosition.elevation?.let {
Text("Высота: ${String.format("%.1f", it.meters)} м")
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text("Показать только видимые")
Checkbox(showOnlyVisible, { showOnlyVisible = it })
}
}
}
}
}
}
2024-06-06 16:54:17 +03:00
}
}
}
fun main() = application {
// 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()
}
}
}