239 lines
9.6 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
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
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
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
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-09 20:51:12 +03:00
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
2024-06-06 16:54:17 +03:00
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
2024-06-09 20:51:12 +03:00
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
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-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-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()
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 {
devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName())
}
}
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) }
HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(0.9f)
2024-06-06 16:54:17 +03:00
) {
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
2024-06-09 20:51:12 +03:00
val position = gmcMetaConverter.read(deviceMessage.value)
val activeDevice = selectedDeviceId?.let { devices[it] }
if (
activeDevice == null ||
id == selectedDeviceId ||
!showOnlyVisible ||
2024-06-09 20:51:12 +03:00
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)
}
}
2024-06-06 16:54:17 +03:00
}
second(200.dp) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
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()
}
}
}