From 13b80be8841761568ed17db6b23a2e9116591411 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 7 Jun 2024 20:20:39 +0300 Subject: [PATCH] Implement visibility range for collective device --- .gitignore | 5 +- .../src/commonMain/kotlin/misc.kt | 12 ++ .../constructor/src/jvmMain/kotlin/Plotter.kt | 19 ++- demo/device-collective/build.gradle.kts | 2 +- .../{RemoteDevice.kt => CollectiveDevice.kt} | 32 ++-- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 42 +++-- .../src/jvmMain/kotlin/GmcVelocity.kt | 2 +- ...nceDeviceState.kt => SampleDeviceState.kt} | 6 +- .../src/jvmMain/kotlin/debugModel.kt | 31 ++-- .../src/jvmMain/kotlin/main.kt | 146 +++++++++++++++--- 10 files changed, 226 insertions(+), 71 deletions(-) create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/misc.kt rename demo/device-collective/src/jvmMain/kotlin/{RemoteDevice.kt => CollectiveDevice.kt} (60%) rename demo/device-collective/src/jvmMain/kotlin/{DebounceDeviceState.kt => SampleDeviceState.kt} (76%) diff --git a/.gitignore b/.gitignore index e688053..5fab474 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ out/ build/ -!gradle-wrapper.jar \ No newline at end of file + +!gradle-wrapper.jar + +/demo/device-collective/mapCache/ diff --git a/controls-visualisation-compose/src/commonMain/kotlin/misc.kt b/controls-visualisation-compose/src/commonMain/kotlin/misc.kt new file mode 100644 index 0000000..caf21e3 --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/misc.kt @@ -0,0 +1,12 @@ +package space.kscience.controls.compose + +import androidx.compose.ui.Modifier + +public inline fun Modifier.conditional( + condition: Boolean, + modifier: Modifier.() -> Modifier, +): Modifier = if (condition) { + then(modifier(Modifier)) +} else { + this +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index dfded59..40f81f9 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.angle import space.kscience.controls.constructor.models.Leadscrew @@ -31,10 +32,18 @@ private class Plotter( context: Context, xDrive: StepDrive, yDrive: StepDrive, + xStartLimit: LimitSwitch, + xEndLimit: LimitSwitch, + yStartLimit: LimitSwitch, + yEndLimit: LimitSwitch, val paint: suspend (Color) -> Unit, ) : DeviceConstructor(context) { val xDrive by device(xDrive) val yDrive by device(yDrive) + val xStartLimit by device(xStartLimit) + val xEndLimit by device(xEndLimit) + val yStartLimit by device(yStartLimit) + val yEndLimit by device(yEndLimit) public fun moveToXY(x: Number, y: Number) { xDrive.target.value = x.toLong() @@ -108,7 +117,15 @@ private class PlotterModel( val xy: DeviceState> = combineState(x, y) { x, y -> XY(x, y) } - val plotter = Plotter(context, xDrive, yDrive) { color -> + val plotter = Plotter( + context = context, + xDrive = xDrive, + yDrive = yDrive, + xStartLimit = LimitSwitch(context,x.atStart), + xEndLimit = LimitSwitch(context,x.atEnd), + yStartLimit = LimitSwitch(context,x.atStart), + yEndLimit = LimitSwitch(context,x.atEnd), + ) { color -> println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") callback(PlotterPoint(x.value, y.value, color)) } diff --git a/demo/device-collective/build.gradle.kts b/demo/device-collective/build.gradle.kts index 5520265..8bb0597 100644 --- a/demo/device-collective/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -37,6 +37,6 @@ kotlin.explicitApi = ExplicitApiMode.Disabled compose.desktop { application { - mainClass = "space.kscience.controls.demo.map.MainKt" + mainClass = "space.kscience.controls.demo.collective.MainKt" } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt similarity index 60% rename from demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt rename to demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 19a4e5b..050794c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -1,6 +1,6 @@ @file:OptIn(DFExperimental::class) -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import space.kscience.controls.api.Device import space.kscience.controls.constructor.DeviceConstructor @@ -10,17 +10,20 @@ import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.Scheme -import space.kscience.dataforge.meta.SchemeSpec +import space.kscience.dataforge.meta.string import space.kscience.dataforge.misc.DFExperimental import space.kscience.maps.coordinates.Gmc import kotlin.time.Duration.Companion.milliseconds -class RemoteDeviceConfiguration : Scheme() { - companion object : SchemeSpec(::RemoteDeviceConfiguration) +class CollectiveDeviceConfiguration(deviceId: DeviceId) : Scheme() { + var deviceId by string(deviceId) + var description by string() } -interface RemoteDevice : Device { +interface CollectiveDevice : Device { + + public val id: DeviceId suspend fun getPosition(): Gmc @@ -28,8 +31,10 @@ interface RemoteDevice : Device { suspend fun setVelocity(value: GmcVelocity) + suspend fun listVisible(): Collection - companion object : DeviceSpec() { + + companion object : DeviceSpec() { val position by property( converter = MetaConverter.serializable(), read = { getPosition() } @@ -44,15 +49,18 @@ interface RemoteDevice : Device { } -class RemoteDeviceConstructor( +class CollectiveDeviceConstructor( context: Context, - val configuration: RemoteDeviceConfiguration, + val configuration: CollectiveDeviceConfiguration, position: MutableDeviceState, velocity: MutableDeviceState, -) : DeviceConstructor(context, configuration.meta), RemoteDevice { + private val listVisible: suspend () -> Collection, +) : DeviceConstructor(context, configuration.meta), CollectiveDevice { - val position = registerAsProperty(RemoteDevice.position, position.debounce(500.milliseconds)) - val velocity = registerAsProperty(RemoteDevice.velocity, velocity) + override val id: DeviceId get() = configuration.deviceId + + val position = registerAsProperty(CollectiveDevice.position, position.sample(500.milliseconds)) + val velocity = registerAsProperty(CollectiveDevice.velocity, velocity) override suspend fun getPosition(): Gmc = position.value @@ -61,4 +69,6 @@ class RemoteDeviceConstructor( override suspend fun setVelocity(value: GmcVelocity) { velocity.value = value } + + override suspend fun listVisible(): Collection = listVisible.invoke() } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index fdf13a1..4abedea 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -1,43 +1,59 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer import space.kscience.dataforge.context.Context -import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.* -typealias RemoteDeviceId = String +typealias DeviceId = String -data class RemoteDeviceState( - val id: RemoteDeviceId, - val configuration: RemoteDeviceConfiguration, +internal data class VirtualDeviceState( + val id: DeviceId, + val configuration: CollectiveDeviceConfiguration, val position: MutableDeviceState, val velocity: MutableDeviceState, ) -public fun RemoteDeviceState( - id: RemoteDeviceId, +internal fun VirtualDeviceState( + id: DeviceId, position: Gmc, - configuration: RemoteDeviceConfiguration.() -> Unit = {}, -) = RemoteDeviceState( + configuration: CollectiveDeviceConfiguration.() -> Unit = {}, +) = VirtualDeviceState( id, - RemoteDeviceConfiguration(configuration), + CollectiveDeviceConfiguration(id).apply(configuration), MutableDeviceState(position), MutableDeviceState(GmcVelocity.zero) ) -class DeviceCollectiveModel( +internal class DeviceCollectiveModel( context: Context, - val deviceStates: Collection, + val deviceStates: Collection, + val visibilityRange: Distance, ) : ModelConstructor(context) { + /** + * Propagate movement + */ private val movement = onTimer { prev, next -> val delta = (next - prev) deviceStates.forEach { state -> state.position.value = state.position.value.moveWith(state.velocity.value, delta) } } + + suspend fun locateVisible(id: DeviceId): Map { + val coordinatesSnapshot = deviceStates.associate { it.id to it.position.value } + + val selected = coordinatesSnapshot[id] ?: error("Can't find device with id $id") + + val allCurves = coordinatesSnapshot + .filterKeys { it != id } + .mapValues { GeoEllipsoid.WGS84.curveBetween(selected, it.value) } + + return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } + } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt index bea840d..9d356c6 100644 --- a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt +++ b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.serialization.Serializable import space.kscience.kmath.geometry.Angle diff --git a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt similarity index 76% rename from demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt rename to demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt index b507cd6..a2ef06c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt +++ b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -7,7 +7,7 @@ import space.kscience.controls.constructor.DeviceState import kotlin.time.Duration @OptIn(FlowPreview::class) -class DebounceDeviceState( +class SampleDeviceState( val origin: DeviceState, val interval: Duration, ) : DeviceState { @@ -17,4 +17,4 @@ class DebounceDeviceState( override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun DeviceState.debounce(interval: Duration) = DebounceDeviceState(this, interval) \ No newline at end of file +fun DeviceState.sample(interval: Duration) = SampleDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index e352fcc..96c9ca8 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -20,31 +20,32 @@ private val radius = 0.01.degrees internal fun generateModel(context: Context): DeviceCollectiveModel { - val devices: List = buildList { - repeat(100) { - add( - RemoteDeviceState( - "device[$it]", - Gmc( - center.latitude + radius * Random.nextDouble(), - center.longitude + radius * Random.nextDouble() - ) - ) + val devices: List = List(100) { index -> + val id = "device[$index]" + + VirtualDeviceState( + id = id, + Gmc( + center.latitude + radius * Random.nextDouble(), + center.longitude + radius * Random.nextDouble() ) + ) { + deviceId = id + description = "Virtual remote device $id" } } - val model = DeviceCollectiveModel(context, devices) + val model = DeviceCollectiveModel(context, devices, 0.2.kilometers) return model } -fun RemoteDevice.moveInCircles(): Job = launch { +fun CollectiveDevice.moveInCircles(): Job = launch { var bearing = Random.nextDouble(-PI, PI).radians - write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) while (isActive) { delay(500) bearing += 5.degrees - write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 0c7f7ad..1db8621 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -1,27 +1,42 @@ -package space.kscience.controls.demo.map +@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.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +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.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi +import org.jetbrains.compose.splitpane.HorizontalSplitPane +import org.jetbrains.compose.splitpane.rememberSplitPaneState +import space.kscience.controls.compose.conditional import space.kscience.controls.manager.DeviceManager -import space.kscience.controls.spec.propertyFlow +import space.kscience.controls.spec.useProperty import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request import space.kscience.maps.compose.MapView import space.kscience.maps.compose.OpenStreetMapTileProvider -import space.kscience.maps.features.ViewConfig -import space.kscience.maps.features.circle -import space.kscience.maps.features.color -import space.kscience.maps.features.rectangle +import space.kscience.maps.features.* import java.nio.file.Path @@ -34,7 +49,6 @@ fun rememberDeviceManager(): DeviceManager = remember { context.request(DeviceManager) } - @Composable fun App() { val scope = rememberCoroutineScope() @@ -47,14 +61,24 @@ fun App() { generateModel(deviceManager.context) } - val devices: Map = remember { + val devices: Map = remember { collectiveModel.deviceStates.associate { - val device = RemoteDeviceConstructor(deviceManager.context, it.configuration, it.position, it.velocity) + val device = CollectiveDeviceConstructor( + context = deviceManager.context, + configuration = it.configuration, + position = it.position, + velocity = it.velocity + ) { + collectiveModel.locateVisible(it.id).keys + } device.moveInCircles() it.id to device } } + var selectedDeviceId by remember { mutableStateOf(null) } + var showOnlyVisible by remember { mutableStateOf(false) } + val mapTileProvider = remember { OpenStreetMapTileProvider( client = HttpClient(CIO), @@ -62,21 +86,93 @@ fun App() { ) } - MapView( - mapTileProvider = mapTileProvider, - config = ViewConfig() + HorizontalSplitPane( + splitPaneState = rememberSplitPaneState(0.9f) ) { - 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").color(Color.Red) - }.launchIn(scope) - } + first(400.dp) { + MapView( + mapTileProvider = mapTileProvider, + 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) + } - devices.forEach { (id, device) -> - device.propertyFlow(RemoteDevice.position).onEach { position -> - rectangle(position, id = id).color(Color.Blue) - }.launchIn(scope) + devices.forEach { (id, device) -> + device.useProperty(CollectiveDevice.position, scope = scope) { position -> + + val activeDevice = selectedDeviceId?.let { devices[it] } + + if (activeDevice == null || id == selectedDeviceId || !showOnlyVisible || id in activeDevice.listVisible()) { + 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) + } + + } + } + } + } + second(200.dp) { + Column { + selectedDeviceId?.let { id -> + Column( + modifier = Modifier + .padding(8.dp) + .border(2.dp, Color.DarkGray) + ) { + Card( + elevation = 16.dp, + ) { + Text( + text = "Выбран: $id", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth() + ) + } + devices[id]?.let { + Text(it.meta.toString(), Modifier.padding(10.dp)) + } + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp).fillMaxWidth()) { + Text("Показать только видимые") + Checkbox(showOnlyVisible, { showOnlyVisible = it }) + } + } + } + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + devices.forEach { (id, device) -> + Card( + elevation = 16.dp, + modifier = Modifier.padding(8.dp).onClick { + selectedDeviceId = id + }.conditional(id == selectedDeviceId) { + border(2.dp, Color.Blue) + } + ) { + Text( + text = id, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth() + ) + } + } + } + } } } }