@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 import androidx.compose.material.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.isSecondaryPressed 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 import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import kotlinx.coroutines.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach 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 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 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.* import java.nio.file.Path import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @Composable fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}): Context = remember { Context(name, contextBuilder) } internal val gmcMetaConverter = MetaConverter.serializable<Gmc>() internal val gmcVelocityMetaConverter = MetaConverter.serializable<GmcVelocity>() @Composable fun App() { val scope = rememberCoroutineScope() val parentContext = rememberContext("Parent") { plugin(DeviceManager) } val collectiveModel = remember { generateModel(parentContext, 100, reportInterval = 1.seconds) } val roster = remember { collectiveModel.roster } val client = remember { CompletableDeferred<MagixEndpoint>() } val devices = remember { mutableStateMapOf<CollectiveDeviceId, DeviceClient>() } LaunchedEffect(collectiveModel) { launchCollectiveMagixServer(collectiveModel) withContext(Dispatchers.IO) { val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") client.complete(magixClient) collectiveModel.roster.forEach { (id, config) -> scope.launch { val deviceClient = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) devices[id] = deviceClient } } } } 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 } } var showOnlyVisible by remember { mutableStateOf(false) } var movementProgram: Job? by remember { mutableStateOf(null) } val trawler: CollectiveDeviceConstructor = remember { collectiveModel.createTrawler(Gmc.ofDegrees(55.925, 37.50)) } HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { first(400.dp) { 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") ) }, config = ViewConfig( onClick = { event, point -> if (event.buttons.isSecondaryPressed) { clickPoint = point.focus } } ) ) { //draw real positions collectiveModel.deviceStates.forEach { device -> circle(device.position.value, id = device.id + ".position").color(Color.Red) 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) } //draw received data scope.launch { client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint val position = gmcMetaConverter.read(deviceMessage.value) rectangle( position, id = id, ).color(Color.Blue).onClick { selectedDeviceId = id } } }.launchIn(scope) } // draw trawler trawler.position.valueFlow.onEach { circle(it, id = "trawler").color(Color.Black) }.launchIn(scope) } } second(200.dp) { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { 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") } } collectiveModel.roster.forEach { (id, _) -> Card( elevation = 16.dp, modifier = Modifier.padding(8.dp).onClick { selectedDeviceId = id }.conditional(id == selectedDeviceId) { border(2.dp, Color.Blue) }, ) { Column( modifier = Modifier.padding(8.dp) ) { 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( "Широта: ${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 }) } } } } } } } } } fun main() = application { // System.setProperty(IO_PARALLELISM_PROPERTY_NAME, 300.toString()) Window(onCloseRequest = ::exitApplication, title = "Maps-kt demo", icon = painterResource("SPC-logo.png")) { MaterialTheme { App() } } }