@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.Card import androidx.compose.material.Checkbox import androidx.compose.material.Text 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.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize 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.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext 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.seconds @Composable fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}): Context = remember { Context(name, contextBuilder) } private val gmcMetaConverter = MetaConverter.serializable<Gmc>() @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 { devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) } } } } 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) } HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { first(400.dp) { MapView( mapTileProvider = remember { OpenStreetMapTileProvider( client = HttpClient(CIO), cacheDirectory = Path.of("mapCache") ) }, config = ViewConfig() ) { collectiveModel.deviceStates.forEach { device -> circle(device.position.value, id = device.id + ".position").color(Color.Red) device.position.valueFlow.onEach { circle(device.position.value, id = device.id + ".position", size = 3.dp) .color(Color.Red) .modifyAttribute(AlphaAttribute, 0.5f) // does not work right now }.launchIn(scope) } 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) val activeDevice = selectedDeviceId?.let { devices[it] } if ( activeDevice == null || id == selectedDeviceId || !showOnlyVisible || id in activeDevice.request(CollectiveDevice.visibleNeighbors) ) { rectangle( position, id = id, size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) .onClick { selectedDeviceId = id } } else { removeFeature(id) } } }.launchIn(scope) } } } second(200.dp) { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { 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() } } }