Fix visibility range for collective

This commit is contained in:
Alexander Nozik 2024-06-09 20:51:12 +03:00
parent e9bde68674
commit c55ce2cf9a
6 changed files with 89 additions and 56 deletions

View File

@ -74,15 +74,16 @@ public fun <T, R> DeviceState.Companion.map(
public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper) public fun <T, R> DeviceState<T>.map(mapper: (T) -> R): DeviceStateWithDependencies<R> = DeviceState.map(this, mapper)
public fun DeviceState<NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> = object : DeviceState<Double> { public fun DeviceState<NumericalValue<out UnitsOfMeasurement>>.values(): DeviceState<Double> =
override val value: Double object : DeviceState<Double> {
get() = this@values.value.value override val value: Double
get() = this@values.value.value
override val valueFlow: Flow<Double> override val valueFlow: Flow<Double>
get() = this@values.valueFlow.map { it.value } get() = this@values.valueFlow.map { it.value }
override fun toString(): String = this@values.toString() override fun toString(): String = this@values.toString()
} }
/** /**
* Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.

View File

@ -41,13 +41,22 @@ private object InstantConverter : MetaConverter<Instant> {
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter
private object DoubleRangeConverter : MetaConverter<ClosedFloatingPointRange<Double>> { private object DoubleRangeConverter : MetaConverter<ClosedFloatingPointRange<Double>> {
override fun readOrNull(source: Meta): ClosedFloatingPointRange<Double>? = source.value?.doubleArray?.let { (start, end)-> override fun readOrNull(source: Meta): ClosedFloatingPointRange<Double>? =
start..end source.value?.doubleArray?.let { (start, end) ->
} start..end
}
override fun convert( override fun convert(
obj: ClosedFloatingPointRange<Double>, obj: ClosedFloatingPointRange<Double>,
): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue()) ): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue())
} }
public val MetaConverter.Companion.doubleRange: MetaConverter<ClosedFloatingPointRange<Double>> get() = DoubleRangeConverter public val MetaConverter.Companion.doubleRange: MetaConverter<ClosedFloatingPointRange<Double>> get() = DoubleRangeConverter
private object StringListConverter : MetaConverter<List<String>> {
override fun convert(obj: List<String>): Meta = Meta(obj.map { it.asValue() }.asValue())
override fun readOrNull(source: Meta): List<String>? = source.stringList ?: source["@jsonArray"]?.stringList
}
public val MetaConverter.Companion.stringList: MetaConverter<List<String>> get() = StringListConverter

View File

@ -19,10 +19,13 @@ import space.kscience.dataforge.meta.Meta
public suspend fun <T> DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T = public suspend fun <T> DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T =
propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid") propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid")
public suspend fun <T> DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = public suspend fun <T> DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T =
propertySpec.converter.read(getOrReadProperty(propertySpec.name)) propertySpec.converter.read(getOrReadProperty(propertySpec.name))
public fun <T> DeviceClient.getCached(propertySpec: DevicePropertySpec<*, T>): T? =
getProperty(propertySpec.name)?.let { propertySpec.converter.read(it) }
public suspend fun <T> DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { public suspend fun <T> DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) {
writeProperty(propertySpec.name, propertySpec.converter.convert(value)) writeProperty(propertySpec.name, propertySpec.converter.convert(value))
} }

View File

@ -3,12 +3,10 @@
package space.kscience.controls.demo.collective package space.kscience.controls.demo.collective
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.constructor.DeviceConstructor import space.kscience.controls.constructor.*
import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.misc.stringList
import space.kscience.controls.constructor.registerAsProperty
import space.kscience.controls.peer.PeerConnection import space.kscience.controls.peer.PeerConnection
import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.unit
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.Scheme import space.kscience.dataforge.meta.Scheme
@ -56,9 +54,16 @@ interface CollectiveDevice : Device {
write = { _, value -> setVelocity(value) } write = { _, value -> setVelocity(value) }
) )
val listVisible by action(MetaConverter.unit, MetaConverter.valueList<String> { it.string }) { val visibleNeighbors by property(
listVisible().toList() MetaConverter.stringList,
} read = {
listVisible().toList()
}
)
// val listVisible by action(MetaConverter.unit, MetaConverter.valueList<String> { it.string }) {
// listVisible().toList()
// }
} }
} }
@ -74,8 +79,28 @@ class CollectiveDeviceConstructor(
override val id: CollectiveDeviceId get() = configuration.deviceId override val id: CollectiveDeviceId get() = configuration.deviceId
val position = registerAsProperty(CollectiveDevice.position, position.sample(configuration.reportInterval.milliseconds)) val position = registerAsProperty(
val velocity = registerAsProperty(CollectiveDevice.velocity, velocity.sample(configuration.reportInterval.milliseconds)) CollectiveDevice.position,
position.sample(configuration.reportInterval.milliseconds)
)
val velocity = registerAsProperty(
CollectiveDevice.velocity,
velocity.sample(configuration.reportInterval.milliseconds)
)
private val _visibleNeighbors: MutableDeviceState<Collection<CollectiveDeviceId>> = stateOf(emptyList())
val visibleNeighbors = registerAsProperty(
CollectiveDevice.visibleNeighbors,
_visibleNeighbors.map { it.toList() }
)
init {
position.onNext {
_visibleNeighbors.value = observation.invoke().keys
}
}
override suspend fun getPosition(): Gmc = position.value override suspend fun getPosition(): Gmc = position.value

View File

@ -20,8 +20,6 @@ import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.rsocket.rSocketWithWebSockets
import space.kscience.magix.server.startMagixServer import space.kscience.magix.server.startMagixServer
import space.kscience.maps.coordinates.* import space.kscience.maps.coordinates.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
internal data class CollectiveDeviceState( internal data class CollectiveDeviceState(
@ -46,9 +44,8 @@ internal fun VirtualDeviceState(
internal class DeviceCollectiveModel( internal class DeviceCollectiveModel(
context: Context, context: Context,
val deviceStates: Collection<CollectiveDeviceState>, val deviceStates: Collection<CollectiveDeviceState>,
val visibilityRange: Distance = 0.4.kilometers, val visibilityRange: Distance = 1.kilometers,
val radioRange: Distance = 5.kilometers, val radioRange: Distance = 5.kilometers,
val reportInterval: Duration = 1000.milliseconds
) : ModelConstructor(context), PeerConnection { ) : ModelConstructor(context), PeerConnection {
/** /**
@ -107,6 +104,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer(
val server = startMagixServer( val server = startMagixServer(
// RSocketMagixFlowPlugin() // RSocketMagixFlowPlugin()
) )
val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
collectiveModel.devices.forEach { (id, device) -> collectiveModel.devices.forEach { (id, device) ->
val deviceContext = collectiveModel.context.buildContext(id.parseAsName()) { val deviceContext = collectiveModel.context.buildContext(id.parseAsName()) {
@ -116,7 +114,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer(
deviceContext.install(id, device) deviceContext.install(id, device)
val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") // val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id)
} }

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Checkbox import androidx.compose.material.Checkbox
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -24,9 +25,12 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.* import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.rememberSplitPaneState import org.jetbrains.compose.splitpane.rememberSplitPaneState
@ -47,7 +51,6 @@ import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.coordinates.meters import space.kscience.maps.coordinates.meters
import space.kscience.maps.features.* import space.kscience.maps.features.*
import java.nio.file.Path import java.nio.file.Path
import kotlin.time.Duration.Companion.seconds
@Composable @Composable
@ -55,6 +58,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}
Context(name, contextBuilder) Context(name, contextBuilder)
} }
private val gmcMetaConverter = MetaConverter.serializable<Gmc>()
@Composable @Composable
fun App() { fun App() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -82,8 +87,7 @@ fun App() {
val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost")
client.complete(magixClient) client.complete(magixClient)
collectiveModel.roster.forEach { (id, config) ->
collectiveModel.roster.forEach { (id, config) ->
devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName())
} }
} }
@ -129,23 +133,14 @@ fun App() {
client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) ->
if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") {
val id = magixMessage.sourceEndpoint val id = magixMessage.sourceEndpoint
val position = MetaConverter.serializable<Gmc>().read(deviceMessage.value) val position = gmcMetaConverter.read(deviceMessage.value)
val activeDevice = selectedDeviceId?.let { devices[it] } val activeDevice = selectedDeviceId?.let { devices[it] }
suspend fun DeviceClient.idIsVisible() = try {
withTimeout(1.seconds) {
id in execute(CollectiveDevice.listVisible)
}
} catch (ex: Exception) {
ex.printStackTrace()
true
}
if ( if (
activeDevice == null || activeDevice == null ||
id == selectedDeviceId || id == selectedDeviceId ||
!showOnlyVisible || !showOnlyVisible ||
activeDevice.idIsVisible() id in activeDevice.request(CollectiveDevice.visibleNeighbors)
) { ) {
rectangle( rectangle(
position, position,
@ -168,24 +163,29 @@ fun App() {
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()) modifier = Modifier.verticalScroll(rememberScrollState())
) { ) {
devices.forEach { (id, _) -> collectiveModel.roster.forEach { (id, _) ->
Card( Card(
elevation = 16.dp, elevation = 16.dp,
modifier = Modifier.padding(8.dp).onClick { modifier = Modifier.padding(8.dp).onClick {
selectedDeviceId = id selectedDeviceId = id
}.conditional(id == selectedDeviceId) { }.conditional(id == selectedDeviceId) {
border(2.dp, Color.Blue) border(2.dp, Color.Blue)
} },
) { ) {
Column( Column(
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp)
) { ) {
Text( Row(verticalAlignment = Alignment.CenterVertically) {
text = id, if (devices[id] == null) {
fontSize = 16.sp, CircularProgressIndicator()
fontWeight = FontWeight.Bold, }
modifier = Modifier.padding(10.dp).fillMaxWidth() Text(
) text = id,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp).fillMaxWidth(),
)
}
if (id == selectedDeviceId) { if (id == selectedDeviceId) {
roster[id]?.let { roster[id]?.let {
Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold) Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold)
@ -195,14 +195,11 @@ fun App() {
} }
currentPosition?.let { currentPosition -> currentPosition?.let { currentPosition ->
Text("Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}")
Text( Text(
"Долгота: ${ "Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}"
String.format( )
"%.3f", Text(
currentPosition.longitude.toDegrees().value "Долгота: ${String.format("%.3f", currentPosition.longitude.toDegrees().value)}"
)
}"
) )
currentPosition.elevation?.let { currentPosition.elevation?.let {
Text("Высота: ${String.format("%.1f", it.meters)} м") Text("Высота: ${String.format("%.1f", it.meters)} м")