Compare commits
2 Commits
a2b5880da9
...
13b80be884
Author | SHA1 | Date | |
---|---|---|---|
13b80be884 | |||
5c7d3d8a7a |
5
.gitignore
vendored
5
.gitignore
vendored
@ -9,4 +9,7 @@
|
|||||||
|
|
||||||
out/
|
out/
|
||||||
build/
|
build/
|
||||||
!gradle-wrapper.jar
|
|
||||||
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
|
/demo/device-collective/mapCache/
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
- PLC4X bindings
|
- PLC4X bindings
|
||||||
- Shortcuts to access all Controls devices in a magix network.
|
- Shortcuts to access all Controls devices in a magix network.
|
||||||
- `DeviceClient` properly evaluates lifecycle and logs
|
- `DeviceClient` properly evaluates lifecycle and logs
|
||||||
|
- `PeerConnection` API for direct device-device binary sharing
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Constructor properties return `DeviceState` in order to be able to subscribe to them
|
- Constructor properties return `DeviceState` in order to be able to subscribe to them
|
||||||
|
@ -166,12 +166,18 @@ public data class ActionResultMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API.
|
* Notifies listeners that a new binary with given [contentId] and [contentMeta] is available.
|
||||||
|
*
|
||||||
|
* [contentMeta] includes public information that could be shared with loop subscribers. It should not contain sensitive data.
|
||||||
|
*
|
||||||
|
* The binary itself could not be provided via [DeviceMessage] API.
|
||||||
|
* [space.kscience.controls.peer.PeerConnection] must be used instead
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("binary.notification")
|
@SerialName("binary.notification")
|
||||||
public data class BinaryNotificationMessage(
|
public data class BinaryNotificationMessage(
|
||||||
val binaryID: String,
|
val contentId: String,
|
||||||
|
val contentMeta: Meta,
|
||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
package space.kscience.controls.peer
|
||||||
|
|
||||||
|
import space.kscience.dataforge.io.Envelope
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager that allows direct synchronous sending and receiving binary data
|
||||||
|
*/
|
||||||
|
public interface PeerConnection {
|
||||||
|
/**
|
||||||
|
* Receive an [Envelope] from a device with name [deviceName] on a given [address] with given [contentId].
|
||||||
|
*
|
||||||
|
* The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or
|
||||||
|
* magix endpoint name.
|
||||||
|
*
|
||||||
|
* Depending on [PeerConnection] implementation, the resulting [Envelope] could be lazy loaded
|
||||||
|
*
|
||||||
|
* Additional metadata in [requestMeta] could be required for authentication.
|
||||||
|
*/
|
||||||
|
public suspend fun receive(
|
||||||
|
address: String,
|
||||||
|
deviceName: Name,
|
||||||
|
contentId: String,
|
||||||
|
requestMeta: Meta = Meta.EMPTY,
|
||||||
|
): Envelope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an [envelope] to a device with name [deviceName] on a given [address]
|
||||||
|
*
|
||||||
|
* The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or
|
||||||
|
* magix endpoint name.
|
||||||
|
*
|
||||||
|
* Additional metadata in [requestMeta] could be required for authentication.
|
||||||
|
*/
|
||||||
|
public suspend fun send(
|
||||||
|
address: String,
|
||||||
|
deviceName: Name,
|
||||||
|
envelope: Envelope,
|
||||||
|
requestMeta: Meta = Meta.EMPTY,
|
||||||
|
)
|
||||||
|
}
|
12
controls-visualisation-compose/src/commonMain/kotlin/misc.kt
Normal file
12
controls-visualisation-compose/src/commonMain/kotlin/misc.kt
Normal file
@ -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
|
||||||
|
}
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import space.kscience.controls.constructor.*
|
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.StepDrive
|
||||||
import space.kscience.controls.constructor.devices.angle
|
import space.kscience.controls.constructor.devices.angle
|
||||||
import space.kscience.controls.constructor.models.Leadscrew
|
import space.kscience.controls.constructor.models.Leadscrew
|
||||||
@ -31,10 +32,18 @@ private class Plotter(
|
|||||||
context: Context,
|
context: Context,
|
||||||
xDrive: StepDrive,
|
xDrive: StepDrive,
|
||||||
yDrive: StepDrive,
|
yDrive: StepDrive,
|
||||||
|
xStartLimit: LimitSwitch,
|
||||||
|
xEndLimit: LimitSwitch,
|
||||||
|
yStartLimit: LimitSwitch,
|
||||||
|
yEndLimit: LimitSwitch,
|
||||||
val paint: suspend (Color) -> Unit,
|
val paint: suspend (Color) -> Unit,
|
||||||
) : DeviceConstructor(context) {
|
) : DeviceConstructor(context) {
|
||||||
val xDrive by device(xDrive)
|
val xDrive by device(xDrive)
|
||||||
val yDrive by device(yDrive)
|
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) {
|
public fun moveToXY(x: Number, y: Number) {
|
||||||
xDrive.target.value = x.toLong()
|
xDrive.target.value = x.toLong()
|
||||||
@ -108,7 +117,15 @@ private class PlotterModel(
|
|||||||
|
|
||||||
val xy: DeviceState<XY<Meters>> = combineState(x, y) { x, y -> XY(x, y) }
|
val xy: DeviceState<XY<Meters>> = 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")
|
println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color")
|
||||||
callback(PlotterPoint(x.value, y.value, color))
|
callback(PlotterPoint(x.value, y.value, color))
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,6 @@ kotlin.explicitApi = ExplicitApiMode.Disabled
|
|||||||
|
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
application {
|
application {
|
||||||
mainClass = "space.kscience.controls.demo.map.MainKt"
|
mainClass = "space.kscience.controls.demo.collective.MainKt"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
@file:OptIn(DFExperimental::class)
|
@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.api.Device
|
||||||
import space.kscience.controls.constructor.DeviceConstructor
|
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.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
|
||||||
import space.kscience.dataforge.meta.SchemeSpec
|
import space.kscience.dataforge.meta.string
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
import space.kscience.maps.coordinates.Gmc
|
import space.kscience.maps.coordinates.Gmc
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class RemoteDeviceConfiguration : Scheme() {
|
class CollectiveDeviceConfiguration(deviceId: DeviceId) : Scheme() {
|
||||||
companion object : SchemeSpec<RemoteDeviceConfiguration>(::RemoteDeviceConfiguration)
|
var deviceId by string(deviceId)
|
||||||
|
var description by string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface RemoteDevice : Device {
|
interface CollectiveDevice : Device {
|
||||||
|
|
||||||
|
public val id: DeviceId
|
||||||
|
|
||||||
suspend fun getPosition(): Gmc
|
suspend fun getPosition(): Gmc
|
||||||
|
|
||||||
@ -28,8 +31,10 @@ interface RemoteDevice : Device {
|
|||||||
|
|
||||||
suspend fun setVelocity(value: GmcVelocity)
|
suspend fun setVelocity(value: GmcVelocity)
|
||||||
|
|
||||||
|
suspend fun listVisible(): Collection<DeviceId>
|
||||||
|
|
||||||
companion object : DeviceSpec<RemoteDevice>() {
|
|
||||||
|
companion object : DeviceSpec<CollectiveDevice>() {
|
||||||
val position by property<Gmc>(
|
val position by property<Gmc>(
|
||||||
converter = MetaConverter.serializable(),
|
converter = MetaConverter.serializable(),
|
||||||
read = { getPosition() }
|
read = { getPosition() }
|
||||||
@ -44,15 +49,18 @@ interface RemoteDevice : Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RemoteDeviceConstructor(
|
class CollectiveDeviceConstructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
val configuration: RemoteDeviceConfiguration,
|
val configuration: CollectiveDeviceConfiguration,
|
||||||
position: MutableDeviceState<Gmc>,
|
position: MutableDeviceState<Gmc>,
|
||||||
velocity: MutableDeviceState<GmcVelocity>,
|
velocity: MutableDeviceState<GmcVelocity>,
|
||||||
) : DeviceConstructor(context, configuration.meta), RemoteDevice {
|
private val listVisible: suspend () -> Collection<DeviceId>,
|
||||||
|
) : DeviceConstructor(context, configuration.meta), CollectiveDevice {
|
||||||
|
|
||||||
val position = registerAsProperty(RemoteDevice.position, position.debounce(500.milliseconds))
|
override val id: DeviceId get() = configuration.deviceId
|
||||||
val velocity = registerAsProperty(RemoteDevice.velocity, velocity)
|
|
||||||
|
val position = registerAsProperty(CollectiveDevice.position, position.sample(500.milliseconds))
|
||||||
|
val velocity = registerAsProperty(CollectiveDevice.velocity, velocity)
|
||||||
|
|
||||||
override suspend fun getPosition(): Gmc = position.value
|
override suspend fun getPosition(): Gmc = position.value
|
||||||
|
|
||||||
@ -61,4 +69,6 @@ class RemoteDeviceConstructor(
|
|||||||
override suspend fun setVelocity(value: GmcVelocity) {
|
override suspend fun setVelocity(value: GmcVelocity) {
|
||||||
velocity.value = value
|
velocity.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun listVisible(): Collection<DeviceId> = listVisible.invoke()
|
||||||
}
|
}
|
@ -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.ModelConstructor
|
||||||
import space.kscience.controls.constructor.MutableDeviceState
|
import space.kscience.controls.constructor.MutableDeviceState
|
||||||
import space.kscience.controls.constructor.onTimer
|
import space.kscience.controls.constructor.onTimer
|
||||||
import space.kscience.dataforge.context.Context
|
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(
|
internal data class VirtualDeviceState(
|
||||||
val id: RemoteDeviceId,
|
val id: DeviceId,
|
||||||
val configuration: RemoteDeviceConfiguration,
|
val configuration: CollectiveDeviceConfiguration,
|
||||||
val position: MutableDeviceState<Gmc>,
|
val position: MutableDeviceState<Gmc>,
|
||||||
val velocity: MutableDeviceState<GmcVelocity>,
|
val velocity: MutableDeviceState<GmcVelocity>,
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun RemoteDeviceState(
|
internal fun VirtualDeviceState(
|
||||||
id: RemoteDeviceId,
|
id: DeviceId,
|
||||||
position: Gmc,
|
position: Gmc,
|
||||||
configuration: RemoteDeviceConfiguration.() -> Unit = {},
|
configuration: CollectiveDeviceConfiguration.() -> Unit = {},
|
||||||
) = RemoteDeviceState(
|
) = VirtualDeviceState(
|
||||||
id,
|
id,
|
||||||
RemoteDeviceConfiguration(configuration),
|
CollectiveDeviceConfiguration(id).apply(configuration),
|
||||||
MutableDeviceState(position),
|
MutableDeviceState(position),
|
||||||
MutableDeviceState(GmcVelocity.zero)
|
MutableDeviceState(GmcVelocity.zero)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceCollectiveModel(
|
internal class DeviceCollectiveModel(
|
||||||
context: Context,
|
context: Context,
|
||||||
val deviceStates: Collection<RemoteDeviceState>,
|
val deviceStates: Collection<VirtualDeviceState>,
|
||||||
|
val visibilityRange: Distance,
|
||||||
) : ModelConstructor(context) {
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagate movement
|
||||||
|
*/
|
||||||
private val movement = onTimer { prev, next ->
|
private val movement = onTimer { prev, next ->
|
||||||
val delta = (next - prev)
|
val delta = (next - prev)
|
||||||
deviceStates.forEach { state ->
|
deviceStates.forEach { state ->
|
||||||
state.position.value = state.position.value.moveWith(state.velocity.value, delta)
|
state.position.value = state.position.value.moveWith(state.velocity.value, delta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun locateVisible(id: DeviceId): Map<DeviceId, GmcCurve> {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.demo.map
|
package space.kscience.controls.demo.collective
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.demo.map
|
package space.kscience.controls.demo.collective
|
||||||
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -7,7 +7,7 @@ import space.kscience.controls.constructor.DeviceState
|
|||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
class DebounceDeviceState<T>(
|
class SampleDeviceState<T>(
|
||||||
val origin: DeviceState<T>,
|
val origin: DeviceState<T>,
|
||||||
val interval: Duration,
|
val interval: Duration,
|
||||||
) : DeviceState<T> {
|
) : DeviceState<T> {
|
||||||
@ -17,4 +17,4 @@ class DebounceDeviceState<T>(
|
|||||||
override fun toString(): String = "DebounceDeviceState($value, interval=$interval)"
|
override fun toString(): String = "DebounceDeviceState($value, interval=$interval)"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval)
|
fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval)
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.demo.map
|
package space.kscience.controls.demo.collective
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -20,31 +20,32 @@ private val radius = 0.01.degrees
|
|||||||
|
|
||||||
|
|
||||||
internal fun generateModel(context: Context): DeviceCollectiveModel {
|
internal fun generateModel(context: Context): DeviceCollectiveModel {
|
||||||
val devices: List<RemoteDeviceState> = buildList {
|
val devices: List<VirtualDeviceState> = List(100) { index ->
|
||||||
repeat(100) {
|
val id = "device[$index]"
|
||||||
add(
|
|
||||||
RemoteDeviceState(
|
VirtualDeviceState(
|
||||||
"device[$it]",
|
id = id,
|
||||||
Gmc(
|
Gmc(
|
||||||
center.latitude + radius * Random.nextDouble(),
|
center.latitude + radius * Random.nextDouble(),
|
||||||
center.longitude + 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
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RemoteDevice.moveInCircles(): Job = launch {
|
fun CollectiveDevice.moveInCircles(): Job = launch {
|
||||||
var bearing = Random.nextDouble(-PI, PI).radians
|
var bearing = Random.nextDouble(-PI, PI).radians
|
||||||
write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(500)
|
delay(500)
|
||||||
bearing += 5.degrees
|
bearing += 5.degrees
|
||||||
write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
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.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.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
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.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.Context
|
||||||
import space.kscience.dataforge.context.request
|
import space.kscience.dataforge.context.request
|
||||||
import space.kscience.maps.compose.MapView
|
import space.kscience.maps.compose.MapView
|
||||||
import space.kscience.maps.compose.OpenStreetMapTileProvider
|
import space.kscience.maps.compose.OpenStreetMapTileProvider
|
||||||
import space.kscience.maps.features.ViewConfig
|
import space.kscience.maps.features.*
|
||||||
import space.kscience.maps.features.circle
|
|
||||||
import space.kscience.maps.features.color
|
|
||||||
import space.kscience.maps.features.rectangle
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +49,6 @@ fun rememberDeviceManager(): DeviceManager = remember {
|
|||||||
context.request(DeviceManager)
|
context.request(DeviceManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@ -47,14 +61,24 @@ fun App() {
|
|||||||
generateModel(deviceManager.context)
|
generateModel(deviceManager.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
val devices: Map<RemoteDeviceId, RemoteDevice> = remember {
|
val devices: Map<DeviceId, CollectiveDevice> = remember {
|
||||||
collectiveModel.deviceStates.associate {
|
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()
|
device.moveInCircles()
|
||||||
it.id to device
|
it.id to device
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var selectedDeviceId by remember { mutableStateOf<DeviceId?>(null) }
|
||||||
|
var showOnlyVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val mapTileProvider = remember {
|
val mapTileProvider = remember {
|
||||||
OpenStreetMapTileProvider(
|
OpenStreetMapTileProvider(
|
||||||
client = HttpClient(CIO),
|
client = HttpClient(CIO),
|
||||||
@ -62,21 +86,93 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MapView(
|
HorizontalSplitPane(
|
||||||
mapTileProvider = mapTileProvider,
|
splitPaneState = rememberSplitPaneState(0.9f)
|
||||||
config = ViewConfig()
|
|
||||||
) {
|
) {
|
||||||
collectiveModel.deviceStates.forEach { device ->
|
first(400.dp) {
|
||||||
circle(device.position.value, id = device.id + ".position").color(Color.Red)
|
MapView(
|
||||||
device.position.valueFlow.onEach {
|
mapTileProvider = mapTileProvider,
|
||||||
circle(device.position.value, id = device.id + ".position").color(Color.Red)
|
config = ViewConfig()
|
||||||
}.launchIn(scope)
|
) {
|
||||||
}
|
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) ->
|
devices.forEach { (id, device) ->
|
||||||
device.propertyFlow(RemoteDevice.position).onEach { position ->
|
device.useProperty(CollectiveDevice.position, scope = scope) { position ->
|
||||||
rectangle(position, id = id).color(Color.Blue)
|
|
||||||
}.launchIn(scope)
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user