Compare commits
No commits in common. "13b80be8841761568ed17db6b23a2e9116591411" and "a2b5880da99f6718afcff5a54a7f3cd9e11db767" have entirely different histories.
13b80be884
...
a2b5880da9
5
.gitignore
vendored
5
.gitignore
vendored
@ -9,7 +9,4 @@
|
|||||||
|
|
||||||
out/
|
out/
|
||||||
build/
|
build/
|
||||||
|
!gradle-wrapper.jar
|
||||||
!gradle-wrapper.jar
|
|
||||||
|
|
||||||
/demo/device-collective/mapCache/
|
|
@ -7,7 +7,6 @@
|
|||||||
- 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,18 +166,12 @@ public data class ActionResultMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies listeners that a new binary with given [contentId] and [contentMeta] is available.
|
* Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API.
|
||||||
*
|
|
||||||
* [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 contentId: String,
|
val binaryID: 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,
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
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,7 +15,6 @@ 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
|
||||||
@ -32,18 +31,10 @@ 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()
|
||||||
@ -117,15 +108,7 @@ 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(
|
val plotter = Plotter(context, xDrive, yDrive) { color ->
|
||||||
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.collective.MainKt"
|
mainClass = "space.kscience.controls.demo.map.MainKt"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.demo.collective
|
package space.kscience.controls.demo.map
|
||||||
|
|
||||||
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 SampleDeviceState<T>(
|
class DebounceDeviceState<T>(
|
||||||
val origin: DeviceState<T>,
|
val origin: DeviceState<T>,
|
||||||
val interval: Duration,
|
val interval: Duration,
|
||||||
) : DeviceState<T> {
|
) : DeviceState<T> {
|
||||||
@ -17,4 +17,4 @@ class SampleDeviceState<T>(
|
|||||||
override fun toString(): String = "DebounceDeviceState($value, interval=$interval)"
|
override fun toString(): String = "DebounceDeviceState($value, interval=$interval)"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval)
|
fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval)
|
@ -1,59 +1,43 @@
|
|||||||
package space.kscience.controls.demo.collective
|
package space.kscience.controls.demo.map
|
||||||
|
|
||||||
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.*
|
import space.kscience.maps.coordinates.Gmc
|
||||||
|
|
||||||
|
|
||||||
typealias DeviceId = String
|
typealias RemoteDeviceId = String
|
||||||
|
|
||||||
|
|
||||||
internal data class VirtualDeviceState(
|
data class RemoteDeviceState(
|
||||||
val id: DeviceId,
|
val id: RemoteDeviceId,
|
||||||
val configuration: CollectiveDeviceConfiguration,
|
val configuration: RemoteDeviceConfiguration,
|
||||||
val position: MutableDeviceState<Gmc>,
|
val position: MutableDeviceState<Gmc>,
|
||||||
val velocity: MutableDeviceState<GmcVelocity>,
|
val velocity: MutableDeviceState<GmcVelocity>,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun VirtualDeviceState(
|
public fun RemoteDeviceState(
|
||||||
id: DeviceId,
|
id: RemoteDeviceId,
|
||||||
position: Gmc,
|
position: Gmc,
|
||||||
configuration: CollectiveDeviceConfiguration.() -> Unit = {},
|
configuration: RemoteDeviceConfiguration.() -> Unit = {},
|
||||||
) = VirtualDeviceState(
|
) = RemoteDeviceState(
|
||||||
id,
|
id,
|
||||||
CollectiveDeviceConfiguration(id).apply(configuration),
|
RemoteDeviceConfiguration(configuration),
|
||||||
MutableDeviceState(position),
|
MutableDeviceState(position),
|
||||||
MutableDeviceState(GmcVelocity.zero)
|
MutableDeviceState(GmcVelocity.zero)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
internal class DeviceCollectiveModel(
|
class DeviceCollectiveModel(
|
||||||
context: Context,
|
context: Context,
|
||||||
val deviceStates: Collection<VirtualDeviceState>,
|
val deviceStates: Collection<RemoteDeviceState>,
|
||||||
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.collective
|
package space.kscience.controls.demo.map
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.kmath.geometry.Angle
|
import space.kscience.kmath.geometry.Angle
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@file:OptIn(DFExperimental::class)
|
@file:OptIn(DFExperimental::class)
|
||||||
|
|
||||||
package space.kscience.controls.demo.collective
|
package space.kscience.controls.demo.map
|
||||||
|
|
||||||
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,20 +10,17 @@ 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.string
|
import space.kscience.dataforge.meta.SchemeSpec
|
||||||
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 CollectiveDeviceConfiguration(deviceId: DeviceId) : Scheme() {
|
class RemoteDeviceConfiguration : Scheme() {
|
||||||
var deviceId by string(deviceId)
|
companion object : SchemeSpec<RemoteDeviceConfiguration>(::RemoteDeviceConfiguration)
|
||||||
var description by string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface CollectiveDevice : Device {
|
interface RemoteDevice : Device {
|
||||||
|
|
||||||
public val id: DeviceId
|
|
||||||
|
|
||||||
suspend fun getPosition(): Gmc
|
suspend fun getPosition(): Gmc
|
||||||
|
|
||||||
@ -31,10 +28,8 @@ interface CollectiveDevice : 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() }
|
||||||
@ -49,18 +44,15 @@ interface CollectiveDevice : Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CollectiveDeviceConstructor(
|
class RemoteDeviceConstructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
val configuration: CollectiveDeviceConfiguration,
|
val configuration: RemoteDeviceConfiguration,
|
||||||
position: MutableDeviceState<Gmc>,
|
position: MutableDeviceState<Gmc>,
|
||||||
velocity: MutableDeviceState<GmcVelocity>,
|
velocity: MutableDeviceState<GmcVelocity>,
|
||||||
private val listVisible: suspend () -> Collection<DeviceId>,
|
) : DeviceConstructor(context, configuration.meta), RemoteDevice {
|
||||||
) : DeviceConstructor(context, configuration.meta), CollectiveDevice {
|
|
||||||
|
|
||||||
override val id: DeviceId get() = configuration.deviceId
|
val position = registerAsProperty(RemoteDevice.position, position.debounce(500.milliseconds))
|
||||||
|
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
|
||||||
|
|
||||||
@ -69,6 +61,4 @@ class CollectiveDeviceConstructor(
|
|||||||
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,4 +1,4 @@
|
|||||||
package space.kscience.controls.demo.collective
|
package space.kscience.controls.demo.map
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -20,32 +20,31 @@ private val radius = 0.01.degrees
|
|||||||
|
|
||||||
|
|
||||||
internal fun generateModel(context: Context): DeviceCollectiveModel {
|
internal fun generateModel(context: Context): DeviceCollectiveModel {
|
||||||
val devices: List<VirtualDeviceState> = List(100) { index ->
|
val devices: List<RemoteDeviceState> = buildList {
|
||||||
val id = "device[$index]"
|
repeat(100) {
|
||||||
|
add(
|
||||||
VirtualDeviceState(
|
RemoteDeviceState(
|
||||||
id = id,
|
"device[$it]",
|
||||||
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, 0.2.kilometers)
|
val model = DeviceCollectiveModel(context, devices)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CollectiveDevice.moveInCircles(): Job = launch {
|
fun RemoteDevice.moveInCircles(): Job = launch {
|
||||||
var bearing = Random.nextDouble(-PI, PI).radians
|
var bearing = Random.nextDouble(-PI, PI).radians
|
||||||
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(500)
|
delay(500)
|
||||||
bearing += 5.degrees
|
bearing += 5.degrees
|
||||||
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,42 +1,27 @@
|
|||||||
@file:OptIn(ExperimentalFoundationApi::class, ExperimentalSplitPaneApi::class)
|
package space.kscience.controls.demo.map
|
||||||
|
|
||||||
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.*
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.useProperty
|
import space.kscience.controls.spec.propertyFlow
|
||||||
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.*
|
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 java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +34,7 @@ fun rememberDeviceManager(): DeviceManager = remember {
|
|||||||
context.request(DeviceManager)
|
context.request(DeviceManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@ -61,24 +47,14 @@ fun App() {
|
|||||||
generateModel(deviceManager.context)
|
generateModel(deviceManager.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
val devices: Map<DeviceId, CollectiveDevice> = remember {
|
val devices: Map<RemoteDeviceId, RemoteDevice> = remember {
|
||||||
collectiveModel.deviceStates.associate {
|
collectiveModel.deviceStates.associate {
|
||||||
val device = CollectiveDeviceConstructor(
|
val device = RemoteDeviceConstructor(deviceManager.context, it.configuration, it.position, it.velocity)
|
||||||
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),
|
||||||
@ -86,93 +62,21 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalSplitPane(
|
MapView(
|
||||||
splitPaneState = rememberSplitPaneState(0.9f)
|
mapTileProvider = mapTileProvider,
|
||||||
|
config = ViewConfig()
|
||||||
) {
|
) {
|
||||||
first(400.dp) {
|
collectiveModel.deviceStates.forEach { device ->
|
||||||
MapView(
|
circle(device.position.value, id = device.id + ".position").color(Color.Red)
|
||||||
mapTileProvider = mapTileProvider,
|
device.position.valueFlow.onEach {
|
||||||
config = ViewConfig()
|
circle(device.position.value, id = device.id + ".position").color(Color.Red)
|
||||||
) {
|
}.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) ->
|
|
||||||
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 {
|
devices.forEach { (id, device) ->
|
||||||
selectedDeviceId?.let { id ->
|
device.propertyFlow(RemoteDevice.position).onEach { position ->
|
||||||
Column(
|
rectangle(position, id = id).color(Color.Blue)
|
||||||
modifier = Modifier
|
}.launchIn(scope)
|
||||||
.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