From 0e9b61fedf37d14e1d4d6968edb41ead9c45e0a5 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 5 Mar 2024 16:13:29 -0300 Subject: [PATCH 01/45] [api]: Implement Quadratic Curve Fitting algorithm --- nebulosa-curve-fitting/build.gradle.kts | 18 ++++++++++ .../nebulosa/curve/fitting/CurveFitting.kt | 6 ++++ .../nebulosa/curve/fitting/CurvePoint.kt | 23 +++++++++++++ .../nebulosa/curve/fitting/FittedCurve.kt | 12 +++++++ .../curve/fitting/QuadraticFitting.kt | 22 +++++++++++++ .../kotlin/nebulosa/curve/fitting/RSquared.kt | 33 +++++++++++++++++++ .../src/test/kotlin/QuadraticFittingTest.kt | 33 +++++++++++++++++++ .../src/main/kotlin/nebulosa/math/Point2D.kt | 18 ++++++++++ .../src/main/kotlin/nebulosa/math/Point3D.kt | 29 ++++++++++++++++ .../src/main/kotlin/nebulosa/math/Vector3D.kt | 4 --- settings.gradle.kts | 2 ++ 11 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 nebulosa-curve-fitting/build.gradle.kts create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt create mode 100644 nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt diff --git a/nebulosa-curve-fitting/build.gradle.kts b/nebulosa-curve-fitting/build.gradle.kts new file mode 100644 index 000000000..2878840c9 --- /dev/null +++ b/nebulosa-curve-fitting/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + api(libs.apache.math) + implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-test")) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt new file mode 100644 index 000000000..b2a67d9ca --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt @@ -0,0 +1,6 @@ +package nebulosa.curve.fitting + +fun interface CurveFitting { + + fun calculate(points: Collection): FittedCurve +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt new file mode 100644 index 000000000..f8135ad04 --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt @@ -0,0 +1,23 @@ +package nebulosa.curve.fitting + +import org.apache.commons.math3.fitting.WeightedObservedPoint + +class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CurvePoint) return false + + if (x != other.x) return false + + return y == other.y + } + + override fun hashCode(): Int { + var result = x.hashCode() + result = 31 * result + y.hashCode() + return result + } + + override fun toString() = "CurvePoint(x=$x, y=$y)" +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt new file mode 100644 index 000000000..63b6f2254 --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt @@ -0,0 +1,12 @@ +package nebulosa.curve.fitting + +interface FittedCurve { + + val minimumX: Double + + val minimumY: Double + + val rSquared: Double + + operator fun invoke(x: Double): Double +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt new file mode 100644 index 000000000..72bb18bda --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt @@ -0,0 +1,22 @@ +package nebulosa.curve.fitting + +import org.apache.commons.math3.analysis.polynomials.PolynomialFunction +import org.apache.commons.math3.fitting.PolynomialCurveFitter + +object QuadraticFitting : CurveFitting { + + private val fitter = PolynomialCurveFitter.create(2) + + override fun calculate(points: Collection) = object : FittedCurve { + + private val poly by lazy { PolynomialFunction(fitter.fit(points)) } + + override val rSquared by lazy { RSquared.calculate(points, poly) } + + override val minimumX by lazy { poly.coefficients[1] / (-2.0 * poly.coefficients[2]) } + + override val minimumY by lazy { this(minimumX) } + + override fun invoke(x: Double) = poly.value(x) + } +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt new file mode 100644 index 000000000..ae7b71479 --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt @@ -0,0 +1,33 @@ +package nebulosa.curve.fitting + +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics +import kotlin.math.pow + +object RSquared { + + fun calculate(points: Collection, function: UnivariateFunction): Double { + val descriptiveStatistics = DescriptiveStatistics(points.size) + val predictedValues = DoubleArray(points.size) + var residualSumOfSquares = 0.0 + + for ((i, point) in points.withIndex()) { + val actualValue = point.y + val predictedValue = function.value(point.x) + predictedValues[i] = predictedValue + + val t = (predictedValue - actualValue).pow(2.0) + residualSumOfSquares += t + descriptiveStatistics.addValue(actualValue) + } + + val avgActualValues = descriptiveStatistics.mean + var totalSumOfSquares = 0.0 + + repeat(points.size) { + totalSumOfSquares += (predictedValues[it] - avgActualValues).pow(2.0) + } + + return 1.0 - (residualSumOfSquares / totalSumOfSquares) + } +} diff --git a/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt b/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt new file mode 100644 index 000000000..816381abf --- /dev/null +++ b/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt @@ -0,0 +1,33 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.doubles.shouldBeExactly +import io.kotest.matchers.shouldBe +import nebulosa.curve.fitting.CurvePoint +import nebulosa.curve.fitting.QuadraticFitting + +class QuadraticFittingTest : StringSpec() { + + init { + "perfect V-curve" { + // (x-5)² + 2 + val points = listOf( + CurvePoint(1.0, 18.0), + CurvePoint(2.0, 11.0), + CurvePoint(3.0, 6.0), + CurvePoint(4.0, 3.0), + CurvePoint(5.0, 2.0), + CurvePoint(6.0, 3.0), + CurvePoint(7.0, 6.0), + CurvePoint(8.0, 11.0), + CurvePoint(9.0, 18.0), + ) + + val curve = QuadraticFitting.calculate(points) + + curve(5.0) shouldBeExactly 2.0 + curve.minimumX shouldBe (5.0 plusOrMinus 1e-12) + curve.minimumY shouldBe (2.0 plusOrMinus 1e-12) + curve.rSquared shouldBe (1.0 plusOrMinus 1e-12) + } + } +} diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt index ea18cf534..2fb90a0bd 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt @@ -1,5 +1,6 @@ package nebulosa.math +import kotlin.math.atan2 import kotlin.math.hypot interface Point2D { @@ -12,7 +13,24 @@ interface Point2D { operator fun component2() = y + val length + get() = hypot(x, y) + fun distance(other: Point2D): Double { return hypot(x - other.x, y - other.y) } + + fun angle(other: Point2D): Angle { + return atan2(other.y - y, other.x - x) + } + + data class XY(override val x: Double, override val y: Double) : Point2D + + companion object { + + @JvmStatic val ZERO: Point2D = XY(0.0, 0.0) + + @JvmStatic + operator fun invoke(x: Double, y: Double): Point2D = XY(x, y) + } } diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Point3D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Point3D.kt index 996d2a2ee..99d131f5b 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Point3D.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Point3D.kt @@ -8,10 +8,39 @@ interface Point3D : Point2D { operator fun component3() = z + override val length + get() = sqrt(x * x + y * y + z * z) + fun distance(other: Point3D): Double { val dx = x - other.x val dy = y - other.y val dz = z - other.z return sqrt(dx * dx + dy * dy + dz * dz) } + + override fun distance(other: Point2D): Double { + val dx = x - other.x + val dy = y - other.y + return sqrt(dx * dx + dy * dy + z * z) + } + + fun angle(other: Point3D): Angle { + val dot = x * other.x + y * other.y + z * other.z + return dot / (length * other.length) + } + + override fun angle(other: Point2D): Angle { + val dot = x * other.x + y * other.y + return dot / (length * other.length) + } + + data class XYZ(override val x: Double, override val y: Double, override val z: Double) : Point3D + + companion object { + + @JvmStatic val ZERO: Point3D = XYZ(0.0, 0.0, 0.0) + + @JvmStatic + operator fun invoke(x: Double, y: Double, z: Double): Point3D = XYZ(x, y, z) + } } diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt index b641ed4ed..4b47df852 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt @@ -3,7 +3,6 @@ package nebulosa.math import kotlin.math.abs import kotlin.math.acos import kotlin.math.atan2 -import kotlin.math.sqrt @Suppress("NOTHING_TO_INLINE") open class Vector3D protected constructor(@PublishedApi @JvmField internal val vector: DoubleArray) : Point3D, Cloneable { @@ -56,9 +55,6 @@ open class Vector3D protected constructor(@PublishedApi @JvmField internal val v vector[0] * other[1] - vector[1] * other[0] ) - inline val length - get() = sqrt(dot(this)) - inline val normalized get() = length.let { if (it == 0.0) this else this / it } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0b00ef413..c698c0a50 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,7 @@ dependencyResolutionManagement { library("apache-lang3", "org.apache.commons:commons-lang3:3.14.0") library("apache-codec", "commons-codec:commons-codec:1.16.1") library("apache-collections", "org.apache.commons:commons-collections4:4.4") + library("apache-math", "org.apache.commons:commons-math3:3.6.1") library("oshi", "com.github.oshi:oshi-core:6.4.13") library("timeshape", "net.iakovlev:timeshape:2022g.17") library("jna", "net.java.dev.jna:jna:5.14.0") @@ -55,6 +56,7 @@ include(":nebulosa-astrometrynet-jna") include(":nebulosa-batch-processing") include(":nebulosa-common") include(":nebulosa-constants") +include(":nebulosa-curve-fitting") include(":nebulosa-erfa") include(":nebulosa-fits") include(":nebulosa-guiding") From 3cfa652914eb467aae4e515960ccdec624e0f460 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 5 Mar 2024 16:28:23 -0300 Subject: [PATCH 02/45] [api]: Reformat code --- .editorconfig | 2 +- .../kotlin/nebulosa/api/image/ImageService.kt | 4 +-- .../nebulosa/api/mounts/MountService.kt | 2 -- .../alpaca/indi/device/cameras/ASCOMCamera.kt | 32 +++++++++---------- .../indi/device/focusers/ASCOMFocuser.kt | 7 ++++ .../alpaca/indi/device/mounts/ASCOMMount.kt | 13 ++++++-- .../indi/device/wheels/ASCOMFilterWheel.kt | 2 ++ .../src/main/kotlin/nebulosa/erfa/Erfa.kt | 8 ++--- .../main/kotlin/nebulosa/fits/HeaderCard.kt | 2 +- .../src/main/kotlin/nebulosa/imaging/Image.kt | 2 +- .../nebulosa/indi/client/device/GPSDevice.kt | 4 +-- .../indi/client/device/cameras/INDICamera.kt | 32 +++++++++---------- .../client/device/focusers/INDIFocuser.kt | 4 +-- .../indi/client/device/mounts/INDIMount.kt | 4 +-- .../client/device/wheels/INDIFilterWheel.kt | 5 +-- .../src/main/kotlin/nebulosa/math/Matrix3D.kt | 4 +-- .../nebulosa/nova/astrometry/FixedStar.kt | 4 +-- .../nova/position/GeographicPosition.kt | 2 +- .../kotlin/nebulosa/nova/position/ICRF.kt | 8 ++--- nebulosa-nova/src/test/kotlin/ICRFTest.kt | 5 ++- .../src/test/kotlin/TAIMinusUTCTest.kt | 2 +- ...DBMinusTTByFairheadAndBretagnon1990Test.kt | 2 +- .../watney/plate/solving/WatneyPlateSolver.kt | 2 +- .../src/main/kotlin/nebulosa/wcs/WCSUtil.kt | 4 +-- 24 files changed, 83 insertions(+), 73 deletions(-) diff --git a/.editorconfig b/.editorconfig index d0ed4ce8d..00bd20fda 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,8 +24,8 @@ trim_trailing_whitespace = false ij_kotlin_line_break_after_multiline_when_entry = false ij_kotlin_variable_annotation_wrap = off ij_kotlin_field_annotation_wrap = off +ij_kotlin_continuation_indent_size = 8 [*.py] ij_python_wrap_long_lines = false ij_python_method_parameters_wrap = off - diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index 4b813faa2..9ac135d75 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -81,8 +81,8 @@ class ImageService( var stretchParams = ScreenTransformFunction.Parameters(midtone, shadow, highlight) val shouldBeTransformed = autoStretch || manualStretch - || mirrorHorizontal || mirrorVertical || invert - || scnrEnabled + || mirrorHorizontal || mirrorVertical || invert + || scnrEnabled var transformedImage = if (shouldBeTransformed) image.clone() else image val instrument = camera?.name ?: image.header.instrument diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index c97723dbe..9843904c0 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -16,8 +16,6 @@ import nebulosa.nova.position.GeographicPosition import nebulosa.nova.position.Geoid import nebulosa.nova.position.ICRF import nebulosa.time.CurrentTime -import nebulosa.time.TimeJD -import nebulosa.time.UTC import nebulosa.wcs.WCS import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt index f6929f99d..2d097fc08 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt @@ -688,22 +688,22 @@ data class ASCOMCamera( } override fun toString() = "Camera(name=$name, connected=$connected, exposuring=$exposuring," + - " hasCoolerControl=$hasCoolerControl, cooler=$cooler," + - " hasDewHeater=$hasDewHeater, dewHeater=$dewHeater," + - " frameFormats=$frameFormats, canAbort=$canAbort," + - " cfaOffsetX=$cfaOffsetX, cfaOffsetY=$cfaOffsetY, cfaType=$cfaType," + - " exposureMin=$exposureMin, exposureMax=$exposureMax," + - " exposureState=$exposureState, exposureTime=$exposureTime," + - " hasCooler=$hasCooler, hasThermometer=$hasThermometer, canSetTemperature=$canSetTemperature," + - " temperature=$temperature, canSubFrame=$canSubFrame," + - " x=$x, minX=$minX, maxX=$maxX, y=$y, minY=$minY, maxY=$maxY," + - " width=$width, minWidth=$minWidth, maxWidth=$maxWidth, height=$height," + - " minHeight=$minHeight, maxHeight=$maxHeight," + - " canBin=$canBin, maxBinX=$maxBinX, maxBinY=$maxBinY," + - " binX=$binX, binY=$binY, gain=$gain, gainMin=$gainMin," + - " gainMax=$gainMax, offset=$offset, offsetMin=$offsetMin," + - " offsetMax=$offsetMax, hasGuiderHead=$hasGuiderHead," + - " canPulseGuide=$canPulseGuide, pulseGuiding=$pulseGuiding)" + " hasCoolerControl=$hasCoolerControl, cooler=$cooler," + + " hasDewHeater=$hasDewHeater, dewHeater=$dewHeater," + + " frameFormats=$frameFormats, canAbort=$canAbort," + + " cfaOffsetX=$cfaOffsetX, cfaOffsetY=$cfaOffsetY, cfaType=$cfaType," + + " exposureMin=$exposureMin, exposureMax=$exposureMax," + + " exposureState=$exposureState, exposureTime=$exposureTime," + + " hasCooler=$hasCooler, hasThermometer=$hasThermometer, canSetTemperature=$canSetTemperature," + + " temperature=$temperature, canSubFrame=$canSubFrame," + + " x=$x, minX=$minX, maxX=$maxX, y=$y, minY=$minY, maxY=$maxY," + + " width=$width, minWidth=$minWidth, maxWidth=$maxWidth, height=$height," + + " minHeight=$minHeight, maxHeight=$maxHeight," + + " canBin=$canBin, maxBinX=$maxBinX, maxBinY=$maxBinY," + + " binX=$binX, binY=$binY, gain=$gain, gainMin=$gainMin," + + " gainMax=$gainMax, offset=$offset, offsetMin=$offsetMin," + + " offsetMax=$offsetMax, hasGuiderHead=$hasGuiderHead," + + " canPulseGuide=$canPulseGuide, pulseGuiding=$pulseGuiding)" data class ImageMetadata( @JvmField val metadataVersion: Int, // Bytes 0..3 - Metadata version = 1 diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt index 70247550c..f15db005f 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt @@ -169,4 +169,11 @@ data class ASCOMFocuser( } } } + + override fun toString() = "Focuser(name=$name, moving=$moving, position=$position," + + " canAbsoluteMove=$canAbsoluteMove, canRelativeMove=$canRelativeMove," + + " canAbort=$canAbort, canReverse=$canReverse, reversed=$reversed," + + " canSync=$canSync, hasBacklash=$hasBacklash," + + " maxPosition=$maxPosition, hasThermometer=$hasThermometer," + + " temperature=$temperature)" } diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt index d87ded2f8..fd04025bf 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt @@ -236,8 +236,8 @@ data class ASCOMMount( override fun coordinates(longitude: Angle, latitude: Angle, elevation: Distance) { service.siteLongitude(device.number, longitude.toDegrees).doRequest {} && - service.siteLatitude(device.number, latitude.toDegrees).doRequest {} && - service.siteElevation(device.number, elevation.toMeters).doRequest {} + service.siteLatitude(device.number, latitude.toDegrees).doRequest {} && + service.siteElevation(device.number, elevation.toMeters).doRequest {} } override fun dateTime(dateTime: OffsetDateTime) { @@ -475,4 +475,13 @@ data class ASCOMMount( } } } + + override fun toString() = "Mount(name=$name, connected=$connected, slewing=$slewing, tracking=$tracking," + + " parking=$parking, parked=$parked, canAbort=$canAbort," + + " canSync=$canSync, canPark=$canPark, slewRates=$slewRates," + + " slewRate=$slewRate, mountType=$mountType, trackModes=$trackModes," + + " trackMode=$trackMode, pierSide=$pierSide, guideRateWE=$guideRateWE," + + " guideRateNS=$guideRateNS, rightAscension=$rightAscension," + + " declination=$declination, canPulseGuide=$canPulseGuide," + + " pulseGuiding=$pulseGuiding)" } diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/wheels/ASCOMFilterWheel.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/wheels/ASCOMFilterWheel.kt index 846b76726..0fbf081fd 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/wheels/ASCOMFilterWheel.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/wheels/ASCOMFilterWheel.kt @@ -77,4 +77,6 @@ data class ASCOMFilterWheel( } } } + + override fun toString() = "FilterWheel(name=$name, slotCount=$count, position=$position, moving=$moving)" } diff --git a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt index 0897411fb..cdccec460 100644 --- a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt +++ b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt @@ -819,8 +819,8 @@ fun eraNut00a(tt1: Double, tt2: Double): DoubleArray { for (i in xpl.indices.reversed()) { val arg = (xpl[i].nl * al + xpl[i].nf * af + xpl[i].nd * ad + xpl[i].nom * aom + xpl[i].nme * alme + - xpl[i].nve * alve + xpl[i].nea * alea + xpl[i].nma * alma + xpl[i].nju * alju + - xpl[i].nsa * alsa + xpl[i].nur * alur + xpl[i].nne * alne + xpl[i].npa * apa).mod(TAU) + xpl[i].nve * alve + xpl[i].nea * alea + xpl[i].nma * alma + xpl[i].nju * alju + + xpl[i].nsa * alsa + xpl[i].nur * alur + xpl[i].nne * alne + xpl[i].npa * apa).mod(TAU) val sarg = sin(arg) val carg = cos(arg) @@ -1311,7 +1311,7 @@ fun eraEpv00(tdb1: Double, tdb2: Double): Pair Date: Tue, 5 Mar 2024 16:40:42 -0300 Subject: [PATCH 03/45] [ci]: Group Apache dependency updates --- .editorconfig | 3 +++ .github/dependabot.yml | 18 +++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.editorconfig b/.editorconfig index 00bd20fda..2a6b20ee4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -29,3 +29,6 @@ ij_kotlin_continuation_indent_size = 8 [*.py] ij_python_wrap_long_lines = false ij_python_method_parameters_wrap = off + +[{*.yml, *.yaml}] +ij_yaml_indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a925fcf27..abfc3e194 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: directory: "/" schedule: interval: "monthly" - open-pull-requests-limit: 16 + open-pull-requests-limit: 64 target-branch: "dev" commit-message: prefix: "[api]" @@ -21,21 +21,21 @@ updates: netty: patterns: - "io.netty*" - retrofit: - patterns: - - "com.squareup.retrofit2*" - okhttp: + squareup: patterns: - - "com.squareup.okhttp3*" + - "com.squareup*" rx: patterns: - - "io.reactivex.rxjava3*" - jackson: + - "io.reactivex*" + fasterxml: patterns: - - "com.fasterxml.jackson*" + - "com.fasterxml*" kotlin: patterns: - "org.jetbrains.kotlin*" + apache: + patterns: + - "org.apache*" - package-ecosystem: "npm" directory: "/desktop" From f321e0e584f068d70384e4343ff9c72d2bc95c43 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 9 Mar 2024 13:37:18 -0300 Subject: [PATCH 04/45] [api]: Implement TrendLine and Hyperbolic Curve Fitting algorithm --- .../kotlin/nebulosa/curve/fitting/Curve.kt | 25 ++++ .../nebulosa/curve/fitting/CurveFitting.kt | 8 +- .../nebulosa/curve/fitting/CurvePoint.kt | 5 + .../nebulosa/curve/fitting/FittedCurve.kt | 12 +- .../curve/fitting/HyperbolicFitting.kt | 120 ++++++++++++++++++ .../nebulosa/curve/fitting/LinearCurve.kt | 19 +++ .../curve/fitting/QuadraticFitting.kt | 31 +++-- .../nebulosa/curve/fitting/TrendLine.kt | 31 +++++ .../curve/fitting/TrendLineFitting.kt | 28 ++++ .../curve/fitting/ZeroUnivariateFunction.kt | 8 ++ .../src/test/kotlin/HyperbolicFittingTest.kt | 31 +++++ .../src/test/kotlin/QuadraticFittingTest.kt | 20 +-- .../src/test/kotlin/TrendLineFittingTest.kt | 32 +++++ .../src/test/kotlin/TrendLineTest.kt | 33 +++++ 14 files changed, 369 insertions(+), 34 deletions(-) create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/LinearCurve.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/ZeroUnivariateFunction.kt create mode 100644 nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt create mode 100644 nebulosa-curve-fitting/src/test/kotlin/TrendLineFittingTest.kt create mode 100644 nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt new file mode 100644 index 000000000..0d505e9ad --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt @@ -0,0 +1,25 @@ +package nebulosa.curve.fitting + +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.analysis.polynomials.PolynomialFunction + +fun interface Curve : UnivariateFunction { + + operator fun invoke(x: Double) = value(x) + + companion object { + + internal fun DoubleArray.curvePoints(): Collection { + val points = ArrayList(size / 2) + + for (i in indices step 2) { + points.add(CurvePoint(this[i], this[i + 1])) + } + + return points + } + + @Suppress("NOTHING_TO_INLINE") + internal inline fun DoubleArray.polynomial() = PolynomialFunction(this) + } +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt index b2a67d9ca..cf63f5f62 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurveFitting.kt @@ -1,6 +1,10 @@ package nebulosa.curve.fitting -fun interface CurveFitting { +import nebulosa.curve.fitting.Curve.Companion.curvePoints - fun calculate(points: Collection): FittedCurve +fun interface CurveFitting { + + fun calculate(points: Collection): T + + fun calculate(vararg points: Double) = calculate(points.curvePoints()) } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt index f8135ad04..33e752394 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt @@ -20,4 +20,9 @@ class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) { } override fun toString() = "CurvePoint(x=$x, y=$y)" + + companion object { + + @JvmStatic val ZERO = CurvePoint(0.0, 0.0) + } } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt index 63b6f2254..0fe2f0bf6 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt @@ -1,12 +1,14 @@ package nebulosa.curve.fitting -interface FittedCurve { +interface FittedCurve : Curve { - val minimumX: Double - - val minimumY: Double + val minimum: CurvePoint val rSquared: Double - operator fun invoke(x: Double): Double + val minimumX + get() = minimum.x + + val minimumY + get() = minimum.y } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt new file mode 100644 index 000000000..d845d404e --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt @@ -0,0 +1,120 @@ +package nebulosa.curve.fitting + +import kotlin.math.* + +// https://bitbucket.org/Isbeorn/nina/src/master/NINA.Core.WPF/Utility/AutoFocus/HyperbolicFitting.cs + +data object HyperbolicFitting : CurveFitting { + + data class Curve( + private val a: Double, + private val b: Double, + private val p: Double, + override val minimum: CurvePoint, + private val points: Collection, + ) : FittedCurve { + + override val rSquared by lazy { RSquared.calculate(points, this) } + + override fun value(x: Double) = a * cosh(asinh((p - x) / b)) + } + + override fun calculate(points: Collection): Curve { + var lowestError = Double.MAX_VALUE + + val nonZeroPoints = points.filter { it.y >= 0.1 } + + if (nonZeroPoints.isEmpty()) { + throw IllegalArgumentException("No non-zero points in curve. No fit can be calculated.") + } + + val lowestPoint = nonZeroPoints.minBy { it.y } + val highestPoint = nonZeroPoints.maxBy { it.y } + + var highestPosition = highestPoint.x + val highestHfr = highestPoint.y + val lowestPosition = lowestPoint.x + val lowestHfr = lowestPoint.y + var oldError = Double.MAX_VALUE + + // Always go up + if (highestPosition < lowestPosition) { + highestPosition = 2 * lowestPosition - highestPosition + } + + // Get good starting values for a, b and p. + var a = lowestHfr // a is near the lowest HFR value + // Alternative hyperbola formula: sqr(y)/sqr(a)-sqr(x)/sqr(b)=1 ==> sqr(b)=sqr(x)*sqr(a)/(sqr(y)-sqr(a) + var b = sqrt((highestPosition - lowestPosition) * (highestPosition - lowestPosition) * a * a / (highestHfr * highestHfr - a * a)) + var p = lowestPosition + + var iterationCycles = 0 // How many cycles where used for curve fitting + + var aRange = a + var bRange = b + var pRange = highestPosition - lowestPosition // Large steps since slope could contain some error + + if (aRange.isNaN() || bRange.isNaN() || aRange == 0.0 || bRange == 0.0 || pRange == 0.0) { + throw IllegalArgumentException("Not enough valid data points to fit a curve.") + } + + do { + val p0 = p + val b0 = b + val a0 = a + + // Reduce range by 50% + aRange *= 0.5 + bRange *= 0.5 + pRange *= 0.5 + + // Start value + var p1 = p0 - pRange + + // Position loop + while (p1 <= p0 + pRange) { + var a1 = a0 - aRange + + while (a1 <= a0 + aRange) { + var b1 = b0 - bRange + + while (b1 <= b0 + bRange) { + val error1 = scaledErrorHyperbola(nonZeroPoints, p1, a1, b1) + + // Better position found + if (error1 < lowestError) { + oldError = lowestError + lowestError = error1 + + // Best value up to now + a = a1 + b = b1 + p = p1 + } + + // do 20 steps within range, many steps guarantees convergence + b1 += bRange * 0.1 + } + + a1 += aRange * 0.1 + } + + p1 += pRange * 0.1 + } + } while (oldError - lowestError >= 0.0001 && lowestError > 0.0001 && ++iterationCycles < 30) + + val minimum = CurvePoint(round(p), a) + + return Curve(a, b, p, minimum, nonZeroPoints) + } + + private fun scaledErrorHyperbola(points: Collection, perfectFocusPosition: Double, a: Double, b: Double): Double { + return sqrt(points.sumOf { (hyperbolicFittingHfrCalc(it.x, perfectFocusPosition, a, b) - it.y).pow(2.0) }) + } + + private fun hyperbolicFittingHfrCalc(position: Double, perfectFocusPosition: Double, a: Double, b: Double): Double { + val x = perfectFocusPosition - position + val t = asinh(x / b) // Calculate t-position in hyperbola + return a * cosh(t) // Convert t-position to y/hfd value + } +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/LinearCurve.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/LinearCurve.kt new file mode 100644 index 000000000..e19659785 --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/LinearCurve.kt @@ -0,0 +1,19 @@ +package nebulosa.curve.fitting + +interface LinearCurve : Curve { + + val slope: Double + + val intercept: Double + + val rSquared: Double + + fun intersect(line: TrendLine): CurvePoint { + if (slope == line.slope) return CurvePoint.ZERO + + val x = (line.intercept - intercept) / (slope - line.slope) + val y = slope * x + intercept + + return CurvePoint(x, y) + } +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt index 72bb18bda..f8dbf690c 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt @@ -1,22 +1,27 @@ package nebulosa.curve.fitting -import org.apache.commons.math3.analysis.polynomials.PolynomialFunction +import nebulosa.curve.fitting.Curve.Companion.polynomial +import org.apache.commons.math3.analysis.UnivariateFunction import org.apache.commons.math3.fitting.PolynomialCurveFitter -object QuadraticFitting : CurveFitting { +data object QuadraticFitting : CurveFitting { - private val fitter = PolynomialCurveFitter.create(2) + data class Curve( + private val poly: UnivariateFunction, + override val minimum: CurvePoint, + override val rSquared: Double, + ) : FittedCurve { - override fun calculate(points: Collection) = object : FittedCurve { - - private val poly by lazy { PolynomialFunction(fitter.fit(points)) } - - override val rSquared by lazy { RSquared.calculate(points, poly) } - - override val minimumX by lazy { poly.coefficients[1] / (-2.0 * poly.coefficients[2]) } - - override val minimumY by lazy { this(minimumX) } + override fun value(x: Double) = poly.value(x) + } - override fun invoke(x: Double) = poly.value(x) + override fun calculate(points: Collection) = with(FITTER.fit(points).polynomial()) { + val rSquared = RSquared.calculate(points, this) + val minimumX = coefficients[1] / (-2.0 * coefficients[2]) + val minimumY = value(minimumX) + val minimum = CurvePoint(minimumX, minimumY) + Curve(this, minimum, rSquared) } + + @JvmStatic private val FITTER = PolynomialCurveFitter.create(2) } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt new file mode 100644 index 000000000..3966f897b --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt @@ -0,0 +1,31 @@ +package nebulosa.curve.fitting + +import nebulosa.curve.fitting.Curve.Companion.curvePoints +import org.apache.commons.math3.stat.regression.SimpleRegression + +data class TrendLine(val points: Collection) : LinearCurve { + + constructor(vararg points: Double) : this(points.curvePoints()) + + private val regression = SimpleRegression() + + init { + points.forEach { regression.addData(it.x, it.y) } + } + + override val slope = regression.slope.zeroIfNaN() + + override val intercept = regression.intercept.zeroIfNaN() + + override val rSquared = regression.rSquare.zeroIfNaN() + + override fun value(x: Double) = regression.predict(x) + + companion object { + + @JvmStatic val ZERO = TrendLine() + + @Suppress("NOTHING_TO_INLINE") + private inline fun Double.zeroIfNaN() = if (isNaN()) 0.0 else this + } +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt new file mode 100644 index 000000000..e13c68376 --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt @@ -0,0 +1,28 @@ +package nebulosa.curve.fitting + +data object TrendLineFitting : CurveFitting { + + data class Curve( + val left: TrendLine, + val right: TrendLine, + override val minimum: CurvePoint, + ) : FittedCurve { + + val intersection = left.intersect(right) + + override val rSquared = (left.rSquared + right.rSquared) / 2.0 + + override fun value(x: Double) = if (x < minimum.x) left(x) + else if (x > minimum.x) right(x) + else minimum.y + } + + override fun calculate(points: Collection): Curve { + val minimum = points.minBy { it.y } + + val left = TrendLine(points.filter { it.x < minimum.x && it.y > minimum.y + 0.1 }) + val right = TrendLine(points.filter { it.x > minimum.x && it.y > minimum.y + 0.1 }) + + return Curve(left, right, minimum) + } +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/ZeroUnivariateFunction.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/ZeroUnivariateFunction.kt new file mode 100644 index 000000000..7b3ab106f --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/ZeroUnivariateFunction.kt @@ -0,0 +1,8 @@ +package nebulosa.curve.fitting + +import org.apache.commons.math3.analysis.UnivariateFunction + +object ZeroUnivariateFunction : UnivariateFunction { + + override fun value(x: Double) = 0.0 +} diff --git a/nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt b/nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt new file mode 100644 index 000000000..6709f570e --- /dev/null +++ b/nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt @@ -0,0 +1,31 @@ +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.shouldBe +import nebulosa.curve.fitting.CurveFitting +import nebulosa.curve.fitting.HyperbolicFitting + +class HyperbolicFittingTest : StringSpec(), CurveFitting by HyperbolicFitting { + + init { + "perfect V-curve with only one minimum point" { + val curve = calculate( + 1.0, 18.0, 2.0, 11.0, 3.0, 6.0, 4.0, 3.0, 5.0, 2.0, + 6.0, 3.0, 7.0, 6.0, 8.0, 11.0, 9.0, 18.0, + ) + + curve.minimumX shouldBe (5.0 plusOrMinus 1e-12) + curve.minimumY shouldBe (1.2 plusOrMinus 1e-12) + } + "bad data:prevent infinit loop" { + shouldThrow { calculate(1000.0, 18.0, 1100.0, 0.0, 1200.0, 0.0) } + .message shouldBe "Not enough valid data points to fit a curve." + shouldThrow { calculate(1000.0, 18.0, 1000.0, 18.0, 1000.0, 18.0, 1100.0, 0.0, 1200.0, 0.0) } + .message shouldBe "Not enough valid data points to fit a curve." + shouldThrow { calculate(900.0, 18.0, 1000.0, 18.0, 1000.0, 18.0, 1100.0, 0.0, 1200.0, 0.0) } + .message shouldBe "Not enough valid data points to fit a curve." + shouldThrow { calculate(800.0, 18.0, 900.0, 0.0, 1000.0, 0.0, 1000.0, 18.0, 1100.0, 0.0, 1200.0, 0.0) } + .message shouldBe "Not enough valid data points to fit a curve." + } + } +} diff --git a/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt b/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt index 816381abf..6746d690b 100644 --- a/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt +++ b/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt @@ -2,28 +2,20 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.doubles.shouldBeExactly import io.kotest.matchers.shouldBe -import nebulosa.curve.fitting.CurvePoint +import nebulosa.curve.fitting.CurveFitting import nebulosa.curve.fitting.QuadraticFitting -class QuadraticFittingTest : StringSpec() { +class QuadraticFittingTest : StringSpec(), CurveFitting by QuadraticFitting { init { "perfect V-curve" { // (x-5)² + 2 - val points = listOf( - CurvePoint(1.0, 18.0), - CurvePoint(2.0, 11.0), - CurvePoint(3.0, 6.0), - CurvePoint(4.0, 3.0), - CurvePoint(5.0, 2.0), - CurvePoint(6.0, 3.0), - CurvePoint(7.0, 6.0), - CurvePoint(8.0, 11.0), - CurvePoint(9.0, 18.0), + val curve = calculate( + 1.0, 18.0, 2.0, 11.0, 3.0, 6.0, + 4.0, 3.0, 5.0, 2.0, 6.0, 3.0, + 7.0, 6.0, 8.0, 11.0, 9.0, 18.0, ) - val curve = QuadraticFitting.calculate(points) - curve(5.0) shouldBeExactly 2.0 curve.minimumX shouldBe (5.0 plusOrMinus 1e-12) curve.minimumY shouldBe (2.0 plusOrMinus 1e-12) diff --git a/nebulosa-curve-fitting/src/test/kotlin/TrendLineFittingTest.kt b/nebulosa-curve-fitting/src/test/kotlin/TrendLineFittingTest.kt new file mode 100644 index 000000000..4cec0f9e0 --- /dev/null +++ b/nebulosa-curve-fitting/src/test/kotlin/TrendLineFittingTest.kt @@ -0,0 +1,32 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.doubles.shouldBeExactly +import io.kotest.matchers.shouldBe +import nebulosa.curve.fitting.CurveFitting +import nebulosa.curve.fitting.TrendLineFitting + +class TrendLineFittingTest : StringSpec(), CurveFitting by TrendLineFitting { + + init { + "perfect V-curve with only one minimum point" { + val curve = calculate( + 1.0, 10.0, 2.0, 8.0, 3.0, 6.0, 4.0, 4.0, // left + 9.0, 10.0, 8.0, 8.0, 7.0, 6.0, 6.0, 4.0, // right + 5.0, 2.0, // tip + ) + + curve.intersection.x shouldBeExactly 5.0 + curve.intersection.y shouldBeExactly 2.0 + } + "perfect V-curve with flat tip with multiple points" { + val curve = calculate( + 1.0, 10.0, 2.0, 8.0, 3.0, 6.0, 4.0, 4.0, // left + 11.0, 10.0, 10.0, 8.0, 9.0, 6.0, 8.0, 4.0, // right + 5.0, 2.1, 6.0, 2.0, 7.0, 2.1, // tip + ) + + curve.intersection.x shouldBe (6.0 plusOrMinus 1e-12) + curve.intersection.y shouldBe (0.0 plusOrMinus 1e-12) + } + } +} diff --git a/nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt b/nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt new file mode 100644 index 000000000..76fa13c1f --- /dev/null +++ b/nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt @@ -0,0 +1,33 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.doubles.shouldBeExactly +import nebulosa.curve.fitting.TrendLine + +class TrendLineTest : StringSpec() { + + init { + "no points" { + val line = TrendLine() + + line.slope shouldBeExactly 0.0 + line.intercept shouldBeExactly 0.0 + } + "one point" { + val line = TrendLine(5.0, 5.0) + + line.slope shouldBeExactly 0.0 + line.intercept shouldBeExactly 0.0 + } + "two points" { + val line = TrendLine(0.0, 0.0, 1.0, 1.0) + + line.slope shouldBeExactly 1.0 + line.intercept shouldBeExactly 0.0 + } + "multiple points" { + val line = TrendLine(1.0, 10.0, 2.0, 8.0, 3.0, 6.0, 4.0, 4.0) + + line.slope shouldBeExactly -2.0 + line.intercept shouldBeExactly 12.0 + } + } +} From 52917a3600fafabe8dfd8342c097b1a0f807cf95 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 21 May 2024 13:21:37 -0300 Subject: [PATCH 05/45] [api]: Bump Kotlin from 2.0.0-RC3 to 2.0.0 --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index dd0f875a7..686ead046 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,8 +6,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0-RC3") - classpath("org.jetbrains.kotlin:kotlin-allopen:2.0.0-RC3") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0") + classpath("org.jetbrains.kotlin:kotlin-allopen:2.0.0") classpath("com.adarshr:gradle-test-logger-plugin:4.0.0") classpath("io.objectbox:objectbox-gradle-plugin:4.0.0") } From 4b179c934e457d4ddbbe54a5e5a5f1ac5f2dc8d2 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 21 May 2024 18:45:33 -0300 Subject: [PATCH 06/45] [desktop]: Improve Settings layout --- desktop/app/main.ts | 5 ++- desktop/settings.png | Bin 13661 -> 13205 bytes desktop/src/app/app.component.scss | 5 +++ desktop/src/app/atlas/atlas.component.scss | 3 -- .../src/app/settings/settings.component.html | 42 +++++++++--------- .../shared/services/browser-window.service.ts | 4 +- desktop/src/shared/types/app.types.ts | 1 + 7 files changed, 32 insertions(+), 28 deletions(-) diff --git a/desktop/app/main.ts b/desktop/app/main.ts index d671c4c9e..1b854f570 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -131,7 +131,8 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { } } - const width = options.width ? Math.trunc(computeWidth(options.width)) : 320 + const minWidth = options.minWidth ?? 0 + const width = Math.max(minWidth, options.width ? Math.trunc(computeWidth(options.width)) : 320) function computeHeight(value: number | string) { if (typeof value === 'number') { @@ -172,7 +173,7 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { frame: false, modal, parent, width: savedSize?.width || width, height: savedSize?.height || height, - minHeight, + minWidth, minHeight, x: savedPosition?.x ?? undefined, y: savedPosition?.y ?? undefined, resizable: serve || resizable, diff --git a/desktop/settings.png b/desktop/settings.png index 706c39806ac96061e9db748d58abbc7e555382d4..ee6099d48c083968b25954a0b02888d6549f8d19 100644 GIT binary patch literal 13205 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 F{{lDNist|T diff --git a/desktop/src/app/app.component.scss b/desktop/src/app/app.component.scss index 011a140d7..67a9efabd 100644 --- a/desktop/src/app/app.component.scss +++ b/desktop/src/app/app.component.scss @@ -24,4 +24,9 @@ width: 16px; } } + + #main { + margin-left: 2px; + margin-right: 2px; + } } \ No newline at end of file diff --git a/desktop/src/app/atlas/atlas.component.scss b/desktop/src/app/atlas/atlas.component.scss index 5f6c0f61e..99334642c 100644 --- a/desktop/src/app/atlas/atlas.component.scss +++ b/desktop/src/app/atlas/atlas.component.scss @@ -5,9 +5,6 @@ ::ng-deep { .p-tabview { - padding-left: 0.21rem; - padding-right: 0.21rem; - p-table.planet .p-datatable-wrapper { height: 229px; } diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 88f68937b..4286a762d 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -1,7 +1,7 @@
- - -
+ + +
-
- -
+ + +
+ + + + + + + + - - - - @@ -48,13 +54,7 @@ (ngModelChange)="solvers.get(solverType)!.apiKey = $event; save()" /> - - - -
- - +
+
\ No newline at end of file diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index f1f890a56..f9398cdcf 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -110,8 +110,8 @@ export class BrowserWindowService { } openSettings(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'settings', width: 435, height: 460 }) - this.openWindow({ ...options, id: 'settings', path: 'settings', data: undefined, resizable: true, minHeight: 460, autoResizable: false }) + Object.assign(options, { icon: 'settings', width: 490, height: 460 }) + this.openWindow({ ...options, id: 'settings', path: 'settings', data: undefined, resizable: true, minWidth: 490, minHeight: 460, autoResizable: false }) } openCalculator(options: OpenWindowOptions = {}) { diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts index 6a76b99e1..2c1ebb5f5 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -36,6 +36,7 @@ export interface OpenWindowOptions { height?: number | string bringToFront?: boolean requestFocus?: boolean + minWidth?: number minHeight?: number } From 617bf572f0d451c4b60c85faf686075ee554fc81 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 21 May 2024 23:34:48 -0300 Subject: [PATCH 07/45] [api]: Improve Curve Fitting algorithm --- .../src/main/kotlin/nebulosa/curve/fitting/Curve.kt | 7 ++----- .../main/kotlin/nebulosa/curve/fitting/FittedCurve.kt | 6 ------ .../kotlin/nebulosa/curve/fitting/PolynomialCurve.kt | 10 ++++++++++ .../kotlin/nebulosa/curve/fitting/QuadraticFitting.kt | 11 ++++------- .../main/kotlin/nebulosa/curve/fitting/RSquared.kt | 1 + .../main/kotlin/nebulosa/curve/fitting/TrendLine.kt | 11 ++++------- .../src/test/kotlin/HyperbolicFittingTest.kt | 4 ++-- .../src/test/kotlin/QuadraticFittingTest.kt | 4 ++-- .../src/test/kotlin/TrendLineTest.kt | 2 +- 9 files changed, 26 insertions(+), 30 deletions(-) create mode 100644 nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/PolynomialCurve.kt diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt index 0d505e9ad..d0ef91549 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/Curve.kt @@ -1,7 +1,6 @@ package nebulosa.curve.fitting import org.apache.commons.math3.analysis.UnivariateFunction -import org.apache.commons.math3.analysis.polynomials.PolynomialFunction fun interface Curve : UnivariateFunction { @@ -9,7 +8,8 @@ fun interface Curve : UnivariateFunction { companion object { - internal fun DoubleArray.curvePoints(): Collection { + @JvmStatic + fun DoubleArray.curvePoints(): Collection { val points = ArrayList(size / 2) for (i in indices step 2) { @@ -18,8 +18,5 @@ fun interface Curve : UnivariateFunction { return points } - - @Suppress("NOTHING_TO_INLINE") - internal inline fun DoubleArray.polynomial() = PolynomialFunction(this) } } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt index 0fe2f0bf6..916e220bd 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/FittedCurve.kt @@ -5,10 +5,4 @@ interface FittedCurve : Curve { val minimum: CurvePoint val rSquared: Double - - val minimumX - get() = minimum.x - - val minimumY - get() = minimum.y } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/PolynomialCurve.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/PolynomialCurve.kt new file mode 100644 index 000000000..ccc153551 --- /dev/null +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/PolynomialCurve.kt @@ -0,0 +1,10 @@ +package nebulosa.curve.fitting + +import org.apache.commons.math3.analysis.UnivariateFunction + +interface PolynomialCurve : Curve { + + val polynomial: UnivariateFunction + + override fun value(x: Double) = polynomial.value(x) +} diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt index f8dbf690c..8d0d30345 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/QuadraticFitting.kt @@ -1,21 +1,18 @@ package nebulosa.curve.fitting -import nebulosa.curve.fitting.Curve.Companion.polynomial import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.analysis.polynomials.PolynomialFunction import org.apache.commons.math3.fitting.PolynomialCurveFitter data object QuadraticFitting : CurveFitting { data class Curve( - private val poly: UnivariateFunction, + override val polynomial: UnivariateFunction, override val minimum: CurvePoint, override val rSquared: Double, - ) : FittedCurve { + ) : FittedCurve, PolynomialCurve - override fun value(x: Double) = poly.value(x) - } - - override fun calculate(points: Collection) = with(FITTER.fit(points).polynomial()) { + override fun calculate(points: Collection) = with(PolynomialFunction(FITTER.fit(points))) { val rSquared = RSquared.calculate(points, this) val minimumX = coefficients[1] / (-2.0 * coefficients[2]) val minimumY = value(minimumX) diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt index ae7b71479..cb2d70493 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/RSquared.kt @@ -6,6 +6,7 @@ import kotlin.math.pow object RSquared { + @JvmStatic fun calculate(points: Collection, function: UnivariateFunction): Double { val descriptiveStatistics = DescriptiveStatistics(points.size) val predictedValues = DoubleArray(points.size) diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt index 3966f897b..de735fc17 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLine.kt @@ -13,19 +13,16 @@ data class TrendLine(val points: Collection) : LinearCurve { points.forEach { regression.addData(it.x, it.y) } } - override val slope = regression.slope.zeroIfNaN() + override val slope = regression.slope.let { if (it.isNaN()) 0.0 else it } - override val intercept = regression.intercept.zeroIfNaN() + override val intercept = regression.intercept.let { if (it.isNaN()) 0.0 else it } - override val rSquared = regression.rSquare.zeroIfNaN() + override val rSquared = regression.rSquare.let { if (it.isNaN()) 0.0 else it } - override fun value(x: Double) = regression.predict(x) + override fun value(x: Double) = if (points.isEmpty()) 0.0 else regression.predict(x) companion object { @JvmStatic val ZERO = TrendLine() - - @Suppress("NOTHING_TO_INLINE") - private inline fun Double.zeroIfNaN() = if (isNaN()) 0.0 else this } } diff --git a/nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt b/nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt index 6709f570e..22830f606 100644 --- a/nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt +++ b/nebulosa-curve-fitting/src/test/kotlin/HyperbolicFittingTest.kt @@ -14,8 +14,8 @@ class HyperbolicFittingTest : StringSpec(), CurveFitting { calculate(1000.0, 18.0, 1100.0, 0.0, 1200.0, 0.0) } diff --git a/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt b/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt index 6746d690b..6839b07b6 100644 --- a/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt +++ b/nebulosa-curve-fitting/src/test/kotlin/QuadraticFittingTest.kt @@ -17,8 +17,8 @@ class QuadraticFittingTest : StringSpec(), CurveFitting ) curve(5.0) shouldBeExactly 2.0 - curve.minimumX shouldBe (5.0 plusOrMinus 1e-12) - curve.minimumY shouldBe (2.0 plusOrMinus 1e-12) + curve.minimum.x shouldBe (5.0 plusOrMinus 1e-12) + curve.minimum.y shouldBe (2.0 plusOrMinus 1e-12) curve.rSquared shouldBe (1.0 plusOrMinus 1e-12) } } diff --git a/nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt b/nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt index 76fa13c1f..45a69d859 100644 --- a/nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt +++ b/nebulosa-curve-fitting/src/test/kotlin/TrendLineTest.kt @@ -6,7 +6,7 @@ class TrendLineTest : StringSpec() { init { "no points" { - val line = TrendLine() + val line = TrendLine.ZERO line.slope shouldBeExactly 0.0 line.intercept shouldBeExactly 0.0 From 31d139102fd408e38be69779744ac4ebb9a9bfd1 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 22 May 2024 00:15:13 -0300 Subject: [PATCH 08/45] [api]: Fix build failure --- .../api/calibration/CalibrationFrameEntity.kt | 5 ++++- .../guiding/internal/GuideCalibrator.kt | 4 ++-- .../nebulosa/guiding/internal/GuidePoint.kt | 22 ------------------- .../guiding/internal/MultiStarGuider.kt | 12 +++++----- .../src/main/kotlin/nebulosa/math/Point2D.kt | 15 ++++++++----- .../nebulosa/nova/astrometry/ELPMPP02.kt | 1 - .../nebulosa/nova/astrometry/FixedStar.kt | 3 +-- 7 files changed, 22 insertions(+), 40 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt index cfc214269..631dc5b9e 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt @@ -1,6 +1,9 @@ package nebulosa.api.calibration -import io.objectbox.annotation.* +import io.objectbox.annotation.Convert +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Index import nebulosa.api.beans.converters.database.FrameTypePropertyConverter import nebulosa.api.beans.converters.database.PathPropertyConverter import nebulosa.api.database.BoxEntity diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt index fe19b05c5..336196527 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt @@ -677,12 +677,12 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { @JvmStatic private fun mountCoords(camera: Point, x: Angle, y: Angle): Point { - val hyp = camera.distance + val length = camera.length val cameraTheta = camera.angle val yAngleError = ((x - y) + PIOVERTWO).normalized - PI val xAngle = cameraTheta - x val yAngle = cameraTheta - (x + yAngleError) - return Point(hyp * xAngle.cos, hyp * yAngle.sin) + return Point(length * xAngle.cos, length * yAngle.sin) } } } diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuidePoint.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuidePoint.kt index 54a4e921d..bb9a53f51 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuidePoint.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuidePoint.kt @@ -1,30 +1,8 @@ package nebulosa.guiding.internal -import nebulosa.math.Angle import nebulosa.math.Point2D -import nebulosa.math.rad -import kotlin.math.atan2 -import kotlin.math.hypot interface GuidePoint : Point2D { val valid: Boolean - - fun dX(point: Point2D): Double { - return x - point.x - } - - fun dY(point: Point2D): Double { - return y - point.y - } - - val distance - get() = hypot(x, y) - - val angle - get() = atan2(y, x).rad - - fun angle(point: Point2D): Angle { - return atan2(dY(point), dX(point)).rad - } } diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt index 6462014ae..8992bcc7e 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt @@ -264,7 +264,7 @@ class MultiStarGuider : InternalGuider { return if (lockPosition(newLockPosition)) { // Update average distance right away so GetCurrentDistance // reflects the increased distance from the dither. - val dist = cameraDelta.distance + val dist = cameraDelta.length val distRA = abs(mountDelta.x) avgDistance += dist avgDistanceLong += dist @@ -276,7 +276,7 @@ class MultiStarGuider : InternalGuider { ditherRecenterDir[0] = if (mountDelta.x < 0.0) 1.0 else -1.0 ditherRecenterDir[1] = if (mountDelta.y < 0.0) 1.0 else -1.0 // Make each step a bit less than the full search region distance to avoid losing the star. - val f = (searchRegion * 0.7) / ditherRecenterRemaining.distance + val f = (searchRegion * 0.7) / ditherRecenterRemaining.length ditherRecenterStep.set(f * ditherRecenterRemaining.x, f * ditherRecenterRemaining.y) } @@ -1107,21 +1107,21 @@ class MultiStarGuider : InternalGuider { private fun transformMountCoordinatesToCameraCoordinates(mount: Point, camera: Point): Boolean { if (!mount.valid) return false - val distance = mount.distance + val length = mount.length var mountTheta = mount.angle if (abs(guideCalibrator.yAngleError) > PIOVERTWO) mountTheta = -mountTheta val xAngle = mountTheta + guideCalibrator.xAngle - camera.set(xAngle.cos * distance, xAngle.sin * distance) + camera.set(xAngle.cos * length, xAngle.sin * length) return true } private fun transformCameraCoordinatesToMountCoordinates(camera: Point, mount: Point): Boolean { if (!camera.valid) return false - val distance = camera.distance + val length = camera.length val cameraTheta = camera.angle val xAngle = cameraTheta - guideCalibrator.xAngle val yAngle = cameraTheta - (guideCalibrator.xAngle + guideCalibrator.yAngleError) - mount.set(xAngle.cos * distance, yAngle.sin * distance) + mount.set(xAngle.cos * length, yAngle.sin * length) return true } diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt index 2fb90a0bd..988209142 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt @@ -16,13 +16,16 @@ interface Point2D { val length get() = hypot(x, y) - fun distance(other: Point2D): Double { - return hypot(x - other.x, y - other.y) - } + val angle + get() = atan2(y, x) - fun angle(other: Point2D): Angle { - return atan2(other.y - y, other.x - x) - } + fun dX(point: Point2D) = x - point.x + + fun dY(point: Point2D) = y - point.y + + fun distance(other: Point2D) = hypot(x - other.x, y - other.y) + + fun angle(other: Point2D): Angle = atan2(other.y - y, other.x - x) data class XY(override val x: Double, override val y: Double) : Point2D diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/ELPMPP02.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/ELPMPP02.kt index 9061cdeca..6487f5867 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/ELPMPP02.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/ELPMPP02.kt @@ -6,7 +6,6 @@ import nebulosa.io.bufferedResource import nebulosa.math.Matrix3D import nebulosa.math.Vector3D import nebulosa.math.normalized -import nebulosa.math.pmod import nebulosa.time.InstantOfTime import kotlin.math.atan2 import kotlin.math.cos diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/FixedStar.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/FixedStar.kt index 6bfe76dd9..15ba0e91a 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/FixedStar.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/FixedStar.kt @@ -50,8 +50,7 @@ data class FixedStar( // unit vector "u1", divided by the speed of light. val lightTime = u1.dot(observer.position) / SPEED_OF_LIGHT_AU_DAY val position = (positionAndVelocity.position + positionAndVelocity.velocity * - (observer.time.tdb.whole - epoch.tdb.whole + lightTime + observer.time.tdb.fraction - epoch.tdb.fraction) - - observer.position) + (observer.time.tdb.whole - epoch.tdb.whole + lightTime + observer.time.tdb.fraction - epoch.tdb.fraction) - observer.position) return PositionAndVelocity(position, observer.velocity - positionAndVelocity.velocity) } From 1255526a43100ed0de01e55348db5efd5c00770f Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 22 May 2024 15:06:32 -0300 Subject: [PATCH 09/45] [desktop]: Apply ROI for a selected camera --- desktop/app/main.ts | 2 +- desktop/src/app/camera/camera.component.ts | 11 ++++ desktop/src/app/image/image.component.html | 7 ++- desktop/src/app/image/image.component.scss | 25 +++++---- desktop/src/app/image/image.component.ts | 52 ++++++++++++++++--- .../device-list-menu.component.ts | 2 +- .../src/shared/services/electron.service.ts | 28 +++++++--- desktop/src/shared/types/app.types.ts | 4 +- desktop/src/shared/types/image.types.ts | 8 +++ desktop/src/shared/types/wheel.types.ts | 5 ++ 10 files changed, 111 insertions(+), 33 deletions(-) diff --git a/desktop/app/main.ts b/desktop/app/main.ts index 1b854f570..f631e27e1 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -543,7 +543,7 @@ try { return false }) - const events: InternalEventType[] = ['WHEEL.RENAMED', 'LOCATION.CHANGED', 'CALIBRATION.CHANGED'] + const events: InternalEventType[] = ['WHEEL.RENAMED', 'LOCATION.CHANGED', 'CALIBRATION.CHANGED', 'ROI.SELECTED'] for (const item of events) { ipcMain.handle(item, (_, data) => { diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 0bf5944ec..a25848f40 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -234,6 +234,17 @@ export class CameraComponent implements AfterContentInit, OnDestroy { ngZone.run(() => this.loadCalibrationGroups()) }) + electron.on('ROI.SELECTED', event => { + if (event.camera.id === this.camera.id) { + ngZone.run(() => { + this.request.x = event.x + this.request.y = event.y + this.request.width = event.width + this.request.height = event.height + }) + } + }) + this.cameraModel[1].visible = !app.modal } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index ed6372920..3b8573ef4 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -40,10 +40,15 @@
- X: {{ imageROI.x }} Y: {{ imageROI.y }} W: {{ imageROI.width }} H: {{ imageROI.height }}
+
+ X: {{ imageROI.x }} Y: {{ imageROI.y }} W: {{ imageROI.width }} H: {{ imageROI.height }} + +
+ diff --git a/desktop/src/app/image/image.component.scss b/desktop/src/app/image/image.component.scss index 088176a61..9d5a0981c 100644 --- a/desktop/src/app/image/image.component.scss +++ b/desktop/src/app/image/image.component.scss @@ -12,22 +12,21 @@ .roi { width: 128px; height: 128px; - border: 1px solid #FFFC; + border: 1px dashed rgba(255, 255, 0, 0.9); background-color: transparent; box-sizing: border-box; +} - >span { - background: #0008; - padding: 4px; - border-radius: 4px; - font-size: 8px !important; - min-width: 71px; - max-width: 144px; - bottom: -22px; - left: 50%; - white-space: nowrap; - transform: translate(-50%, 0%); - } +.roi-coordinates { + background: rgba(0, 0, 0, 0.5); + padding: 4px 8px; + border-radius: 2px; + font-size: 12px !important; + min-width: 71px; + top: 46px; + left: 50%; + white-space: nowrap; + transform: translate(-50%, 0%); } .coordinates { diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 4440b1f58..cea750ec0 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -17,6 +17,7 @@ import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { CheckableMenuItem, ToggleableMenuItem } from '../../shared/types/app.types' import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from '../../shared/types/atlas.types' +import { Camera } from '../../shared/types/camera.types' import { DEFAULT_FOV, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageDetectStars, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' import { DEFAULT_SOLVER_TYPES } from '../../shared/types/settings.types' @@ -154,8 +155,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { readonly imageROI: ImageROI = { x: 0, y: 0, - width: 0, - height: 0 + width: 128, + height: 128 } readonly saveAs: ImageSaveDialog = { @@ -283,7 +284,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-telescope', disabled: true, command: () => { - this.executeMount((mount) => { + this.executeMount(mount => { this.api.pointMountHere(mount, this.imageData.path!, this.imageMouseX, this.imageMouseY) }) }, @@ -670,6 +671,17 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }) } + roiForCamera() { + this.executeCamera(camera => { + const x = camera.x + this.imageROI.x + const y = camera.y + this.imageROI.y + const width = camera.binX * this.imageROI.width + const height = camera.binY * this.imageROI.height + + this.electron.send('ROI.SELECTED', { camera, x, y, width, height }) + }, false) + } + private loadImageFromData(data: ImageData) { console.info('loading image from data: %s', data) @@ -961,19 +973,19 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } mountSync(coordinate: EquatorialCoordinateJ2000) { - this.executeMount((mount) => { + this.executeMount(mount => { this.api.mountSync(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) }) } async mountGoTo(coordinate: EquatorialCoordinateJ2000) { - this.executeMount((mount) => { + this.executeMount(mount => { this.api.mountGoTo(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) }) } async mountSlew(coordinate: EquatorialCoordinateJ2000) { - this.executeMount((mount) => { + this.executeMount(mount => { this.api.mountSlew(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) }) } @@ -1132,8 +1144,31 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.preference.imagePreference.set(preference) } - private async executeMount(action: (mount: Mount) => void) { - if (await this.prime.confirm('Are you sure that you want to proceed?')) { + private async executeCamera(action: (camera: Camera) => void, showConfirmation: boolean = true) { + if (showConfirmation && await this.prime.confirm('Are you sure that you want to proceed?')) { + return + } + + const cameras = await this.api.cameras() + + if (cameras.length === 1) { + action(cameras[0]) + return true + } else { + this.deviceMenu.header = 'Cameras' + const camera = await this.deviceMenu.show(cameras) + + if (camera && camera !== 'NONE' && camera.connected) { + action(camera) + return true + } + } + + return false + } + + private async executeMount(action: (mount: Mount) => void, showConfirmation: boolean = true) { + if (showConfirmation && await this.prime.confirm('Are you sure that you want to proceed?')) { return } @@ -1143,6 +1178,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { action(mounts[0]) return true } else { + this.deviceMenu.header = 'Mounts' const mount = await this.deviceMenu.show(mounts) if (mount && mount !== 'NONE' && mount.connected) { diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts index a56ad7979..e395a818a 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts @@ -68,7 +68,7 @@ export class DeviceListMenuComponent { for (const device of devices.sort(deviceComparator)) { model.push({ - icon: 'mdi mdi-connection', + icon: 'mdi mdi-circle-medium ' + (device.connected ? 'text-green-500' : 'text-red-500'), label: device.name, checked: selected === device, disabled: this.disableIfDeviceIsNotConnected && !device.connected, diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 58a2c58d2..8c7cbea52 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -8,8 +8,8 @@ import * as childProcess from 'child_process' import { ipcRenderer, webFrame } from 'electron' import * as fs from 'fs' import { DARVEvent, TPPAEvent } from '../types/alignment.types' -import { ApiEventType, DeviceMessageEvent } from '../types/api.types' -import { CloseWindow, InternalEventType, JsonFile, OpenDirectory, OpenFile, SaveJson } from '../types/app.types' +import { DeviceMessageEvent } from '../types/api.types' +import { CloseWindow, JsonFile, OpenDirectory, OpenFile, SaveJson } from '../types/app.types' import { Location, SkyAtlasUpdated } from '../types/atlas.types' import { Camera, CameraCaptureEvent } from '../types/camera.types' import { INDIMessageEvent } from '../types/device.types' @@ -17,11 +17,11 @@ import { FlatWizardEvent } from '../types/flat-wizard.types' import { Focuser } from '../types/focuser.types' import { GuideOutput, Guider, GuiderHistoryStep, GuiderMessageEvent } from '../types/guider.types' import { ConnectionClosed } from '../types/home.types' +import { ROISelected } from '../types/image.types' import { Mount } from '../types/mount.types' import { Rotator } from '../types/rotator.types' import { SequencerEvent } from '../types/sequencer.types' -import { FilterWheel } from '../types/wheel.types' -import { ApiService } from './api.service' +import { FilterWheel, WheelRenamed } from '../types/wheel.types' type EventMappedType = { 'DEVICE.PROPERTY_CHANGED': INDIMessageEvent @@ -60,6 +60,20 @@ type EventMappedType = { 'CONNECTION.CLOSED': ConnectionClosed 'SKY_ATLAS.PROGRESS_CHANGED': SkyAtlasUpdated 'CALIBRATION.CHANGED': unknown + 'FILE.OPEN': OpenFile + 'FILE.SAVE': OpenFile + 'DIRECTORY.OPEN': OpenDirectory + 'JSON.WRITE': JsonFile + 'JSON.READ': string + 'WINDOW.RESIZE': number + 'WINDOW.PIN': unknown + 'WINDOW.UNPIN': unknown + 'WINDOW.MINIMIZE': unknown + 'WINDOW.MAXIMIZE': unknown + 'WINDOW.FULLSCREEN': boolean + 'WINDOW.CLOSE': CloseWindow + 'WHEEL.RENAMED': WheelRenamed + 'ROI.SELECTED': ROISelected } @Injectable({ providedIn: 'root' }) @@ -70,7 +84,7 @@ export class ElectronService { childProcess!: typeof childProcess fs!: typeof fs - constructor(private api: ApiService) { + constructor() { if (this.isElectron) { this.ipcRenderer = (window as any).require('electron').ipcRenderer this.webFrame = (window as any).require('electron').webFrame @@ -98,8 +112,8 @@ export class ElectronService { return !!(window && window.process && window.process.type) } - send(channel: ApiEventType | InternalEventType, ...data: any[]) { - return this.ipcRenderer.invoke(channel, ...data) + send(channel: K, data?: EventMappedType[K]) { + return this.ipcRenderer.invoke(channel, data) } on(channel: K, listener: (arg: EventMappedType[K]) => void) { diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts index 2c1ebb5f5..59325b479 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -24,7 +24,7 @@ export const INTERNAL_EVENT_TYPES = [ 'DIRECTORY.OPEN', 'FILE.OPEN', 'FILE.SAVE', 'WINDOW.OPEN', 'WINDOW.CLOSE', 'WINDOW.PIN', 'WINDOW.UNPIN', 'WINDOW.MINIMIZE', 'WINDOW.MAXIMIZE', 'WINDOW.RESIZE', 'WHEEL.RENAMED', 'LOCATION.CHANGED', 'JSON.WRITE', 'JSON.READ', - 'CALIBRATION.CHANGED', 'WINDOW.FULLSCREEN' + 'CALIBRATION.CHANGED', 'WINDOW.FULLSCREEN', 'ROI.SELECTED' ] as const export type InternalEventType = (typeof INTERNAL_EVENT_TYPES)[number] @@ -51,7 +51,7 @@ export interface OpenWindow extends OpenWindowOptionsWithData { autoResizable?: boolean } -export interface CloseWindow { +export interface CloseWindow { id?: string data?: T } diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index e169ab0d7..37f87cc93 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -267,3 +267,11 @@ export interface ImageAnnotationDialog { minorPlanetsMagLimit: number useSimbad: boolean } + +export interface ROISelected { + camera: Camera + x: number + y: number + width: number + height: number +} \ No newline at end of file diff --git a/desktop/src/shared/types/wheel.types.ts b/desktop/src/shared/types/wheel.types.ts index 00e2f2ec2..bd4928393 100644 --- a/desktop/src/shared/types/wheel.types.ts +++ b/desktop/src/shared/types/wheel.types.ts @@ -38,3 +38,8 @@ export interface FilterSlot { name: string dark: boolean } + +export interface WheelRenamed { + wheel: FilterWheel + filter: FilterSlot +} \ No newline at end of file From 203ea558bc7b0e48b6556ec03d7529cfd54ab36d Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 22 May 2024 16:35:19 -0300 Subject: [PATCH 10/45] [desktop]: Add icon attributions on About --- desktop/src/app/about/about.component.html | 55 +++++++++++++++--- desktop/src/assets/icons/CREDITS.md | 32 ---------- desktop/src/assets/icons/histogram.png | Bin 1570 -> 0 bytes desktop/src/assets/icons/location.png | Bin 3206 -> 0 bytes desktop/src/assets/icons/online-search.png | Bin 4092 -> 0 bytes desktop/src/assets/icons/remote-control.png | Bin 2580 -> 0 bytes .../shared/services/browser-window.service.ts | 2 +- 7 files changed, 49 insertions(+), 40 deletions(-) delete mode 100644 desktop/src/assets/icons/CREDITS.md delete mode 100644 desktop/src/assets/icons/histogram.png delete mode 100644 desktop/src/assets/icons/location.png delete mode 100644 desktop/src/assets/icons/online-search.png delete mode 100644 desktop/src/assets/icons/remote-control.png diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index c59cd2d6a..ef1d28fa2 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -11,13 +11,54 @@

-

© 2022-2024 Tiago Melo All rights reserved

-

This software is WIP, comes with absolutely no warranty and the copyright holder is not liable or responsible for anything.

- - +
+ + + +
\ No newline at end of file diff --git a/desktop/src/assets/icons/CREDITS.md b/desktop/src/assets/icons/CREDITS.md deleted file mode 100644 index ff387b1b3..000000000 --- a/desktop/src/assets/icons/CREDITS.md +++ /dev/null @@ -1,32 +0,0 @@ -# Icons by [FlatIcon](https://www.flaticon.com/) - -* https://www.flaticon.com/free-icon/galaxy_1534067 -* https://www.flaticon.com/free-icon/information_9195785 -* https://www.flaticon.com/free-icon/remote-control_3567313 -* https://www.flaticon.com/free-icon/sky_3982229 -* https://www.flaticon.com/free-icon/histogram_2709742 -* https://www.flaticon.com/free-icon/target_3207593 -* https://www.flaticon.com/free-icon/camera-lens_5708327 -* https://www.flaticon.com/free-icon/telescope_4011463 -* https://www.flaticon.com/free-icon/observatory_2256076 -* https://www.flaticon.com/free-icon/focus_3801224 -* https://www.flaticon.com/free-icon/switch_404449 -* https://www.flaticon.com/free-icon/image_4371206 -* https://www.flaticon.com/free-icon/image-processing_6062419 -* https://www.flaticon.com/free-icon/star_740882 -* https://www.flaticon.com/free-icon/rotate_3303063 -* https://www.flaticon.com/free-icon/location_4631354 -* https://www.flaticon.com/free-icon/rgb-print_7664547 -* https://www.flaticon.com/free-icon/cogwheel_3953226 -* https://www.flaticon.com/free-icon/target_10542035 -* https://www.flaticon.com/free-icon/contrast_439842 -* https://www.flaticon.com/free-icon/full-moon_9689786 -* https://www.flaticon.com/free-icon/jupiter_1086078 -* https://www.flaticon.com/free-icon/asteroid_1086068 -* https://www.flaticon.com/free-icon/satellite_1086093 -* https://www.flaticon.com/free-icon/stack_3342239 -* https://www.flaticon.com/free-icon/witch-hat_5606276 -* https://www.flaticon.com/free-icon/picture_2659360 -* https://www.flaticon.com/free-icon/magnifier_13113714 -* https://www.flaticon.com/free-icon/calculator_7182540 -* https://thenounproject.com/icon/random-dither-4259782 diff --git a/desktop/src/assets/icons/histogram.png b/desktop/src/assets/icons/histogram.png deleted file mode 100644 index ba4e5ec4d5f392ca664bbb7cbb1bb359087fd9b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1570 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 diff --git a/desktop/src/assets/icons/online-search.png b/desktop/src/assets/icons/online-search.png deleted file mode 100644 index 6c74d6dc16893afdb2e267be415fbe767200204b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4092 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 Date: Thu, 23 May 2024 09:38:56 -0300 Subject: [PATCH 11/45] [desktop]: Add toolbarMenu option to Slide Menu --- desktop/src/app/camera/camera.component.ts | 4 ++-- desktop/src/app/home/home.component.ts | 6 +++--- desktop/src/app/image/image.component.ts | 3 ++- desktop/src/app/mount/mount.component.ts | 3 ++- .../device-list-menu/device-list-menu.component.ts | 11 +++++------ .../components/menu-item/menu-item.component.html | 9 +++++++++ .../components/menu-item/menu-item.component.ts | 8 +++++++- .../components/slide-menu/slide-menu.component.ts | 14 ++++++++------ 8 files changed, 38 insertions(+), 20 deletions(-) diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index a25848f40..1fc78786d 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router' import { MenuItem } from 'primeng/api' import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component' -import { SlideMenuItemCommandEvent } from '../../shared/components/slide-menu/slide-menu.component' +import { SlideMenuItem, SlideMenuItemCommandEvent } from '../../shared/components/slide-menu/slide-menu.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -420,7 +420,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } } - const menu: ExtendedMenuItem[] = [] + const menu: SlideMenuItem[] = [] menu.push({ icon: 'mdi mdi-wrench', diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 37a9d7173..207e94e4b 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,8 +1,8 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { dirname } from 'path' -import { MenuItem } from 'primeng/api' import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { DialogMenuComponent } from '../../shared/components/dialog-menu/dialog-menu.component' +import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' @@ -117,9 +117,9 @@ export class HomeComponent implements AfterContentInit, OnDestroy { return this.connection?.type === 'ALPACA' && this.hasDevices } - readonly deviceModel: MenuItem[] = [] + readonly deviceModel: SlideMenuItem[] = [] - readonly imageModel: MenuItem[] = [ + readonly imageModel: SlideMenuItem[] = [ { icon: 'mdi mdi-image-plus', label: 'Open new image', diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index cea750ec0..5759c8f28 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -9,6 +9,7 @@ import { ContextMenu } from 'primeng/contextmenu' import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { HistogramComponent } from '../../shared/components/histogram/histogram.component' import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -583,7 +584,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - const menu: ExtendedMenuItem[] = [] + const menu: SlideMenuItem[] = [] menu.push({ label: 'Open', diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index d1ea98a65..607e28603 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -2,6 +2,7 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy } from '@a import { ActivatedRoute } from '@angular/router' import { MenuItem } from 'primeng/api' import { Subject, Subscription, interval, throttleTime } from 'rxjs' +import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -86,7 +87,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { }, ] - readonly targetCoordinateModel: MenuItem[] = [ + readonly targetCoordinateModel: SlideMenuItem[] = [ { icon: 'mdi mdi-telescope', label: 'Go To', diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts index e395a818a..de139677f 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts @@ -1,11 +1,10 @@ import { Component, Input, ViewChild } from '@angular/core' -import { MenuItem } from 'primeng/api' import { SEPARATOR_MENU_ITEM } from '../../constants' import { PrimeService } from '../../services/prime.service' import { Device } from '../../types/device.types' import { deviceComparator } from '../../utils/comparators' import { DialogMenuComponent } from '../dialog-menu/dialog-menu.component' -import { ExtendedMenuItem } from '../menu-item/menu-item.component' +import { SlideMenuItem } from '../slide-menu/slide-menu.component' @Component({ selector: 'neb-device-list-menu', @@ -15,7 +14,7 @@ import { ExtendedMenuItem } from '../menu-item/menu-item.component' export class DeviceListMenuComponent { @Input() - readonly model: MenuItem[] = [] + readonly model: SlideMenuItem[] = [] @Input() readonly modelAtFirst: boolean = true @@ -35,16 +34,16 @@ export class DeviceListMenuComponent { constructor(private prime: PrimeService) { } show(devices: T[], selected?: NoInfer) { - const model: ExtendedMenuItem[] = [] + const model: SlideMenuItem[] = [] - return new Promise((resolve) => { + return new Promise(resolve => { if (devices.length <= 0) { resolve(undefined) this.prime.message('Please connect your equipment first!', 'warn') return } - const subscription = this.menu.visibleChange.subscribe((visible) => { + const subscription = this.menu.visibleChange.subscribe(visible => { if (!visible) { subscription.unsubscribe() resolve(undefined) diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html index a1482f4b4..f3f4cb51d 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.html +++ b/desktop/src/shared/components/menu-item/menu-item.component.html @@ -2,6 +2,15 @@
{{ item.label }}
+ @if (item.toolbarMenu?.length) { +
+ @for (m of item.toolbarMenu; track $index; let i = $index) { + + } +
+ } @if (item.checked) { } @else if(item.toggleable) { diff --git a/desktop/src/shared/components/menu-item/menu-item.component.ts b/desktop/src/shared/components/menu-item/menu-item.component.ts index 6edd04381..71fbac809 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.ts +++ b/desktop/src/shared/components/menu-item/menu-item.component.ts @@ -1,9 +1,15 @@ import { Component, Input } from '@angular/core' -import { MenuItem } from 'primeng/api' +import { MenuItem, MenuItemCommandEvent } from 'primeng/api' import { CheckableMenuItem, ToggleableMenuItem } from '../../types/app.types' +export interface ExtendedMenuItemCommandEvent extends MenuItemCommandEvent { + item?: ExtendedMenuItem +} + export interface ExtendedMenuItem extends MenuItem, Partial, Partial { menu?: ExtendedMenuItem[] + toolbarMenu?: ExtendedMenuItem[] + command?: (event: ExtendedMenuItemCommandEvent) => void } @Component({ diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.ts b/desktop/src/shared/components/slide-menu/slide-menu.component.ts index 72bf9bad9..f7c254c71 100644 --- a/desktop/src/shared/components/slide-menu/slide-menu.component.ts +++ b/desktop/src/shared/components/slide-menu/slide-menu.component.ts @@ -1,16 +1,18 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core' -import { MenuItemCommandEvent } from 'primeng/api' -import { ExtendedMenuItem } from '../menu-item/menu-item.component' +import { ExtendedMenuItem, ExtendedMenuItemCommandEvent } from '../menu-item/menu-item.component' -export type SlideMenuItem = ExtendedMenuItem -export type SlideMenu = SlideMenuItem[] +export interface SlideMenuItem extends ExtendedMenuItem { + command?: (event: SlideMenuItemCommandEvent) => void +} -export interface SlideMenuItemCommandEvent extends MenuItemCommandEvent { +export interface SlideMenuItemCommandEvent extends ExtendedMenuItemCommandEvent { item?: SlideMenuItem parent?: SlideMenuItem - level: number + level?: number } +export type SlideMenu = SlideMenuItem[] + @Component({ selector: 'neb-slide-menu', templateUrl: './slide-menu.component.html', From 5afa4fd08fb255c8121e433af3acea542f1966da Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 24 May 2024 01:01:23 -0300 Subject: [PATCH 12/45] [api]: Fix concurrent modification exception --- .../indi/device/AbstractINDIDeviceProvider.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt index 235b7e947..1fa681d4f 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt @@ -198,12 +198,12 @@ abstract class AbstractINDIDeviceProvider : INDIDeviceProvider { } override fun close() { - cameras().onEach(Device::close).onEach(::unregisterCamera) - mounts().onEach(Device::close).onEach(::unregisterMount) - wheels().onEach(Device::close).onEach(::unregisterFilterWheel) - focusers().onEach(Device::close).onEach(::unregisterFocuser) - rotators().onEach(Device::close).onEach(::unregisterRotator) - gps().onEach(Device::close).onEach(::unregisterGPS) + cameras().onEach(Device::close).toList().onEach(::unregisterCamera) + mounts().onEach(Device::close).toList().onEach(::unregisterMount) + wheels().onEach(Device::close).toList().onEach(::unregisterFilterWheel) + focusers().onEach(Device::close).toList().onEach(::unregisterFocuser) + rotators().onEach(Device::close).toList().onEach(::unregisterRotator) + gps().onEach(Device::close).toList().onEach(::unregisterGPS) cameras.clear() mounts.clear() From 9378b8bb41259138225dc2c8ac2a272d8898e71b Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 24 May 2024 01:07:24 -0300 Subject: [PATCH 13/45] [api]: Add Rotator to ConnectionEventHandler --- .../kotlin/nebulosa/api/connection/ConnectionEventHandler.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt index 715c1def3..e08e9538b 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt @@ -4,6 +4,7 @@ import nebulosa.api.cameras.CameraEventHandler import nebulosa.api.focusers.FocuserEventHandler import nebulosa.api.guiding.GuideOutputEventHandler import nebulosa.api.mounts.MountEventHandler +import nebulosa.api.rotators.RotatorEventHandler import nebulosa.api.wheels.WheelEventHandler import nebulosa.indi.device.DeviceConnectionEvent import nebulosa.indi.device.DeviceEvent @@ -13,6 +14,7 @@ import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.guide.GuideOutput import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator import org.springframework.stereotype.Component @Component @@ -22,6 +24,7 @@ class ConnectionEventHandler( private val focuserEventHandler: FocuserEventHandler, private val wheelEventHandler: WheelEventHandler, private val guideOutputEventHandler: GuideOutputEventHandler, + private val rotatorEventHandler: RotatorEventHandler, ) : DeviceEventHandler.EventReceived { @Suppress("CascadeIf") @@ -33,6 +36,7 @@ class ConnectionEventHandler( else if (device is Mount) mountEventHandler.sendUpdate(device) else if (device is Focuser) focuserEventHandler.sendUpdate(device) else if (device is FilterWheel) wheelEventHandler.sendUpdate(device) + else if (device is Rotator) rotatorEventHandler.sendUpdate(device) if (device is GuideOutput) guideOutputEventHandler.sendUpdate(device) } From 7b57e583c1496d0b9d5df85cca418d41e974657c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 24 May 2024 12:20:09 -0300 Subject: [PATCH 14/45] [api][desktop]: Improvements --- .../nebulosa/api/indi/INDIController.kt | 8 +++--- .../nebulosa/api/indi/INDIEventHandler.kt | 26 ++++++++++--------- .../kotlin/nebulosa/api/indi/INDIService.kt | 2 +- .../src/app/alignment/alignment.component.ts | 12 +++++---- desktop/src/app/camera/camera.component.ts | 10 +++---- desktop/src/app/indi/indi.component.ts | 6 ++--- desktop/src/shared/services/api.service.ts | 8 +++--- 7 files changed, 38 insertions(+), 34 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt index dae71b651..f66afda59 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt @@ -35,14 +35,14 @@ class INDIController( } @Synchronized - @PutMapping("listener/{device}/start") - fun startListening(device: Device) { + @PutMapping("{device}/listen") + fun listen(device: Device) { indiService.registerDeviceToSendMessage(device) } @Synchronized - @PutMapping("listener/{device}/stop") - fun stopListening(device: Device) { + @PutMapping("{device}/unlisten") + fun unlisten(device: Device) { indiService.unregisterDeviceToSendMessage(device) } } diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt index 77855d479..fdc12cd3f 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt @@ -12,43 +12,45 @@ import java.util.* @Subscriber class INDIEventHandler( private val messageService: MessageService, -) : LinkedList() { +) { - private val canSendEvents = HashSet() + private val canSendEvents = HashSet(8) + private val messages = LinkedList() @Subscribe(threadMode = ThreadMode.ASYNC) fun onDeviceEvent(event: DeviceEvent<*>) { when (event) { is DevicePropertyChanged -> sendINDIPropertyChanged(event) is DevicePropertyDeleted -> sendINDIPropertyDeleted(event) - is DeviceMessageReceived -> if (event.device == null) addFirst(event.message) - else sendINDIMessageReceived(event) + is DeviceMessageReceived -> event.device?.also { sendINDIMessageReceived(event) } ?: messages.addFirst(event.message) is DeviceDetached<*> -> unregisterDevice(event.device) } } fun registerDevice(device: Device) { - canSendEvents.add(device.id) + canSendEvents.add(device) } fun unregisterDevice(device: Device) { - canSendEvents.remove(device.id) + canSendEvents.remove(device) } - fun sendINDIPropertyChanged(event: DevicePropertyEvent) { - if (event.device.id in canSendEvents) { + fun messages(): List = messages + + private fun sendINDIPropertyChanged(event: DevicePropertyEvent) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_CHANGED, event)) } } - fun sendINDIPropertyDeleted(event: DevicePropertyEvent) { - if (event.device.id in canSendEvents) { + private fun sendINDIPropertyDeleted(event: DevicePropertyEvent) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_DELETED, event)) } } - fun sendINDIMessageReceived(event: DeviceMessageReceived) { - if (event.device != null && event.device!!.id in canSendEvents) { + private fun sendINDIMessageReceived(event: DeviceMessageReceived) { + if (event.device != null && event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_MESSAGE_RECEIVED, event)) } } diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt index e66b2ef2c..0fe78fc8c 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt @@ -19,7 +19,7 @@ class INDIService( } fun messages(): List { - return indiEventHandler + return indiEventHandler.messages() } fun properties(device: Device): Collection> { diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index 922073481..fe628cd54 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -235,16 +235,18 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } @HostListener('window:unload') - ngOnDestroy() { - this.darvStop() - this.tppaStop() + async ngOnDestroy() { + try { + await this.darvStop() + } finally { + await this.tppaStop() + } } async cameraChanged() { if (this.camera.id) { const camera = await this.api.camera(this.camera.id) Object.assign(this.camera, camera) - this.loadPreference() } } @@ -326,7 +328,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } darvStop() { - this.api.darvStop(this.camera) + return this.api.darvStop(this.camera) } async tppaStart() { diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 1fc78786d..9989630c6 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -248,7 +248,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { this.cameraModel[1].visible = !app.modal } - async ngAfterContentInit() { + ngAfterContentInit() { this.route.queryParams.subscribe(e => { const decodedData = JSON.parse(decodeURIComponent(e.data)) @@ -267,9 +267,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } @HostListener('window:unload') - ngOnDestroy() { + async ngOnDestroy() { if (this.mode === 'CAPTURE') { - this.abortCapture() + await this.abortCapture() } } @@ -413,7 +413,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { label: name ?? 'None', icon: name ? 'mdi mdi-wrench' : 'mdi mdi-close', checked: this.request.calibrationGroup === name, - command: (event: SlideMenuItemCommandEvent) => { + command: () => { this.request.calibrationGroup = name this.loadCalibrationGroups() }, @@ -525,7 +525,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } abortCapture() { - this.api.cameraAbortCapture(this.camera) + return this.api.cameraAbortCapture(this.camera) } static exposureUnitFactor(unit: ExposureTimeUnit) { diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 7469cd37b..eadc7fb04 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -94,19 +94,19 @@ export class INDIComponent implements AfterViewInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { if (this.device) { - this.api.indiStopListening(this.device) + this.api.indiUnlisten(this.device) } } async deviceChanged(device: Device) { if (this.device) { - await this.api.indiStopListening(this.device) + await this.api.indiUnlisten(this.device) } this.device = device this.updateProperties() - await this.api.indiStartListening(device) + await this.api.indiListen(device) this.messages = await this.api.indiLog(device) } diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 426feeabc..614cbdbfd 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -390,12 +390,12 @@ export class ApiService { return this.http.put(`indi/${device.id}/send`, property) } - indiStartListening(device: Device) { - return this.http.put(`indi/listener/${device.id}/start`) + indiListen(device: Device) { + return this.http.put(`indi/${device.id}/listen`) } - indiStopListening(device: Device) { - return this.http.put(`indi/listener/${device.id}/stop`) + indiUnlisten(device: Device) { + return this.http.put(`indi/${device.id}/unlisten`) } indiLog(device: Device) { From 392e3e18435cfaf86b7832904292af461605c0c5 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 24 May 2024 18:29:22 -0300 Subject: [PATCH 15/45] [desktop]: Update DARV instructions --- desktop/src/app/alignment/alignment.component.html | 6 +++--- desktop/src/app/image/image.component.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 765ccbfd7..451e633d5 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -141,13 +141,13 @@
- 1. Locate a star in the Meridian and close to declination 0. + 1. Locate a star near the south meridian and close to declination 0. 2. Start DARV and wait for routine to complete. 3. If you see V shaped track, adjust the Azimuth and repeat the step 2 till you get a line. - 4. Locate a star in the Eastern or Western horizon and close to declination 0. + 4. Locate a star in the eastern or western horizon and close to declination 0. 5. Start DARV and wait for routine to complete. 6. If you see V shaped track, adjust the Altitude and repeat the step 5 till you get a line. - 7. Increase the drift time and repeat the step 1 to get a more accurate alignment. + 7. Increase the drift time and repeat the step 1 to refine the alignment.
diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 5759c8f28..a9c802253 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -1156,7 +1156,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { action(cameras[0]) return true } else { - this.deviceMenu.header = 'Cameras' + this.deviceMenu.header = 'CAMERA' const camera = await this.deviceMenu.show(cameras) if (camera && camera !== 'NONE' && camera.connected) { @@ -1179,7 +1179,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { action(mounts[0]) return true } else { - this.deviceMenu.header = 'Mounts' + this.deviceMenu.header = 'MOUNT' const mount = await this.deviceMenu.show(mounts) if (mount && mount !== 'NONE' && mount.connected) { From 12f68dbb46508fcf54958d3a07186971c90d0016 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 24 May 2024 22:44:02 -0300 Subject: [PATCH 16/45] [desktop]: Add hotkeys for Mount and Filter Wheel --- .../filterwheel/filterwheel.component.html | 2 +- .../app/filterwheel/filterwheel.component.ts | 57 +++++++++++++++++++ desktop/src/app/focuser/focuser.component.ts | 26 ++++----- desktop/src/app/image/image.component.ts | 17 +++--- desktop/src/app/mount/mount.component.html | 4 +- desktop/src/app/mount/mount.component.ts | 36 ++++++++---- desktop/src/shared/types/mount.types.ts | 2 + 7 files changed, 107 insertions(+), 37 deletions(-) diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index d9f6693d2..45ad9d202 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -47,7 +47,7 @@
+ (onClick)="moveToSelectedFilter()" size="small" />
diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 316c4d3cb..8a38367ae 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -1,5 +1,6 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import hotkeys from 'hotkeys-js' import { CheckboxChangeEvent } from 'primeng/checkbox' import { Subject, Subscription, debounceTime } from 'rxjs' import { ApiService } from '../../shared/services/api.service' @@ -118,6 +119,19 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { this.savePreference() this.electron.send('WHEEL.RENAMED', { wheel: this.wheel, filter }) }) + + hotkeys('enter', event => { event.preventDefault(); this.moveToSelectedFilter() }) + hotkeys('up', event => { event.preventDefault(); this.moveUp() }) + hotkeys('down', event => { event.preventDefault(); this.moveDown() }) + hotkeys('1', event => { event.preventDefault(); this.moveToPosition(1) }) + hotkeys('2', event => { event.preventDefault(); this.moveToPosition(2) }) + hotkeys('3', event => { event.preventDefault(); this.moveToPosition(3) }) + hotkeys('4', event => { event.preventDefault(); this.moveToPosition(4) }) + hotkeys('5', event => { event.preventDefault(); this.moveToPosition(5) }) + hotkeys('6', event => { event.preventDefault(); this.moveToPosition(6) }) + hotkeys('7', event => { event.preventDefault(); this.moveToPosition(7) }) + hotkeys('8', event => { event.preventDefault(); this.moveToPosition(8) }) + hotkeys('9', event => { event.preventDefault(); this.moveToPosition(9) }) } async ngAfterContentInit() { @@ -197,6 +211,49 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { } } + moveToSelectedFilter() { + if (this.filter) { + this.moveTo(this.filter) + } + } + + moveUp() { + this.moveToPosition(this.wheel.position - 1) + } + + moveDown() { + this.moveToPosition(this.wheel.position + 1) + } + + moveToIndex(index: number) { + if (!this.moving) { + if (index >= 0 && index < this.filters.length) { + this.moveTo(this.filters[index]) + } else if (index < 0) { + this.moveToIndex(this.filters.length + index) + } else { + this.moveToIndex(index % this.filters.length) + } + } + } + + moveToPosition(position: number) { + if (!this.moving) { + if (position >= 1 && position <= this.wheel.count) { + for (const filter of this.filters) { + if (filter.position === position) { + this.moveTo(filter) + break + } + } + } else if (position < 1) { + this.moveToPosition(this.wheel.count + position) + } else { + this.moveToPosition(position % this.wheel.count) + } + } + } + shutterToggled(filter: FilterSlot, event: CheckboxChangeEvent) { this.filters.forEach(e => e.dark = event.checked && e === filter) this.filterChangedPublisher.next(structuredClone(filter)) diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index d25235a30..a914f0567 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -47,20 +47,18 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { } }) - hotkeys('left', (event) => { event.preventDefault(); this.moveIn() }) - hotkeys('alt+left', (event) => { event.preventDefault(); this.moveIn(10) }) - hotkeys('ctrl+left', (event) => { event.preventDefault(); this.moveIn(2) }) - hotkeys('shift+left', (event) => { event.preventDefault(); this.moveIn(0.5) }) - hotkeys('right', (event) => { event.preventDefault(); this.moveOut() }) - hotkeys('alt+right', (event) => { event.preventDefault(); this.moveOut(10) }) - hotkeys('ctrl+right', (event) => { event.preventDefault(); this.moveOut(2) }) - hotkeys('shift+right', (event) => { event.preventDefault(); this.moveOut(0.5) }) - hotkeys('space', (event) => { event.preventDefault(); this.abort() }) - hotkeys('ctrl+enter', (event) => { event.preventDefault(); this.moveTo() }) - hotkeys('up', (event) => { event.preventDefault(); this.stepsRelative = Math.min(this.focuser.maxPosition, this.stepsRelative + 1) }) - hotkeys('down', (event) => { event.preventDefault(); this.stepsRelative = Math.max(0, this.stepsRelative - 1) }) - hotkeys('-', (event) => { event.preventDefault(); this.stepsAbsolute = Math.max(0, this.stepsAbsolute - 1) }) - hotkeys('=', (event) => { event.preventDefault(); this.stepsAbsolute = Math.min(this.focuser.maxPosition, this.stepsAbsolute + 1) }) + hotkeys('left', event => { event.preventDefault(); this.moveIn() }) + hotkeys('ctrl+left', event => { event.preventDefault(); this.moveIn(2) }) + hotkeys('alt+left', event => { event.preventDefault(); this.moveIn(0.5) }) + hotkeys('right', event => { event.preventDefault(); this.moveOut() }) + hotkeys('ctrl+right', event => { event.preventDefault(); this.moveOut(2) }) + hotkeys('alt+right', event => { event.preventDefault(); this.moveOut(0.5) }) + hotkeys('space', event => { event.preventDefault(); this.abort() }) + hotkeys('enter', event => { event.preventDefault(); this.moveTo() }) + hotkeys('up', event => { event.preventDefault(); this.stepsRelative = Math.min(this.focuser.maxPosition, this.stepsRelative + 1) }) + hotkeys('down', event => { event.preventDefault(); this.stepsRelative = Math.max(0, this.stepsRelative - 1) }) + hotkeys('ctrl+up', event => { event.preventDefault(); this.stepsAbsolute = Math.max(0, this.stepsAbsolute - 1) }) + hotkeys('ctrl+down', event => { event.preventDefault(); this.stepsAbsolute = Math.min(this.focuser.maxPosition, this.stepsAbsolute + 1) }) } async ngAfterViewInit() { diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index a9c802253..5866d9700 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -519,14 +519,15 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }) }) - hotkeys('ctrl+a', (event) => { event.preventDefault(); this.toggleStretch() }) - hotkeys('ctrl+i', (event) => { event.preventDefault(); this.invertImage() }) - hotkeys('ctrl+x', (event) => { event.preventDefault(); this.toggleCrosshair() }) - hotkeys('ctrl+-', (event) => { event.preventDefault(); this.zoomOut() }) - hotkeys('ctrl+=', (event) => { event.preventDefault(); this.zoomIn() }) - hotkeys('ctrl+0', (event) => { event.preventDefault(); this.resetZoom() }) - hotkeys('f12', (event) => { if (this.app.showTopBar) { event.preventDefault(); this.enterFullscreen() } }) - hotkeys('escape', (event) => { if (!this.app.showTopBar) { event.preventDefault(); this.exitFullscreen() } }) + hotkeys('ctrl+a', event => { event.preventDefault(); this.toggleStretch() }) + hotkeys('ctrl+i', event => { event.preventDefault(); this.invertImage() }) + hotkeys('ctrl+x', event => { event.preventDefault(); this.toggleCrosshair() }) + hotkeys('ctrl+-', event => { event.preventDefault(); this.zoomOut() }) + hotkeys('ctrl+=', event => { event.preventDefault(); this.zoomIn() }) + hotkeys('ctrl+0', event => { event.preventDefault(); this.resetZoom() }) + hotkeys('ctrl+alt+0', event => { event.preventDefault(); this.resetZoom(true) }) + hotkeys('f12', event => { if (this.app.showTopBar) { event.preventDefault(); this.enterFullscreen() } }) + hotkeys('escape', event => { if (!this.app.showTopBar) { event.preventDefault(); this.exitFullscreen() } }) this.loadPreference() } diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index 0c32f55fc..cbeb33472 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -132,8 +132,8 @@
-
diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index 607e28603..fa90749d8 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -1,5 +1,6 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import hotkeys from 'hotkeys-js' import { MenuItem } from 'primeng/api' import { Subject, Subscription, interval, throttleTime } from 'rxjs' import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component' @@ -10,7 +11,7 @@ import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' import { PrimeService } from '../../shared/services/prime.service' import { Angle, ComputedLocation, Constellation, EMPTY_COMPUTED_LOCATION } from '../../shared/types/atlas.types' -import { EMPTY_MOUNT, Mount, MountRemoteControlDialog, MountRemoteControlType, PierSide, SlewRate, TargetCoordinateType, TrackMode } from '../../shared/types/mount.types' +import { EMPTY_MOUNT, Mount, MountRemoteControlDialog, MountRemoteControlType, MoveDirectionType, PierSide, SlewRate, TargetCoordinateType, TrackMode } from '../../shared/types/mount.types' import { AppComponent } from '../app.component' import { SkyAtlasTab } from '../atlas/atlas.component' @@ -43,7 +44,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { tracking = false canPark = false canHome = false - slewingDirection?: string + slewingDirection?: MoveDirectionType rightAscensionJ2000: Angle = '00h00m00s' declinationJ2000: Angle = `00°00'00"` @@ -92,7 +93,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { icon: 'mdi mdi-telescope', label: 'Go To', command: () => { - this.targetCoordinateOption = this.targetCoordinateModel[0] + this.targetCoordinateCommand = this.targetCoordinateModel[0] this.goTo() }, }, @@ -100,7 +101,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { icon: 'mdi mdi-telescope', label: 'Slew', command: () => { - this.targetCoordinateOption = this.targetCoordinateModel[1] + this.targetCoordinateCommand = this.targetCoordinateModel[1] this.slewTo() }, }, @@ -108,7 +109,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { icon: 'mdi mdi-sync', label: 'Sync', command: () => { - this.targetCoordinateOption = this.targetCoordinateModel[2] + this.targetCoordinateCommand = this.targetCoordinateModel[2] this.sync() }, }, @@ -208,7 +209,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { }, ] - targetCoordinateOption = this.targetCoordinateModel[0] + targetCoordinateCommand = this.targetCoordinateModel[0] readonly remoteControl: MountRemoteControlDialog = { showDialog: false, @@ -265,6 +266,17 @@ export class MountComponent implements AfterContentInit, OnDestroy { this.computeCoordinateSubscriptions[2] = this.computeTargetCoordinatePublisher .pipe(throttleTime(1000)) .subscribe(() => this.computeTargetCoordinates()) + + hotkeys('space', event => { event.preventDefault(); this.abort() }) + hotkeys('enter', event => { event.preventDefault(); this.targetCoordinateCommandClicked() }) + hotkeys('w,up', { keyup: true }, event => { event.preventDefault(); this.moveTo('N', event.type === 'keydown') }) + hotkeys('s,down', { keyup: true }, event => { event.preventDefault(); this.moveTo('S', event.type === 'keydown') }) + hotkeys('a,left', { keyup: true }, event => { event.preventDefault(); this.moveTo('W', event.type === 'keydown') }) + hotkeys('d,right', { keyup: true }, event => { event.preventDefault(); this.moveTo('E', event.type === 'keydown') }) + hotkeys('q', { keyup: true }, event => { event.preventDefault(); this.moveTo('NW', event.type === 'keydown') }) + hotkeys('e', { keyup: true }, event => { event.preventDefault(); this.moveTo('NE', event.type === 'keydown') }) + hotkeys('z', { keyup: true }, event => { event.preventDefault(); this.moveTo('SW', event.type === 'keydown') }) + hotkeys('c', { keyup: true }, event => { event.preventDefault(); this.moveTo('SE', event.type === 'keydown') }) } async ngAfterContentInit() { @@ -338,18 +350,18 @@ export class MountComponent implements AfterContentInit, OnDestroy { this.savePreference() } - targetCoordinateOptionClicked() { - if (this.targetCoordinateOption === this.targetCoordinateModel[0]) { + targetCoordinateCommandClicked() { + if (this.targetCoordinateCommand === this.targetCoordinateModel[0]) { this.goTo() - } else if (this.targetCoordinateOption === this.targetCoordinateModel[1]) { + } else if (this.targetCoordinateCommand === this.targetCoordinateModel[1]) { this.slewTo() - } else if (this.targetCoordinateOption === this.targetCoordinateModel[2]) { + } else if (this.targetCoordinateCommand === this.targetCoordinateModel[2]) { this.sync() } } - moveTo(direction: string, pressed: boolean, event: MouseEvent) { - if (event.button === 0) { + moveTo(direction: MoveDirectionType, pressed: boolean, event?: MouseEvent) { + if (!event || event.button === 0) { this.slewingDirection = pressed ? direction : undefined if (this.moveToDirection[0] !== pressed) { diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts index e09bdaf24..7fc7611af 100644 --- a/desktop/src/shared/types/mount.types.ts +++ b/desktop/src/shared/types/mount.types.ts @@ -12,6 +12,8 @@ export type CelestialLocationType = 'ZENITH' | 'NORTH_POLE' | 'SOUTH_POLE' | 'GA export type MountRemoteControlType = 'LX200' | 'STELLARIUM' +export type MoveDirectionType = 'N' | 'S' | 'W' | 'E' | 'NW' | 'NE' | 'SW' | 'SE' + export interface SlewRate { name: string label: string From 095564c9e6a7063758de07328f08f9d6c470b752 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 24 May 2024 23:27:05 -0300 Subject: [PATCH 17/45] [desktop]: Support mouse scroll on input number --- .../app/alignment/alignment.component.html | 6 ++-- desktop/src/app/app.module.ts | 2 ++ desktop/src/app/atlas/atlas.component.html | 15 +++++---- .../calculator/formula/formula.component.html | 2 +- desktop/src/app/camera/camera.component.html | 30 +++++++++-------- .../filterwheel/filterwheel.component.html | 2 +- .../flat-wizard/flat-wizard.component.html | 8 ++--- .../src/app/focuser/focuser.component.html | 8 +++-- .../src/app/framing/framing.component.html | 8 ++--- desktop/src/app/guider/guider.component.html | 16 +++++----- desktop/src/app/home/home.component.html | 2 +- desktop/src/app/image/image.component.html | 32 ++++++++++--------- desktop/src/app/mount/mount.component.html | 2 +- .../src/app/rotator/rotator.component.html | 6 ++-- .../app/sequencer/sequencer.component.html | 18 ++++++----- .../src/app/settings/settings.component.html | 4 +-- .../dialogs/location/location.dialog.html | 8 ++--- .../directives/input-number-scrollable.ts | 18 +++++++++++ 18 files changed, 109 insertions(+), 78 deletions(-) create mode 100644 desktop/src/shared/directives/input-number-scrollable.ts diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 451e633d5..d57ed21c1 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -43,7 +43,7 @@
+ [showButtons]="true" [min]="1" [max]="60" (ngModelChange)="savePreference()" scrollableNumber />
@@ -110,14 +110,14 @@
+ [showButtons]="true" [min]="1" [max]="60" (ngModelChange)="initialPauseChanged()" scrollableNumber />
+ [showButtons]="true" [min]="1" [max]="600" (ngModelChange)="driftForChanged()" scrollableNumber />
diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 74dad403c..68543be5d 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -51,6 +51,7 @@ import { MenuItemComponent } from '../shared/components/menu-item/menu-item.comp import { MoonComponent } from '../shared/components/moon/moon.component' import { SlideMenuComponent } from '../shared/components/slide-menu/slide-menu.component' import { LocationDialog } from '../shared/dialogs/location/location.dialog' +import { ScrollableNumberDirective } from '../shared/directives/input-number-scrollable' import { NoDropdownDirective } from '../shared/directives/no-dropdown.directive' import { StopPropagationDirective } from '../shared/directives/stop-propagation.directive' import { LocationInterceptor } from '../shared/interceptors/location.interceptor' @@ -109,6 +110,7 @@ import { SettingsComponent } from './settings/settings.component' HistogramComponent, HomeComponent, ImageComponent, + ScrollableNumberDirective, INDIComponent, INDIPropertyComponent, LocationDialog, diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index b8ab9cc80..6bbcf5f94 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -92,12 +92,14 @@
+ inputStyleClass="p-inputtext-sm border-0 w-full" [(ngModel)]="closeApproachDays" + scrollableNumber /> + inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" [(ngModel)]="closeApproachDistance" + scrollableNumber /> + inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" [(ngModel)]="skyObjectFilter.radius" scrollableNumber />
@@ -428,12 +430,11 @@
+ [max]="23" class="w-fit" [format]="false" [allowEmpty]="false" (ngModelChange)="dateTimeChanged(false)" + styleClass="p-inputtext-sm border-0" [style]="{width: '54px'}" scrollableNumber /> + styleClass="p-inputtext-sm border-0" [style]="{width: '54px'}" scrollableNumber />
diff --git a/desktop/src/app/calculator/formula/formula.component.html b/desktop/src/app/calculator/formula/formula.component.html index e3fea9186..6d4083c3f 100644 --- a/desktop/src/app/calculator/formula/formula.component.html +++ b/desktop/src/app/calculator/formula/formula.component.html @@ -12,7 +12,7 @@ + [showButtons]="true" styleClass="border-0 p-inputtext-sm" locale="en" scrollableNumber /> diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 0bac743f4..2127b8875 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -59,7 +59,8 @@ + [max]="50" locale="en" styleClass="p-inputtext-sm border-0" [allowEmpty]="false" [minFractionDigits]="1" + scrollableNumber /> + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -100,7 +101,7 @@ + [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -108,7 +109,8 @@ + locale="en" styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" + scrollableNumber /> @@ -118,7 +120,7 @@ + [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -126,7 +128,7 @@ + [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -134,7 +136,7 @@ + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -142,7 +144,7 @@ + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -160,7 +162,7 @@ + (ngModelChange)="savePreference()" scrollableNumber /> @@ -168,7 +170,7 @@ + (ngModelChange)="savePreference()" scrollableNumber /> @@ -186,7 +188,7 @@ + (ngModelChange)="savePreference()" scrollableNumber /> @@ -194,7 +196,7 @@ + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -230,7 +232,7 @@ + locale="en" [minFractionDigits]="1" (ngModelChange)="savePreference()" scrollableNumber /> @@ -238,7 +240,7 @@ + [(ngModel)]="request.dither!.afterExposures" [step]="1" (ngModelChange)="savePreference()" scrollableNumber /> diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index 45ad9d202..b6ff0a35f 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -69,7 +69,7 @@ + (ngModelChange)="focusOffsetChanged()" scrollableNumber /> diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.html b/desktop/src/app/flat-wizard/flat-wizard.component.html index 8ce296581..c697e228f 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.html +++ b/desktop/src/app/flat-wizard/flat-wizard.component.html @@ -31,7 +31,7 @@ + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -39,7 +39,7 @@ + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -47,7 +47,7 @@ + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> @@ -55,7 +55,7 @@ + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" scrollableNumber /> diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html index 52fd7de6b..385b82a5d 100644 --- a/desktop/src/app/focuser/focuser.component.html +++ b/desktop/src/app/focuser/focuser.component.html @@ -24,7 +24,8 @@
- +
@@ -39,7 +40,7 @@ icon="mdi mdi-arrow-left-bold" [text]="true" size="small" pTooltip="Move In" tooltipPosition="bottom" /> + styleClass="p-inputtext-sm border-0" [(ngModel)]="stepsRelative" [allowEmpty]="false" locale="en" scrollableNumber /> + styleClass="p-inputtext-sm border-0 max-w-full" [(ngModel)]="stepsAbsolute" [allowEmpty]="false" locale="en" + scrollableNumber /> + [(ngModel)]="width" locale="en" scrollableNumber />
+ [(ngModel)]="height" locale="en" scrollableNumber />
+ [(ngModel)]="fov" locale="en" [minFractionDigits]="1" scrollableNumber />
+ [(ngModel)]="rotation" locale="en" [minFractionDigits]="1" scrollableNumber />
diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 5e6b0b46a..067d5df5c 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -13,7 +13,7 @@
+ placeholder="7624" [(ngModel)]="port" [format]="false" scrollableNumber />
@@ -103,7 +103,7 @@ + [minFractionDigits]="1" scrollableNumber /> @@ -111,7 +111,7 @@ + [(ngModel)]="settleTime" (ngModelChange)="settleChanged()" [step]="1" scrollableNumber /> @@ -119,7 +119,7 @@ + [(ngModel)]="settleTimeout" (ngModelChange)="settleChanged()" [step]="1" scrollableNumber /> @@ -150,7 +150,7 @@
+ [max]="1800000" locale="en" styleClass="p-inputtext-sm border-0" [showButtons]="true" scrollableNumber />
@@ -158,7 +158,7 @@
+ [max]="1800000" locale="en" styleClass="p-inputtext-sm border-0" [showButtons]="true" scrollableNumber />
@@ -206,7 +206,7 @@ + styleClass="p-inputtext-sm border-0" [showButtons]="true" scrollableNumber /> @@ -215,7 +215,7 @@ + styleClass="p-inputtext-sm border-0" [showButtons]="true" scrollableNumber /> diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 9bd830683..728ba83e0 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -183,7 +183,7 @@
+ [(ngModel)]="newConnection[0].port" [min]="80" [max]="65535" [format]="false" scrollableNumber />
diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index 3b8573ef4..b9872f8ce 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -69,7 +69,7 @@ + [minFractionDigits]="1" locale="en" scrollableNumber /> @@ -182,7 +182,7 @@
+ [(ngModel)]="solver.radius" scrollableNumber />
@@ -252,12 +252,12 @@
+ [(ngModel)]="stretchShadow" locale="en" scrollableNumber /> + [(ngModel)]="stretchHighlight" locale="en" scrollableNumber />
@@ -269,7 +269,7 @@
+ [(ngModel)]="stretchMidtone" locale="en" scrollableNumber />
@@ -313,7 +313,7 @@ + [(ngModel)]="scnr.amount" locale="en" [allowEmpty]="false" scrollableNumber /> @@ -425,13 +425,13 @@ - +
- +
@@ -447,47 +447,49 @@
- +
- +
+ [minFractionDigits]="0" [maxFractionDigits]="2" [(ngModel)]="fov.pixelSize.width" locale="en" scrollableNumber />
+ [minFractionDigits]="0" [maxFractionDigits]="2" [(ngModel)]="fov.pixelSize.height" locale="en" scrollableNumber />
+ [minFractionDigits]="0" [maxFractionDigits]="2" [(ngModel)]="fov.barlowReducer" locale="en" scrollableNumber />
- +
diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index cbeb33472..769ee72a2 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -240,7 +240,7 @@
+ [showButtons]="true" [min]="1024" [max]="65535" placeholder="10001" scrollableNumber />
diff --git a/desktop/src/app/rotator/rotator.component.html b/desktop/src/app/rotator/rotator.component.html index 845f17a58..17e1b1e6f 100644 --- a/desktop/src/app/rotator/rotator.component.html +++ b/desktop/src/app/rotator/rotator.component.html @@ -20,7 +20,8 @@
- +
@@ -38,7 +39,8 @@
+ styleClass="p-inputtext-sm border-0 max-w-full" [(ngModel)]="angle" [allowEmpty]="false" locale="en" + scrollableNumber /> + styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePlan()" scrollableNumber />
@@ -54,7 +54,7 @@ + [step]="0.1" locale="en" [minFractionDigits]="1" (ngModelChange)="savePlan()" scrollableNumber />
@@ -62,7 +62,7 @@ + [(ngModel)]="plan.dither.afterExposures" [step]="1" (ngModelChange)="savePlan()" scrollableNumber /> @@ -95,7 +95,7 @@ + [(ngModel)]="plan.autoFocus.afterElapsedTime" [step]="1" (ngModelChange)="savePlan()" scrollableNumber /> @@ -105,7 +105,7 @@ + [(ngModel)]="plan.autoFocus.afterExposures" [step]="1" (ngModelChange)="savePlan()" scrollableNumber /> @@ -115,7 +115,8 @@ + [(ngModel)]="plan.autoFocus.afterTemperatureChange" [step]="1" (ngModelChange)="savePlan()" + scrollableNumber /> @@ -125,7 +126,7 @@ + [(ngModel)]="plan.autoFocus.afterHFDIncrease" [step]="1" (ngModelChange)="savePlan()" scrollableNumber /> @@ -138,7 +139,8 @@ - +
diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 4286a762d..e2084571b 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -27,13 +27,13 @@ + [min]="0" [max]="4" scrollableNumber /> + [min]="0" [max]="300" scrollableNumber /> diff --git a/desktop/src/shared/dialogs/location/location.dialog.html b/desktop/src/shared/dialogs/location/location.dialog.html index ca24459b9..7fe83c726 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.html +++ b/desktop/src/shared/dialogs/location/location.dialog.html @@ -8,28 +8,28 @@
+ [(ngModel)]="location.offsetInMinutes" [showButtons]="true" [allowEmpty]="false" scrollableNumber />
+ [(ngModel)]="location.latitude" [allowEmpty]="false" scrollableNumber />
+ [(ngModel)]="location.longitude" [allowEmpty]="false" scrollableNumber />
+ [(ngModel)]="location.elevation" [showButtons]="true" [allowEmpty]="false" scrollableNumber />
diff --git a/desktop/src/shared/directives/input-number-scrollable.ts b/desktop/src/shared/directives/input-number-scrollable.ts new file mode 100644 index 000000000..64e6b4b3b --- /dev/null +++ b/desktop/src/shared/directives/input-number-scrollable.ts @@ -0,0 +1,18 @@ +import { Directive, Host, HostListener } from '@angular/core' +import { InputNumber } from 'primeng/inputnumber' + +@Directive({ selector: '[scrollableNumber]' }) +export class ScrollableNumberDirective { + + constructor( + @Host() private inputNumber: InputNumber, + ) { } + + @HostListener('wheel', ['$event']) + handleEvent(event: WheelEvent) { + if (!this.inputNumber.disabled && !this.inputNumber.readonly && this.inputNumber.showButtons) { + this.inputNumber.spin(event, -Math.sign(event.deltaY)) + event.stopImmediatePropagation() + } + } +} \ No newline at end of file From 3828e2bc1a4ba133c82f99fea75509db97b831af Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 24 May 2024 23:49:47 -0300 Subject: [PATCH 18/45] [desktop]: Fix aliased for loop track --- .../src/shared/components/menu-item/menu-item.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html index f3f4cb51d..03f61f537 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.html +++ b/desktop/src/shared/components/menu-item/menu-item.component.html @@ -4,7 +4,7 @@
@if (item.toolbarMenu?.length) {
- @for (m of item.toolbarMenu; track $index; let i = $index) { + @for (m of item.toolbarMenu; track i; let i = $index) { From b9dc8b3e479f27d20743078e4fee8766cb1d94aa Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 25 May 2024 14:17:11 -0300 Subject: [PATCH 19/45] [api]: Support Auto Focus --- api/build.gradle.kts | 1 + .../api/alignment/polar/darv/DARVJob.kt | 5 +- .../api/alignment/polar/darv/DARVTask.kt | 9 +- .../api/alignment/polar/tppa/TPPAJob.kt | 5 +- .../api/alignment/polar/tppa/TPPATask.kt | 9 +- .../api/autofocus/AutoFocusController.kt | 19 + .../nebulosa/api/autofocus/AutoFocusEvent.kt | 8 + .../api/autofocus/AutoFocusExecutor.kt | 70 ++++ .../api/autofocus/AutoFocusFittingMode.kt | 9 + .../nebulosa/api/autofocus/AutoFocusJob.kt | 20 + .../api/autofocus/AutoFocusRequest.kt | 15 + .../api/autofocus/AutoFocusService.kt | 15 + .../nebulosa/api/autofocus/AutoFocusTask.kt | 362 ++++++++++++++++++ .../api/autofocus/BacklashCompensationMode.kt | 6 + .../nebulosa/api/cameras/CameraCaptureJob.kt | 4 +- .../nebulosa/api/cameras/CameraCaptureTask.kt | 4 +- .../nebulosa/api/cameras/CameraEventAware.kt | 8 + .../api/cameras/CameraExposureTask.kt | 6 +- .../api/focusers/FocuserEventAware.kt | 8 + .../api/focusers/FocuserMoveAbsoluteTask.kt | 61 +++ .../api/focusers/FocuserMoveRelativeTask.kt | 63 +++ .../nebulosa/api/focusers/FocuserMoveTask.kt | 9 + .../nebulosa/api/mounts/MountEventAware.kt | 8 + .../nebulosa/api/mounts/MountSlewTask.kt | 4 +- .../nebulosa/api/sequencer/SequencerJob.kt | 8 +- .../nebulosa/api/sequencer/SequencerTask.kt | 12 +- .../nebulosa/api/wheels/WheelEventAware.kt | 8 + .../nebulosa/api/wheels/WheelMoveTask.kt | 4 +- .../nebulosa/api/wizard/flat/FlatWizardJob.kt | 5 +- .../api/wizard/flat/FlatWizardTask.kt | 9 +- .../nebulosa/curve/fitting/CurvePoint.kt | 11 +- .../nebulosa/watney/star/detection/Star.kt | 3 +- 32 files changed, 739 insertions(+), 49 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusFittingMode.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraEventAware.kt create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/FocuserEventAware.kt create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveTask.kt create mode 100644 api/src/main/kotlin/nebulosa/api/mounts/MountEventAware.kt create mode 100644 api/src/main/kotlin/nebulosa/api/wheels/WheelEventAware.kt diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 8832af464..601a46919 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(project(":nebulosa-astrometrynet")) implementation(project(":nebulosa-alpaca-indi")) implementation(project(":nebulosa-common")) + implementation(project(":nebulosa-curve-fitting")) implementation(project(":nebulosa-guiding-phd2")) implementation(project(":nebulosa-hips2fits")) implementation(project(":nebulosa-horizons")) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt index 5889a5907..97d290089 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt @@ -1,13 +1,14 @@ package nebulosa.api.alignment.polar.darv +import nebulosa.api.cameras.CameraEventAware import nebulosa.api.tasks.Job import nebulosa.indi.device.camera.CameraEvent -data class DARVJob(override val task: DARVTask) : Job() { +data class DARVJob(override val task: DARVTask) : Job(), CameraEventAware { override val name = "${task.camera.name} DARV Job" - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { task.handleCameraEvent(event) } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt index 1a6041ab4..bdfb8911c 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt @@ -1,10 +1,7 @@ package nebulosa.api.alignment.polar.darv import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.cameras.AutoSubFolderMode -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraCaptureState -import nebulosa.api.cameras.CameraCaptureTask +import nebulosa.api.cameras.* import nebulosa.api.guiding.GuidePulseEvent import nebulosa.api.guiding.GuidePulseRequest import nebulosa.api.guiding.GuidePulseTask @@ -30,7 +27,7 @@ data class DARVTask( @JvmField val guideOutput: GuideOutput, @JvmField val request: DARVStartRequest, private val executor: Executor, -) : AbstractTask(), Consumer { +) : AbstractTask(), Consumer, CameraEventAware { @JvmField val cameraRequest = request.capture.copy( exposureTime = request.capture.exposureTime + request.capture.exposureDelay, @@ -60,7 +57,7 @@ data class DARVTask( backwardGuidePulseTask.subscribe(this) } - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { cameraCaptureTask.handleCameraEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt index a11a45abe..e71b4e21b 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt @@ -1,13 +1,14 @@ package nebulosa.api.alignment.polar.tppa +import nebulosa.api.cameras.CameraEventAware import nebulosa.api.tasks.Job import nebulosa.indi.device.camera.CameraEvent -data class TPPAJob(override val task: TPPATask) : Job() { +data class TPPAJob(override val task: TPPATask) : Job(), CameraEventAware { override val name = "${task.camera.name} TPPA Job" - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { task.handleCameraEvent(event) } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt index 3664401cc..1b8f1e1be 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt @@ -3,10 +3,7 @@ package nebulosa.api.alignment.polar.tppa import io.reactivex.rxjava3.functions.Consumer import nebulosa.alignment.polar.point.three.ThreePointPolarAlignment import nebulosa.alignment.polar.point.three.ThreePointPolarAlignmentResult -import nebulosa.api.cameras.AutoSubFolderMode -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraCaptureState -import nebulosa.api.cameras.CameraCaptureTask +import nebulosa.api.cameras.* import nebulosa.api.messages.MessageEvent import nebulosa.api.mounts.MountMoveRequest import nebulosa.api.mounts.MountMoveTask @@ -38,7 +35,7 @@ data class TPPATask( @JvmField val mount: Mount? = null, @JvmField val longitude: Angle = mount!!.longitude, @JvmField val latitude: Angle = mount!!.latitude, -) : AbstractTask(), Consumer, PauseListener { +) : AbstractTask(), Consumer, PauseListener, CameraEventAware { @JvmField val mountMoveRequest = MountMoveRequest(request.stepDirection, request.stepDuration, request.stepSpeed) @@ -73,7 +70,7 @@ data class TPPATask( settleDelayTask.subscribe(this) } - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { if (camera === event.device) { cameraCaptureTask.handleCameraEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt new file mode 100644 index 000000000..8710b7cdf --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt @@ -0,0 +1,19 @@ +package nebulosa.api.autofocus + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.focuser.Focuser +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("auto-focus") +class AutoFocusController(private val autoFocusService: AutoFocusService) { + + @PutMapping("{camera}/{focuser}/start") + fun start( + camera: Camera, focuser: Focuser, + @RequestBody body: AutoFocusRequest, + ) = autoFocusService.start(camera, focuser, body) +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt new file mode 100644 index 000000000..da044fbe5 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt @@ -0,0 +1,8 @@ +package nebulosa.api.autofocus + +import nebulosa.api.messages.MessageEvent + +class AutoFocusEvent : MessageEvent { + + override val eventName = "AUTO_FOCUS.ELAPSED" +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt new file mode 100644 index 000000000..63d842b1d --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt @@ -0,0 +1,70 @@ +package nebulosa.api.autofocus + +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.image.ImageBucket +import nebulosa.api.messages.MessageEvent +import nebulosa.api.messages.MessageService +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.watney.star.detection.WatneyStarDetector +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.springframework.stereotype.Component +import java.util.concurrent.ConcurrentHashMap + +@Component +@Subscriber +class AutoFocusExecutor( + private val messageService: MessageService, + private val imageBucket: ImageBucket, +) : Consumer { + + private val jobs = ConcurrentHashMap.newKeySet(2) + + @Subscribe(threadMode = ThreadMode.ASYNC) + fun onCameraEvent(event: CameraEvent) { + jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + fun onFocuserEvent(event: FocuserEvent) { + jobs.find { it.task.focuser === event.device }?.handleFocuserEvent(event) + } + + override fun accept(event: MessageEvent) { + messageService.sendMessage(event) + } + + @Synchronized + fun execute(camera: Camera, focuser: Focuser, request: AutoFocusRequest) { + check(camera.connected) { "${camera.name} Camera is not connected" } + check(focuser.connected) { "${focuser.name} Camera is not connected" } + check(jobs.none { it.task.camera === camera }) { "${camera.name} Auto Focus is already in progress" } + check(jobs.none { it.task.focuser === focuser }) { "${camera.name} Auto Focus is already in progress" } + + val task = AutoFocusTask(camera, focuser, request, STAR_DETECTOR, imageBucket) + task.subscribe(this) + + with(AutoFocusJob(task)) { + jobs.add(this) + whenComplete { _, _ -> jobs.remove(this) } + start() + } + } + + fun stop(camera: Camera) { + jobs.find { it.task.camera === camera }?.stop() + } + + fun status(camera: Camera): AutoFocusEvent? { + return jobs.find { it.task.camera === camera }?.task?.get() as? AutoFocusEvent + } + + companion object { + + @JvmStatic private val STAR_DETECTOR = WatneyStarDetector(computeHFD = true, minHFD = 0.1f) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusFittingMode.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusFittingMode.kt new file mode 100644 index 000000000..ea43d243a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusFittingMode.kt @@ -0,0 +1,9 @@ +package nebulosa.api.autofocus + +enum class AutoFocusFittingMode { + TRENDLINES, + PARABOLIC, + TREND_PARABOLIC, + HYPERBOLIC, + TREND_HYPERBOLIC +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt new file mode 100644 index 000000000..f1e807513 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt @@ -0,0 +1,20 @@ +package nebulosa.api.autofocus + +import nebulosa.api.cameras.CameraEventAware +import nebulosa.api.focusers.FocuserEventAware +import nebulosa.api.tasks.Job +import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.focuser.FocuserEvent + +data class AutoFocusJob(override val task: AutoFocusTask) : Job(), CameraEventAware, FocuserEventAware { + + override val name = "${task.camera.name} Auto Focus Job" + + override fun handleCameraEvent(event: CameraEvent) { + task.handleCameraEvent(event) + } + + override fun handleFocuserEvent(event: FocuserEvent) { + task.handleFocuserEvent(event) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt new file mode 100644 index 000000000..b19e26940 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt @@ -0,0 +1,15 @@ +package nebulosa.api.autofocus + +import nebulosa.api.cameras.CameraStartCaptureRequest + +data class AutoFocusRequest( + @JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC, + @JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, + @JvmField val rSquaredThreshold: Double = 0.7, + @JvmField val backlashCompensationMode: BacklashCompensationMode = BacklashCompensationMode.OVERSHOOT, + @JvmField val backlashIn: Int = 0, + @JvmField val backlashOut: Int = 0, + @JvmField val initialOffsetSteps: Int = 4, + @JvmField val stepSize: Int = 50, + @JvmField val totalNumberOfAttempts: Int = 1, +) diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt new file mode 100644 index 000000000..8d268a4de --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt @@ -0,0 +1,15 @@ +package nebulosa.api.autofocus + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.focuser.Focuser +import org.springframework.stereotype.Service + +@Service +class AutoFocusService( + private val autoFocusExecutor: AutoFocusExecutor, +) { + + fun start(camera: Camera, focuser: Focuser, body: AutoFocusRequest) { + autoFocusExecutor.execute(camera, focuser, body) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt new file mode 100644 index 000000000..09dec9fdf --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -0,0 +1,362 @@ +package nebulosa.api.autofocus + +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.cameras.* +import nebulosa.api.focusers.FocuserEventAware +import nebulosa.api.focusers.FocuserMoveAbsoluteTask +import nebulosa.api.focusers.FocuserMoveTask +import nebulosa.api.image.ImageBucket +import nebulosa.api.messages.MessageEvent +import nebulosa.api.tasks.AbstractTask +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.curve.fitting.CurvePoint +import nebulosa.curve.fitting.CurvePoint.Companion.midPoint +import nebulosa.curve.fitting.HyperbolicFitting +import nebulosa.curve.fitting.QuadraticFitting +import nebulosa.curve.fitting.TrendLineFitting +import nebulosa.image.Image +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.log.loggerFor +import nebulosa.star.detection.ImageStar +import nebulosa.star.detection.StarDetector +import java.nio.file.Files +import java.time.Duration +import kotlin.math.max +import kotlin.math.roundToInt +import kotlin.math.sqrt + +data class AutoFocusTask( + @JvmField val camera: Camera, + @JvmField val focuser: Focuser, + @JvmField val request: AutoFocusRequest, + @JvmField val starDetection: StarDetector, + @JvmField val imageBucket: ImageBucket, +) : AbstractTask(), Consumer, CameraEventAware, FocuserEventAware { + + data class MeasuredStars( + @JvmField val averageHFD: Double = 0.0, + @JvmField var hfdStandardDeviation: Double = 0.0, + ) + + @JvmField val cameraRequest = request.capture.copy( + exposureAmount = 0, exposureDelay = Duration.ZERO, + savePath = Files.createTempDirectory("af"), + exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), + frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF + ) + + private val focusPoints = ArrayList() + private val measurements = ArrayList(request.capture.exposureAmount) + private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = request.capture.exposureAmount) + + @Volatile private var focuserMoveTask: FocuserMoveTask? = null + @Volatile private var trendLineCurve: Lazy? = null + @Volatile private var parabolicCurve: Lazy? = null + @Volatile private var hyperbolicCurve: Lazy? = null + + init { + cameraCaptureTask.subscribe(this) + } + + override fun handleCameraEvent(event: CameraEvent) { + cameraCaptureTask.handleCameraEvent(event) + } + + override fun handleFocuserEvent(event: FocuserEvent) { + focuserMoveTask?.handleFocuserEvent(event) + } + + override fun canUseAsLastEvent(event: MessageEvent) = event is AutoFocusEvent + + override fun execute(cancellationToken: CancellationToken) { + val initialFocusPosition = focuser.position + + // Get initial position information, as average of multiple exposures, if configured this way. + val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken).averageHFD else Double.NaN + + val reverse = request.backlashCompensationMode == BacklashCompensationMode.OVERSHOOT && request.backlashIn > 0 && request.backlashOut == 0 + + var numberOfAttempts = 0 + var reattempt: Boolean + val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 + + do { + reattempt = false + numberOfAttempts++ + + val offsetSteps = request.initialOffsetSteps + val numberOfSteps = offsetSteps + 1 + + obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken) + + var leftCount = trendLineCurve!!.value.left.points.size + var rightCount = trendLineCurve!!.value.right.points.size + + // When datapoints are not sufficient analyze and take more. + do { + if (leftCount == 0 && rightCount == 0) { + // TODO: ERROR NotEnoughtSpreadedPoints + // Reattempting in this situation is very likely meaningless - just move back to initial focus position and call it a day. + moveFocuser(initialFocusPosition, cancellationToken) + return + } + + // Let's keep moving in, one step at a time, until we have enough left trend points. + // Then we can think about moving out to fill in the right trend points. + if (trendLineCurve!!.value.left.points.size < offsetSteps + && focusPoints.count { it.x < trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps + ) { + LOG.info("more data points needed to the left of the minimum") + + // Move to the leftmost point - this should never be necessary since we're already there, but just in case + if (focuser.position != focusPoints.first().x.roundToInt()) { + moveFocuser(focusPoints.first().x.roundToInt(), cancellationToken) + } + + // More points needed to the left. + obtainFocusPoints(1, -1, false, cancellationToken) + } else if (trendLineCurve!!.value.right.points.size < offsetSteps + && focusPoints.count { it.x > trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps + ) { + // Now we can go to the right, if necessary. + LOG.info("more data points needed to the right of the minimum") + + // More points needed to the right. Let's get to the rightmost point, and keep going right one point at a time. + if (focuser.position != focusPoints.last().x.roundToInt()) { + moveFocuser(focusPoints.last().x.roundToInt(), cancellationToken) + } + + // More points needed to the right. + obtainFocusPoints(1, 1, false, cancellationToken) + } + + leftCount = trendLineCurve!!.value.left.points.size + rightCount = trendLineCurve!!.value.right.points.size + + if (maximumFocusPoints < focusPoints.size) { + // Break out when the maximum limit of focus points is reached + // TODO: ERROR + LOG.error("failed to complete. Maximum number of focus points exceeded ($maximumFocusPoints).") + break + } + + if (focuser.position == 0) { + // Break out when the focuser hits the zero position. It can't continue from there. + LOG.error("failed to complete. position reached 0") + break + } + } while (rightCount + focusPoints.count { it.x > trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps) + + val finalFocusPoint = determineFinalFocusPoint() + val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken) + + if (!goodAutoFocus) { + if (numberOfAttempts < request.totalNumberOfAttempts) { + moveFocuser(initialFocusPosition, cancellationToken) + LOG.warn("potentially bad auto-focus. reattempting") + reset() + reattempt = true + } else { + LOG.warn("potentially bad auto-focus. Restoring original focus position") + reattempt = false + moveFocuser(initialFocusPosition, cancellationToken) + } + } + } while (reattempt) + + reset() + } + + private fun determineFinalFocusPoint(): CurvePoint { + val trendLine by lazy { TrendLineFitting.calculate(focusPoints) } + val hyperbolic by lazy { HyperbolicFitting.calculate(focusPoints) } + val parabolic by lazy { QuadraticFitting.calculate(focusPoints) } + + return when (request.fittingMode) { + AutoFocusFittingMode.TRENDLINES -> trendLine.intersection + AutoFocusFittingMode.PARABOLIC -> parabolic.minimum + AutoFocusFittingMode.TREND_PARABOLIC -> trendLine.intersection midPoint parabolic.minimum + AutoFocusFittingMode.HYPERBOLIC -> hyperbolic.minimum + AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLine.intersection midPoint hyperbolic.minimum + } + } + + private fun evaluateAllMeasurements(): MeasuredStars { + var sumHFD = 0.0 + var sumVariances = 0.0 + + for ((averageHFD, hfdStandardDeviation) in measurements) { + sumHFD += averageHFD + sumVariances += hfdStandardDeviation * hfdStandardDeviation + } + + return MeasuredStars(sumHFD / request.capture.exposureAmount, sqrt(sumVariances / request.capture.exposureAmount)) + } + + override fun accept(event: CameraCaptureEvent) { + if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { + val image = imageBucket.open(event.savePath!!) + val detectedStars = starDetection.detect(image) + val measure = detectedStars.measureDetectedStars() + measurements.add(measure) + } + } + + private fun takeExposure(cancellationToken: CancellationToken): MeasuredStars { + measurements.clear() + cameraCaptureTask.execute(cancellationToken) + return evaluateAllMeasurements() + } + + private fun obtainFocusPoints(numberOfSteps: Int, offset: Int, reverse: Boolean, cancellationToken: CancellationToken) { + val stepSize = request.stepSize + val direction = if (reverse) -1 else 1 + + var focusPosition = 0 + + if (offset != 0) { + focuserMoveTask = FocuserMoveAbsoluteTask(focuser, direction * offset * stepSize) + focuserMoveTask!!.execute(cancellationToken) + focusPosition = focuser.position + } + + var remainingSteps = numberOfSteps + + while (!cancellationToken.isDone && remainingSteps > 0) { + val currentFocusPosition = focusPosition + + if (remainingSteps > 1) { + focuserMoveTask = FocuserMoveAbsoluteTask(focuser, direction * -stepSize) + focuserMoveTask!!.execute(cancellationToken) + focusPosition = focuser.position + } + + val measurement = takeExposure(cancellationToken) + + // If star measurement is 0, we didn't detect any stars or shapes, + // and want this point to be ignored by the fitting as much as possible. + // Setting a very high Stdev will do the trick. + if (measurement.averageHFD == 0.0) { + LOG.warn("No stars detected in step. Setting a high standard deviation to ignore the point.") + measurement.hfdStandardDeviation = 1000.0 + } + + val weight = max(0.001, measurement.hfdStandardDeviation) + focusPoints.add(CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD, weight)) + focusPoints.sortBy { it.x } + + computeCurveFittings() + + remainingSteps-- + } + } + + private fun computeCurveFittings() { + with(focusPoints.toList()) { + trendLineCurve = lazy { TrendLineFitting.calculate(this) } + + if (size >= 3) { + if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { + parabolicCurve = lazy { QuadraticFitting.calculate(this) } + } + if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) { + hyperbolicCurve = lazy { HyperbolicFitting.calculate(this) } + } + } + } + } + + private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double, cancellationToken: CancellationToken): Boolean { + val threshold = request.rSquaredThreshold + + fun isTrendLineBad() = trendLineCurve?.value?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: false + + fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: false + + fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: false + + if (threshold > 0.0) { + val bad = when (request.fittingMode) { + AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() + AutoFocusFittingMode.PARABOLIC -> isParabolicBad() + AutoFocusFittingMode.TREND_PARABOLIC -> isParabolicBad() || isTrendLineBad() + AutoFocusFittingMode.HYPERBOLIC -> isHyperbolicBad() + AutoFocusFittingMode.TREND_HYPERBOLIC -> isHyperbolicBad() || isTrendLineBad() + } + + if (bad) { + LOG.error("coefficient of determination is below threshold") + return false + } + } + + val min = focusPoints.minOf { it.x } + val max = focusPoints.maxOf { it.x } + + if (focusPoint.x < min || focusPoint.y > max) { + LOG.error("determined focus point position is outside of the overall measurement points of the curve") + return false + } + + moveFocuser(focusPoint.x.roundToInt(), cancellationToken) + val hfd = takeExposure(cancellationToken).averageHFD + + if (threshold <= 0) { + if (initialHFD != 0.0 && hfd > initialHFD * 1.15) { + LOG.warn("New focus point HFR $hfd is significantly worse than original HFR $initialHFD") + return false + } + } + + return true + } + + private fun moveFocuser(position: Int, cancellationToken: CancellationToken) { + focuserMoveTask = FocuserMoveAbsoluteTask(focuser, position) + focuserMoveTask!!.execute(cancellationToken) + } + + override fun reset() { + cameraCaptureTask.reset() + focusPoints.clear() + + trendLineCurve = null + parabolicCurve = null + hyperbolicCurve = null + } + + override fun close() { + super.close() + cameraCaptureTask.close() + } + + companion object { + + @JvmStatic private val MIN_EXPOSURE_TIME = Duration.ofSeconds(1L) + @JvmStatic private val LOG = loggerFor() + + @JvmStatic + private fun List.measureDetectedStars(): MeasuredStars { + val mean = sumOf { it.hfd } / size + + var stdDev = 0.0 + + if (size > 1) { + for (star in this) { + stdDev += (star.hfd - mean).let { it * it } + } + + stdDev /= size - 1 + stdDev = sqrt(stdDev) + } else { + stdDev = Double.NaN + } + + return MeasuredStars(mean, stdDev) + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt b/api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt new file mode 100644 index 000000000..d80ab414b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt @@ -0,0 +1,6 @@ +package nebulosa.api.autofocus + +enum class BacklashCompensationMode { + ABSOLUTE, + OVERSHOOT, +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt index 584e66013..652365814 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -3,11 +3,11 @@ package nebulosa.api.cameras import nebulosa.api.tasks.Job import nebulosa.indi.device.camera.CameraEvent -data class CameraCaptureJob(override val task: CameraCaptureTask) : Job() { +data class CameraCaptureJob(override val task: CameraCaptureTask) : Job(), CameraEventAware { override val name = "${task.camera.name} Camera Capture Job" - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { task.handleCameraEvent(event) } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index 79bf64ffc..83a83251b 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -24,7 +24,7 @@ data class CameraCaptureTask( private val useFirstExposure: Boolean = false, private val exposureMaxRepeat: Int = 0, private val executor: Executor? = null, -) : AbstractTask(), Consumer { +) : AbstractTask(), Consumer, CameraEventAware { private val delayTask = DelayTask(request.exposureDelay) private val waitForSettleTask = WaitForSettleTask(guider) @@ -58,7 +58,7 @@ data class CameraCaptureTask( } } - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { cameraExposureTask.handleCameraEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventAware.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventAware.kt new file mode 100644 index 000000000..44acf68fa --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventAware.kt @@ -0,0 +1,8 @@ +package nebulosa.api.cameras + +import nebulosa.indi.device.camera.CameraEvent + +fun interface CameraEventAware { + + fun handleCameraEvent(event: CameraEvent) +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt index d7f210a64..018417492 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt @@ -20,7 +20,7 @@ import kotlin.io.path.outputStream data class CameraExposureTask( @JvmField val camera: Camera, @JvmField val request: CameraStartCaptureRequest, -) : AbstractTask(), CancellationListener { +) : AbstractTask(), CancellationListener, CameraEventAware { private val latch = CountUpDownLatch() private val aborted = AtomicBoolean() @@ -34,7 +34,7 @@ data class CameraExposureTask( val isAborted get() = aborted.get() - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { if (event.device === camera) { when (event) { is CameraFrameCaptured -> { @@ -125,7 +125,7 @@ data class CameraExposureTask( } else if (event.image != null) { savedPath.sink().use(event.image!!::write) } else { - LOG.warn("invalid event. camera={}", event.device) + LOG.warn("invalid event. event={}", event) return } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventAware.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventAware.kt new file mode 100644 index 000000000..555b783b5 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventAware.kt @@ -0,0 +1,8 @@ +package nebulosa.api.focusers + +import nebulosa.indi.device.focuser.FocuserEvent + +fun interface FocuserEventAware { + + fun handleFocuserEvent(event: FocuserEvent) +} diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt new file mode 100644 index 000000000..d51ffd0da --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt @@ -0,0 +1,61 @@ +package nebulosa.api.focusers + +import nebulosa.common.concurrency.cancel.CancellationListener +import nebulosa.common.concurrency.cancel.CancellationSource +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.indi.device.focuser.FocuserMoveFailed +import nebulosa.indi.device.focuser.FocuserPositionChanged +import nebulosa.log.loggerFor +import kotlin.math.abs + +data class FocuserMoveAbsoluteTask( + override val focuser: Focuser, + @JvmField val position: Int, +) : FocuserMoveTask, CancellationListener { + + private val latch = CountUpDownLatch() + + override fun handleFocuserEvent(event: FocuserEvent) { + if (event.device === focuser) { + when (event) { + is FocuserPositionChanged -> if (focuser.position == position) latch.reset() + is FocuserMoveFailed -> latch.reset() + } + } + } + + override fun execute(cancellationToken: CancellationToken) { + if (!cancellationToken.isDone && focuser.connected + && !focuser.moving && position != focuser.position + ) { + try { + cancellationToken.listen(this) + + LOG.info("Focuser move started. focuser={}, position={}", focuser, position) + + if (focuser.canAbsoluteMove) focuser.moveFocusTo(position) + else if (focuser.position - position < 0) focuser.moveFocusIn(abs(focuser.position - position)) + else focuser.moveFocusOut(abs(focuser.position - position)) + + latch.await() + } finally { + cancellationToken.unlisten(this) + } + + LOG.info("Focuser move finished. focuser={}, position={}", focuser, position) + } + } + + override fun onCancel(source: CancellationSource) { + focuser.abortFocus() + latch.reset() + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt new file mode 100644 index 000000000..3ad2bae08 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt @@ -0,0 +1,63 @@ +package nebulosa.api.focusers + +import nebulosa.common.concurrency.cancel.CancellationListener +import nebulosa.common.concurrency.cancel.CancellationSource +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.indi.device.focuser.FocuserMoveFailed +import nebulosa.indi.device.focuser.FocuserPositionChanged +import nebulosa.log.loggerFor +import kotlin.math.abs + +data class FocuserMoveRelativeTask( + override val focuser: Focuser, + @JvmField val offset: Int, +) : FocuserMoveTask, CancellationListener { + + private val latch = CountUpDownLatch() + + @Volatile private var initialPosition = 0 + + override fun handleFocuserEvent(event: FocuserEvent) { + if (event.device === focuser) { + when (event) { + is FocuserPositionChanged -> if (abs(focuser.position - initialPosition) == abs(offset)) latch.reset() + is FocuserMoveFailed -> latch.reset() + } + } + } + + override fun execute(cancellationToken: CancellationToken) { + if (!cancellationToken.isDone && focuser.connected && !focuser.moving && offset != 0) { + try { + cancellationToken.listen(this) + + initialPosition = focuser.position + + LOG.info("Focuser move started. focuser={}, offset={}", focuser, offset) + + if (!focuser.canRelativeMove) focuser.moveFocusTo(focuser.position + offset) + else if (offset > 0) focuser.moveFocusOut(offset) + else focuser.moveFocusIn(offset) + + latch.await() + } finally { + cancellationToken.unlisten(this) + } + + LOG.info("Focuser move finished. focuser={}, offset={}", focuser, offset) + } + } + + override fun onCancel(source: CancellationSource) { + focuser.abortFocus() + latch.reset() + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveTask.kt new file mode 100644 index 000000000..729ca77af --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveTask.kt @@ -0,0 +1,9 @@ +package nebulosa.api.focusers + +import nebulosa.api.tasks.Task +import nebulosa.indi.device.focuser.Focuser + +interface FocuserMoveTask : Task, FocuserEventAware { + + val focuser: Focuser +} diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountEventAware.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountEventAware.kt new file mode 100644 index 000000000..0cc5c0b31 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountEventAware.kt @@ -0,0 +1,8 @@ +package nebulosa.api.mounts + +import nebulosa.indi.device.mount.MountEvent + +fun interface MountEventAware { + + fun handleMountEvent(event: MountEvent) +} diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt index 01d9dd9d8..e6eecd2ae 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt @@ -20,7 +20,7 @@ data class MountSlewTask( @JvmField val mount: Mount, @JvmField val rightAscension: Angle, @JvmField val declination: Angle, @JvmField val j2000: Boolean = false, @JvmField val goTo: Boolean = true, -) : Task, CancellationListener { +) : Task, CancellationListener, MountEventAware { private val delayTask = DelayTask(SETTLE_DURATION) private val latch = CountUpDownLatch() @@ -28,7 +28,7 @@ data class MountSlewTask( @Volatile private var initialRA = mount.rightAscension @Volatile private var initialDEC = mount.declination - fun handleMountEvent(event: MountEvent) { + override fun handleMountEvent(event: MountEvent) { if (event.device === mount) { if (event is MountSlewingChanged) { if (!mount.slewing && (mount.rightAscension != initialRA || mount.declination != initialDEC)) { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt index 0eb144303..55b2e7d70 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt @@ -1,18 +1,20 @@ package nebulosa.api.sequencer +import nebulosa.api.cameras.CameraEventAware import nebulosa.api.tasks.Job +import nebulosa.api.wheels.WheelEventAware import nebulosa.indi.device.camera.CameraEvent import nebulosa.indi.device.filterwheel.FilterWheelEvent -data class SequencerJob(override val task: SequencerTask) : Job() { +data class SequencerJob(override val task: SequencerTask) : Job(), CameraEventAware, WheelEventAware { override val name = "${task.camera.name} Sequencer Job" - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { task.handleCameraEvent(event) } - fun handleFilterWheelEvent(event: FilterWheelEvent) { + override fun handleFilterWheelEvent(event: FilterWheelEvent) { task.handleFilterWheelEvent(event) } } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt index f099436eb..ed3fdb6be 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt @@ -1,15 +1,13 @@ package nebulosa.api.sequencer import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraCaptureState -import nebulosa.api.cameras.CameraCaptureTask -import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.cameras.* import nebulosa.api.messages.MessageEvent import nebulosa.api.tasks.AbstractTask import nebulosa.api.tasks.Task import nebulosa.api.tasks.delay.DelayEvent import nebulosa.api.tasks.delay.DelayTask +import nebulosa.api.wheels.WheelEventAware import nebulosa.api.wheels.WheelMoveTask import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.guiding.Guider @@ -39,7 +37,7 @@ data class SequencerTask( @JvmField val wheel: FilterWheel? = null, @JvmField val focuser: Focuser? = null, private val executor: Executor? = null, -) : AbstractTask(), Consumer { +) : AbstractTask(), Consumer, CameraEventAware, WheelEventAware { private val usedEntries = plan.entries.filter { it.enabled } @@ -108,7 +106,7 @@ data class SequencerTask( } } - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { val task = currentTask.get() if (task is CameraCaptureTask) { @@ -116,7 +114,7 @@ data class SequencerTask( } } - fun handleFilterWheelEvent(event: FilterWheelEvent) { + override fun handleFilterWheelEvent(event: FilterWheelEvent) { val task = currentTask.get() if (task is WheelMoveTask) { diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventAware.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventAware.kt new file mode 100644 index 000000000..664c99f1a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventAware.kt @@ -0,0 +1,8 @@ +package nebulosa.api.wheels + +import nebulosa.indi.device.filterwheel.FilterWheelEvent + +fun interface WheelEventAware { + + fun handleFilterWheelEvent(event: FilterWheelEvent) +} diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt index 01340b1b9..cc9235d2f 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt @@ -12,13 +12,13 @@ import nebulosa.log.loggerFor data class WheelMoveTask( @JvmField val wheel: FilterWheel, @JvmField val position: Int, -) : Task { +) : Task, WheelEventAware { private val latch = CountUpDownLatch() @Volatile private var initialPosition = wheel.position - fun handleFilterWheelEvent(event: FilterWheelEvent) { + override fun handleFilterWheelEvent(event: FilterWheelEvent) { if (event is FilterWheelPositionChanged) { if (initialPosition != wheel.position && wheel.position == position) { latch.reset() diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt index 05c21acc6..4dfcb7a3a 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt @@ -1,13 +1,14 @@ package nebulosa.api.wizard.flat +import nebulosa.api.cameras.CameraEventAware import nebulosa.api.tasks.Job import nebulosa.indi.device.camera.CameraEvent -data class FlatWizardJob(override val task: FlatWizardTask) : Job() { +data class FlatWizardJob(override val task: FlatWizardTask) : Job(), CameraEventAware { override val name = "${task.camera.name} Flat Wizard Job" - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { task.handleCameraEvent(event) } } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt index 865bed152..06f8c848b 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt @@ -1,9 +1,6 @@ package nebulosa.api.wizard.flat -import nebulosa.api.cameras.AutoSubFolderMode -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraCaptureState -import nebulosa.api.cameras.CameraCaptureTask +import nebulosa.api.cameras.* import nebulosa.api.messages.MessageEvent import nebulosa.api.tasks.AbstractTask import nebulosa.common.concurrency.cancel.CancellationToken @@ -20,7 +17,7 @@ import java.time.Duration data class FlatWizardTask( @JvmField val camera: Camera, @JvmField val request: FlatWizardRequest, -) : AbstractTask() { +) : AbstractTask(), CameraEventAware { private val meanTarget = request.meanTarget / 65535f private val meanRange = (meanTarget * request.meanTolerance / 100f).let { (meanTarget - it)..(meanTarget + it) } @@ -34,7 +31,7 @@ data class FlatWizardTask( @Volatile private var capture: CameraCaptureEvent? = null @Volatile private var savedPath: Path? = null - fun handleCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { cameraCaptureTask?.handleCameraEvent(event) } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt index 33e752394..553267852 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt @@ -2,27 +2,32 @@ package nebulosa.curve.fitting import org.apache.commons.math3.fitting.WeightedObservedPoint -class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) { +class CurvePoint(x: Double, y: Double, weight: Double = 1.0) : WeightedObservedPoint(weight, x, y) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CurvePoint) return false if (x != other.x) return false + if (y != other.y) return false - return y == other.y + return weight == other.weight } override fun hashCode(): Int { var result = x.hashCode() result = 31 * result + y.hashCode() + result = 31 * result + weight.hashCode() return result } - override fun toString() = "CurvePoint(x=$x, y=$y)" + override fun toString() = "CurvePoint(x=$x, y=$y, weight=$weight)" companion object { @JvmStatic val ZERO = CurvePoint(0.0, 0.0) + + @JvmStatic + infix fun CurvePoint.midPoint(point: CurvePoint) = CurvePoint((x + point.x) / 2, (y + point.y) / 2) } } diff --git a/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/Star.kt b/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/Star.kt index 57cfd3a13..ee650cec0 100644 --- a/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/Star.kt +++ b/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/Star.kt @@ -3,7 +3,8 @@ package nebulosa.watney.star.detection import nebulosa.star.detection.ImageStar data class Star( - override val x: Double = 0.0, override val y: Double = 0.0, + override val x: Double = 0.0, + override val y: Double = 0.0, val size: Double = 0.0, override var hfd: Double = 0.0, override var snr: Double = 0.0, From 67e0cf4ea7fb174ad22fd01f218494c53bb0c724 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 25 May 2024 21:59:23 -0300 Subject: [PATCH 20/45] [api]: Support Auto Focus --- .../api/alignment/polar/tppa/TPPATask.kt | 12 +- .../nebulosa/api/autofocus/AutoFocusTask.kt | 118 +++++++------ .../nebulosa/api/cameras/CameraCaptureTask.kt | 4 +- .../api/focusers/FocuserMoveAbsoluteTask.kt | 8 +- .../api/focusers/FocuserMoveRelativeTask.kt | 10 +- .../api/guiding/DitherAfterExposureTask.kt | 2 +- .../nebulosa/api/guiding/GuidePulseTask.kt | 2 +- .../nebulosa/api/guiding/WaitForSettleTask.kt | 2 +- .../kotlin/nebulosa/api/image/ImageBucket.kt | 26 ++- .../kotlin/nebulosa/api/image/ImageService.kt | 7 +- .../nebulosa/api/mounts/MountMoveTask.kt | 2 +- .../nebulosa/api/mounts/MountService.kt | 2 +- .../nebulosa/api/mounts/MountSlewTask.kt | 2 +- .../nebulosa/api/sequencer/SequencerTask.kt | 2 +- .../nebulosa/api/tasks/delay/DelayTask.kt | 4 +- .../api/wizard/flat/FlatWizardTask.kt | 6 +- api/src/test/kotlin/APITest.kt | 158 +++++++++++++++--- .../point/three/ThreePointPolarAlignment.kt | 4 +- .../src/test/kotlin/CancellationTokenTest.kt | 10 +- 19 files changed, 266 insertions(+), 115 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt index 1b8f1e1be..1f4504d6b 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt @@ -114,14 +114,14 @@ data class TPPATask( cancellationToken.listenToPause(this) - while (!cancellationToken.isDone) { + while (!cancellationToken.isCancelled) { if (cancellationToken.isPaused) { pausing.set(false) sendEvent(TPPAState.PAUSED) cancellationToken.waitForPause() } - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break mount?.tracking(true) @@ -134,7 +134,7 @@ data class TPPATask( mountMoveState[alignment.state.ordinal] = true } - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break rightAscension = mount.rightAscension declination = mount.declination @@ -146,14 +146,14 @@ data class TPPATask( } } - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break sendEvent(TPPAState.EXPOSURING) // CAPTURE. cameraCaptureTask.execute(cancellationToken) - if (cancellationToken.isDone || savedImage == null) { + if (cancellationToken.isCancelled || savedImage == null) { break } @@ -177,7 +177,7 @@ data class TPPATask( LOG.info("TPPA alignment completed. result=$result") - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break when (result) { is ThreePointPolarAlignmentResult.NeedMoreMeasurement -> { diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 09dec9fdf..7325c85c1 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -4,6 +4,7 @@ import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.cameras.* import nebulosa.api.focusers.FocuserEventAware import nebulosa.api.focusers.FocuserMoveAbsoluteTask +import nebulosa.api.focusers.FocuserMoveRelativeTask import nebulosa.api.focusers.FocuserMoveTask import nebulosa.api.image.ImageBucket import nebulosa.api.messages.MessageEvent @@ -40,7 +41,13 @@ data class AutoFocusTask( data class MeasuredStars( @JvmField val averageHFD: Double = 0.0, @JvmField var hfdStandardDeviation: Double = 0.0, - ) + ) { + + companion object { + + @JvmStatic val ZERO = MeasuredStars() + } + } @JvmField val cameraRequest = request.capture.copy( exposureAmount = 0, exposureDelay = Duration.ZERO, @@ -54,7 +61,7 @@ data class AutoFocusTask( private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = request.capture.exposureAmount) @Volatile private var focuserMoveTask: FocuserMoveTask? = null - @Volatile private var trendLineCurve: Lazy? = null + @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null @Volatile private var parabolicCurve: Lazy? = null @Volatile private var hyperbolicCurve: Lazy? = null @@ -77,69 +84,71 @@ data class AutoFocusTask( // Get initial position information, as average of multiple exposures, if configured this way. val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken).averageHFD else Double.NaN - val reverse = request.backlashCompensationMode == BacklashCompensationMode.OVERSHOOT && request.backlashIn > 0 && request.backlashOut == 0 + LOG.info("Auto Focus started. initialHFD={}, reverse={}, camera={}, focuser={}", initialHFD, reverse, camera, focuser) + + var exited = false var numberOfAttempts = 0 - var reattempt: Boolean val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 - do { - reattempt = false + while (!exited && !cancellationToken.isCancelled) { numberOfAttempts++ val offsetSteps = request.initialOffsetSteps val numberOfSteps = offsetSteps + 1 + LOG.info("attempt #{}. offsetSteps={}, numberOfSteps={}", numberOfAttempts, offsetSteps, numberOfSteps) + obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken) - var leftCount = trendLineCurve!!.value.left.points.size - var rightCount = trendLineCurve!!.value.right.points.size + var leftCount = trendLineCurve!!.left.points.size + var rightCount = trendLineCurve!!.right.points.size - // When datapoints are not sufficient analyze and take more. + // When data points are not sufficient analyze and take more. do { if (leftCount == 0 && rightCount == 0) { - // TODO: ERROR NotEnoughtSpreadedPoints - // Reattempting in this situation is very likely meaningless - just move back to initial focus position and call it a day. - moveFocuser(initialFocusPosition, cancellationToken) - return + LOG.warn("Not enought spreaded points") + exited = true + break } + LOG.info("data points are not sufficient. attempt={}, numberOfSteps={}", numberOfAttempts, numberOfSteps) + // Let's keep moving in, one step at a time, until we have enough left trend points. // Then we can think about moving out to fill in the right trend points. - if (trendLineCurve!!.value.left.points.size < offsetSteps - && focusPoints.count { it.x < trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps + if (trendLineCurve!!.left.points.size < offsetSteps + && focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps ) { LOG.info("more data points needed to the left of the minimum") // Move to the leftmost point - this should never be necessary since we're already there, but just in case if (focuser.position != focusPoints.first().x.roundToInt()) { - moveFocuser(focusPoints.first().x.roundToInt(), cancellationToken) + moveFocuser(focusPoints.first().x.roundToInt(), cancellationToken, false) } // More points needed to the left. obtainFocusPoints(1, -1, false, cancellationToken) - } else if (trendLineCurve!!.value.right.points.size < offsetSteps - && focusPoints.count { it.x > trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps + } else if (trendLineCurve!!.right.points.size < offsetSteps + && focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps ) { // Now we can go to the right, if necessary. LOG.info("more data points needed to the right of the minimum") // More points needed to the right. Let's get to the rightmost point, and keep going right one point at a time. if (focuser.position != focusPoints.last().x.roundToInt()) { - moveFocuser(focusPoints.last().x.roundToInt(), cancellationToken) + moveFocuser(focusPoints.last().x.roundToInt(), cancellationToken, false) } // More points needed to the right. obtainFocusPoints(1, 1, false, cancellationToken) } - leftCount = trendLineCurve!!.value.left.points.size - rightCount = trendLineCurve!!.value.right.points.size + leftCount = trendLineCurve!!.left.points.size + rightCount = trendLineCurve!!.right.points.size if (maximumFocusPoints < focusPoints.size) { // Break out when the maximum limit of focus points is reached - // TODO: ERROR LOG.error("failed to complete. Maximum number of focus points exceeded ($maximumFocusPoints).") break } @@ -149,26 +158,35 @@ data class AutoFocusTask( LOG.error("failed to complete. position reached 0") break } - } while (rightCount + focusPoints.count { it.x > trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps) + } while (!cancellationToken.isCancelled && (rightCount + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps)) + + if (exited) break val finalFocusPoint = determineFinalFocusPoint() val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken) if (!goodAutoFocus) { if (numberOfAttempts < request.totalNumberOfAttempts) { - moveFocuser(initialFocusPosition, cancellationToken) + moveFocuser(initialFocusPosition, cancellationToken, false) LOG.warn("potentially bad auto-focus. reattempting") reset() - reattempt = true + continue } else { LOG.warn("potentially bad auto-focus. Restoring original focus position") - reattempt = false - moveFocuser(initialFocusPosition, cancellationToken) + moveFocuser(initialFocusPosition, cancellationToken, false) + break } } - } while (reattempt) + } + + if (exited || cancellationToken.isCancelled) { + LOG.warn("did not complete successfully, so restoring the focuser position to $initialFocusPosition") + moveFocuser(initialFocusPosition, CancellationToken.NONE, false) + } reset() + + LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser) } private fun determineFinalFocusPoint(): CurvePoint { @@ -201,8 +219,11 @@ data class AutoFocusTask( if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { val image = imageBucket.open(event.savePath!!) val detectedStars = starDetection.detect(image) + LOG.info("detected ${detectedStars.size} stars") val measure = detectedStars.measureDetectedStars() + LOG.info("HFD measurement. mean={}, stdDev={}", measure.averageHFD, measure.hfdStandardDeviation) measurements.add(measure) + onNext(event) } } @@ -216,23 +237,21 @@ data class AutoFocusTask( val stepSize = request.stepSize val direction = if (reverse) -1 else 1 + LOG.info("retrieving focus points. numberOfSteps={}, offset={}, reverse={}", numberOfSteps, offset, reverse) + var focusPosition = 0 if (offset != 0) { - focuserMoveTask = FocuserMoveAbsoluteTask(focuser, direction * offset * stepSize) - focuserMoveTask!!.execute(cancellationToken) - focusPosition = focuser.position + focusPosition = moveFocuser(direction * offset * stepSize, cancellationToken, true) } var remainingSteps = numberOfSteps - while (!cancellationToken.isDone && remainingSteps > 0) { + while (!cancellationToken.isCancelled && remainingSteps > 0) { val currentFocusPosition = focusPosition if (remainingSteps > 1) { - focuserMoveTask = FocuserMoveAbsoluteTask(focuser, direction * -stepSize) - focuserMoveTask!!.execute(cancellationToken) - focusPosition = focuser.position + focusPosition = moveFocuser(direction * -stepSize, cancellationToken, true) } val measurement = takeExposure(cancellationToken) @@ -246,18 +265,21 @@ data class AutoFocusTask( } val weight = max(0.001, measurement.hfdStandardDeviation) - focusPoints.add(CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD, weight)) + val point = CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD, weight) + focusPoints.add(point) focusPoints.sortBy { it.x } - computeCurveFittings() - remainingSteps-- + + LOG.info("focus point added. remainingSteps={}, x={}, y={}, weight={}", remainingSteps, point.x, point.y, point.weight) + + computeCurveFittings() } } private fun computeCurveFittings() { with(focusPoints.toList()) { - trendLineCurve = lazy { TrendLineFitting.calculate(this) } + trendLineCurve = TrendLineFitting.calculate(this) if (size >= 3) { if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { @@ -273,14 +295,12 @@ data class AutoFocusTask( private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double, cancellationToken: CancellationToken): Boolean { val threshold = request.rSquaredThreshold - fun isTrendLineBad() = trendLineCurve?.value?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: false - + fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: false fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: false - fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: false if (threshold > 0.0) { - val bad = when (request.fittingMode) { + val isBad = when (request.fittingMode) { AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() AutoFocusFittingMode.PARABOLIC -> isParabolicBad() AutoFocusFittingMode.TREND_PARABOLIC -> isParabolicBad() || isTrendLineBad() @@ -288,7 +308,7 @@ data class AutoFocusTask( AutoFocusFittingMode.TREND_HYPERBOLIC -> isHyperbolicBad() || isTrendLineBad() } - if (bad) { + if (isBad) { LOG.error("coefficient of determination is below threshold") return false } @@ -302,7 +322,7 @@ data class AutoFocusTask( return false } - moveFocuser(focusPoint.x.roundToInt(), cancellationToken) + moveFocuser(focusPoint.x.roundToInt(), cancellationToken, false) val hfd = takeExposure(cancellationToken).averageHFD if (threshold <= 0) { @@ -315,9 +335,11 @@ data class AutoFocusTask( return true } - private fun moveFocuser(position: Int, cancellationToken: CancellationToken) { - focuserMoveTask = FocuserMoveAbsoluteTask(focuser, position) + private fun moveFocuser(position: Int, cancellationToken: CancellationToken, relative: Boolean): Int { + focuserMoveTask = if (relative) FocuserMoveRelativeTask(focuser, position) + else FocuserMoveAbsoluteTask(focuser, position) focuserMoveTask!!.execute(cancellationToken) + return focuser.position } override fun reset() { @@ -341,6 +363,8 @@ data class AutoFocusTask( @JvmStatic private fun List.measureDetectedStars(): MeasuredStars { + if (isEmpty()) return MeasuredStars.ZERO + val mean = sumOf { it.hfd } / size var stdDev = 0.0 diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index 83a83251b..6d6ade6f0 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -67,7 +67,7 @@ data class CameraCaptureTask( cameraExposureTask.reset() - while (!cancellationToken.isDone && + while (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && ((exposureMaxRepeat > 0 && exposureRepeatCount < exposureMaxRepeat) || (exposureMaxRepeat <= 0 && (request.isLoop || exposureCount < request.exposureAmount))) @@ -100,7 +100,7 @@ data class CameraCaptureTask( cameraExposureTask.execute(cancellationToken) // DITHER. - if (!cancellationToken.isDone && !cameraExposureTask.isAborted && guider != null + if (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && guider != null && exposureCount >= 1 && exposureCount % request.dither.afterExposures == 0 ) { ditherAfterExposureTask.execute(cancellationToken) diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt index d51ffd0da..b154ee55e 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt @@ -28,13 +28,15 @@ data class FocuserMoveAbsoluteTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && focuser.connected + if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && position != focuser.position ) { try { cancellationToken.listen(this) - LOG.info("Focuser move started. focuser={}, position={}", focuser, position) + LOG.info("Focuser move started. position={}, focuser={}", position, focuser) + + latch.countUp() if (focuser.canAbsoluteMove) focuser.moveFocusTo(position) else if (focuser.position - position < 0) focuser.moveFocusIn(abs(focuser.position - position)) @@ -45,7 +47,7 @@ data class FocuserMoveAbsoluteTask( cancellationToken.unlisten(this) } - LOG.info("Focuser move finished. focuser={}, position={}", focuser, position) + LOG.info("Focuser move finished. position={}, focuser={}", position, focuser) } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt index 3ad2bae08..531dafce0 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt @@ -30,24 +30,26 @@ data class FocuserMoveRelativeTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && focuser.connected && !focuser.moving && offset != 0) { + if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && offset != 0) { try { cancellationToken.listen(this) initialPosition = focuser.position - LOG.info("Focuser move started. focuser={}, offset={}", focuser, offset) + LOG.info("Focuser move started. offset={}, focuser={}", offset, focuser) + + latch.countUp() if (!focuser.canRelativeMove) focuser.moveFocusTo(focuser.position + offset) else if (offset > 0) focuser.moveFocusOut(offset) - else focuser.moveFocusIn(offset) + else focuser.moveFocusIn(abs(offset)) latch.await() } finally { cancellationToken.unlisten(this) } - LOG.info("Focuser move finished. focuser={}, offset={}", focuser, offset) + LOG.info("Focuser move finished. offset={}, focuser={}", offset, focuser) } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt index fb2de4a50..aa59f6828 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt @@ -27,7 +27,7 @@ data class DitherAfterExposureTask( override fun execute(cancellationToken: CancellationToken) { if (guider != null && guider.canDither && request.enabled && guider.state == GuideState.GUIDING - && !cancellationToken.isDone + && !cancellationToken.isCancelled ) { LOG.info("Dither started. request={}", request) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt index 787757139..5bc3f2079 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt @@ -24,7 +24,7 @@ data class GuidePulseTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && guideOutput.pulseGuide(request.duration, request.direction)) { + if (!cancellationToken.isCancelled && guideOutput.pulseGuide(request.duration, request.direction)) { LOG.info("Guide Pulse started. guideOutput={}, duration={}, direction={}", guideOutput, request.duration.toMillis(), request.direction) try { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt index 1f4aa2435..87006cb6e 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt @@ -10,7 +10,7 @@ data class WaitForSettleTask( ) : Task { override fun execute(cancellationToken: CancellationToken) { - if (guider != null && guider.isSettling && !cancellationToken.isDone) { + if (guider != null && guider.isSettling && !cancellationToken.isCancelled) { LOG.info("Wait For Settle started") guider.waitForSettle(cancellationToken) LOG.info("Wait For Settle finished") diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt index a9119ede1..7d51daeb3 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt @@ -11,17 +11,23 @@ import kotlin.io.path.extension @Component class ImageBucket { - private val bucket = HashMap>(256) + data class OpenedImage( + @JvmField val image: Image, + @JvmField var solution: PlateSolution? = null, + @JvmField val debayer: Boolean = true, + ) + + private val bucket = HashMap(256) @Synchronized - fun put(path: Path, image: Image, solution: PlateSolution? = null) { - bucket[path] = image to (solution ?: PlateSolution.from(image.header)) + fun put(path: Path, image: Image, solution: PlateSolution? = null, debayer: Boolean = true) { + bucket[path] = OpenedImage(image, solution ?: PlateSolution.from(image.header), debayer) } @Synchronized fun put(path: Path, solution: PlateSolution): Boolean { val item = bucket[path] ?: return false - bucket[path] = item.first to solution + item.solution = solution return true } @@ -29,7 +35,9 @@ class ImageBucket { fun open(path: Path, debayer: Boolean = true, solution: PlateSolution? = null, force: Boolean = false): Image { val openedImage = this[path] - if (openedImage != null && !force) return openedImage.first + if (openedImage != null && !force && debayer == openedImage.debayer) { + return openedImage.image + } val representation = when (path.extension.lowercase()) { "fit", "fits" -> path.fits() @@ -38,7 +46,7 @@ class ImageBucket { } val image = representation.use { Image.open(it, debayer) } - put(path, image, solution) + put(path, image, solution, debayer) return image } @@ -47,7 +55,7 @@ class ImageBucket { bucket.remove(path) } - operator fun get(path: Path): Pair? { + operator fun get(path: Path): OpenedImage? { return bucket[path] } @@ -56,10 +64,10 @@ class ImageBucket { } operator fun contains(image: Image): Boolean { - return bucket.any { it.value.first === image } + return bucket.any { it.value.image === image } } operator fun contains(solution: PlateSolution): Boolean { - return bucket.any { it.value.second === solution } + return bucket.any { it.value.solution === solution } } } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index 59d7ce7da..4b9303a4b 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -94,7 +94,7 @@ class ImageService( stretchParams!!.shadow, stretchParams.highlight, stretchParams.midtone, transformedImage.header.rightAscension.takeIf { it.isFinite() }, transformedImage.header.declination.takeIf { it.isFinite() }, - imageBucket[path]?.second?.let(::ImageSolved), + imageBucket[path]?.solution?.let(::ImageSolved), transformedImage.header.mapNotNull { if (it.isCommentStyle) null else ImageHeaderItem(it.key, it.value) }, transformedImage.header.bitpix, instrument, statistics, ) @@ -271,7 +271,7 @@ class ImageService( } fun saveImageAs(inputPath: Path, save: SaveImage, camera: Camera?) { - val (image) = imageBucket[inputPath]?.first?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE) + val (image) = imageBucket[inputPath]?.image?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found") require(save.path != null) @@ -292,8 +292,7 @@ class ImageService( width: Int, height: Int, fov: Angle, rotation: Angle = 0.0, id: String = "CDS/P/DSS2/COLOR", ): Path { - val (image, calibration, path) = framingService - .frame(rightAscension, declination, width, height, fov, rotation, id)!! + val (image, calibration, path) = framingService.frame(rightAscension, declination, width, height, fov, rotation, id)!! imageBucket.put(path, image, calibration) return path } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt index 9279f7400..db1546b52 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt @@ -23,7 +23,7 @@ data class MountMoveTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && request.duration.toMillis() > 0) { + if (!cancellationToken.isCancelled && request.duration.toMillis() > 0) { mount.slewRates.takeIf { !request.speed.isNullOrBlank() } ?.find { it.name == request.speed } ?.also { mount.slewRate(it) } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index 3ac78f22b..b21b85a2c 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -220,7 +220,7 @@ class MountService(private val imageBucket: ImageBucket) { } fun pointMountHere(mount: Mount, path: Path, x: Double, y: Double) { - val calibration = imageBucket[path]?.second ?: return + val calibration = imageBucket[path]?.solution ?: return if (calibration.isNotEmpty() && calibration.solved) { val wcs = WCS(calibration) diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt index e6eecd2ae..9c057a224 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt @@ -42,7 +42,7 @@ data class MountSlewTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && + if (!cancellationToken.isCancelled && mount.connected && !mount.parked && !mount.parking && !mount.slewing && rightAscension.isFinite() && declination.isFinite() && (mount.rightAscension != rightAscension || mount.declination != declination) diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt index ed3fdb6be..4efde90bf 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt @@ -128,7 +128,7 @@ data class SequencerTask( camera.snoop(listOf(mount, wheel, focuser)) for (task in tasks) { - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break currentTask.set(task) task.execute(cancellationToken) currentTask.set(null) diff --git a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt b/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt index af20bc2cf..6d3dbe5e4 100644 --- a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt +++ b/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt @@ -17,10 +17,10 @@ data class DelayTask( val durationTime = duration.toMillis() var remainingTime = durationTime - if (!cancellationToken.isDone && remainingTime > 0L) { + if (!cancellationToken.isCancelled && remainingTime > 0L) { LOG.info("Delay started. duration={}", remainingTime) - while (!cancellationToken.isDone && remainingTime > 0L) { + while (!cancellationToken.isCancelled && remainingTime > 0L) { val waitTime = minOf(remainingTime, DELAY_INTERVAL) if (waitTime > 0L) { diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt index 06f8c848b..8a0a0ec3d 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt @@ -38,7 +38,7 @@ data class FlatWizardTask( override fun canUseAsLastEvent(event: MessageEvent) = event is FlatWizardEvent override fun execute(cancellationToken: CancellationToken) { - while (!cancellationToken.isDone) { + while (!cancellationToken.isCancelled) { val delta = exposureMax.toMillis() - exposureMin.toMillis() if (delta < 10) { @@ -75,7 +75,7 @@ data class FlatWizardTask( it.execute(cancellationToken) } - if (cancellationToken.isDone) { + if (cancellationToken.isCancelled) { state = FlatWizardState.IDLE break } else if (savedPath == null) { @@ -103,7 +103,7 @@ data class FlatWizardTask( } } - if (state != FlatWizardState.FAILED && cancellationToken.isDone) { + if (state != FlatWizardState.FAILED && cancellationToken.isCancelled) { state = FlatWizardState.IDLE } diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt index 673abd1cb..8d89f6d8d 100644 --- a/api/src/test/kotlin/APITest.kt +++ b/api/src/test/kotlin/APITest.kt @@ -4,6 +4,9 @@ import com.fasterxml.jackson.module.kotlin.kotlinModule import io.kotest.core.annotation.EnabledIf import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.booleans.shouldBeTrue +import kotlinx.coroutines.delay +import nebulosa.api.autofocus.AutoFocusRequest +import nebulosa.api.beans.converters.time.DurationSerializer import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.common.json.PathSerializer import nebulosa.test.NonGitHubOnlyCondition @@ -15,26 +18,133 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor import java.nio.file.Path import java.time.Duration +import java.time.temporal.ChronoUnit @EnabledIf(NonGitHubOnlyCondition::class) class APITest : StringSpec() { init { - "Connect" { put("connection?host=localhost&port=7624") } - "Cameras" { get("cameras") } - "Camera Connect" { put("cameras/$CAMERA_NAME/connect") } - "Camera" { get("cameras/$CAMERA_NAME") } - "Camera Capture Start" { putJson("cameras/$CAMERA_NAME/capture/start", CAMERA_START_CAPTURE_REQUEST) } - "Camera Capture Stop" { put("cameras/$CAMERA_NAME/capture/abort") } - "Camera Disconnect" { put("cameras/$CAMERA_NAME/disconnect") } - "Mounts" { get("mounts") } - "Mount Connect" { put("mounts/$MOUNT_NAME/connect") } - "Mount" { get("mounts/$MOUNT_NAME") } - "Mount Telescope Control Start" { put("mounts/$MOUNT_NAME/remote-control/start?type=LX200&host=0.0.0.0&port=10001") } - "Mount Telescope Control List" { get("mounts/$MOUNT_NAME/remote-control") } - "Mount Telescope Control Stop" { put("mounts/$MOUNT_NAME/remote-control/stop?type=LX200") } - "Mount Disconnect" { put("mounts/$MOUNT_NAME/disconnect") } - "Disconnect" { delete("connection") } + // GENERAL. + + "Connect" { connect() } + "Disconnect" { disconnect() } + + // CAMERA. + + "Cameras" { cameras() } + "Camera Connect" { cameraConnect() } + "Camera" { camera() } + "Camera Capture Start" { cameraStartCapture() } + "Camera Capture Stop" { cameraStopCapture() } + "Camera Disconnect" { cameraDisconnect() } + + // MOUNT. + + "Mounts" { mounts() } + "Mount Connect" { mountConnect() } + "Mount" { mount() } + "Mount Remote Control Start" { mountRemoteControlStart() } + "Mount Remote Control List" { mountRemoteControlList() } + "Mount Remote Control Stop" { mountRemoteControlStop() } + "Mount Disconnect" { mountDisconnect() } + + // FOCUSER. + + "Focusers" { focusers() } + "Focuser Connect" { focuserConnect() } + "Focuser" { focuser() } + "Focuser Disconnect" { focuserDisconnect() } + + // AUTO FOCUS. + + "Auto Focus Start" { + connect() + delay(2000) + cameraConnect() + focuserConnect() + delay(1000) + autoFocusStart() + } + } + + private fun connect(host: String = "0.0.0.0", port: Int = 7624) { + put("connection?host=$host&port=$port") + } + + private fun disconnect() { + delete("connection") + } + + private fun cameras() { + get("cameras") + } + + private fun cameraConnect(camera: String = CAMERA_NAME) { + put("cameras/$camera/connect") + } + + private fun cameraDisconnect(camera: String = CAMERA_NAME) { + put("cameras/$camera/disconnect") + } + + private fun camera(camera: String = CAMERA_NAME) { + get("cameras/$camera") + } + + private fun cameraStartCapture(camera: String = CAMERA_NAME) { + putJson("cameras/$camera/capture/start", CAMERA_START_CAPTURE_REQUEST) + } + + private fun cameraStopCapture(camera: String = CAMERA_NAME) { + put("cameras/$camera/capture/abort") + } + + private fun mounts() { + get("mounts") + } + + private fun mountConnect(mount: String = MOUNT_NAME) { + put("mounts/$mount/connect") + } + + private fun mountDisconnect(mount: String = MOUNT_NAME) { + put("mounts/$mount/disconnect") + } + + private fun mount(mount: String = MOUNT_NAME) { + get("mounts/$mount") + } + + private fun mountRemoteControlStart(mount: String = MOUNT_NAME, host: String = "0.0.0.0", port: Int = 10001) { + put("mounts/$mount/remote-control/start?type=LX200&host=$host&port=$port") + } + + private fun mountRemoteControlList(mount: String = MOUNT_NAME) { + get("mounts/$mount/remote-control") + } + + private fun mountRemoteControlStop(mount: String = MOUNT_NAME) { + put("mounts/$mount/remote-control/stop?type=LX200") + } + + private fun focusers() { + get("focusers") + } + + private fun focuserConnect(focuser: String = FOCUSER_NAME) { + put("focusers/$focuser/connect") + } + + private fun focuserDisconnect(focuser: String = FOCUSER_NAME) { + put("focusers/$focuser/disconnect") + } + + private fun focuser(focuser: String = FOCUSER_NAME) { + get("focusers/$focuser") + } + + private fun autoFocusStart(camera: String = CAMERA_NAME, focuser: String = FOCUSER_NAME) { + putJson("auto-focus/$camera/$focuser/start", AUTO_FOCUS_REQUEST) } companion object { @@ -42,18 +152,25 @@ class APITest : StringSpec() { private const val BASE_URL = "http://localhost:7000" private const val CAMERA_NAME = "CCD Simulator" private const val MOUNT_NAME = "Telescope Simulator" + private const val FOCUSER_NAME = "Focuser Simulator" @JvmStatic private val EXPOSURE_TIME = Duration.ofSeconds(5) @JvmStatic private val CAPTURES_PATH = Path.of("/home/tiagohm/Git/nebulosa/data/captures") - @JvmStatic private val CAMERA_START_CAPTURE_REQUEST = - CameraStartCaptureRequest(exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO", savePath = CAPTURES_PATH) - .copy(exposureAmount = 2) + + @JvmStatic private val CAMERA_START_CAPTURE_REQUEST = CameraStartCaptureRequest( + exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO", + savePath = CAPTURES_PATH, exposureAmount = 2 + ) + + @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest(capture = CAMERA_START_CAPTURE_REQUEST) @JvmStatic private val CLIENT = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build() - @JvmStatic private val KOTLIN_MODULE = kotlinModule().addSerializer(PathSerializer) + @JvmStatic private val KOTLIN_MODULE = kotlinModule() + .addSerializer(PathSerializer) + .addSerializer(DurationSerializer()) @JvmStatic private val OBJECT_MAPPER = ObjectMapper() .registerModule(JavaTimeModule()) @@ -78,8 +195,7 @@ class APITest : StringSpec() { private fun putJson(path: String, data: Any) { val bytes = OBJECT_MAPPER.writeValueAsBytes(data) val body = bytes.toRequestBody(APPLICATION_JSON) - val request = Request.Builder().put(body).url("$BASE_URL/$path").build() - CLIENT.newCall(request).execute().use { it.isSuccessful.shouldBeTrue() } + put(path, body) } @JvmStatic diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt index 3f0f5c33c..b85a3385b 100644 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt @@ -56,7 +56,7 @@ data class ThreePointPolarAlignment( compensateRefraction: Boolean = false, cancellationToken: CancellationToken = CancellationToken.NONE, ): ThreePointPolarAlignmentResult { - if (cancellationToken.isDone) { + if (cancellationToken.isCancelled) { return Cancelled } @@ -66,7 +66,7 @@ data class ThreePointPolarAlignment( return NoPlateSolution(e) } - if (cancellationToken.isDone) { + if (cancellationToken.isCancelled) { return Cancelled } else if (!solution.solved) { return NoPlateSolution(null) diff --git a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt index 16f6425a0..169eef2dc 100644 --- a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt +++ b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt @@ -15,7 +15,7 @@ class CancellationTokenTest : StringSpec() { token.cancel(false) token.get() shouldBe source source shouldBe CancellationSource.Cancel(false) - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "cancel may interrupt if running" { var source: CancellationSource? = null @@ -24,7 +24,7 @@ class CancellationTokenTest : StringSpec() { token.cancel() token.get() shouldBe source source shouldBe CancellationSource.Cancel(true) - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "close" { var source: CancellationSource? = null @@ -33,7 +33,7 @@ class CancellationTokenTest : StringSpec() { token.close() token.get() shouldBe source source shouldBe CancellationSource.Close - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "listen" { var source: CancellationSource? = null @@ -42,12 +42,12 @@ class CancellationTokenTest : StringSpec() { token.listen { source = it } token.get() shouldBe CancellationSource.Cancel(true) source shouldBe CancellationSource.Listen - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "none" { var source: CancellationSource? = null val token = CancellationToken.NONE - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() token.listen { source = it } token.cancel() token.get() shouldBe CancellationSource.None From 860b3ac177b4c30dcca279f3f95b333bdc9d2568 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 26 May 2024 13:02:42 -0300 Subject: [PATCH 21/45] [api]: Support Auto Focus --- .../api/autofocus/AutoFocusController.kt | 11 ++- .../nebulosa/api/autofocus/AutoFocusEvent.kt | 8 +- .../api/autofocus/AutoFocusService.kt | 8 ++ .../nebulosa/api/autofocus/AutoFocusState.kt | 11 +++ .../nebulosa/api/autofocus/AutoFocusTask.kt | 78 ++++++++++++------- api/src/test/kotlin/APITest.kt | 18 ++++- .../src/test/kotlin/CancellationTokenTest.kt | 10 ++- .../nebulosa/curve/fitting/CurvePoint.kt | 6 +- 8 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt index 8710b7cdf..1b2534adf 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusController.kt @@ -2,10 +2,7 @@ package nebulosa.api.autofocus import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.focuser.Focuser -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("auto-focus") @@ -16,4 +13,10 @@ class AutoFocusController(private val autoFocusService: AutoFocusService) { camera: Camera, focuser: Focuser, @RequestBody body: AutoFocusRequest, ) = autoFocusService.start(camera, focuser, body) + + @PutMapping("{camera}/stop") + fun stop(camera: Camera) = autoFocusService.stop(camera) + + @GetMapping("{camera}/status") + fun status(camera: Camera) = autoFocusService.status(camera) } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt index da044fbe5..0b3441aeb 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt @@ -1,8 +1,14 @@ package nebulosa.api.autofocus +import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.messages.MessageEvent +import nebulosa.curve.fitting.CurvePoint -class AutoFocusEvent : MessageEvent { +data class AutoFocusEvent( + @JvmField val state: AutoFocusState = AutoFocusState.IDLE, + @JvmField val focusPoint: CurvePoint = CurvePoint.ZERO, + @JvmField val capture: CameraCaptureEvent? = null, +) : MessageEvent { override val eventName = "AUTO_FOCUS.ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt index 8d268a4de..fc70aaef5 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusService.kt @@ -12,4 +12,12 @@ class AutoFocusService( fun start(camera: Camera, focuser: Focuser, body: AutoFocusRequest) { autoFocusExecutor.execute(camera, focuser, body) } + + fun stop(camera: Camera) { + autoFocusExecutor.stop(camera) + } + + fun status(camera: Camera): AutoFocusEvent? { + return autoFocusExecutor.status(camera) + } } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt new file mode 100644 index 000000000..80f8f4e80 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt @@ -0,0 +1,11 @@ +package nebulosa.api.autofocus + +enum class AutoFocusState { + IDLE, + MOVING, + EXPOSURING, + COMPUTING, + FOCUS_POINT_ADDED, + FAILED, + FINISHED, +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 7325c85c1..490532c79 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -40,7 +40,7 @@ data class AutoFocusTask( data class MeasuredStars( @JvmField val averageHFD: Double = 0.0, - @JvmField var hfdStandardDeviation: Double = 0.0, + @JvmField val hfdStandardDeviation: Double = 0.0, ) { companion object { @@ -58,13 +58,15 @@ data class AutoFocusTask( private val focusPoints = ArrayList() private val measurements = ArrayList(request.capture.exposureAmount) - private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = request.capture.exposureAmount) + private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = max(1, request.capture.exposureAmount)) @Volatile private var focuserMoveTask: FocuserMoveTask? = null @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null @Volatile private var parabolicCurve: Lazy? = null @Volatile private var hyperbolicCurve: Lazy? = null + @Volatile private var focusPoint = CurvePoint.ZERO + init { cameraCaptureTask.subscribe(this) } @@ -80,6 +82,8 @@ data class AutoFocusTask( override fun canUseAsLastEvent(event: MessageEvent) = event is AutoFocusEvent override fun execute(cancellationToken: CancellationToken) { + reset() + val initialFocusPosition = focuser.position // Get initial position information, as average of multiple exposures, if configured this way. @@ -92,6 +96,8 @@ data class AutoFocusTask( var numberOfAttempts = 0 val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 + camera.snoop(listOf(focuser)) + while (!exited && !cancellationToken.isCancelled) { numberOfAttempts++ @@ -153,9 +159,9 @@ data class AutoFocusTask( break } - if (focuser.position == 0) { - // Break out when the focuser hits the zero position. It can't continue from there. - LOG.error("failed to complete. position reached 0") + if (focuser.position <= 0 || focuser.position >= focuser.maxPosition) { + // Break out when the focuser hits the min/max position. It can't continue from there. + LOG.error("failed to complete. position reached ${focuser.position}") break } } while (!cancellationToken.isCancelled && (rightCount + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps)) @@ -173,18 +179,22 @@ data class AutoFocusTask( continue } else { LOG.warn("potentially bad auto-focus. Restoring original focus position") - moveFocuser(initialFocusPosition, cancellationToken, false) - break } + } else { + LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y) } + + exited = true } - if (exited || cancellationToken.isCancelled) { - LOG.warn("did not complete successfully, so restoring the focuser position to $initialFocusPosition") + if (exited) { + sendEvent(AutoFocusState.FAILED) + LOG.warn("Auto Focus did not complete successfully, so restoring the focuser position to $initialFocusPosition") moveFocuser(initialFocusPosition, CancellationToken.NONE, false) } reset() + sendEvent(AutoFocusState.FINISHED) LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser) } @@ -212,11 +222,12 @@ data class AutoFocusTask( sumVariances += hfdStandardDeviation * hfdStandardDeviation } - return MeasuredStars(sumHFD / request.capture.exposureAmount, sqrt(sumVariances / request.capture.exposureAmount)) + return MeasuredStars(sumHFD / measurements.size, sqrt(sumVariances / measurements.size)) } override fun accept(event: CameraCaptureEvent) { if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { + sendEvent(AutoFocusState.COMPUTING, capture = event) val image = imageBucket.open(event.savePath!!) val detectedStars = starDetection.detect(image) LOG.info("detected ${detectedStars.size} stars") @@ -224,13 +235,20 @@ data class AutoFocusTask( LOG.info("HFD measurement. mean={}, stdDev={}", measure.averageHFD, measure.hfdStandardDeviation) measurements.add(measure) onNext(event) + } else { + sendEvent(AutoFocusState.EXPOSURING, capture = event) } } private fun takeExposure(cancellationToken: CancellationToken): MeasuredStars { - measurements.clear() - cameraCaptureTask.execute(cancellationToken) - return evaluateAllMeasurements() + return if (!cancellationToken.isCancelled) { + measurements.clear() + sendEvent(AutoFocusState.EXPOSURING) + cameraCaptureTask.execute(cancellationToken) + evaluateAllMeasurements() + } else { + MeasuredStars.ZERO + } } private fun obtainFocusPoints(numberOfSteps: Int, offset: Int, reverse: Boolean, cancellationToken: CancellationToken) { @@ -258,22 +276,22 @@ data class AutoFocusTask( // If star measurement is 0, we didn't detect any stars or shapes, // and want this point to be ignored by the fitting as much as possible. - // Setting a very high Stdev will do the trick. if (measurement.averageHFD == 0.0) { - LOG.warn("No stars detected in step. Setting a high standard deviation to ignore the point.") - measurement.hfdStandardDeviation = 1000.0 - } + LOG.warn("No stars detected in step") + sendEvent(AutoFocusState.FAILED) + } else { + focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD) + focusPoints.add(focusPoint) + focusPoints.sortBy { it.x } - val weight = max(0.001, measurement.hfdStandardDeviation) - val point = CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD, weight) - focusPoints.add(point) - focusPoints.sortBy { it.x } + LOG.info("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint) - remainingSteps-- + computeCurveFittings() - LOG.info("focus point added. remainingSteps={}, x={}, y={}, weight={}", remainingSteps, point.x, point.y, point.weight) + sendEvent(AutoFocusState.FOCUS_POINT_ADDED) + } - computeCurveFittings() + remainingSteps-- } } @@ -295,9 +313,9 @@ data class AutoFocusTask( private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double, cancellationToken: CancellationToken): Boolean { val threshold = request.rSquaredThreshold - fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: false - fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: false - fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: false + fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true + fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true + fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true if (threshold > 0.0) { val isBad = when (request.fittingMode) { @@ -338,10 +356,16 @@ data class AutoFocusTask( private fun moveFocuser(position: Int, cancellationToken: CancellationToken, relative: Boolean): Int { focuserMoveTask = if (relative) FocuserMoveRelativeTask(focuser, position) else FocuserMoveAbsoluteTask(focuser, position) + sendEvent(AutoFocusState.MOVING) focuserMoveTask!!.execute(cancellationToken) return focuser.position } + @Suppress("NOTHING_TO_INLINE") + private inline fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) { + onNext(AutoFocusEvent(state, focusPoint, capture)) + } + override fun reset() { cameraCaptureTask.reset() focusPoints.clear() diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt index 8d89f6d8d..e3683fadb 100644 --- a/api/src/test/kotlin/APITest.kt +++ b/api/src/test/kotlin/APITest.kt @@ -18,7 +18,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor import java.nio.file.Path import java.time.Duration -import java.time.temporal.ChronoUnit @EnabledIf(NonGitHubOnlyCondition::class) class APITest : StringSpec() { @@ -59,12 +58,15 @@ class APITest : StringSpec() { "Auto Focus Start" { connect() - delay(2000) + delay(1000) cameraConnect() focuserConnect() delay(1000) + focuserMoveTo(position = 36000) + delay(2000) autoFocusStart() } + "Auto Focus Stop" { autoFocusStop() } } private fun connect(host: String = "0.0.0.0", port: Int = 7624) { @@ -143,10 +145,18 @@ class APITest : StringSpec() { get("focusers/$focuser") } + private fun focuserMoveTo(focuser: String = FOCUSER_NAME, position: Int) { + put("focusers/$focuser/move-to?steps=$position") + } + private fun autoFocusStart(camera: String = CAMERA_NAME, focuser: String = FOCUSER_NAME) { putJson("auto-focus/$camera/$focuser/start", AUTO_FOCUS_REQUEST) } + private fun autoFocusStop(camera: String = CAMERA_NAME) { + put("auto-focus/$camera/stop") + } + companion object { private const val BASE_URL = "http://localhost:7000" @@ -159,10 +169,10 @@ class APITest : StringSpec() { @JvmStatic private val CAMERA_START_CAPTURE_REQUEST = CameraStartCaptureRequest( exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO", - savePath = CAPTURES_PATH, exposureAmount = 2 + savePath = CAPTURES_PATH, exposureAmount = 1 ) - @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest(capture = CAMERA_START_CAPTURE_REQUEST) + @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest(capture = CAMERA_START_CAPTURE_REQUEST, stepSize = 11000) @JvmStatic private val CLIENT = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) diff --git a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt index 169eef2dc..84688e538 100644 --- a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt +++ b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt @@ -1,4 +1,5 @@ import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe @@ -16,6 +17,7 @@ class CancellationTokenTest : StringSpec() { token.get() shouldBe source source shouldBe CancellationSource.Cancel(false) token.isCancelled.shouldBeTrue() + token.isDone.shouldBeTrue() } "cancel may interrupt if running" { var source: CancellationSource? = null @@ -25,6 +27,7 @@ class CancellationTokenTest : StringSpec() { token.get() shouldBe source source shouldBe CancellationSource.Cancel(true) token.isCancelled.shouldBeTrue() + token.isDone.shouldBeTrue() } "close" { var source: CancellationSource? = null @@ -34,8 +37,9 @@ class CancellationTokenTest : StringSpec() { token.get() shouldBe source source shouldBe CancellationSource.Close token.isCancelled.shouldBeTrue() + token.isDone.shouldBeTrue() } - "listen" { + "listen after cancel" { var source: CancellationSource? = null val token = CancellationToken() token.cancel() @@ -43,15 +47,17 @@ class CancellationTokenTest : StringSpec() { token.get() shouldBe CancellationSource.Cancel(true) source shouldBe CancellationSource.Listen token.isCancelled.shouldBeTrue() + token.isDone.shouldBeTrue() } "none" { var source: CancellationSource? = null val token = CancellationToken.NONE - token.isCancelled.shouldBeTrue() token.listen { source = it } token.cancel() token.get() shouldBe CancellationSource.None source.shouldBeNull() + token.isCancelled.shouldBeFalse() + token.isDone.shouldBeTrue() } } } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt index 553267852..9cb84ed17 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt @@ -2,22 +2,20 @@ package nebulosa.curve.fitting import org.apache.commons.math3.fitting.WeightedObservedPoint -class CurvePoint(x: Double, y: Double, weight: Double = 1.0) : WeightedObservedPoint(weight, x, y) { +class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CurvePoint) return false if (x != other.x) return false - if (y != other.y) return false - return weight == other.weight + return y == other.y } override fun hashCode(): Int { var result = x.hashCode() result = 31 * result + y.hashCode() - result = 31 * result + weight.hashCode() return result } From 908a1d50fe5fa4d2fb01bcb4d65077b727c779ef Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 26 May 2024 13:18:29 -0300 Subject: [PATCH 22/45] [api]: Don't create a new temp directory on each task execution --- .../main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt | 3 ++- .../main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt | 3 ++- api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt index bdfb8911c..65e4ea05c 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt @@ -31,7 +31,7 @@ data class DARVTask( @JvmField val cameraRequest = request.capture.copy( exposureTime = request.capture.exposureTime + request.capture.exposureDelay, - savePath = Files.createTempDirectory("darv"), + savePath = CAPTURE_SAVE_PATH, exposureAmount = 1, exposureDelay = Duration.ZERO, frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF ) @@ -123,5 +123,6 @@ data class DARVTask( companion object { @JvmStatic private val LOG = loggerFor() + @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("darv-") } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt index 1f4504d6b..e3ae706ff 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt @@ -40,7 +40,7 @@ data class TPPATask( @JvmField val mountMoveRequest = MountMoveRequest(request.stepDirection, request.stepDuration, request.stepSpeed) @JvmField val cameraRequest = request.capture.copy( - savePath = Files.createTempDirectory("tppa"), + savePath = CAPTURE_SAVE_PATH, exposureAmount = 0, exposureDelay = Duration.ZERO, exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF @@ -310,6 +310,7 @@ data class TPPATask( @JvmStatic private val MIN_EXPOSURE_TIME = Duration.ofSeconds(1L) @JvmStatic private val SETTLE_TIME = Duration.ofSeconds(5) + @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("tppa-") @JvmStatic private val LOG = loggerFor() const val MAX_ATTEMPTS = 30 diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 490532c79..44e92e2ed 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -51,7 +51,7 @@ data class AutoFocusTask( @JvmField val cameraRequest = request.capture.copy( exposureAmount = 0, exposureDelay = Duration.ZERO, - savePath = Files.createTempDirectory("af"), + savePath = CAPTURE_SAVE_PATH, exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF ) @@ -383,6 +383,7 @@ data class AutoFocusTask( companion object { @JvmStatic private val MIN_EXPOSURE_TIME = Duration.ofSeconds(1L) + @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("af-") @JvmStatic private val LOG = loggerFor() @JvmStatic From a43e2639bab8a8ebac579af6eaa8f95d149cd95a Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 26 May 2024 15:40:47 -0300 Subject: [PATCH 23/45] [desktop]: Support Auto Focus --- desktop/src/app/app-routing.module.ts | 5 + desktop/src/app/app.module.ts | 2 + .../app/autofocus/autofocus.component.html | 10 ++ .../app/autofocus/autofocus.component.scss | 0 .../src/app/autofocus/autofocus.component.ts | 130 ++++++++++++++++++ desktop/src/app/camera/camera.component.ts | 6 +- desktop/src/app/home/home.component.html | 7 + desktop/src/app/home/home.component.ts | 7 + desktop/src/assets/icons/auto-focus.png | Bin 0 -> 2296 bytes .../device-list-button.component.html | 3 +- .../device-list-button.component.ts | 14 +- .../device-list-menu.component.ts | 18 ++- .../menu-item/menu-item.component.html | 6 +- desktop/src/shared/services/api.service.ts | 11 ++ .../shared/services/browser-window.service.ts | 5 + .../src/shared/services/preference.service.ts | 8 +- desktop/src/shared/types/autofocus.type.ts | 13 ++ desktop/src/shared/types/camera.types.ts | 2 +- desktop/src/shared/types/home.types.ts | 3 +- 19 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 desktop/src/app/autofocus/autofocus.component.html create mode 100644 desktop/src/app/autofocus/autofocus.component.scss create mode 100644 desktop/src/app/autofocus/autofocus.component.ts create mode 100644 desktop/src/assets/icons/auto-focus.png create mode 100644 desktop/src/shared/types/autofocus.type.ts diff --git a/desktop/src/app/app-routing.module.ts b/desktop/src/app/app-routing.module.ts index bdd1b6a47..419218d23 100644 --- a/desktop/src/app/app-routing.module.ts +++ b/desktop/src/app/app-routing.module.ts @@ -4,6 +4,7 @@ import { APP_CONFIG } from '../environments/environment' import { AboutComponent } from './about/about.component' import { AlignmentComponent } from './alignment/alignment.component' import { AtlasComponent } from './atlas/atlas.component' +import { AutoFocusComponent } from './autofocus/autofocus.component' import { CalculatorComponent } from './calculator/calculator.component' import { CalibrationComponent } from './calibration/calibration.component' import { CameraComponent } from './camera/camera.component' @@ -86,6 +87,10 @@ const routes: Routes = [ path: 'calibration', component: CalibrationComponent, }, + { + path: 'auto-focus', + component: AutoFocusComponent, + }, { path: 'calculator', component: CalculatorComponent, diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 68543be5d..13a469da0 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -66,6 +66,7 @@ import { AlignmentComponent } from './alignment/alignment.component' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './app.component' import { AtlasComponent } from './atlas/atlas.component' +import { AutoFocusComponent } from './autofocus/autofocus.component' import { CalculatorComponent } from './calculator/calculator.component' import { FormulaComponent } from './calculator/formula/formula.component' import { CalibrationComponent } from './calibration/calibration.component' @@ -91,6 +92,7 @@ import { SettingsComponent } from './settings/settings.component' AnglePipe, AppComponent, AtlasComponent, + AutoFocusComponent, CalculatorComponent, CalibrationComponent, CameraComponent, diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html new file mode 100644 index 000000000..33e4e855a --- /dev/null +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -0,0 +1,10 @@ +
+
+ + + +
+
\ No newline at end of file diff --git a/desktop/src/app/autofocus/autofocus.component.scss b/desktop/src/app/autofocus/autofocus.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts new file mode 100644 index 000000000..5ccb2ac50 --- /dev/null +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -0,0 +1,130 @@ +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' +import { ApiService } from '../../shared/services/api.service' +import { BrowserWindowService } from '../../shared/services/browser-window.service' +import { ElectronService } from '../../shared/services/electron.service' +import { PreferenceService } from '../../shared/services/preference.service' +import { AutoFocusPreference, AutoFocusRequest } from '../../shared/types/autofocus.type' +import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' +import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types' +import { deviceComparator } from '../../shared/utils/comparators' +import { AppComponent } from '../app.component' +import { CameraComponent } from '../camera/camera.component' + +@Component({ + selector: 'app-autofocus', + templateUrl: './autofocus.component.html', + styleUrls: ['./autofocus.component.scss'], +}) +export class AutoFocusComponent implements AfterViewInit, OnDestroy { + + cameras: Camera[] = [] + camera = structuredClone(EMPTY_CAMERA) + + focusers: Focuser[] = [] + focuser = structuredClone(EMPTY_FOCUSER) + + running = false + + readonly request: AutoFocusRequest = { + capture: structuredClone(EMPTY_CAMERA_START_CAPTURE) + } + + constructor( + app: AppComponent, + private api: ApiService, + private browserWindow: BrowserWindowService, + private preference: PreferenceService, + electron: ElectronService, + ngZone: NgZone, + ) { + app.title = 'Auto Focus' + + this.loadPreference() + } + + async ngAfterViewInit() { + this.cameras = (await this.api.cameras()).sort(deviceComparator) + this.focusers = (await this.api.focusers()).sort(deviceComparator) + } + + @HostListener('window:unload') + async ngOnDestroy() { + await this.stop() + } + + async cameraChanged() { + if (this.camera.id) { + const camera = await this.api.camera(this.camera.id) + Object.assign(this.camera, camera) + } + } + + cameraConnect(camera?: Camera) { + camera ??= this.camera + + if (camera.id) { + if (camera.connected) { + this.api.cameraDisconnect(camera) + } else { + this.api.cameraConnect(camera) + } + } + } + + async focuserChanged() { + if (this.focuser.id) { + const focuser = await this.api.focuser(this.focuser.id) + Object.assign(this.focuser, focuser) + } + } + + focuserConnect(focuser?: Focuser) { + focuser ??= this.focuser + + if (focuser.id) { + if (focuser.connected) { + this.api.focuserDisconnect(focuser) + } else { + this.api.focuserConnect(focuser) + } + } + } + + async showCameraDialog() { + if (this.camera.id) { + if (await CameraComponent.showAsDialog(this.browserWindow, 'AUTO_FOCUS', this.camera, this.request.capture)) { + this.savePreference() + } + } + } + + stop() { + return this.api.tppaStop(this.camera) + } + + openCameraImage() { + return this.browserWindow.openCameraImage(this.camera) + } + + private loadPreference() { + const preference = this.preference.autoFocusPreference.get() + + if (this.camera.id) { + const cameraPreference = this.preference.cameraPreference(this.camera).get() + Object.assign(this.request.capture, this.preference.cameraStartCaptureForAutoFocus(this.camera).get(cameraPreference)) + + if (this.camera.connected) { + updateCameraStartCaptureFromCamera(this.request.capture, this.camera) + } + } + } + + savePreference() { + this.preference.cameraStartCaptureForAutoFocus(this.camera).set(this.request.capture) + + const preference: AutoFocusPreference = { + } + + this.preference.autoFocusPreference.set(preference) + } +} \ No newline at end of file diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 9989630c6..e056f9565 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -51,7 +51,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } get canExposureTime() { - return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' || this.mode === 'TPPA' + return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' || this.mode === 'TPPA' || this.mode === 'AUTO_FOCUS' } get canExposureTimeUnit() { @@ -59,7 +59,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } get canExposureAmount() { - return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' + return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' || this.mode === 'AUTO_FOCUS' } get canFrameType() { @@ -284,7 +284,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } private loadDefaultsForMode(mode: CameraDialogMode) { - if (mode === 'SEQUENCER') { + if (mode === 'SEQUENCER' || mode === 'AUTO_FOCUS') { this.exposureMode = 'FIXED' } else if (this.mode === 'FLAT_WIZARD') { this.exposureMode = 'SINGLE' diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 728ba83e0..b2dfa5193 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -127,6 +127,13 @@
Framing
+
+ + +
Auto Focus
+
+
diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 207e94e4b..1dba47feb 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -100,6 +100,10 @@ export class HomeComponent implements AfterContentInit, OnDestroy { return this.hasCamera } + get hasAutoFocus() { + return this.hasCamera && this.hasFocuser + } + get hasFlatWizard() { return this.hasCamera } @@ -409,6 +413,9 @@ export class HomeComponent implements AfterContentInit, OnDestroy { case 'SEQUENCER': this.browserWindow.openSequencer({ bringToFront: true }) break + case 'AUTO_FOCUS': + this.browserWindow.openAutoFocus({ bringToFront: true }) + break case 'FLAT_WIZARD': this.browserWindow.openFlatWizard({ bringToFront: true }) break diff --git a/desktop/src/assets/icons/auto-focus.png b/desktop/src/assets/icons/auto-focus.png new file mode 100644 index 0000000000000000000000000000000000000000..eb3b6d8a7e0fc763a778dadd0f7794a2f113dcb4 GIT binary patch literal 2296 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2#rZZK~#8N?VD?C z9Mu)a@15DT9Xm0EBtjt}X>@S%0{sW{$sq<)Bm3JF0-$P!X3)Gz&vs(vUE zC{%!i5OyJ?1Qp5ldYwe5C_p0=lF&Bg>6nB(93_rZ%Zv9tGyR{Lv&-(@yWW}2ZdeQd zq|rU+?CYL0_n!NhyJDGTmia*=xeUaC1I~)}_Uyf;Y2E>~ootRHR!Av3NG@O)&iTGR z^C0XFn*!lDj*P{uK74*7n_OT|ODR&UW-QA(o5{3I?%XL0HsAOTKrH60mePI{KmQ1v z-3pW6&kjc-p~qMb#Q1oA7{9fP)ml)HKSr?jIgasqBqIOknHR^G6R}u+H;QL*t#u8- za6%z-?L!aAYpna?;6dk>eBPR6H7GL(-0!2&&|g`viovB8G<6fszX)*%S3S-X>GpP! zWZ5_K^KKJQds<-Dp^FTo?k|uJY}2x606IIZy(k7ftuDxk!EM`QU`MDw!wLHBdV@eb z1s|SgSxeO}fd2Own$c^lmXgIaiu`m0x$O*Ecn0Mh-ph$DOm{m&QdgIluE+UO-h0pK zSXdCZv5JJ7>m&iek*LGXkYJN{Au;yC7R5_#^m-|%+wN-JK^p){ctTiTL9A%@@}!i; z!~Ok|UTZlfCY+vp-rkQJazC32%m`uj=n|v03wYUHbv~IidRx_bqOVVWY1>9GTE-`A zDuA5e`lL`L2RM(gP#{Zp9{uuL)?1bxJ7gjhG9HFVU9||h$ZuFILkR#)a?odf%4&hp zR=)F8Y01)jjl^i`H!loo(Vgc2pwZ1dFxYz&tun&LbWHVtap z9XZ>2hfP&6&9G8uoo~7v5aO~3T11}3#c{mZx25C4aB||-^goc5Eq>+tsN7G0 z{Mz>Ip+h8>q6C08Fp(+uU`Dv6tINCv@-V;D6mrHYVM%w1FKpR{cI&HG`l7!-Gz1@3 zlP{1T*KN-F-PrWSrK?Hz2fn%%78tP>e0kD^B5IcptScZb$&$jK@VTr%omIzVRl_GFkJ;=kwwZe|knd z^Zc`7=&k*1Z5~Yzz-Loa;`F(5td*0=WyRm$EuYc;aqn|13Gv0$r1pchKjb zd*vn88c)*#5dYvqk@$9jwb~4i(mG1dd?F!64o$GuSeg=mib|*b` zQRQZ&EC6Cq!HYiumPhkPJkVzK-X`Nl65Q4ILd zcC4MpStU}mEcUY}P{;u%(<4~gHfHjYOso-~pYCIg?wBKfymsd2zkXoDhiuY9cb=Sw zSUk6cuG!b$AEy0|B1#Lun)z-P3#`CGuI!M~jpWMZ9+v2CS>jgv%(`066YcF{1B$Xk zTaIJUW4|(dB|*aoHd?GU?iA7q;|g8(W!v5!Ef&n#JGv*gJUP&F)iu<>Z=IDl#bVYV zVnyeIN-#G^BJy)qs&Z*2IfHMk_bic?iT`HB`9z1$x1!H~%&H6g+ST75dYx4lr3L67Dx&Eq4;5P-U7az(RD9DV zLFJ($g5s+Sv=X|9yg>Dh%TnAUZlxfaH6KN_8ipa2r6syR?Eqlbq}jiccd@W!(9CCm z^$iO}lmO7FYZMi``pz#{STbPR+sk4Bdtsri^0A*90Dh!Ynehkc5F*Ph&)ArAABu>l z6*;auPOr-WK)%63fi^mUIWpq3d@i6mN@>%6df6y|F0qfrGPD7p^Y$oaJk0}>i2Du0 zKGMo9f$qF%+V8`IpRlRG4Ev-|b-t{h5V>SAo{k}+NAN>maZ-l|Tl zXSM2KY1HGFFjKSF%~>9eL_(FV+GWuN1D&@=@sy|41^u;Uke<@A9tc{` - \ No newline at end of file + \ No newline at end of file diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.ts b/desktop/src/shared/components/device-list-button/device-list-button.component.ts index 17c101c42..ce96deefc 100644 --- a/desktop/src/shared/components/device-list-button/device-list-button.component.ts +++ b/desktop/src/shared/components/device-list-button/device-list-button.component.ts @@ -7,7 +7,7 @@ import { DeviceListMenuComponent } from '../device-list-menu/device-list-menu.co templateUrl: './device-list-button.component.html', styleUrls: ['./device-list-button.component.scss'], }) -export class DeviceListButtonComponent { +export class DeviceListButtonComponent { @Input({ required: true }) readonly title!: string @@ -19,16 +19,22 @@ export class DeviceListButtonComponent { readonly icon!: string @Input({ required: true }) - readonly devices!: Device[] + readonly devices!: T[] @Input() readonly hasNone: boolean = false @Input() - device?: Device + device?: T @Output() - readonly deviceChange = new EventEmitter() + readonly deviceChange = new EventEmitter() + + @Output() + readonly deviceConnect = new EventEmitter() + + @Output() + readonly deviceDisconnect = new EventEmitter() @ViewChild('deviceMenu') private readonly deviceMenu!: DeviceListMenuComponent diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts index de139677f..f60d66c5b 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, ViewChild } from '@angular/core' +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' import { SEPARATOR_MENU_ITEM } from '../../constants' import { PrimeService } from '../../services/prime.service' import { Device } from '../../types/device.types' @@ -28,6 +28,12 @@ export class DeviceListMenuComponent { @Input() readonly hasNone: boolean = false + @Output() + readonly deviceConnect = new EventEmitter() + + @Output() + readonly deviceDisconnect = new EventEmitter() + @ViewChild('menu') private readonly menu!: DialogMenuComponent @@ -71,6 +77,16 @@ export class DeviceListMenuComponent { label: device.name, checked: selected === device, disabled: this.disableIfDeviceIsNotConnected && !device.connected, + toolbarMenu: [ + { + icon: 'mdi ' + (device.connected ? 'mdi-close text-red-500' : 'mdi-connection text-blue-500'), + label: device.connected ? 'Disconnect' : 'Connect', + command: event => { + if (device.connected) this.deviceDisconnect.emit(device) + else this.deviceConnect.emit(device) + } + } + ], command: () => { resolve(device) }, diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html index 03f61f537..6ad336a57 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.html +++ b/desktop/src/shared/components/menu-item/menu-item.component.html @@ -5,15 +5,13 @@ @if (item.toolbarMenu?.length) {
@for (m of item.toolbarMenu; track i; let i = $index) { - }
} - @if (item.checked) { - - } @else if(item.toggleable) { + @if(item.toggleable) { } @if (item.items?.length || item.menu?.length) { diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 614cbdbfd..fb1eb0b5a 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core' import moment from 'moment' import { DARVStart, TPPAStart } from '../types/alignment.types' import { Angle, BodyPosition, CloseApproach, ComputedLocation, Constellation, DeepSkyObject, MinorPlanet, Satellite, SatelliteGroupType, SkyObjectType, Twilight } from '../types/atlas.types' +import { AutoFocusRequest } from '../types/autofocus.type' import { CalibrationFrame, CalibrationFrameGroup } from '../types/calibration.types' import { Camera, CameraStartCapture } from '../types/camera.types' import { Device, INDIProperty, INDISendProperty } from '../types/device.types' @@ -635,6 +636,16 @@ export class ApiService { return this.http.put(`plate-solver?${query}`) } + // AUTO FOCUS + + autoFocusStart(camera: Camera, focuser: Focuser, request: AutoFocusRequest) { + return this.http.put(`auto-focus/${camera.name}/${focuser.name}/start`, request) + } + + autoFocusStop(camera: Camera) { + return this.http.put(`auto-focus/${camera.name}/stop`) + } + // PREFERENCE clearPreferences() { diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 5b2e9d4f5..ac77d57fd 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -104,6 +104,11 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'sequencer', path: 'sequencer', data: undefined }) } + openAutoFocus(options: OpenWindowOptions = {}) { + Object.assign(options, { icon: 'auto-focus', width: 385, height: 370 }) + this.openWindow({ ...options, id: 'auto-focus', path: 'auto-focus', data: undefined }) + } + openFlatWizard(options: OpenWindowOptions = {}) { Object.assign(options, { icon: 'star', width: 385, height: 370 }) this.openWindow({ ...options, id: 'flat-wizard', path: 'flat-wizard', data: undefined }) diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index 98b902672..46eec8892 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core' import { SkyAtlasPreference } from '../../app/atlas/atlas.component' import { AlignmentPreference, EMPTY_ALIGNMENT_PREFERENCE } from '../types/alignment.types' import { EMPTY_LOCATION, Location } from '../types/atlas.types' +import { AutoFocusPreference, EMPTY_AUTO_FOCUS_PREFERENCE } from '../types/autofocus.type' import { CalibrationPreference } from '../types/calibration.types' import { Camera, CameraPreference, CameraStartCapture, EMPTY_CAMERA_PREFERENCE } from '../types/camera.types' import { Device } from '../types/device.types' @@ -63,6 +64,10 @@ export class PreferenceService { return new PreferenceData(this.storage, `camera.${camera.name}.tppa`, () => this.cameraPreference(camera).get()) } + cameraStartCaptureForAutoFocus(camera: Camera) { + return new PreferenceData(this.storage, `camera.${camera.name}.autoFocus`, () => this.cameraPreference(camera).get()) + } + plateSolverPreference(type: PlateSolverType) { return new PreferenceData(this.storage, `plateSolver.${type}`, () => { ...EMPTY_PLATE_SOLVER_PREFERENCE, type }) } @@ -92,4 +97,5 @@ export class PreferenceService { readonly alignmentPreference = new PreferenceData(this.storage, 'alignment', () => structuredClone(EMPTY_ALIGNMENT_PREFERENCE)) readonly imageFOVs = new PreferenceData(this.storage, 'image.fovs', () => []) readonly calibrationPreference = new PreferenceData(this.storage, 'calibration', () => {}) -} \ No newline at end of file + readonly autoFocusPreference = new PreferenceData(this.storage, 'autoFocus', () => structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE)) +} diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts new file mode 100644 index 000000000..48f3847b6 --- /dev/null +++ b/desktop/src/shared/types/autofocus.type.ts @@ -0,0 +1,13 @@ +import { CameraStartCapture } from './camera.types' + +export interface AutoFocusRequest { + capture: CameraStartCapture +} + +export interface AutoFocusPreference { + +} + +export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { + +} diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index e97040128..056ccc482 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -3,7 +3,7 @@ import { Thermometer } from './auxiliary.types' import { CompanionDevice, Device, PropertyState } from './device.types' import { GuideOutput } from './guider.types' -export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' +export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' | 'AUTO_FOCUS' export type FrameType = 'LIGHT' | 'DARK' | 'FLAT' | 'BIAS' diff --git a/desktop/src/shared/types/home.types.ts b/desktop/src/shared/types/home.types.ts index fd22c33fc..0885999c1 100644 --- a/desktop/src/shared/types/home.types.ts +++ b/desktop/src/shared/types/home.types.ts @@ -5,7 +5,8 @@ import { Rotator } from './rotator.types' import { FilterWheel } from './wheel.types' export type HomeWindowType = 'CAMERA' | 'MOUNT' | 'GUIDER' | 'WHEEL' | 'FOCUSER' | 'DOME' | 'ROTATOR' | 'SWITCH' | - 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' + 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | + 'AUTO_FOCUS' export const CONNECTION_TYPES = ['INDI', 'ALPACA'] as const From 908d7b801951650e986d587590aeaf0bbaf2be93 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 26 May 2024 21:34:48 -0300 Subject: [PATCH 24/45] [ci]: Remove wcstools --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bc372b73..dc6b32b0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,6 @@ jobs: distribution: 'zulu' cache: gradle - - name: Install wcstools - run: sudo apt install -y wcstools - - name: Grant execute permission for gradlew run: chmod +x gradlew From a2b5000a04c47c23d52a353680a898fedde906f7 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 27 May 2024 01:11:41 -0300 Subject: [PATCH 25/45] [api][desktop]: Device Chooser --- .../nebulosa/api/indi/INDIController.kt | 15 +++ .../kotlin/nebulosa/api/indi/INDIService.kt | 8 ++ .../app/alignment/alignment.component.html | 18 +-- .../src/app/alignment/alignment.component.ts | 23 ---- desktop/src/app/app.component.ts | 8 +- desktop/src/app/app.module.ts | 4 +- desktop/src/app/atlas/atlas.component.ts | 4 +- .../app/autofocus/autofocus.component.html | 9 +- .../src/app/autofocus/autofocus.component.ts | 82 +++++++++---- desktop/src/app/camera/camera.component.ts | 37 +++--- .../filterwheel/filterwheel.component.html | 4 +- .../flat-wizard/flat-wizard.component.html | 13 +- .../app/flat-wizard/flat-wizard.component.ts | 10 -- desktop/src/app/guider/guider.component.html | 6 +- desktop/src/app/guider/guider.component.ts | 8 -- desktop/src/app/home/home.component.html | 3 +- desktop/src/app/home/home.component.ts | 98 ++++++++++----- desktop/src/app/image/image.component.ts | 43 ++++--- desktop/src/app/mount/mount.component.ts | 4 +- .../app/sequencer/sequencer.component.html | 10 +- .../device-chooser.component.html} | 2 +- .../device-chooser.component.scss} | 0 .../device-chooser.component.ts | 114 ++++++++++++++++++ .../device-list-button.component.ts | 54 --------- .../device-list-menu.component.scss | 5 + .../device-list-menu.component.ts | 20 ++- .../dialog-menu/dialog-menu.component.ts | 6 +- .../menu-item/menu-item.component.html | 5 +- .../menu-item/menu-item.component.ts | 30 +++-- .../slide-menu/slide-menu.component.html | 2 +- .../slide-menu/slide-menu.component.ts | 12 +- desktop/src/shared/constants.ts | 4 +- desktop/src/shared/services/api.service.ts | 13 +- desktop/src/shared/types/app.types.ts | 13 +- desktop/src/shared/types/auxiliary.types.ts | 4 + desktop/src/shared/types/camera.types.ts | 10 +- desktop/src/shared/types/device.types.ts | 6 + desktop/src/shared/types/focuser.types.ts | 4 + desktop/src/shared/types/home.types.ts | 6 +- desktop/src/shared/types/mount.types.ts | 5 + desktop/src/shared/types/rotator.types.ts | 4 + desktop/src/shared/types/wheel.types.ts | 6 +- desktop/src/styles.scss | 2 + 43 files changed, 445 insertions(+), 289 deletions(-) rename desktop/src/shared/components/{device-list-button/device-list-button.component.html => device-chooser/device-chooser.component.html} (84%) rename desktop/src/shared/components/{device-list-button/device-list-button.component.scss => device-chooser/device-chooser.component.scss} (100%) create mode 100644 desktop/src/shared/components/device-chooser/device-chooser.component.ts delete mode 100644 desktop/src/shared/components/device-list-button/device-list-button.component.ts diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt index f66afda59..ca889fd60 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt @@ -11,6 +11,21 @@ class INDIController( private val indiService: INDIService, ) { + @GetMapping("{device}") + fun device(device: Device): Device { + return device + } + + @PutMapping("{device}/connect") + fun connect(device: Device) { + indiService.connect(device) + } + + @PutMapping("{device}/disconnect") + fun disconnect(device: Device) { + indiService.disconnect(device) + } + @GetMapping("{device}/properties") fun properties(device: Device): Collection> { return indiService.properties(device) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt index 0fe78fc8c..91f4c5d92 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt @@ -18,6 +18,14 @@ class INDIService( indiEventHandler.unregisterDevice(device) } + fun connect(device: Device) { + device.connect() + } + + fun disconnect(device: Device) { + device.disconnect() + } + fun messages(): List { return indiEventHandler.messages() } diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index d57ed21c1..d42354c7b 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -1,22 +1,14 @@
- + @if (tab === 0) { - - - + } @else { - - - + }
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index fe628cd54..d00c70296 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -109,7 +109,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } this.cameras.splice(index, 1) - this.cameras.sort(deviceComparator) } }) }) @@ -139,7 +138,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } this.mounts.splice(index, 1) - this.mounts.sort(deviceComparator) } }) }) @@ -169,7 +167,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } this.guideOutputs.splice(index, 1) - this.guideOutputs.sort(deviceComparator) } }) }) @@ -265,26 +262,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } } - mountConnect() { - if (this.mount.id) { - if (this.mount.connected) { - this.api.mountDisconnect(this.mount) - } else { - this.api.mountConnect(this.mount) - } - } - } - - guideOutputConnect() { - if (this.guideOutput.id) { - if (this.guideOutput.connected) { - this.api.guideOutputDisconnect(this.guideOutput) - } else { - this.api.guideOutputConnect(this.guideOutput) - } - } - } - async showCameraDialog() { if (this.camera.id) { if (this.tab === 0) { diff --git a/desktop/src/app/app.component.ts b/desktop/src/app/app.component.ts index a9058300f..ba2f53ea7 100644 --- a/desktop/src/app/app.component.ts +++ b/desktop/src/app/app.component.ts @@ -1,14 +1,10 @@ import { AfterViewInit, Component } from '@angular/core' import { Title } from '@angular/platform-browser' import { ActivatedRoute } from '@angular/router' -import { MenuItem } from 'primeng/api' import { APP_CONFIG } from '../environments/environment' +import { MenuItem } from '../shared/components/menu-item/menu-item.component' import { ElectronService } from '../shared/services/electron.service' -export interface ExtendedMenuItem extends MenuItem { - badgeSeverity?: 'success' | 'info' | 'warning' | 'danger' -} - @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -21,7 +17,7 @@ export class AppComponent implements AfterViewInit { readonly modal = window.options.modal ?? false subTitle? = '' backgroundColor = '#212121' - topMenu: ExtendedMenuItem[] = [] + topMenu: MenuItem[] = [] showTopBar = true get title() { diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 13a469da0..dd8e85990 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -42,7 +42,7 @@ import { ToastModule } from 'primeng/toast' import { TooltipModule } from 'primeng/tooltip' import { TreeModule } from 'primeng/tree' import { CameraExposureComponent } from '../shared/components/camera-exposure/camera-exposure.component' -import { DeviceListButtonComponent } from '../shared/components/device-list-button/device-list-button.component' +import { DeviceChooserComponent } from '../shared/components/device-chooser/device-chooser.component' import { DeviceListMenuComponent } from '../shared/components/device-list-menu/device-list-menu.component' import { DialogMenuComponent } from '../shared/components/dialog-menu/dialog-menu.component' import { HistogramComponent } from '../shared/components/histogram/histogram.component' @@ -97,7 +97,7 @@ import { SettingsComponent } from './settings/settings.component' CalibrationComponent, CameraComponent, CameraExposureComponent, - DeviceListButtonComponent, + DeviceChooserComponent, DeviceListMenuComponent, DialogMenuComponent, EnumPipe, diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 4e95d9c85..2397b406d 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -8,7 +8,7 @@ import { ListboxChangeEvent } from 'primeng/listbox' import { OverlayPanel } from 'primeng/overlaypanel' import { Subscription, timer } from 'rxjs' import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' -import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { MenuItem } from '../../shared/components/menu-item/menu-item.component' import { ONE_DECIMAL_PLACE_FORMATTER, TWO_DIGITS_FORMATTER } from '../../shared/constants' import { SkyObjectPipe } from '../../shared/pipes/skyObject.pipe' import { ApiService } from '../../shared/services/api.service' @@ -406,7 +406,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, 'ONEWEB', 'SCIENCE', 'STARLINK', 'STATIONS', 'VISUAL' ] - readonly ephemerisModel: ExtendedMenuItem[] = [ + readonly ephemerisModel: MenuItem[] = [ { icon: 'mdi mdi-magnify', label: 'Find sky objects around this object', diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 33e4e855a..7edd35cf2 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -1,10 +1,9 @@
- + - +
\ No newline at end of file diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index 5ccb2ac50..ec576a103 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -39,6 +39,64 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Auto Focus' + electron.on('CAMERA.UPDATED', event => { + if (event.device.id === this.camera.id) { + ngZone.run(() => { + Object.assign(this.camera, event.device) + }) + } + }) + + electron.on('CAMERA.ATTACHED', event => { + ngZone.run(() => { + this.cameras.push(event.device) + this.cameras.sort(deviceComparator) + }) + }) + + electron.on('CAMERA.DETACHED', event => { + ngZone.run(() => { + const index = this.cameras.findIndex(e => e.id === event.device.id) + + if (index >= 0) { + if (this.cameras[index] === this.camera) { + Object.assign(this.camera, this.cameras[0] ?? EMPTY_CAMERA) + } + + this.cameras.splice(index, 1) + } + }) + }) + + electron.on('FOCUSER.UPDATED', event => { + if (event.device.id === this.focuser.id) { + ngZone.run(() => { + Object.assign(this.focuser, event.device) + }) + } + }) + + electron.on('FOCUSER.ATTACHED', event => { + ngZone.run(() => { + this.focusers.push(event.device) + this.focusers.sort(deviceComparator) + }) + }) + + electron.on('FOCUSER.DETACHED', event => { + ngZone.run(() => { + const index = this.focusers.findIndex(e => e.id === event.device.id) + + if (index >= 0) { + if (this.focusers[index] === this.focuser) { + Object.assign(this.focuser, this.focusers[0] ?? EMPTY_FOCUSER) + } + + this.focusers.splice(index, 1) + } + }) + }) + this.loadPreference() } @@ -59,18 +117,6 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { } } - cameraConnect(camera?: Camera) { - camera ??= this.camera - - if (camera.id) { - if (camera.connected) { - this.api.cameraDisconnect(camera) - } else { - this.api.cameraConnect(camera) - } - } - } - async focuserChanged() { if (this.focuser.id) { const focuser = await this.api.focuser(this.focuser.id) @@ -78,18 +124,6 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { } } - focuserConnect(focuser?: Focuser) { - focuser ??= this.focuser - - if (focuser.id) { - if (focuser.connected) { - this.api.focuserDisconnect(focuser) - } else { - this.api.focuserConnect(focuser) - } - } - } - async showCameraDialog() { if (this.camera.id) { if (await CameraComponent.showAsDialog(this.browserWindow, 'AUTO_FOCUS', this.camera, this.request.capture)) { diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index e056f9565..0df5f366f 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -2,7 +2,6 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild import { ActivatedRoute } from '@angular/router' import { MenuItem } from 'primeng/api' import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' -import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component' import { SlideMenuItem, SlideMenuItemCommandEvent } from '../../shared/components/slide-menu/slide-menu.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' @@ -76,9 +75,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy { showDitherDialog = false - calibrationModel: ExtendedMenuItem[] = [] + calibrationModel: MenuItem[] = [] - readonly cameraModel: ExtendedMenuItem[] = [ + readonly cameraModel: MenuItem[] = [ { icon: 'icomoon random-dither', label: 'Dither', @@ -89,26 +88,26 @@ export class CameraComponent implements AfterContentInit, OnDestroy { { icon: 'mdi mdi-connection', label: 'Snoop Devices', - menu: [ + subMenu: [ { icon: 'mdi mdi-telescope', label: 'Mount', - menu: [], + subMenu: [], }, { icon: 'mdi mdi-palette', label: 'Filter Wheel', - menu: [], + subMenu: [], }, { icon: 'mdi mdi-image-filter-center-focus', label: 'Focuser', - menu: [], + subMenu: [], }, { icon: 'mdi mdi-rotate-right', label: 'Rotator', - menu: [], + subMenu: [], }, ] }, @@ -324,7 +323,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } const makeItem = (checked: boolean, command: () => void, device?: Device) => { - return { + return { icon: device ? 'mdi mdi-connection' : 'mdi mdi-close', label: device?.name ?? 'None', checked, @@ -332,7 +331,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { command() buildStartTooltip() this.preference.equipmentForDevice(this.camera).set(this.equipment) - event.parent?.menu?.forEach(item => item.checked = item === event.item) + event.parent?.subMenu?.forEach(item => item.checked = item === event.item) }, } } @@ -346,10 +345,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy { return makeItem(this.equipment.mount?.name === mount?.name, () => this.equipment.mount = mount, mount) } - this.cameraModel[1].menu![0].menu!.push(makeMountItem()) + this.cameraModel[1].subMenu![0].subMenu!.push(makeMountItem()) for (const mount of mounts) { - this.cameraModel[1].menu![0].menu!.push(makeMountItem(mount)) + this.cameraModel[1].subMenu![0].subMenu!.push(makeMountItem(mount)) } // FILTER WHEEL @@ -361,10 +360,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy { return makeItem(this.equipment.wheel?.name === wheel?.name, () => this.equipment.wheel = wheel, wheel) } - this.cameraModel[1].menu![1].menu!.push(makeWheelItem()) + this.cameraModel[1].subMenu![1].subMenu!.push(makeWheelItem()) for (const wheel of wheels) { - this.cameraModel[1].menu![1].menu!.push(makeWheelItem(wheel)) + this.cameraModel[1].subMenu![1].subMenu!.push(makeWheelItem(wheel)) } // FOCUSER @@ -376,10 +375,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy { return makeItem(this.equipment.focuser?.name === focuser?.name, () => this.equipment.focuser = focuser, focuser) } - this.cameraModel[1].menu![2].menu!.push(makeFocuserItem()) + this.cameraModel[1].subMenu![2].subMenu!.push(makeFocuserItem()) for (const focuser of focusers) { - this.cameraModel[1].menu![2].menu!.push(makeFocuserItem(focuser)) + this.cameraModel[1].subMenu![2].subMenu!.push(makeFocuserItem(focuser)) } // ROTATOR @@ -391,10 +390,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy { return makeItem(this.equipment.rotator?.name === rotator?.name, () => this.equipment.rotator = rotator, rotator) } - this.cameraModel[1].menu![3].menu!.push(makeRotatorItem()) + this.cameraModel[1].subMenu![3].subMenu!.push(makeRotatorItem()) for (const rotator of rotators) { - this.cameraModel[1].menu![3].menu!.push(makeRotatorItem(rotator)) + this.cameraModel[1].subMenu![3].subMenu!.push(makeRotatorItem(rotator)) } buildStartTooltip() @@ -409,7 +408,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } const makeItem = (name?: string) => { - return { + return { label: name ?? 'None', icon: name ? 'mdi mdi-wrench' : 'mdi mdi-close', checked: this.request.calibrationGroup === name, diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index b6ff0a35f..3f8b256e3 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -63,8 +63,8 @@ (onChange)="shutterToggled(filter, $event)" label="Shutter" />
- +
- + - - - - +
diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index 65ef1982e..c4edc631f 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -108,7 +108,6 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } this.cameras.splice(index, 1) - this.cameras.sort(deviceComparator) } }) }) @@ -139,7 +138,6 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } this.wheels.splice(index, 1) - this.wheels.sort(deviceComparator) } }) }) @@ -167,14 +165,6 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } } - wheelConnect() { - if (this.wheel.connected) { - this.api.wheelDisconnect(this.wheel) - } else { - this.api.wheelConnect(this.wheel) - } - } - private updateEntryFromCamera(camera?: Camera) { if (camera && camera.connected) { updateCameraStartCaptureFromCamera(this.request.capture, camera) diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 067d5df5c..9202e47e7 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -139,11 +139,7 @@
- - - +
diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index a3c79fd0c..14f72e44f 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -374,14 +374,6 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { } } - connectGuideOutput() { - if (this.guideOutputConnected) { - this.api.guideOutputDisconnect(this.guideOutput!) - } else { - this.api.guideOutputConnect(this.guideOutput!) - } - } - guidePulseStart(...directions: GuideDirection[]) { for (const direction of directions) { switch (direction) { diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index b2dfa5193..2fa0bf5cc 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -204,5 +204,6 @@ - + \ No newline at end of file diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 1dba47feb..d9dd33d22 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,7 +1,7 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { dirname } from 'path' -import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' -import { DialogMenuComponent } from '../../shared/components/dialog-menu/dialog-menu.component' +import { DeviceChooserComponent } from '../../shared/components/device-chooser/device-chooser.component' +import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' @@ -15,7 +15,6 @@ import { CONNECTION_TYPES, ConnectionDetails, EMPTY_CONNECTION_DETAILS, HomeWind import { Mount } from '../../shared/types/mount.types' import { Rotator } from '../../shared/types/rotator.types' import { FilterWheel } from '../../shared/types/wheel.types' -import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' type MappedDevice = { @@ -34,7 +33,7 @@ type MappedDevice = { export class HomeComponent implements AfterContentInit, OnDestroy { @ViewChild('deviceMenu') - private readonly deviceMenu!: DialogMenuComponent + private readonly deviceMenu!: DeviceListMenuComponent @ViewChild('imageMenu') private readonly imageMenu!: DeviceListMenuComponent @@ -137,6 +136,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { type: K, onAdd: (device: MappedDevice[K]) => number, onRemove: (device: MappedDevice[K]) => number, + onUpdate: (device: MappedDevice[K]) => void, ) { this.electron.on(`${type}.ATTACHED`, event => { this.ngZone.run(() => { @@ -149,6 +149,12 @@ export class HomeComponent implements AfterContentInit, OnDestroy { onRemove(event.device as never) }) }) + + this.electron.on(`${type}.UPDATED`, event => { + this.ngZone.run(() => { + onUpdate(event.device as never) + }) + }) } constructor( @@ -163,53 +169,78 @@ export class HomeComponent implements AfterContentInit, OnDestroy { app.title = 'Nebulosa' this.startListening('CAMERA', - (device) => { + device => { return this.cameras.push(device) }, - (device) => { + device => { this.cameras.splice(this.cameras.findIndex(e => e.id === device.id), 1) return this.cameras.length }, + device => { + const found = this.cameras.find(e => e.id === device.id) + if (!found) return + Object.assign(found, device) + } ) this.startListening('MOUNT', - (device) => { + device => { return this.mounts.push(device) }, - (device) => { + device => { this.mounts.splice(this.mounts.findIndex(e => e.id === device.id), 1) return this.mounts.length }, + device => { + const found = this.mounts.find(e => e.id === device.id) + if (!found) return + Object.assign(found, device) + } ) this.startListening('FOCUSER', - (device) => { + device => { return this.focusers.push(device) }, - (device) => { + device => { this.focusers.splice(this.focusers.findIndex(e => e.id === device.id), 1) return this.focusers.length }, + device => { + const found = this.focusers.find(e => e.id === device.id) + if (!found) return + Object.assign(found, device) + } ) this.startListening('WHEEL', - (device) => { + device => { return this.wheels.push(device) }, - (device) => { + device => { this.wheels.splice(this.wheels.findIndex(e => e.id === device.id), 1) return this.wheels.length }, + device => { + const found = this.wheels.find(e => e.id === device.id) + if (!found) return + Object.assign(found, device) + } ) this.startListening('ROTATOR', - (device) => { + device => { return this.rotators.push(device) }, - (device) => { + device => { this.rotators.splice(this.rotators.findIndex(e => e.id === device.id), 1) return this.rotators.length }, + device => { + const found = this.rotators.find(e => e.id === device.id) + if (!found) return + Object.assign(found, device) + } ) electron.on('CONNECTION.CLOSED', event => { @@ -323,7 +354,23 @@ export class HomeComponent implements AfterContentInit, OnDestroy { } } - private openDevice(type: K, header: string) { + protected findDeviceById(id: string) { + return this.cameras.find(e => e.id === id) || + this.mounts.find(e => e.id === id) || + this.wheels.find(e => e.id === id) || + this.focusers.find(e => e.id === id) || + this.rotators.find(e => e.id === id) + } + + protected async deviceConnected(event: DeviceConnectionCommandEvent) { + DeviceChooserComponent.handleConnectDevice(this.api, event.device, event.item) + } + + protected async deviceDisconnected(event: DeviceConnectionCommandEvent) { + DeviceChooserComponent.handleDisconnectDevice(this.api, event.device, event.item) + } + + private async openDevice(type: K) { this.deviceModel.length = 0 const devices: Device[] = type === 'CAMERA' ? this.cameras @@ -334,20 +381,13 @@ export class HomeComponent implements AfterContentInit, OnDestroy { : [] if (devices.length === 0) return - if (devices.length === 1) return this.openDeviceWindow(type, devices[0] as any) - - for (const device of [...devices].sort(deviceComparator)) { - this.deviceModel.push({ - icon: 'mdi mdi-connection', - label: device.name, - command: () => { - this.openDeviceWindow(type, device as any) - } - }) - } - this.deviceMenu.header = header - this.deviceMenu.show() + this.deviceMenu.header = type + const device = await this.deviceMenu.show(devices) + + if (device && device !== 'NONE') { + this.openDeviceWindow(type, device as any) + } } private openDeviceWindow(type: K, device: MappedDevice[K]) { @@ -396,7 +436,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { case 'FOCUSER': case 'WHEEL': case 'ROTATOR': - this.openDevice(type, type) + this.openDevice(type) break case 'GUIDER': this.browserWindow.openGuider({ bringToFront: true }) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 5866d9700..74d19f19b 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -8,7 +8,7 @@ import { basename, dirname, extname } from 'path' import { ContextMenu } from 'primeng/contextmenu' import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { HistogramComponent } from '../../shared/components/histogram/histogram.component' -import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { MenuItem } from '../../shared/components/menu-item/menu-item.component' import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' @@ -16,7 +16,6 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' -import { CheckableMenuItem, ToggleableMenuItem } from '../../shared/types/app.types' import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' import { DEFAULT_FOV, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageDetectStars, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types' @@ -169,7 +168,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { transformation: this.transformation } - private readonly saveAsMenuItem: ExtendedMenuItem = { + private readonly saveAsMenuItem: MenuItem = { label: 'Save as...', icon: 'mdi mdi-content-save', command: async () => { @@ -191,7 +190,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly plateSolveMenuItem: ExtendedMenuItem = { + private readonly plateSolveMenuItem: MenuItem = { label: 'Plate Solve', icon: 'mdi mdi-sigma', command: () => { @@ -199,7 +198,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly stretchMenuItem: ExtendedMenuItem = { + private readonly stretchMenuItem: MenuItem = { label: 'Stretch', icon: 'mdi mdi-chart-histogram', command: () => { @@ -207,7 +206,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly autoStretchMenuItem: CheckableMenuItem = { + private readonly autoStretchMenuItem: MenuItem = { id: 'auto-stretch-menuitem', label: 'Auto stretch', icon: 'mdi mdi-auto-fix', @@ -217,7 +216,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly scnrMenuItem: ExtendedMenuItem = { + private readonly scnrMenuItem: MenuItem = { label: 'SCNR', icon: 'mdi mdi-palette', disabled: true, @@ -226,7 +225,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly horizontalMirrorMenuItem: CheckableMenuItem = { + private readonly horizontalMirrorMenuItem: MenuItem = { label: 'Horizontal mirror', icon: 'mdi mdi-flip-horizontal', checked: false, @@ -237,7 +236,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly verticalMirrorMenuItem: CheckableMenuItem = { + private readonly verticalMirrorMenuItem: MenuItem = { label: 'Vertical mirror', icon: 'mdi mdi-flip-vertical', checked: false, @@ -248,7 +247,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly invertMenuItem: CheckableMenuItem = { + private readonly invertMenuItem: MenuItem = { label: 'Invert', icon: 'mdi mdi-invert-colors', checked: false, @@ -257,13 +256,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly calibrationMenuItem: ExtendedMenuItem = { + private readonly calibrationMenuItem: MenuItem = { label: 'Calibration', icon: 'mdi mdi-wrench', items: [], } - private readonly statisticsMenuItem: ExtendedMenuItem = { + private readonly statisticsMenuItem: MenuItem = { icon: 'mdi mdi-chart-histogram', label: 'Statistics', command: () => { @@ -272,7 +271,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly fitsHeaderMenuItem: ExtendedMenuItem = { + private readonly fitsHeaderMenuItem: MenuItem = { icon: 'mdi mdi-list-box', label: 'FITS Header', command: () => { @@ -280,7 +279,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly pointMountHereMenuItem: ExtendedMenuItem = { + private readonly pointMountHereMenuItem: MenuItem = { label: 'Point mount here', icon: 'mdi mdi-telescope', disabled: true, @@ -291,7 +290,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly frameAtThisCoordinateMenuItem: ExtendedMenuItem = { + private readonly frameAtThisCoordinateMenuItem: MenuItem = { label: 'Frame at this coordinate', icon: 'mdi mdi-image', disabled: true, @@ -304,7 +303,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly crosshairMenuItem: CheckableMenuItem = { + private readonly crosshairMenuItem: MenuItem = { label: 'Crosshair', icon: 'mdi mdi-bullseye', checked: false, @@ -313,7 +312,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly annotationMenuItem: ToggleableMenuItem = { + private readonly annotationMenuItem: MenuItem = { label: 'Annotate', icon: 'mdi mdi-marker', disabled: true, @@ -328,7 +327,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly detectStarsMenuItem: ToggleableMenuItem = { + private readonly detectStarsMenuItem: MenuItem = { label: 'Detect stars', icon: 'mdi mdi-creation', disabled: false, @@ -346,7 +345,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly roiMenuItem: CheckableMenuItem = { + private readonly roiMenuItem: MenuItem = { label: 'ROI', icon: 'mdi mdi-select', checked: false, @@ -386,7 +385,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly fovMenuItem: ExtendedMenuItem = { + private readonly fovMenuItem: MenuItem = { label: 'Field of View', icon: 'mdi mdi-camera-metering-spot', command: () => { @@ -398,7 +397,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } - private readonly overlayMenuItem: ExtendedMenuItem = { + private readonly overlayMenuItem: MenuItem = { label: 'Overlay', icon: 'mdi mdi-layers', items: [ @@ -573,7 +572,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const label = name ?? 'None' const icon = name ? 'mdi mdi-wrench' : 'mdi mdi-close' - return { + return { label, icon, checked: this.transformation.calibrationGroup === name, disabled: this.calibrationViaCamera, diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index fa90749d8..c853d97cf 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -124,7 +124,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { { icon: 'mdi mdi-crosshairs-gps', label: 'Locations', - menu: [ + subMenu: [ { icon: 'mdi mdi-crosshairs-gps', label: 'Current location', @@ -178,7 +178,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { { icon: 'mdi mdi-crosshairs', label: 'Intersection points', - menu: [ + subMenu: [ { icon: 'mdi mdi-crosshairs-gps', label: 'Meridian x Equator', diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index dfb9eabb2..afbb1ab77 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -136,11 +136,11 @@
- - - - + + + +
diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.html b/desktop/src/shared/components/device-chooser/device-chooser.component.html similarity index 84% rename from desktop/src/shared/components/device-list-button/device-list-button.component.html rename to desktop/src/shared/components/device-chooser/device-chooser.component.html index e7e0351a9..10733a640 100644 --- a/desktop/src/shared/components/device-list-button/device-list-button.component.html +++ b/desktop/src/shared/components/device-chooser/device-chooser.component.html @@ -13,4 +13,4 @@ \ No newline at end of file + (deviceConnect)="deviceConnected($event)" (deviceDisconnect)="deviceDisconnected($event)" /> \ No newline at end of file diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.scss b/desktop/src/shared/components/device-chooser/device-chooser.component.scss similarity index 100% rename from desktop/src/shared/components/device-list-button/device-list-button.component.scss rename to desktop/src/shared/components/device-chooser/device-chooser.component.scss diff --git a/desktop/src/shared/components/device-chooser/device-chooser.component.ts b/desktop/src/shared/components/device-chooser/device-chooser.component.ts new file mode 100644 index 000000000..33c6a6087 --- /dev/null +++ b/desktop/src/shared/components/device-chooser/device-chooser.component.ts @@ -0,0 +1,114 @@ +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { ApiService } from '../../services/api.service' +import { Device } from '../../types/device.types' +import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../device-list-menu/device-list-menu.component' +import { MenuItem } from '../menu-item/menu-item.component' + +@Component({ + selector: 'neb-device-chooser', + templateUrl: './device-chooser.component.html', + styleUrls: ['./device-chooser.component.scss'], +}) +export class DeviceChooserComponent { + + @Input({ required: true }) + readonly title!: string + + @Input() + readonly noDeviceMessage?: string + + @Input({ required: true }) + readonly icon!: string + + @Input({ required: true }) + readonly devices!: T[] + + @Input() + readonly hasNone: boolean = false + + @Input() + device?: T + + @Output() + readonly deviceChange = new EventEmitter() + + @Output() + readonly deviceConnect = new EventEmitter() + + @Output() + readonly deviceDisconnect = new EventEmitter() + + @ViewChild('deviceMenu') + private readonly deviceMenu!: DeviceListMenuComponent + + constructor(private api: ApiService) { } + + async show() { + const device = await this.deviceMenu.show(this.devices, this.device) + + if (device) { + this.device = device === 'NONE' ? undefined : device + this.deviceChange.emit(this.device) + } + } + + hide() { + this.deviceMenu.hide() + } + + protected async deviceConnected(event: DeviceConnectionCommandEvent) { + const newEvent = await DeviceChooserComponent.handleConnectDevice(this.api, event.device, event.item) + if (newEvent) this.deviceConnect.emit(newEvent) + } + + protected async deviceDisconnected(event: DeviceConnectionCommandEvent) { + const newEvent = await DeviceChooserComponent.handleDisconnectDevice(this.api, event.device, event.item) + if (newEvent) this.deviceDisconnect.emit(newEvent) + } + + static async handleConnectDevice(api: ApiService, device: Device, item: MenuItem) { + await api.indiDeviceConnect(device) + + item.disabled = true + + return new Promise((resolve) => { + setTimeout(async () => { + Object.assign(device, await api.indiDevice(device)) + + if (device.connected) { + item.icon = 'mdi mdi-close' + item.toolbarButtonSeverity = 'danger' + item.label = 'Disconnect' + resolve({ device, item }) + } else { + resolve(undefined) + } + + item.disabled = false + }, 1000) + }) + } + + static async handleDisconnectDevice(api: ApiService, device: Device, item: MenuItem) { + await api.indiDeviceDisconnect(device) + + item.disabled = true + + return new Promise((resolve) => { + setTimeout(async () => { + Object.assign(device, await api.indiDevice(device)) + + if (!device.connected) { + item.icon = 'mdi mdi-connection' + item.toolbarButtonSeverity = 'info' + item.label = 'Connect' + resolve({ device, item }) + } else { + resolve(undefined) + } + + item.disabled = false + }, 1000) + }) + } +} \ No newline at end of file diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.ts b/desktop/src/shared/components/device-list-button/device-list-button.component.ts deleted file mode 100644 index ce96deefc..000000000 --- a/desktop/src/shared/components/device-list-button/device-list-button.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' -import { Device } from '../../types/device.types' -import { DeviceListMenuComponent } from '../device-list-menu/device-list-menu.component' - -@Component({ - selector: 'neb-device-list-button', - templateUrl: './device-list-button.component.html', - styleUrls: ['./device-list-button.component.scss'], -}) -export class DeviceListButtonComponent { - - @Input({ required: true }) - readonly title!: string - - @Input() - readonly noDeviceMessage?: string - - @Input({ required: true }) - readonly icon!: string - - @Input({ required: true }) - readonly devices!: T[] - - @Input() - readonly hasNone: boolean = false - - @Input() - device?: T - - @Output() - readonly deviceChange = new EventEmitter() - - @Output() - readonly deviceConnect = new EventEmitter() - - @Output() - readonly deviceDisconnect = new EventEmitter() - - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent - - async show() { - const device = await this.deviceMenu.show(this.devices, this.device) - - if (device) { - this.device = device === 'NONE' ? undefined : device - this.deviceChange.emit(this.device) - } - } - - hide() { - this.deviceMenu.hide() - } -} \ No newline at end of file diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss index e69de29bb..1e445858e 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss @@ -0,0 +1,5 @@ +:host { + ::ng-deep .p-menuitem-link { + min-height: 43px; + } +} \ No newline at end of file diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts index f60d66c5b..d09d1e0b5 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts @@ -1,11 +1,18 @@ import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' import { SEPARATOR_MENU_ITEM } from '../../constants' import { PrimeService } from '../../services/prime.service' +import { isGuideHead } from '../../types/camera.types' import { Device } from '../../types/device.types' import { deviceComparator } from '../../utils/comparators' import { DialogMenuComponent } from '../dialog-menu/dialog-menu.component' +import { MenuItem } from '../menu-item/menu-item.component' import { SlideMenuItem } from '../slide-menu/slide-menu.component' +export interface DeviceConnectionCommandEvent { + device: Device + item: MenuItem +} + @Component({ selector: 'neb-device-list-menu', templateUrl: './device-list-menu.component.html', @@ -29,10 +36,10 @@ export class DeviceListMenuComponent { readonly hasNone: boolean = false @Output() - readonly deviceConnect = new EventEmitter() + readonly deviceConnect = new EventEmitter() @Output() - readonly deviceDisconnect = new EventEmitter() + readonly deviceDisconnect = new EventEmitter() @ViewChild('menu') private readonly menu!: DialogMenuComponent @@ -73,17 +80,18 @@ export class DeviceListMenuComponent { for (const device of devices.sort(deviceComparator)) { model.push({ - icon: 'mdi mdi-circle-medium ' + (device.connected ? 'text-green-500' : 'text-red-500'), label: device.name, checked: selected === device, disabled: this.disableIfDeviceIsNotConnected && !device.connected, toolbarMenu: [ { - icon: 'mdi ' + (device.connected ? 'mdi-close text-red-500' : 'mdi-connection text-blue-500'), + icon: 'mdi ' + (device.connected ? 'mdi-close' : 'mdi-connection'), + toolbarButtonSeverity: device.connected ? 'danger' : 'info', label: device.connected ? 'Disconnect' : 'Connect', + visible: !isGuideHead(device), command: event => { - if (device.connected) this.deviceDisconnect.emit(device) - else this.deviceConnect.emit(device) + if (device.connected) this.deviceDisconnect.emit({ device, item: event.item! }) + else this.deviceConnect.emit({ device, item: event.item! }) } } ], diff --git a/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts b/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts index 7c811cfb5..efdc0dcc2 100644 --- a/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts +++ b/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core' -import { ExtendedMenuItem } from '../menu-item/menu-item.component' +import { MenuItem } from '../menu-item/menu-item.component' import { SlideMenuItemCommandEvent } from '../slide-menu/slide-menu.component' @Component({ @@ -16,7 +16,7 @@ export class DialogMenuComponent { readonly visibleChange = new EventEmitter() @Input() - model: ExtendedMenuItem[] = [] + model: MenuItem[] = [] @Input() header?: string @@ -38,7 +38,7 @@ export class DialogMenuComponent { } next(event: SlideMenuItemCommandEvent) { - if (!event.item?.menu?.length) { + if (!event.item?.subMenu?.length) { this.hide() } else { this.navigationHeader.push(this.header) diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html index 6ad336a57..4fd51b180 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.html +++ b/desktop/src/shared/components/menu-item/menu-item.component.html @@ -6,7 +6,8 @@
@for (m of item.toolbarMenu; track i; let i = $index) { }
@@ -14,7 +15,7 @@ @if(item.toggleable) { } - @if (item.items?.length || item.menu?.length) { + @if (item.items?.length || item.subMenu?.length) { } \ No newline at end of file diff --git a/desktop/src/shared/components/menu-item/menu-item.component.ts b/desktop/src/shared/components/menu-item/menu-item.component.ts index 71fbac809..616b6ee1f 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.ts +++ b/desktop/src/shared/components/menu-item/menu-item.component.ts @@ -1,15 +1,27 @@ import { Component, Input } from '@angular/core' -import { MenuItem, MenuItemCommandEvent } from 'primeng/api' -import { CheckableMenuItem, ToggleableMenuItem } from '../../types/app.types' +import { MenuItem as PrimeMenuItem, MenuItemCommandEvent as PrimeMenuItemCommandEvent } from 'primeng/api' +import { CheckboxChangeEvent } from 'primeng/checkbox' +import { Severity } from '../../types/app.types' -export interface ExtendedMenuItemCommandEvent extends MenuItemCommandEvent { - item?: ExtendedMenuItem +export interface MenuItemCommandEvent extends PrimeMenuItemCommandEvent { + item?: MenuItem } -export interface ExtendedMenuItem extends MenuItem, Partial, Partial { - menu?: ExtendedMenuItem[] - toolbarMenu?: ExtendedMenuItem[] - command?: (event: ExtendedMenuItemCommandEvent) => void +export interface MenuItem extends PrimeMenuItem { + badgeSeverity?: Severity + + checked?: boolean + + toggleable?: boolean + toggled?: boolean + + subMenu?: MenuItem[] + + toolbarMenu?: MenuItem[] + toolbarButtonSeverity?: Severity + + command?: (event: MenuItemCommandEvent) => void + toggle?: (event: CheckboxChangeEvent) => void } @Component({ @@ -20,5 +32,5 @@ export interface ExtendedMenuItem extends MenuItem, Partial, export class MenuItemComponent { @Input({ required: true }) - readonly item!: ExtendedMenuItem + readonly item!: MenuItem } \ No newline at end of file diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.html b/desktop/src/shared/components/slide-menu/slide-menu.component.html index 3cc80d1a2..27dd182cd 100644 --- a/desktop/src/shared/components/slide-menu/slide-menu.component.html +++ b/desktop/src/shared/components/slide-menu/slide-menu.component.html @@ -1,5 +1,5 @@
- + diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.ts b/desktop/src/shared/components/slide-menu/slide-menu.component.ts index f7c254c71..59cfbd2fa 100644 --- a/desktop/src/shared/components/slide-menu/slide-menu.component.ts +++ b/desktop/src/shared/components/slide-menu/slide-menu.component.ts @@ -1,11 +1,11 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core' -import { ExtendedMenuItem, ExtendedMenuItemCommandEvent } from '../menu-item/menu-item.component' +import { MenuItem, MenuItemCommandEvent } from '../menu-item/menu-item.component' -export interface SlideMenuItem extends ExtendedMenuItem { +export interface SlideMenuItem extends MenuItem { command?: (event: SlideMenuItemCommandEvent) => void } -export interface SlideMenuItemCommandEvent extends ExtendedMenuItemCommandEvent { +export interface SlideMenuItemCommandEvent extends MenuItemCommandEvent { item?: SlideMenuItem parent?: SlideMenuItem level?: number @@ -51,9 +51,9 @@ export class SlideMenuComponent implements OnInit { for (const item of menu) { const command = item.command - if (item.menu?.length) { + if (item.subMenu?.length) { item.command = (event: SlideMenuItemCommandEvent) => { - this.menu = item.menu! + this.menu = item.subMenu! this.navigation.push(menu) event.parent = parent event.level = level @@ -61,7 +61,7 @@ export class SlideMenuComponent implements OnInit { this.onNext.emit(event) } - this.processMenu(item.menu, level + 1, item) + this.processMenu(item.subMenu, level + 1, item) } else { item.command = (event: SlideMenuItemCommandEvent) => { event.parent = parent diff --git a/desktop/src/shared/constants.ts b/desktop/src/shared/constants.ts index 5a2628fdf..bda08b458 100644 --- a/desktop/src/shared/constants.ts +++ b/desktop/src/shared/constants.ts @@ -1,4 +1,4 @@ -import { ExtendedMenuItem } from './components/menu-item/menu-item.component' +import { MenuItem } from './components/menu-item/menu-item.component' export const EVERY_MINUTE_CRON_TIME = '0 */1 * * * *' @@ -6,6 +6,6 @@ export const TWO_DIGITS_FORMATTER = new Intl.NumberFormat('en-US', { minimumInte export const THREE_DIGITS_FORMATTER = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 3, minimumFractionDigits: 0, maximumFractionDigits: 0 }) export const ONE_DECIMAL_PLACE_FORMATTER = new Intl.NumberFormat('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) -export const SEPARATOR_MENU_ITEM: ExtendedMenuItem = { +export const SEPARATOR_MENU_ITEM: MenuItem = { separator: true, } diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index fb1eb0b5a..223d97d0b 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -69,7 +69,6 @@ export class ApiService { return this.http.get(`cameras/${camera.id}/capturing`) } - // TODO: Rotator cameraSnoop(camera: Camera, equipment: Equipment) { const { mount, wheel, focuser, rotator } = equipment const query = this.http.query({ mount: mount?.name, wheel: wheel?.name, focuser: focuser?.name, rotator: rotator?.name }) @@ -383,6 +382,18 @@ export class ApiService { // INDI + indiDevice(device: T) { + return this.http.get(`indi/${device.id}`) + } + + indiDeviceConnect(device: Device) { + return this.http.put(`indi/${device.id}/connect`) + } + + indiDeviceDisconnect(device: Device) { + return this.http.put(`indi/${device.id}/disconnect`) + } + indiProperties(device: Device) { return this.http.get[]>(`indi/${device.id}/properties`) } diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts index 59325b479..633e583b5 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -1,17 +1,6 @@ -import { MenuItem } from 'primeng/api' -import { CheckboxChangeEvent } from 'primeng/checkbox' import { MessageEvent } from './api.types' -export interface CheckableMenuItem extends MenuItem { - checked: boolean -} - -export interface ToggleableMenuItem extends MenuItem { - toggleable: boolean - toggled: boolean - - toggle: (event: CheckboxChangeEvent) => void -} +export type Severity = 'success' | 'info' | 'warning' | 'danger' export interface NotificationEvent extends MessageEvent { type: string diff --git a/desktop/src/shared/types/auxiliary.types.ts b/desktop/src/shared/types/auxiliary.types.ts index 549799e4f..eca875e7d 100644 --- a/desktop/src/shared/types/auxiliary.types.ts +++ b/desktop/src/shared/types/auxiliary.types.ts @@ -4,3 +4,7 @@ export interface Thermometer extends Device { hasThermometer: boolean temperature: number } + +export function isThermometer(device?: Device): device is Thermometer { + return !!device && 'temperature' in device +} diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 056ccc482..5d48be72f 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -1,6 +1,6 @@ import { MessageEvent } from './api.types' import { Thermometer } from './auxiliary.types' -import { CompanionDevice, Device, PropertyState } from './device.types' +import { CompanionDevice, Device, PropertyState, isCompanionDevice } from './device.types' import { GuideOutput } from './guider.types' export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' | 'AUTO_FOCUS' @@ -266,3 +266,11 @@ export const EMPTY_CAMERA_CAPTURE_INFO: CameraCaptureInfo = { progress: 0, count: 0, } + +export function isCamera(device?: Device): device is Camera { + return !!device && 'exposuring' in device +} + +export function isGuideHead(device?: Device): device is GuideHead { + return isCamera(device) && isCompanionDevice(device) && !!device.main +} diff --git a/desktop/src/shared/types/device.types.ts b/desktop/src/shared/types/device.types.ts index 934a9509f..b26f23daa 100644 --- a/desktop/src/shared/types/device.types.ts +++ b/desktop/src/shared/types/device.types.ts @@ -8,6 +8,8 @@ export type INDIPropertyType = 'NUMBER' | 'SWITCH' | 'TEXT' export type SwitchRule = 'ONE_OF_MANY' | 'AT_MOST_ONE' | 'ANY_OF_MANY' +export type DeviceType = 'CAMERA' | 'MOUNT' | 'WHEEL' | 'FOCUSER' | 'ROTATOR' | 'GPS' | 'DOME' | 'SWITCH' + export interface Device { readonly sender: string readonly id: string @@ -57,3 +59,7 @@ export interface INDIDeviceMessage { device?: Device message: string } + +export function isCompanionDevice(device?: T | CompanionDevice): device is CompanionDevice { + return !!device && 'main' in device +} diff --git a/desktop/src/shared/types/focuser.types.ts b/desktop/src/shared/types/focuser.types.ts index 50bf8c5ea..5f05f8302 100644 --- a/desktop/src/shared/types/focuser.types.ts +++ b/desktop/src/shared/types/focuser.types.ts @@ -37,3 +37,7 @@ export interface FocuserPreference { stepsRelative?: number stepsAbsolute?: number } + +export function isFocuser(device?: Device): device is Focuser { + return !!device && 'maxPosition' in device +} diff --git a/desktop/src/shared/types/home.types.ts b/desktop/src/shared/types/home.types.ts index 0885999c1..6c6195728 100644 --- a/desktop/src/shared/types/home.types.ts +++ b/desktop/src/shared/types/home.types.ts @@ -1,12 +1,12 @@ import { Camera } from './camera.types' +import { DeviceType } from './device.types' import { Focuser } from './focuser.types' import { Mount } from './mount.types' import { Rotator } from './rotator.types' import { FilterWheel } from './wheel.types' -export type HomeWindowType = 'CAMERA' | 'MOUNT' | 'GUIDER' | 'WHEEL' | 'FOCUSER' | 'DOME' | 'ROTATOR' | 'SWITCH' | - 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | - 'AUTO_FOCUS' +export type HomeWindowType = DeviceType | 'GUIDER' | 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | + 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | 'AUTO_FOCUS' export const CONNECTION_TYPES = ['INDI', 'ALPACA'] as const diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts index 7fc7611af..afc1c453f 100644 --- a/desktop/src/shared/types/mount.types.ts +++ b/desktop/src/shared/types/mount.types.ts @@ -1,4 +1,5 @@ import { Angle, EquatorialCoordinate } from './atlas.types' +import { Device } from './device.types' import { GPS } from './gps.types' import { GuideOutput } from './guider.types' @@ -95,3 +96,7 @@ export interface MountRemoteControlDialog { port: number data: MountRemoteControl[] } + +export function isMount(device?: Device): device is Mount { + return !!device && 'tracking' in device +} diff --git a/desktop/src/shared/types/rotator.types.ts b/desktop/src/shared/types/rotator.types.ts index 4348ce342..daebb23a2 100644 --- a/desktop/src/shared/types/rotator.types.ts +++ b/desktop/src/shared/types/rotator.types.ts @@ -33,3 +33,7 @@ export const EMPTY_ROTATOR: Rotator = { export interface RotatorPreference { angle?: number } + +export function isRotator(device?: Device): device is Rotator { + return !!device && 'angle' in device +} diff --git a/desktop/src/shared/types/wheel.types.ts b/desktop/src/shared/types/wheel.types.ts index bd4928393..d60f738c8 100644 --- a/desktop/src/shared/types/wheel.types.ts +++ b/desktop/src/shared/types/wheel.types.ts @@ -42,4 +42,8 @@ export interface FilterSlot { export interface WheelRenamed { wheel: FilterWheel filter: FilterSlot -} \ No newline at end of file +} + +export function isFilterWheel(device?: Device): device is FilterWheel { + return !!device && 'count' in device +} diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index b1a1660ff..ef9dc1cb9 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -182,6 +182,8 @@ i.mdi { } .p-menuitem-link { + padding: 0.5rem 0.75rem; + &.p-menuitem-checked { background-color: #C5E1A5; color: #212121 !important; From df472e92f0ac0a5c691fe878245afca52076fd6c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 27 May 2024 22:31:20 -0300 Subject: [PATCH 26/45] [api][desktop]: Support Auto Focus --- .../api/autofocus/AutoFocusRequest.kt | 5 +- .../nebulosa/api/autofocus/AutoFocusTask.kt | 18 +-- .../api/focusers/AbstractFocuserMoveTask.kt | 56 ++++++++ .../api/focusers/BacklashCompensation.kt | 13 ++ .../BacklashCompensationFocuserMoveTask.kt | 135 ++++++++++++++++++ .../BacklashCompensationMode.kt | 2 +- .../api/focusers/FocuserMoveAbsoluteTask.kt | 59 +------- .../api/focusers/FocuserMoveRelativeTask.kt | 59 +------- .../app/alignment/alignment.component.html | 4 +- .../src/app/alignment/alignment.component.ts | 3 +- .../app/autofocus/autofocus.component.html | 40 ++++++ .../src/app/autofocus/autofocus.component.ts | 22 ++- desktop/src/app/image/image.component.ts | 3 +- .../src/app/settings/settings.component.ts | 4 +- desktop/src/shared/types/autofocus.type.ts | 31 +++- desktop/src/shared/types/settings.types.ts | 2 - 16 files changed, 323 insertions(+), 133 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt rename api/src/main/kotlin/nebulosa/api/{autofocus => focusers}/BacklashCompensationMode.kt (69%) diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt index b19e26940..3899a409f 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt @@ -1,14 +1,13 @@ package nebulosa.api.autofocus import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.focusers.BacklashCompensation data class AutoFocusRequest( @JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC, @JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, @JvmField val rSquaredThreshold: Double = 0.7, - @JvmField val backlashCompensationMode: BacklashCompensationMode = BacklashCompensationMode.OVERSHOOT, - @JvmField val backlashIn: Int = 0, - @JvmField val backlashOut: Int = 0, + @JvmField val backlashCompensation: BacklashCompensation = BacklashCompensation.EMPTY, @JvmField val initialOffsetSteps: Int = 4, @JvmField val stepSize: Int = 50, @JvmField val totalNumberOfAttempts: Int = 1, diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 44e92e2ed..6c79ad5b1 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -2,10 +2,9 @@ package nebulosa.api.autofocus import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.cameras.* +import nebulosa.api.focusers.BacklashCompensationFocuserMoveTask +import nebulosa.api.focusers.BacklashCompensationMode import nebulosa.api.focusers.FocuserEventAware -import nebulosa.api.focusers.FocuserMoveAbsoluteTask -import nebulosa.api.focusers.FocuserMoveRelativeTask -import nebulosa.api.focusers.FocuserMoveTask import nebulosa.api.image.ImageBucket import nebulosa.api.messages.MessageEvent import nebulosa.api.tasks.AbstractTask @@ -56,11 +55,12 @@ data class AutoFocusTask( frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF ) + private val focusPoints = ArrayList() private val measurements = ArrayList(request.capture.exposureAmount) private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = max(1, request.capture.exposureAmount)) + private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation) - @Volatile private var focuserMoveTask: FocuserMoveTask? = null @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null @Volatile private var parabolicCurve: Lazy? = null @Volatile private var hyperbolicCurve: Lazy? = null @@ -76,7 +76,7 @@ data class AutoFocusTask( } override fun handleFocuserEvent(event: FocuserEvent) { - focuserMoveTask?.handleFocuserEvent(event) + focuserMoveTask.handleFocuserEvent(event) } override fun canUseAsLastEvent(event: MessageEvent) = event is AutoFocusEvent @@ -88,7 +88,8 @@ data class AutoFocusTask( // Get initial position information, as average of multiple exposures, if configured this way. val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken).averageHFD else Double.NaN - val reverse = request.backlashCompensationMode == BacklashCompensationMode.OVERSHOOT && request.backlashIn > 0 && request.backlashOut == 0 + val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 && + request.backlashCompensation.backlashOut == 0 LOG.info("Auto Focus started. initialHFD={}, reverse={}, camera={}, focuser={}", initialHFD, reverse, camera, focuser) @@ -354,10 +355,9 @@ data class AutoFocusTask( } private fun moveFocuser(position: Int, cancellationToken: CancellationToken, relative: Boolean): Int { - focuserMoveTask = if (relative) FocuserMoveRelativeTask(focuser, position) - else FocuserMoveAbsoluteTask(focuser, position) sendEvent(AutoFocusState.MOVING) - focuserMoveTask!!.execute(cancellationToken) + focuserMoveTask.position = if (relative) focuser.position + position else position + focuserMoveTask.execute(cancellationToken) return focuser.position } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt new file mode 100644 index 000000000..115f8a354 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt @@ -0,0 +1,56 @@ +package nebulosa.api.focusers + +import nebulosa.common.concurrency.cancel.CancellationListener +import nebulosa.common.concurrency.cancel.CancellationSource +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.indi.device.focuser.FocuserMoveFailed +import nebulosa.indi.device.focuser.FocuserPositionChanged +import nebulosa.log.loggerFor + +abstract class AbstractFocuserMoveTask : FocuserMoveTask, CancellationListener { + + @JvmField protected val latch = CountUpDownLatch() + + @Volatile private var initialPosition = 0 + + override fun handleFocuserEvent(event: FocuserEvent) { + if (event.device === focuser) { + when (event) { + is FocuserPositionChanged -> if (focuser.position != initialPosition && !focuser.moving) latch.reset() + is FocuserMoveFailed -> latch.reset() + } + } + } + + protected abstract fun canMove(): Boolean + + protected abstract fun move() + + override fun execute(cancellationToken: CancellationToken) { + if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && canMove()) { + try { + cancellationToken.listen(this) + initialPosition = focuser.position + LOG.info("Focuser move started. focuser={}", focuser) + latch.countUp() + move() + latch.await() + } finally { + cancellationToken.unlisten(this) + LOG.info("Focuser move finished. focuser={}", focuser) + } + } + } + + override fun onCancel(source: CancellationSource) { + focuser.abortFocus() + latch.reset() + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt new file mode 100644 index 000000000..e7b4b5982 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt @@ -0,0 +1,13 @@ +package nebulosa.api.focusers + +data class BacklashCompensation( + @JvmField val mode: BacklashCompensationMode = BacklashCompensationMode.OVERSHOOT, + @JvmField val backlashIn: Int = 0, + @JvmField val backlashOut: Int = 0, +) { + + companion object { + + @JvmStatic val EMPTY = BacklashCompensation() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt new file mode 100644 index 000000000..49e4ebc94 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt @@ -0,0 +1,135 @@ +package nebulosa.api.focusers + +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.log.loggerFor + +/** + * This decorator will wrap an absolute backlash [compensation] model around the [focuser]. + * On each move an absolute backlash compensation value will be applied, if the focuser changes its moving direction + * The returned position will then accommodate for this backlash and simulating the position without backlash. + */ +data class BacklashCompensationFocuserMoveTask( + override val focuser: Focuser, + @JvmField @Volatile var position: Int, + @JvmField val compensation: BacklashCompensation, +) : FocuserMoveTask { + + enum class OvershootDirection { + NONE, + IN, + OUT, + } + + @Volatile private var offset = 0 + @Volatile private var lastDirection = OvershootDirection.NONE + + private val task = FocuserMoveAbsoluteTask(focuser, 0) + + /** + * Returns the adjusted position based on the amount of backlash compensation. + */ + val adjustedPosition + get() = focuser.position - offset + + override fun handleFocuserEvent(event: FocuserEvent) { + task.handleFocuserEvent(event) + } + + override fun execute(cancellationToken: CancellationToken) { + if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving) { + val startPosition = focuser.position + + if (compensation.mode == BacklashCompensationMode.ABSOLUTE) { + val adjustedTargetPosition = position + offset + + val finalizedTargetPosition = if (adjustedTargetPosition < 0) { + offset = 0 + 0 + } else if (adjustedTargetPosition > focuser.maxPosition) { + offset = 0 + focuser.maxPosition + } else { + val backlashCompensation = calculateAbsoluteBacklashCompensation(startPosition, adjustedTargetPosition) + offset += backlashCompensation + adjustedTargetPosition + backlashCompensation + } + + moveFocuser(finalizedTargetPosition, cancellationToken) + } else { + val backlashCompensation = calculateOvershootBacklashCompensation(startPosition, position) + + if (backlashCompensation != 0) { + val overshoot = position + backlashCompensation + + if (overshoot < 0) { + LOG.info("overshooting position is below minimum 0, skipping overshoot") + } else if (overshoot > focuser.maxPosition) { + LOG.info("overshooting position is above maximum ${focuser.maxPosition}, skipping overshoot") + } else { + LOG.info("overshooting from $startPosition to overshoot position $overshoot using a compensation of $backlashCompensation") + + moveFocuser(overshoot, cancellationToken) + + LOG.info("moving back to position $position") + } + } + + moveFocuser(position, cancellationToken) + } + } + } + + private fun moveFocuser(position: Int, cancellationToken: CancellationToken) { + if (position > 0 && position <= focuser.maxPosition) { + lastDirection = determineMovingDirection(focuser.position, position) + task.position = position + task.execute(cancellationToken) + } + } + + override fun reset() { + task.reset() + + offset = 0 + lastDirection = OvershootDirection.NONE + } + + override fun close() { + task.close() + } + + private fun determineMovingDirection(prevPosition: Int, newPosition: Int): OvershootDirection { + return if (newPosition > prevPosition) OvershootDirection.OUT + else if (newPosition < prevPosition) OvershootDirection.IN + else lastDirection + } + + private fun calculateAbsoluteBacklashCompensation(lastPosition: Int, newPosition: Int): Int { + val direction = determineMovingDirection(lastPosition, newPosition) + + return if (direction == OvershootDirection.IN && lastDirection == OvershootDirection.OUT) { + LOG.info("Focuser is reversing direction from outwards to inwards") + -compensation.backlashIn + } else if (direction == OvershootDirection.OUT && lastDirection === OvershootDirection.IN) { + LOG.info("Focuser is reversing direction from inwards to outwards") + compensation.backlashOut + } else { + 0 + } + } + + private fun calculateOvershootBacklashCompensation(lastPosition: Int, newPosition: Int): Int { + val direction = determineMovingDirection(lastPosition, newPosition) + + return if (direction == OvershootDirection.IN && compensation.backlashIn != 0) -compensation.backlashIn + else if (direction == OvershootDirection.OUT && compensation.backlashOut != 0) compensation.backlashOut + else 0 + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt similarity index 69% rename from api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt rename to api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt index d80ab414b..a81802872 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt @@ -1,4 +1,4 @@ -package nebulosa.api.autofocus +package nebulosa.api.focusers enum class BacklashCompensationMode { ABSOLUTE, diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt index b154ee55e..85379b053 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt @@ -1,63 +1,18 @@ package nebulosa.api.focusers -import nebulosa.common.concurrency.cancel.CancellationListener -import nebulosa.common.concurrency.cancel.CancellationSource -import nebulosa.common.concurrency.cancel.CancellationToken -import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.focuser.FocuserEvent -import nebulosa.indi.device.focuser.FocuserMoveFailed -import nebulosa.indi.device.focuser.FocuserPositionChanged -import nebulosa.log.loggerFor import kotlin.math.abs data class FocuserMoveAbsoluteTask( override val focuser: Focuser, - @JvmField val position: Int, -) : FocuserMoveTask, CancellationListener { + @JvmField @Volatile var position: Int, +) : AbstractFocuserMoveTask() { - private val latch = CountUpDownLatch() + override fun canMove() = position != focuser.position && position > 0 && position < focuser.maxPosition - override fun handleFocuserEvent(event: FocuserEvent) { - if (event.device === focuser) { - when (event) { - is FocuserPositionChanged -> if (focuser.position == position) latch.reset() - is FocuserMoveFailed -> latch.reset() - } - } - } - - override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isCancelled && focuser.connected - && !focuser.moving && position != focuser.position - ) { - try { - cancellationToken.listen(this) - - LOG.info("Focuser move started. position={}, focuser={}", position, focuser) - - latch.countUp() - - if (focuser.canAbsoluteMove) focuser.moveFocusTo(position) - else if (focuser.position - position < 0) focuser.moveFocusIn(abs(focuser.position - position)) - else focuser.moveFocusOut(abs(focuser.position - position)) - - latch.await() - } finally { - cancellationToken.unlisten(this) - } - - LOG.info("Focuser move finished. position={}, focuser={}", position, focuser) - } - } - - override fun onCancel(source: CancellationSource) { - focuser.abortFocus() - latch.reset() - } - - companion object { - - @JvmStatic private val LOG = loggerFor() + override fun move() { + if (focuser.canAbsoluteMove) focuser.moveFocusTo(position) + else if (position < focuser.position) focuser.moveFocusIn(abs(position - focuser.position)) + else focuser.moveFocusOut(abs(position - focuser.position)) } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt index 531dafce0..ec4671b3a 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt @@ -1,65 +1,18 @@ package nebulosa.api.focusers -import nebulosa.common.concurrency.cancel.CancellationListener -import nebulosa.common.concurrency.cancel.CancellationSource -import nebulosa.common.concurrency.cancel.CancellationToken -import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.focuser.FocuserEvent -import nebulosa.indi.device.focuser.FocuserMoveFailed -import nebulosa.indi.device.focuser.FocuserPositionChanged -import nebulosa.log.loggerFor import kotlin.math.abs data class FocuserMoveRelativeTask( override val focuser: Focuser, @JvmField val offset: Int, -) : FocuserMoveTask, CancellationListener { +) : AbstractFocuserMoveTask() { - private val latch = CountUpDownLatch() + override fun canMove() = offset != 0 - @Volatile private var initialPosition = 0 - - override fun handleFocuserEvent(event: FocuserEvent) { - if (event.device === focuser) { - when (event) { - is FocuserPositionChanged -> if (abs(focuser.position - initialPosition) == abs(offset)) latch.reset() - is FocuserMoveFailed -> latch.reset() - } - } - } - - override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && offset != 0) { - try { - cancellationToken.listen(this) - - initialPosition = focuser.position - - LOG.info("Focuser move started. offset={}, focuser={}", offset, focuser) - - latch.countUp() - - if (!focuser.canRelativeMove) focuser.moveFocusTo(focuser.position + offset) - else if (offset > 0) focuser.moveFocusOut(offset) - else focuser.moveFocusIn(abs(offset)) - - latch.await() - } finally { - cancellationToken.unlisten(this) - } - - LOG.info("Focuser move finished. offset={}, focuser={}", offset, focuser) - } - } - - override fun onCancel(source: CancellationSource) { - focuser.abortFocus() - latch.reset() - } - - companion object { - - @JvmStatic private val LOG = loggerFor() + override fun move() { + if (!focuser.canRelativeMove) focuser.moveFocusTo(focuser.position + offset) + else if (offset > 0) focuser.moveFocusOut(offset) + else focuser.moveFocusIn(abs(offset)) } } diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index d42354c7b..3acb4adf8 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -27,8 +27,8 @@
- +
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index d00c70296..eb4e91581 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -9,7 +9,7 @@ import { Angle } from '../../shared/types/atlas.types' import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureTimeUnit, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { EMPTY_GUIDE_OUTPUT, GuideDirection, GuideOutput } from '../../shared/types/guider.types' import { EMPTY_MOUNT, Mount } from '../../shared/types/mount.types' -import { DEFAULT_SOLVER_TYPES, EMPTY_PLATE_SOLVER_PREFERENCE } from '../../shared/types/settings.types' +import { EMPTY_PLATE_SOLVER_PREFERENCE } from '../../shared/types/settings.types' import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' import { CameraComponent } from '../camera/camera.component' @@ -47,7 +47,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { stepDuration: 5, } - readonly plateSolverTypes = Array.from(DEFAULT_SOLVER_TYPES) tppaFailed = false tppaRightAscension: Angle = `00h00m00s` tppaDeclination: Angle = `00°00'00"` diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 7edd35cf2..9cf121fdb 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -6,4 +6,44 @@
+
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
\ No newline at end of file diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index ec576a103..ee6271829 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -26,7 +26,17 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { running = false readonly request: AutoFocusRequest = { - capture: structuredClone(EMPTY_CAMERA_START_CAPTURE) + capture: structuredClone(EMPTY_CAMERA_START_CAPTURE), + fittingMode: 'HYPERBOLIC', + rSquaredThreshold: 0.7, + backlashCompensation: { + mode: 'OVERSHOOT', + backlashIn: 0, + backlashOut: 0 + }, + initialOffsetSteps: 4, + stepSize: 100, + totalNumberOfAttempts: 4 } constructor( @@ -157,6 +167,16 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { this.preference.cameraStartCaptureForAutoFocus(this.camera).set(this.request.capture) const preference: AutoFocusPreference = { + fittingMode: 'TRENDLINES', + rSquaredThreshold: 0, + backlashCompensation: { + mode: 'OVERSHOOT', + backlashIn: 0, + backlashOut: 0 + }, + initialOffsetSteps: 0, + stepSize: 0, + totalNumberOfAttempts: 0 } this.preference.autoFocusPreference.set(preference) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 74d19f19b..956ba9d9e 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -20,7 +20,6 @@ import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, St import { Camera } from '../../shared/types/camera.types' import { DEFAULT_FOV, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageDetectStars, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' -import { DEFAULT_SOLVER_TYPES } from '../../shared/types/settings.types' import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @@ -104,7 +103,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { centerDEC: '', radius: 4, solved: structuredClone(EMPTY_IMAGE_SOLVED), - types: Array.from(DEFAULT_SOLVER_TYPES), + types: ['ASTAP', 'ASTROMETRY_NET_ONLINE'], type: 'ASTAP' } diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 4567d72ce..7c288e160 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -6,7 +6,7 @@ import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { EMPTY_LOCATION, Location } from '../../shared/types/atlas.types' -import { DEFAULT_SOLVER_TYPES, PlateSolverPreference, PlateSolverType } from '../../shared/types/settings.types' +import { PlateSolverPreference, PlateSolverType } from '../../shared/types/settings.types' import { AppComponent } from '../app.component' @Component({ @@ -19,7 +19,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { readonly locations: Location[] location: Location - readonly solverTypes = Array.from(DEFAULT_SOLVER_TYPES) + readonly solverTypes: PlateSolverType[] = ['ASTAP', 'ASTROMETRY_NET_ONLINE'] solverType = this.solverTypes[0] readonly solvers = new Map() diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts index 48f3847b6..5a07e32e5 100644 --- a/desktop/src/shared/types/autofocus.type.ts +++ b/desktop/src/shared/types/autofocus.type.ts @@ -1,13 +1,36 @@ import { CameraStartCapture } from './camera.types' +export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC' + +export type BacklashCompensationMode = 'ABSOLUTE' | 'OVERSHOOT' + +export interface BacklashCompensation { + mode: BacklashCompensationMode + backlashIn: number + backlashOut: number +} + export interface AutoFocusRequest { + fittingMode: AutoFocusFittingMode capture: CameraStartCapture + rSquaredThreshold: number + backlashCompensation: BacklashCompensation + initialOffsetSteps: number + stepSize: number + totalNumberOfAttempts: number } -export interface AutoFocusPreference { - -} +export interface AutoFocusPreference extends Omit { } export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { - + fittingMode: 'HYPERBOLIC', + rSquaredThreshold: 0.7, + initialOffsetSteps: 4, + stepSize: 100, + totalNumberOfAttempts: 4, + backlashCompensation: { + mode: 'OVERSHOOT', + backlashIn: 0, + backlashOut: 0 + } } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index d3eeacabb..7b26a7f55 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -1,8 +1,6 @@ export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP' -export const DEFAULT_SOLVER_TYPES: PlateSolverType[] = ['ASTROMETRY_NET_ONLINE', 'ASTAP'] - export interface PlateSolverPreference { type: PlateSolverType executablePath: string From dddc47bfe65bbf34c5f7f1995574ba70df970853 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 28 May 2024 21:48:36 -0300 Subject: [PATCH 27/45] [desktop]: Support Auto Focus --- .../src/app/alignment/alignment.component.ts | 2 +- .../app/autofocus/autofocus.component.html | 33 ++++++++++++++++--- .../src/app/autofocus/autofocus.component.ts | 31 +++++++++-------- .../src/app/sequencer/sequencer.component.ts | 2 +- .../device-list-menu.component.scss | 1 + desktop/src/shared/types/autofocus.type.ts | 6 ++-- desktop/src/shared/types/image.types.ts | 2 +- desktop/src/styles.scss | 2 -- 8 files changed, 54 insertions(+), 25 deletions(-) diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index eb4e91581..769d1430b 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -326,7 +326,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } openCameraImage() { - return this.browserWindow.openCameraImage(this.camera) + return this.browserWindow.openCameraImage(this.camera, 'ALIGNMENT') } private loadPreference() { diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 9cf121fdb..610a59a5b 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -10,7 +10,7 @@
- @@ -39,11 +39,36 @@
- - + + + +
+
+ + + + +
+
+ + +
+
+ + + +
\ No newline at end of file diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index ee6271829..61aec9bbe 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -30,13 +30,13 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { fittingMode: 'HYPERBOLIC', rSquaredThreshold: 0.7, backlashCompensation: { - mode: 'OVERSHOOT', + mode: 'NONE', backlashIn: 0, backlashOut: 0 }, initialOffsetSteps: 4, stepSize: 100, - totalNumberOfAttempts: 4 + totalNumberOfAttempts: 1 } constructor( @@ -142,8 +142,13 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { } } + start() { + this.browserWindow.openCameraImage(this.camera, 'AUTO_FOCUS') + return this.api.autoFocusStart(this.camera, this.focuser, this.request) + } + stop() { - return this.api.tppaStop(this.camera) + return this.api.autoFocusStop(this.camera) } openCameraImage() { @@ -153,6 +158,15 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { private loadPreference() { const preference = this.preference.autoFocusPreference.get() + this.request.fittingMode = preference.fittingMode ?? 'HYPERBOLIC' + this.request.initialOffsetSteps = preference.initialOffsetSteps ?? 4 + // this.request.rSquaredThreshold + this.request.stepSize = preference.stepSize ?? 100 + this.request.totalNumberOfAttempts = preference.totalNumberOfAttempts ?? 1 + this.request.backlashCompensation.mode = preference.backlashCompensation.mode ?? 'NONE' + this.request.backlashCompensation.backlashIn = preference.backlashCompensation.backlashIn ?? 0 + this.request.backlashCompensation.backlashOut = preference.backlashCompensation.backlashOut ?? 0 + if (this.camera.id) { const cameraPreference = this.preference.cameraPreference(this.camera).get() Object.assign(this.request.capture, this.preference.cameraStartCaptureForAutoFocus(this.camera).get(cameraPreference)) @@ -167,16 +181,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { this.preference.cameraStartCaptureForAutoFocus(this.camera).set(this.request.capture) const preference: AutoFocusPreference = { - fittingMode: 'TRENDLINES', - rSquaredThreshold: 0, - backlashCompensation: { - mode: 'OVERSHOOT', - backlashIn: 0, - backlashOut: 0 - }, - initialOffsetSteps: 0, - stepSize: 0, - totalNumberOfAttempts: 0 + ...this.request } this.preference.autoFocusPreference.set(preference) diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index 9833a323f..abd8516e1 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -470,7 +470,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { this.savePlan() - await this.browserWindow.openCameraImage(this.camera!) + await this.browserWindow.openCameraImage(this.camera!, 'SEQUENCER') this.api.sequencerStart(this.camera!, this.plan) } diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss index 1e445858e..abfb55957 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss @@ -1,5 +1,6 @@ :host { ::ng-deep .p-menuitem-link { + padding: 0.5rem 0.75rem; min-height: 43px; } } \ No newline at end of file diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts index 5a07e32e5..7bb415e5c 100644 --- a/desktop/src/shared/types/autofocus.type.ts +++ b/desktop/src/shared/types/autofocus.type.ts @@ -2,7 +2,7 @@ import { CameraStartCapture } from './camera.types' export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC' -export type BacklashCompensationMode = 'ABSOLUTE' | 'OVERSHOOT' +export type BacklashCompensationMode = 'NONE' | 'ABSOLUTE' | 'OVERSHOOT' export interface BacklashCompensation { mode: BacklashCompensationMode @@ -27,9 +27,9 @@ export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { rSquaredThreshold: 0.7, initialOffsetSteps: 4, stepSize: 100, - totalNumberOfAttempts: 4, + totalNumberOfAttempts: 1, backlashCompensation: { - mode: 'OVERSHOOT', + mode: 'NONE', backlashIn: 0, backlashOut: 0 } diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 37f87cc93..6fb7c9c74 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -8,7 +8,7 @@ export type ImageChannel = 'RED' | 'GREEN' | 'BLUE' | 'GRAY' export const SCNR_PROTECTION_METHODS = ['MAXIMUM_MASK', 'ADDITIVE_MASK', 'AVERAGE_NEUTRAL', 'MAXIMUM_NEUTRAL', 'MINIMUM_NEUTRAL'] as const export type SCNRProtectionMethod = (typeof SCNR_PROTECTION_METHODS)[number] -export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' | 'FLAT_WIZARD' +export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' | 'FLAT_WIZARD' | 'SEQUENCER' | 'ALIGNMENT' | 'AUTO_FOCUS' export type ImageFormat = 'FITS' | 'XISF' | 'PNG' | 'JPG' diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index ef9dc1cb9..b1a1660ff 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -182,8 +182,6 @@ i.mdi { } .p-menuitem-link { - padding: 0.5rem 0.75rem; - &.p-menuitem-checked { background-color: #C5E1A5; color: #212121 !important; From 9384a3c8f830deb478c125df90fe1253437d0ed8 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 30 May 2024 11:24:46 -0300 Subject: [PATCH 28/45] [api][desktop]: Support Auto Focus --- api/build.gradle.kts | 1 - .../api/alignment/polar/tppa/TPPAExecutor.kt | 6 +- .../api/autofocus/AutoFocusExecutor.kt | 11 +-- .../api/autofocus/AutoFocusRequest.kt | 2 + .../nebulosa/api/autofocus/AutoFocusTask.kt | 11 ++- .../beans/configurations/BeanConfiguration.kt | 7 -- .../nebulosa/api/image/ImageController.kt | 6 -- .../kotlin/nebulosa/api/image/ImageService.kt | 8 --- .../api/solver/PlateSolverController.kt | 10 ++- .../nebulosa/api/solver/PlateSolverOptions.kt | 18 +++++ .../nebulosa/api/solver/PlateSolverService.kt | 26 +------ .../stardetection/StarDetectionController.kt | 18 +++++ .../api/stardetection/StarDetectionOptions.kt | 23 ++++++ .../api/stardetection/StarDetectionService.kt | 14 ++++ .../api/stardetection/StarDetectorType.kt | 5 ++ .../src/app/alignment/alignment.component.ts | 6 +- .../app/autofocus/autofocus.component.html | 71 +++++++++++-------- .../src/app/autofocus/autofocus.component.ts | 11 +-- desktop/src/app/image/image.component.ts | 10 ++- .../src/app/settings/settings.component.html | 27 ++++++- .../src/app/settings/settings.component.ts | 37 +++++++--- desktop/src/shared/services/api.service.ts | 12 ++-- .../shared/services/browser-window.service.ts | 2 +- .../src/shared/services/preference.service.ts | 10 ++- desktop/src/shared/types/alignment.types.ts | 4 +- desktop/src/shared/types/autofocus.type.ts | 5 +- desktop/src/shared/types/settings.types.ts | 19 ++++- .../astap/star/detection/AstapStarDetector.kt | 2 +- 28 files changed, 241 insertions(+), 141 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 601a46919..942f4e926 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -27,7 +27,6 @@ dependencies { implementation(project(":nebulosa-sbd")) implementation(project(":nebulosa-simbad")) implementation(project(":nebulosa-stellarium-protocol")) - implementation(project(":nebulosa-watney")) implementation(project(":nebulosa-wcs")) implementation(project(":nebulosa-xisf")) implementation(libs.rx) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt index d4b900519..c7808faaa 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt @@ -4,10 +4,10 @@ import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService -import nebulosa.api.solver.PlateSolverService import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent import nebulosa.indi.device.mount.Mount +import okhttp3.OkHttpClient import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap @Subscriber class TPPAExecutor( private val messageService: MessageService, - private val plateSolverService: PlateSolverService, + private val httpClient: OkHttpClient, ) : Consumer { private val jobs = ConcurrentHashMap.newKeySet(1) @@ -38,7 +38,7 @@ class TPPAExecutor( check(jobs.none { it.task.camera === camera }) { "${camera.name} TPPA Job is already in progress" } check(jobs.none { it.task.mount === mount }) { "${camera.name} TPPA Job is already in progress" } - val solver = plateSolverService.solverFor(request.plateSolver) + val solver = request.plateSolver.get(httpClient) val task = TPPATask(camera, solver, request, mount) task.subscribe(this) diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt index 63d842b1d..bfdd6698d 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt @@ -2,14 +2,12 @@ package nebulosa.api.autofocus import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.image.ImageBucket import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserEvent -import nebulosa.watney.star.detection.WatneyStarDetector import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component @@ -19,7 +17,6 @@ import java.util.concurrent.ConcurrentHashMap @Subscriber class AutoFocusExecutor( private val messageService: MessageService, - private val imageBucket: ImageBucket, ) : Consumer { private val jobs = ConcurrentHashMap.newKeySet(2) @@ -45,7 +42,8 @@ class AutoFocusExecutor( check(jobs.none { it.task.camera === camera }) { "${camera.name} Auto Focus is already in progress" } check(jobs.none { it.task.focuser === focuser }) { "${camera.name} Auto Focus is already in progress" } - val task = AutoFocusTask(camera, focuser, request, STAR_DETECTOR, imageBucket) + val starDetector = request.starDetector.get() + val task = AutoFocusTask(camera, focuser, request, starDetector) task.subscribe(this) with(AutoFocusJob(task)) { @@ -62,9 +60,4 @@ class AutoFocusExecutor( fun status(camera: Camera): AutoFocusEvent? { return jobs.find { it.task.camera === camera }?.task?.get() as? AutoFocusEvent } - - companion object { - - @JvmStatic private val STAR_DETECTOR = WatneyStarDetector(computeHFD = true, minHFD = 0.1f) - } } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt index 3899a409f..815209a3b 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt @@ -2,6 +2,7 @@ package nebulosa.api.autofocus import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.api.focusers.BacklashCompensation +import nebulosa.api.stardetection.StarDetectionOptions data class AutoFocusRequest( @JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC, @@ -11,4 +12,5 @@ data class AutoFocusRequest( @JvmField val initialOffsetSteps: Int = 4, @JvmField val stepSize: Int = 50, @JvmField val totalNumberOfAttempts: Int = 1, + @JvmField val starDetector: StarDetectionOptions = StarDetectionOptions.EMPTY, ) diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 6c79ad5b1..688e29ba8 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -5,7 +5,6 @@ import nebulosa.api.cameras.* import nebulosa.api.focusers.BacklashCompensationFocuserMoveTask import nebulosa.api.focusers.BacklashCompensationMode import nebulosa.api.focusers.FocuserEventAware -import nebulosa.api.image.ImageBucket import nebulosa.api.messages.MessageEvent import nebulosa.api.tasks.AbstractTask import nebulosa.common.concurrency.cancel.CancellationToken @@ -14,7 +13,6 @@ import nebulosa.curve.fitting.CurvePoint.Companion.midPoint import nebulosa.curve.fitting.HyperbolicFitting import nebulosa.curve.fitting.QuadraticFitting import nebulosa.curve.fitting.TrendLineFitting -import nebulosa.image.Image import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent import nebulosa.indi.device.camera.FrameType @@ -24,6 +22,7 @@ import nebulosa.log.loggerFor import nebulosa.star.detection.ImageStar import nebulosa.star.detection.StarDetector import java.nio.file.Files +import java.nio.file.Path import java.time.Duration import kotlin.math.max import kotlin.math.roundToInt @@ -33,8 +32,7 @@ data class AutoFocusTask( @JvmField val camera: Camera, @JvmField val focuser: Focuser, @JvmField val request: AutoFocusRequest, - @JvmField val starDetection: StarDetector, - @JvmField val imageBucket: ImageBucket, + @JvmField val starDetection: StarDetector, ) : AbstractTask(), Consumer, CameraEventAware, FocuserEventAware { data class MeasuredStars( @@ -97,7 +95,7 @@ data class AutoFocusTask( var numberOfAttempts = 0 val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 - camera.snoop(listOf(focuser)) + // camera.snoop(listOf(focuser)) while (!exited && !cancellationToken.isCancelled) { numberOfAttempts++ @@ -229,8 +227,7 @@ data class AutoFocusTask( override fun accept(event: CameraCaptureEvent) { if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { sendEvent(AutoFocusState.COMPUTING, capture = event) - val image = imageBucket.open(event.savePath!!) - val detectedStars = starDetection.detect(image) + val detectedStars = starDetection.detect(event.savePath!!) LOG.info("detected ${detectedStars.size} stars") val measure = detectedStars.measureDetectedStars() LOG.info("HFD measurement. mean={}, stdDev={}", measure.averageHFD, measure.hfdStandardDeviation) diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt index fe3a7d905..b03a02883 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -18,13 +18,10 @@ import nebulosa.guiding.Guider import nebulosa.guiding.phd2.PHD2Guider import nebulosa.hips2fits.Hips2FitsService import nebulosa.horizons.HorizonsService -import nebulosa.image.Image import nebulosa.log.loggerFor import nebulosa.phd2.client.PHD2Client import nebulosa.sbd.SmallBodyDatabaseService import nebulosa.simbad.SimbadService -import nebulosa.star.detection.StarDetector -import nebulosa.watney.star.detection.WatneyStarDetector import okhttp3.Cache import okhttp3.ConnectionPool import okhttp3.OkHttpClient @@ -159,10 +156,6 @@ class BeanConfiguration { @Bean fun phd2Guider(phd2Client: PHD2Client): Guider = PHD2Guider(phd2Client) - @Bean - @Primary - fun watneyStarDetector(): StarDetector = WatneyStarDetector(computeHFD = true) - @Bean @Primary fun boxStore(dataPath: Path) = MyObjectBox.builder() diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt index 63d9a58a6..985da8ff7 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt @@ -5,7 +5,6 @@ import jakarta.validation.Valid import nebulosa.api.atlas.Location import nebulosa.api.beans.converters.location.LocationParam import nebulosa.indi.device.camera.Camera -import nebulosa.star.detection.ImageStar import org.hibernate.validator.constraints.Range import org.springframework.http.HttpHeaders import org.springframework.web.bind.annotation.* @@ -54,11 +53,6 @@ class ImageController( return imageService.coordinateInterpolation(path) } - @PutMapping("detect-stars") - fun detectStars(@RequestParam path: Path): List { - return imageService.detectStars(path) - } - @GetMapping("histogram") fun histogram( @RequestParam path: Path, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index 4b9303a4b..ff452a3e2 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -24,8 +24,6 @@ import nebulosa.simbad.SimbadSearch import nebulosa.simbad.SimbadService import nebulosa.skycatalog.ClassificationType import nebulosa.skycatalog.SkyObjectType -import nebulosa.star.detection.ImageStar -import nebulosa.star.detection.StarDetector import nebulosa.time.TimeYMDHMS import nebulosa.time.UTC import nebulosa.wcs.WCS @@ -55,7 +53,6 @@ class ImageService( private val imageBucket: ImageBucket, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, private val connectionService: ConnectionService, - private val starDetector: StarDetector, ) { private enum class ImageOperation { @@ -333,11 +330,6 @@ class ImageService( return CoordinateInterpolation(ma, md, 0, 0, width, height, delta, image.header.observationDate) } - fun detectStars(path: Path): List { - val (image) = imageBucket[path] ?: return emptyList() - return starDetector.detect(image) - } - fun histogram(path: Path, bitLength: Int = 16): IntArray { val (image) = imageBucket[path] ?: return IntArray(0) return image.compute(Histogram(bitLength = bitLength)) diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt index 80c9e3d98..f4936f0fa 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt @@ -1,11 +1,9 @@ package nebulosa.api.solver +import jakarta.validation.Valid import nebulosa.api.beans.converters.angle.AngleParam import nebulosa.math.Angle -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import java.nio.file.Path @RestController @@ -17,10 +15,10 @@ class PlateSolverController( @PutMapping fun solveImage( @RequestParam path: Path, - options: PlateSolverOptions, + @RequestBody @Valid solver: PlateSolverOptions, @RequestParam(required = false, defaultValue = "true") blind: Boolean, @AngleParam(required = false, isHours = true, defaultValue = "0.0") centerRA: Angle, @AngleParam(required = false, defaultValue = "0.0") centerDEC: Angle, @AngleParam(required = false, defaultValue = "4.0") radius: Angle, - ) = plateSolverService.solveImage(options, path, centerRA, centerDEC, if (blind) 0.0 else radius) + ) = plateSolverService.solveImage(solver, path, centerRA, centerDEC, if (blind) 0.0 else radius) } diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt index 0e3c8be85..1612c43cc 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt @@ -1,5 +1,10 @@ package nebulosa.api.solver +import nebulosa.astap.plate.solving.AstapPlateSolver +import nebulosa.astrometrynet.nova.NovaAstrometryNetService +import nebulosa.astrometrynet.plate.solving.LocalAstrometryNetPlateSolver +import nebulosa.astrometrynet.plate.solving.NovaAstrometryNetPlateSolver +import okhttp3.OkHttpClient import org.hibernate.validator.constraints.time.DurationMax import org.hibernate.validator.constraints.time.DurationMin import org.springframework.boot.convert.DurationUnit @@ -17,8 +22,21 @@ data class PlateSolverOptions( @JvmField val timeout: Duration = Duration.ZERO, ) { + fun get(httpClient: OkHttpClient? = null) = with(this) { + when (type) { + PlateSolverType.ASTAP -> AstapPlateSolver(executablePath!!) + PlateSolverType.ASTROMETRY_NET -> LocalAstrometryNetPlateSolver(executablePath!!) + PlateSolverType.ASTROMETRY_NET_ONLINE -> { + val key = "$apiUrl@$apiKey" + val service = NOVA_ASTROMETRY_NET_CACHE.getOrPut(key) { NovaAstrometryNetService(apiUrl, httpClient) } + NovaAstrometryNetPlateSolver(service, apiKey) + } + } + } + companion object { @JvmStatic val EMPTY = PlateSolverOptions() + @JvmStatic private val NOVA_ASTROMETRY_NET_CACHE = HashMap() } } diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt index 89e0ba376..61f0e0790 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt @@ -2,12 +2,7 @@ package nebulosa.api.solver import nebulosa.api.image.ImageBucket import nebulosa.api.image.ImageSolved -import nebulosa.astap.plate.solving.AstapPlateSolver -import nebulosa.astrometrynet.nova.NovaAstrometryNetService -import nebulosa.astrometrynet.plate.solving.LocalAstrometryNetPlateSolver -import nebulosa.astrometrynet.plate.solving.NovaAstrometryNetPlateSolver import nebulosa.math.Angle -import nebulosa.plate.solving.PlateSolver import okhttp3.OkHttpClient import org.springframework.stereotype.Service import java.nio.file.Path @@ -27,29 +22,10 @@ class PlateSolverService( return ImageSolved(calibration) } - fun solverFor(options: PlateSolverOptions): PlateSolver { - return with(options) { - when (type) { - PlateSolverType.ASTAP -> AstapPlateSolver(executablePath!!) - PlateSolverType.ASTROMETRY_NET -> LocalAstrometryNetPlateSolver(executablePath!!) - PlateSolverType.ASTROMETRY_NET_ONLINE -> { - val key = "$apiUrl@$apiKey" - val service = NOVA_ASTROMETRY_NET_CACHE.getOrPut(key) { NovaAstrometryNetService(apiUrl, httpClient) } - NovaAstrometryNetPlateSolver(service, apiKey) - } - } - } - } - @Synchronized fun solve( options: PlateSolverOptions, path: Path, centerRA: Angle = 0.0, centerDEC: Angle = 0.0, radius: Angle = 0.0, - ) = solverFor(options) + ) = options.get(httpClient) .solve(path, null, centerRA, centerDEC, radius, 1, options.timeout.takeIf { it.toSeconds() > 0 }) - - companion object { - - @JvmStatic private val NOVA_ASTROMETRY_NET_CACHE = HashMap() - } } diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt new file mode 100644 index 000000000..810c13811 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt @@ -0,0 +1,18 @@ +package nebulosa.api.stardetection + +import jakarta.validation.Valid +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.* +import java.nio.file.Path + +@Validated +@RestController +@RequestMapping("star-detection") +class StarDetectionController(private val starDetectionService: StarDetectionService) { + + @PutMapping + fun detectStars( + @RequestParam path: Path, + @RequestBody @Valid body: StarDetectionOptions + ) = starDetectionService.detectStars(path, body) +} diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt new file mode 100644 index 000000000..5f8aa4940 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt @@ -0,0 +1,23 @@ +package nebulosa.api.stardetection + +import nebulosa.astap.star.detection.AstapStarDetector +import nebulosa.star.detection.StarDetector +import java.nio.file.Path +import java.time.Duration +import java.util.function.Supplier + +data class StarDetectionOptions( + @JvmField val type: StarDetectorType = StarDetectorType.ASTAP, + @JvmField val executablePath: Path? = null, + @JvmField val timeout: Duration = Duration.ZERO, +) : Supplier> { + + override fun get() = when (type) { + StarDetectorType.ASTAP -> AstapStarDetector(executablePath!!) + } + + companion object { + + @JvmStatic val EMPTY = StarDetectionOptions() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt new file mode 100644 index 000000000..243239456 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt @@ -0,0 +1,14 @@ +package nebulosa.api.stardetection + +import nebulosa.star.detection.ImageStar +import org.springframework.stereotype.Service +import java.nio.file.Path + +@Service +class StarDetectionService { + + fun detectStars(path: Path, options: StarDetectionOptions): List { + val starDetector = options.get() + return starDetector.detect(path) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt new file mode 100644 index 000000000..31ae2f97c --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt @@ -0,0 +1,5 @@ +package nebulosa.api.stardetection + +enum class StarDetectorType { + ASTAP +} diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index 769d1430b..25bbae873 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -9,7 +9,7 @@ import { Angle } from '../../shared/types/atlas.types' import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureTimeUnit, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { EMPTY_GUIDE_OUTPUT, GuideDirection, GuideOutput } from '../../shared/types/guider.types' import { EMPTY_MOUNT, Mount } from '../../shared/types/mount.types' -import { EMPTY_PLATE_SOLVER_PREFERENCE } from '../../shared/types/settings.types' +import { EMPTY_PLATE_SOLVER_OPTIONS } from '../../shared/types/settings.types' import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' import { CameraComponent } from '../camera/camera.component' @@ -39,7 +39,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { readonly tppaRequest: TPPAStart = { capture: structuredClone(EMPTY_CAMERA_START_CAPTURE), - plateSolver: structuredClone(EMPTY_PLATE_SOLVER_PREFERENCE), + plateSolver: structuredClone(EMPTY_PLATE_SOLVER_OPTIONS), startFromCurrentPosition: true, stepDirection: 'EAST', compensateRefraction: true, @@ -279,7 +279,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } plateSolverChanged() { - this.tppaRequest.plateSolver = this.preference.plateSolverPreference(this.tppaRequest.plateSolver.type).get() + this.tppaRequest.plateSolver = this.preference.plateSolverOptions(this.tppaRequest.plateSolver.type).get() this.savePreference() } diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 610a59a5b..41eb277f7 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -15,51 +15,64 @@
-
- - - - -
-
+
- - + +
-
+
-
+
- - +
-
+
- - + +
-
- - - - +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index 61aec9bbe..71a861a67 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -6,6 +6,7 @@ import { PreferenceService } from '../../shared/services/preference.service' import { AutoFocusPreference, AutoFocusRequest } from '../../shared/types/autofocus.type' import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types' +import { EMPTY_STAR_DETECTION_OPTIONS } from '../../shared/types/settings.types' import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' import { CameraComponent } from '../camera/camera.component' @@ -36,7 +37,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { }, initialOffsetSteps: 4, stepSize: 100, - totalNumberOfAttempts: 1 + totalNumberOfAttempts: 1, + starDetector: structuredClone(EMPTY_STAR_DETECTION_OPTIONS), } constructor( @@ -142,8 +144,9 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { } } - start() { - this.browserWindow.openCameraImage(this.camera, 'AUTO_FOCUS') + async start() { + await this.openCameraImage() + this.request.starDetector = this.preference.starDetectionOptions('ASTAP').get() return this.api.autoFocusStart(this.camera, this.focuser, this.request) } @@ -152,7 +155,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { } openCameraImage() { - return this.browserWindow.openCameraImage(this.camera) + return this.browserWindow.openCameraImage(this.camera, 'ALIGNMENT') } private loadPreference() { diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 956ba9d9e..1f06971c5 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -333,7 +333,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { toggleable: false, toggled: false, command: async () => { - this.detectedStars.stars = await this.api.detectStars(this.imageData.path!) + const options = this.preference.starDetectionOptions('ASTAP').get() + this.detectedStars.stars = await this.api.detectStars(this.imageData.path!, options) this.detectedStars.visible = this.detectedStars.stars.length > 0 this.detectStarsMenuItem.toggleable = this.detectedStars.visible this.detectStarsMenuItem.toggled = this.detectedStars.visible @@ -947,7 +948,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.solver.solving = true try { - const solver = this.preference.plateSolverPreference(this.solver.type).get() + const solver = this.preference.plateSolverOptions(this.solver.type).get() const solved = await this.api.solveImage(solver, this.imageData.path!, this.solver.blind, this.solver.centerRA, this.solver.centerDEC, this.solver.radius) @@ -957,7 +958,10 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.updateImageSolved(this.imageInfo?.solved) } finally { this.solver.solving = false - this.retrieveCoordinateInterpolation() + + if (this.solver.solved.solved) { + this.retrieveCoordinateInterpolation() + } } } diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index e2084571b..0931108e9 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -19,7 +19,7 @@
- @@ -42,7 +42,7 @@ (ngModelChange)="solvers.get(solverType)!.executablePath = $event; save()" /> - +
+ +
+ + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 7c288e160..2d8931004 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -6,7 +6,7 @@ import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { EMPTY_LOCATION, Location } from '../../shared/types/atlas.types' -import { PlateSolverPreference, PlateSolverType } from '../../shared/types/settings.types' +import { PlateSolverOptions, PlateSolverType, StarDetectionOptions, StarDetectorType } from '../../shared/types/settings.types' import { AppComponent } from '../app.component' @Component({ @@ -19,9 +19,11 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { readonly locations: Location[] location: Location - readonly solverTypes: PlateSolverType[] = ['ASTAP', 'ASTROMETRY_NET_ONLINE'] - solverType = this.solverTypes[0] - readonly solvers = new Map() + solverType: PlateSolverType = 'ASTAP' + readonly solvers = new Map() + + starDetectorType: StarDetectorType = 'ASTAP' + readonly starDetectors = new Map() constructor( app: AppComponent, @@ -35,9 +37,10 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.locations = preference.locations.get() this.location = preference.selectedLocation.get(this.locations[0]) - for (const type of this.solverTypes) { - this.solvers.set(type, preference.plateSolverPreference(type).get()) - } + this.solvers.set('ASTAP', preference.plateSolverOptions('ASTAP').get()) + this.solvers.set('ASTROMETRY_NET_ONLINE', preference.plateSolverOptions('ASTROMETRY_NET_ONLINE').get()) + + this.starDetectors.set('ASTAP', preference.starDetectionOptions('ASTAP').get()) } async ngAfterViewInit() { } @@ -99,19 +102,31 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.electron.send('LOCATION.CHANGED', this.location) } - async chooseExecutablePath() { + async chooseExecutablePathForPlateSolver() { const options = this.solvers.get(this.solverType)! + this.chooseExecutablePath(options) + } + + async chooseExecutablePathForStarDetection() { + const options = this.solvers.get(this.starDetectorType)! + this.chooseExecutablePath(options) + } + + private async chooseExecutablePath(options: { executablePath: string }) { const executablePath = await this.electron.openFile({ defaultPath: path.dirname(options.executablePath) }) if (executablePath) { options.executablePath = executablePath this.save() } + + return executablePath } save() { - for (const type of this.solverTypes) { - this.preference.plateSolverPreference(type).set(this.solvers.get(type)!) - } + this.preference.plateSolverOptions('ASTAP').set(this.solvers.get('ASTAP')!) + this.preference.plateSolverOptions('ASTROMETRY_NET_ONLINE').set(this.solvers.get('ASTROMETRY_NET_ONLINE')!) + + this.preference.starDetectionOptions('ASTAP').set(this.starDetectors.get('ASTAP')!) } } \ No newline at end of file diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 223d97d0b..56db681fa 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -15,7 +15,7 @@ import { CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAn import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlType, SlewRate, TrackMode } from '../types/mount.types' import { Rotator } from '../types/rotator.types' import { SequencePlan } from '../types/sequencer.types' -import { PlateSolverPreference } from '../types/settings.types' +import { PlateSolverOptions, StarDetectionOptions } from '../types/settings.types' import { FilterWheel } from '../types/wheel.types' import { HttpService } from './http.service' @@ -531,9 +531,9 @@ export class ApiService { return this.http.get(`image/coordinate-interpolation?${query}`) } - detectStars(path: string) { + detectStars(path: string, starDetector: StarDetectionOptions) { const query = this.http.query({ path }) - return this.http.put(`image/detect-stars?${query}`) + return this.http.put(`star-detection?${query}`, starDetector) } imageHistogram(path: string, bitLength: number = 16) { @@ -640,11 +640,11 @@ export class ApiService { // SOLVER solveImage( - solver: PlateSolverPreference, path: string, blind: boolean, + solver: PlateSolverOptions, path: string, blind: boolean, centerRA: Angle, centerDEC: Angle, radius: Angle, ) { - const query = this.http.query({ ...solver, path, blind, centerRA, centerDEC, radius }) - return this.http.put(`plate-solver?${query}`) + const query = this.http.query({ path, blind, centerRA, centerDEC, radius }) + return this.http.put(`plate-solver?${query}`, solver) } // AUTO FOCUS diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index ac77d57fd..041cdc134 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -105,7 +105,7 @@ export class BrowserWindowService { } openAutoFocus(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'auto-focus', width: 385, height: 370 }) + Object.assign(options, { icon: 'auto-focus', width: 410, height: 370 }) this.openWindow({ ...options, id: 'auto-focus', path: 'auto-focus', data: undefined }) } diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index 46eec8892..92808fee0 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -10,7 +10,7 @@ import { Focuser, FocuserPreference } from '../types/focuser.types' import { ConnectionDetails, Equipment, HomePreference } from '../types/home.types' import { EMPTY_IMAGE_PREFERENCE, FOV, ImagePreference } from '../types/image.types' import { Rotator, RotatorPreference } from '../types/rotator.types' -import { EMPTY_PLATE_SOLVER_PREFERENCE, PlateSolverPreference, PlateSolverType } from '../types/settings.types' +import { EMPTY_PLATE_SOLVER_OPTIONS, EMPTY_STAR_DETECTION_OPTIONS, PlateSolverOptions, PlateSolverType, StarDetectionOptions, StarDetectorType } from '../types/settings.types' import { FilterWheel, WheelPreference } from '../types/wheel.types' import { LocalStorageService } from './local-storage.service' @@ -68,8 +68,12 @@ export class PreferenceService { return new PreferenceData(this.storage, `camera.${camera.name}.autoFocus`, () => this.cameraPreference(camera).get()) } - plateSolverPreference(type: PlateSolverType) { - return new PreferenceData(this.storage, `plateSolver.${type}`, () => { ...EMPTY_PLATE_SOLVER_PREFERENCE, type }) + plateSolverOptions(type: PlateSolverType) { + return new PreferenceData(this.storage, `plateSolver.${type}`, () => { ...EMPTY_PLATE_SOLVER_OPTIONS, type }) + } + + starDetectionOptions(type: StarDetectorType) { + return new PreferenceData(this.storage, `starDetection.${type}`, () => { ...EMPTY_STAR_DETECTION_OPTIONS, type }) } equipmentForDevice(device: Device) { diff --git a/desktop/src/shared/types/alignment.types.ts b/desktop/src/shared/types/alignment.types.ts index 3b48be378..19b4ddfd0 100644 --- a/desktop/src/shared/types/alignment.types.ts +++ b/desktop/src/shared/types/alignment.types.ts @@ -1,7 +1,7 @@ import { Angle } from './atlas.types' import { Camera, CameraCaptureEvent, CameraStartCapture } from './camera.types' import { GuideDirection } from './guider.types' -import { PlateSolverPreference, PlateSolverType } from './settings.types' +import { PlateSolverOptions, PlateSolverType } from './settings.types' export type Hemisphere = 'NORTHERN' | 'SOUTHERN' @@ -52,7 +52,7 @@ export interface DARVEvent extends MessageEvent { export interface TPPAStart { capture: CameraStartCapture - plateSolver: PlateSolverPreference + plateSolver: PlateSolverOptions startFromCurrentPosition: boolean compensateRefraction: boolean stopTrackingWhenDone: boolean diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts index 7bb415e5c..aa24a424b 100644 --- a/desktop/src/shared/types/autofocus.type.ts +++ b/desktop/src/shared/types/autofocus.type.ts @@ -1,4 +1,5 @@ import { CameraStartCapture } from './camera.types' +import { EMPTY_STAR_DETECTION_OPTIONS, StarDetectionOptions } from './settings.types' export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC' @@ -18,6 +19,7 @@ export interface AutoFocusRequest { initialOffsetSteps: number stepSize: number totalNumberOfAttempts: number + starDetector: StarDetectionOptions } export interface AutoFocusPreference extends Omit { } @@ -32,5 +34,6 @@ export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { mode: 'NONE', backlashIn: 0, backlashOut: 0 - } + }, + starDetector: EMPTY_STAR_DETECTION_OPTIONS, } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index 7b26a7f55..b427182f6 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -1,7 +1,6 @@ - export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP' -export interface PlateSolverPreference { +export interface PlateSolverOptions { type: PlateSolverType executablePath: string downsampleFactor: number @@ -10,7 +9,7 @@ export interface PlateSolverPreference { timeout: number } -export const EMPTY_PLATE_SOLVER_PREFERENCE: PlateSolverPreference = { +export const EMPTY_PLATE_SOLVER_OPTIONS: PlateSolverOptions = { type: 'ASTAP', executablePath: '', downsampleFactor: 0, @@ -18,3 +17,17 @@ export const EMPTY_PLATE_SOLVER_PREFERENCE: PlateSolverPreference = { apiKey: '', timeout: 600, } + +export type StarDetectorType = 'ASTAP' + +export interface StarDetectionOptions { + type: StarDetectorType + executablePath: string + timeout: number +} + +export const EMPTY_STAR_DETECTION_OPTIONS: StarDetectionOptions = { + type: 'ASTAP', + executablePath: '', + timeout: 600, +} diff --git a/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt b/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt index 87306d63d..6c25aa07b 100644 --- a/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt +++ b/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt @@ -21,7 +21,7 @@ class AstapStarDetector(path: Path) : StarDetector { val arguments = mutableMapOf() arguments["-f"] = input - arguments["-z"] = 2 + arguments["-z"] = 0 arguments["-extract"] = 0 val process = executor.execute(arguments, workingDir = input.parent) From 32e5c0f337ddcd30421b50a60792b7ca50a6bdba Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 30 May 2024 17:38:18 -0300 Subject: [PATCH 29/45] [api][desktop]: Support Auto Focus --- .../api/autofocus/AutoFocusRequest.kt | 2 +- .../nebulosa/api/autofocus/AutoFocusTask.kt | 128 ++++++++---------- .../api/focusers/AbstractFocuserMoveTask.kt | 8 +- .../BacklashCompensationFocuserMoveTask.kt | 70 +++++----- .../api/focusers/BacklashCompensationMode.kt | 1 + api/src/test/kotlin/APITest.kt | 13 +- .../src/app/autofocus/autofocus.component.ts | 15 +- desktop/src/shared/types/autofocus.type.ts | 2 +- .../alpaca/indi/device/ASCOMDevice.kt | 4 +- .../indi/device/focusers/ASCOMFocuser.kt | 27 ++-- .../nebulosa/curve/fitting/CurvePoint.kt | 2 +- .../curve/fitting/TrendLineFitting.kt | 7 +- .../src/test/kotlin/AutoFocusTest.kt | 64 +++++++++ 13 files changed, 201 insertions(+), 142 deletions(-) create mode 100644 nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt index 815209a3b..5f1c5edd1 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt @@ -7,7 +7,7 @@ import nebulosa.api.stardetection.StarDetectionOptions data class AutoFocusRequest( @JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC, @JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, - @JvmField val rSquaredThreshold: Double = 0.7, + @JvmField val rSquaredThreshold: Double = 0.5, @JvmField val backlashCompensation: BacklashCompensation = BacklashCompensation.EMPTY, @JvmField val initialOffsetSteps: Int = 4, @JvmField val stepSize: Int = 50, diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 688e29ba8..ef0348e28 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -26,7 +26,6 @@ import java.nio.file.Path import java.time.Duration import kotlin.math.max import kotlin.math.roundToInt -import kotlin.math.sqrt data class AutoFocusTask( @JvmField val camera: Camera, @@ -35,17 +34,6 @@ data class AutoFocusTask( @JvmField val starDetection: StarDetector, ) : AbstractTask(), Consumer, CameraEventAware, FocuserEventAware { - data class MeasuredStars( - @JvmField val averageHFD: Double = 0.0, - @JvmField val hfdStandardDeviation: Double = 0.0, - ) { - - companion object { - - @JvmStatic val ZERO = MeasuredStars() - } - } - @JvmField val cameraRequest = request.capture.copy( exposureAmount = 0, exposureDelay = Duration.ZERO, savePath = CAPTURE_SAVE_PATH, @@ -53,9 +41,8 @@ data class AutoFocusTask( frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF ) - private val focusPoints = ArrayList() - private val measurements = ArrayList(request.capture.exposureAmount) + private val measurements = DoubleArray(request.capture.exposureAmount) private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = max(1, request.capture.exposureAmount)) private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation) @@ -63,6 +50,7 @@ data class AutoFocusTask( @Volatile private var parabolicCurve: Lazy? = null @Volatile private var hyperbolicCurve: Lazy? = null + @Volatile private var measurementPos = 0 @Volatile private var focusPoint = CurvePoint.ZERO init { @@ -85,11 +73,10 @@ data class AutoFocusTask( val initialFocusPosition = focuser.position // Get initial position information, as average of multiple exposures, if configured this way. - val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken).averageHFD else Double.NaN - val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 && - request.backlashCompensation.backlashOut == 0 + val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken) else 0.0 + val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 - LOG.info("Auto Focus started. initialHFD={}, reverse={}, camera={}, focuser={}", initialHFD, reverse, camera, focuser) + LOG.info("Auto Focus started. initialHFD={}, reverse={}, request={}, camera={}, focuser={}", initialHFD, reverse, request, camera, focuser) var exited = false var numberOfAttempts = 0 @@ -107,8 +94,10 @@ data class AutoFocusTask( obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken) - var leftCount = trendLineCurve!!.left.points.size - var rightCount = trendLineCurve!!.right.points.size + var leftCount = trendLineCurve?.left?.points?.size ?: 0 + var rightCount = trendLineCurve?.right?.points?.size ?: 0 + + LOG.info("trend line computed. left=$leftCount, right=$rightCount") // When data points are not sufficient analyze and take more. do { @@ -152,6 +141,8 @@ data class AutoFocusTask( leftCount = trendLineCurve!!.left.points.size rightCount = trendLineCurve!!.right.points.size + LOG.info("trend line computed. left=$leftCount, right=$rightCount") + if (maximumFocusPoints < focusPoints.size) { // Break out when the maximum limit of focus points is reached LOG.error("failed to complete. Maximum number of focus points exceeded ($maximumFocusPoints).") @@ -173,27 +164,32 @@ data class AutoFocusTask( if (!goodAutoFocus) { if (numberOfAttempts < request.totalNumberOfAttempts) { moveFocuser(initialFocusPosition, cancellationToken, false) - LOG.warn("potentially bad auto-focus. reattempting") + LOG.warn("potentially bad auto-focus. Reattempting") reset() continue } else { LOG.warn("potentially bad auto-focus. Restoring original focus position") + exited = true } } else { LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y) + moveFocuser(finalFocusPoint.x.roundToInt(), cancellationToken, false) + break } - - exited = true } - if (exited) { - sendEvent(AutoFocusState.FAILED) + if (exited || cancellationToken.isCancelled) { LOG.warn("Auto Focus did not complete successfully, so restoring the focuser position to $initialFocusPosition") - moveFocuser(initialFocusPosition, CancellationToken.NONE, false) + sendEvent(if (exited) AutoFocusState.FAILED else AutoFocusState.FINISHED) + + if (exited) { + moveFocuser(initialFocusPosition, CancellationToken.NONE, false) + } + } else { + sendEvent(AutoFocusState.FINISHED) } reset() - sendEvent(AutoFocusState.FINISHED) LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser) } @@ -212,16 +208,8 @@ data class AutoFocusTask( } } - private fun evaluateAllMeasurements(): MeasuredStars { - var sumHFD = 0.0 - var sumVariances = 0.0 - - for ((averageHFD, hfdStandardDeviation) in measurements) { - sumHFD += averageHFD - sumVariances += hfdStandardDeviation * hfdStandardDeviation - } - - return MeasuredStars(sumHFD / measurements.size, sqrt(sumVariances / measurements.size)) + private fun evaluateAllMeasurements(): Double { + return if (measurements.isEmpty()) 0.0 else measurements.average() } override fun accept(event: CameraCaptureEvent) { @@ -230,22 +218,22 @@ data class AutoFocusTask( val detectedStars = starDetection.detect(event.savePath!!) LOG.info("detected ${detectedStars.size} stars") val measure = detectedStars.measureDetectedStars() - LOG.info("HFD measurement. mean={}, stdDev={}", measure.averageHFD, measure.hfdStandardDeviation) - measurements.add(measure) + LOG.info("HFD measurement. mean={}", measure) + measurements[measurementPos++] = measure onNext(event) } else { sendEvent(AutoFocusState.EXPOSURING, capture = event) } } - private fun takeExposure(cancellationToken: CancellationToken): MeasuredStars { + private fun takeExposure(cancellationToken: CancellationToken): Double { return if (!cancellationToken.isCancelled) { - measurements.clear() + measurementPos = 0 sendEvent(AutoFocusState.EXPOSURING) cameraCaptureTask.execute(cancellationToken) evaluateAllMeasurements() } else { - MeasuredStars.ZERO + 0.0 } } @@ -266,19 +254,20 @@ data class AutoFocusTask( while (!cancellationToken.isCancelled && remainingSteps > 0) { val currentFocusPosition = focusPosition - if (remainingSteps > 1) { + val measurement = takeExposure(cancellationToken) + + LOG.info("HFD measured after exposures. mean={}", measurement) + + if (remainingSteps-- > 1) { focusPosition = moveFocuser(direction * -stepSize, cancellationToken, true) } - val measurement = takeExposure(cancellationToken) - // If star measurement is 0, we didn't detect any stars or shapes, // and want this point to be ignored by the fitting as much as possible. - if (measurement.averageHFD == 0.0) { + if (measurement == 0.0) { LOG.warn("No stars detected in step") - sendEvent(AutoFocusState.FAILED) } else { - focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD) + focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement) focusPoints.add(focusPoint) focusPoints.sortBy { it.x } @@ -288,8 +277,6 @@ data class AutoFocusTask( sendEvent(AutoFocusState.FOCUS_POINT_ADDED) } - - remainingSteps-- } } @@ -300,8 +287,7 @@ data class AutoFocusTask( if (size >= 3) { if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { parabolicCurve = lazy { QuadraticFitting.calculate(this) } - } - if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) { + } else if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) { hyperbolicCurve = lazy { HyperbolicFitting.calculate(this) } } } @@ -311,11 +297,13 @@ data class AutoFocusTask( private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double, cancellationToken: CancellationToken): Boolean { val threshold = request.rSquaredThreshold - fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true - fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true - fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true + LOG.info("validating calculated focus position. threshold={}", threshold) if (threshold > 0.0) { + fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true + fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true + fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true + val isBad = when (request.fittingMode) { AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() AutoFocusFittingMode.PARABOLIC -> isParabolicBad() @@ -339,7 +327,7 @@ data class AutoFocusTask( } moveFocuser(focusPoint.x.roundToInt(), cancellationToken, false) - val hfd = takeExposure(cancellationToken).averageHFD + val hfd = takeExposure(cancellationToken) if (threshold <= 0) { if (initialHFD != 0.0 && hfd > initialHFD * 1.15) { @@ -384,25 +372,17 @@ data class AutoFocusTask( @JvmStatic private val LOG = loggerFor() @JvmStatic - private fun List.measureDetectedStars(): MeasuredStars { - if (isEmpty()) return MeasuredStars.ZERO - - val mean = sumOf { it.hfd } / size - - var stdDev = 0.0 - - if (size > 1) { - for (star in this) { - stdDev += (star.hfd - mean).let { it * it } - } - - stdDev /= size - 1 - stdDev = sqrt(stdDev) - } else { - stdDev = Double.NaN - } + private fun DoubleArray.median(): Double { + return if (size % 2 == 0) (this[size / 2] + this[size / 2 - 1]) / 2.0 + else this[size / 2] + } - return MeasuredStars(mean, stdDev) + @JvmStatic + private fun List.measureDetectedStars(): Double { + return if (isEmpty()) 0.0 + else if (size == 1) this[0].hfd + else if (size == 2) (this[0].hfd + this[1].hfd) / 2.0 + else DoubleArray(size) { this[it].hfd }.also { it.sort() }.median() } } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt index 115f8a354..e130bb6c6 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt @@ -6,6 +6,7 @@ import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.indi.device.focuser.FocuserEvent import nebulosa.indi.device.focuser.FocuserMoveFailed +import nebulosa.indi.device.focuser.FocuserMovingChanged import nebulosa.indi.device.focuser.FocuserPositionChanged import nebulosa.log.loggerFor @@ -13,12 +14,13 @@ abstract class AbstractFocuserMoveTask : FocuserMoveTask, CancellationListener { @JvmField protected val latch = CountUpDownLatch() - @Volatile private var initialPosition = 0 + @Volatile private var moving = false override fun handleFocuserEvent(event: FocuserEvent) { if (event.device === focuser) { when (event) { - is FocuserPositionChanged -> if (focuser.position != initialPosition && !focuser.moving) latch.reset() + is FocuserMovingChanged -> if (event.device.moving) moving = true else latch.reset() + is FocuserPositionChanged -> if (moving && !event.device.moving) latch.reset() is FocuserMoveFailed -> latch.reset() } } @@ -32,12 +34,12 @@ abstract class AbstractFocuserMoveTask : FocuserMoveTask, CancellationListener { if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && canMove()) { try { cancellationToken.listen(this) - initialPosition = focuser.position LOG.info("Focuser move started. focuser={}", focuser) latch.countUp() move() latch.await() } finally { + moving = false cancellationToken.unlisten(this) LOG.info("Focuser move finished. focuser={}", focuser) } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt index 49e4ebc94..1131b1bd7 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt @@ -41,43 +41,49 @@ data class BacklashCompensationFocuserMoveTask( if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving) { val startPosition = focuser.position - if (compensation.mode == BacklashCompensationMode.ABSOLUTE) { - val adjustedTargetPosition = position + offset - - val finalizedTargetPosition = if (adjustedTargetPosition < 0) { - offset = 0 - 0 - } else if (adjustedTargetPosition > focuser.maxPosition) { - offset = 0 - focuser.maxPosition - } else { - val backlashCompensation = calculateAbsoluteBacklashCompensation(startPosition, adjustedTargetPosition) - offset += backlashCompensation - adjustedTargetPosition + backlashCompensation - } - - moveFocuser(finalizedTargetPosition, cancellationToken) - } else { - val backlashCompensation = calculateOvershootBacklashCompensation(startPosition, position) - - if (backlashCompensation != 0) { - val overshoot = position + backlashCompensation - - if (overshoot < 0) { - LOG.info("overshooting position is below minimum 0, skipping overshoot") - } else if (overshoot > focuser.maxPosition) { - LOG.info("overshooting position is above maximum ${focuser.maxPosition}, skipping overshoot") + val newPosition = when (compensation.mode) { + BacklashCompensationMode.ABSOLUTE -> { + val adjustedTargetPosition = position + offset + + if (adjustedTargetPosition < 0) { + offset = 0 + 0 + } else if (adjustedTargetPosition > focuser.maxPosition) { + offset = 0 + focuser.maxPosition } else { - LOG.info("overshooting from $startPosition to overshoot position $overshoot using a compensation of $backlashCompensation") - - moveFocuser(overshoot, cancellationToken) - - LOG.info("moving back to position $position") + val backlashCompensation = calculateAbsoluteBacklashCompensation(startPosition, adjustedTargetPosition) + offset += backlashCompensation + adjustedTargetPosition + backlashCompensation } } + BacklashCompensationMode.OVERSHOOT -> { + val backlashCompensation = calculateOvershootBacklashCompensation(startPosition, position) + + if (backlashCompensation != 0) { + val overshoot = position + backlashCompensation + + if (overshoot < 0) { + LOG.warn("overshooting position is below minimum 0, skipping overshoot") + } else if (overshoot > focuser.maxPosition) { + LOG.warn("overshooting position is above maximum ${focuser.maxPosition}, skipping overshoot") + } else { + LOG.info("overshooting from $startPosition to overshoot position $overshoot using a compensation of $backlashCompensation") + moveFocuser(overshoot, cancellationToken) + LOG.info("moving back to position $position") + } + } - moveFocuser(position, cancellationToken) + position + } + else -> { + position + } } + + LOG.info("moving to position {} using {} backlash compensation", newPosition, compensation.mode) + + moveFocuser(newPosition, cancellationToken) } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt index a81802872..d75206b3b 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt @@ -1,6 +1,7 @@ package nebulosa.api.focusers enum class BacklashCompensationMode { + NONE, ABSOLUTE, OVERSHOOT, } diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt index e3683fadb..76197a043 100644 --- a/api/src/test/kotlin/APITest.kt +++ b/api/src/test/kotlin/APITest.kt @@ -8,6 +8,8 @@ import kotlinx.coroutines.delay import nebulosa.api.autofocus.AutoFocusRequest import nebulosa.api.beans.converters.time.DurationSerializer import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.connection.ConnectionType +import nebulosa.api.stardetection.StarDetectionOptions import nebulosa.common.json.PathSerializer import nebulosa.test.NonGitHubOnlyCondition import okhttp3.MediaType.Companion.toMediaType @@ -69,8 +71,8 @@ class APITest : StringSpec() { "Auto Focus Stop" { autoFocusStop() } } - private fun connect(host: String = "0.0.0.0", port: Int = 7624) { - put("connection?host=$host&port=$port") + private fun connect(host: String = "0.0.0.0", port: Int = 7624, type: ConnectionType = ConnectionType.INDI) { + put("connection?host=$host&port=$port&type=$type") } private fun disconnect() { @@ -167,12 +169,17 @@ class APITest : StringSpec() { @JvmStatic private val EXPOSURE_TIME = Duration.ofSeconds(5) @JvmStatic private val CAPTURES_PATH = Path.of("/home/tiagohm/Git/nebulosa/data/captures") + @JvmStatic private val STAR_DETECTION_OPTIONS = StarDetectionOptions(executablePath = Path.of("astap")) + @JvmStatic private val CAMERA_START_CAPTURE_REQUEST = CameraStartCaptureRequest( exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO", savePath = CAPTURES_PATH, exposureAmount = 1 ) - @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest(capture = CAMERA_START_CAPTURE_REQUEST, stepSize = 11000) + @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest( + capture = CAMERA_START_CAPTURE_REQUEST, stepSize = 500, + starDetector = STAR_DETECTION_OPTIONS + ) @JvmStatic private val CLIENT = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index 71a861a67..ca7532a3a 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -3,10 +3,9 @@ import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { AutoFocusPreference, AutoFocusRequest } from '../../shared/types/autofocus.type' +import { AutoFocusPreference, AutoFocusRequest, EMPTY_AUTO_FOCUS_PREFERENCE } from '../../shared/types/autofocus.type' import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types' -import { EMPTY_STAR_DETECTION_OPTIONS } from '../../shared/types/settings.types' import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' import { CameraComponent } from '../camera/camera.component' @@ -27,18 +26,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { running = false readonly request: AutoFocusRequest = { + ...structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE), capture: structuredClone(EMPTY_CAMERA_START_CAPTURE), - fittingMode: 'HYPERBOLIC', - rSquaredThreshold: 0.7, - backlashCompensation: { - mode: 'NONE', - backlashIn: 0, - backlashOut: 0 - }, - initialOffsetSteps: 4, - stepSize: 100, - totalNumberOfAttempts: 1, - starDetector: structuredClone(EMPTY_STAR_DETECTION_OPTIONS), } constructor( diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts index aa24a424b..5f728901a 100644 --- a/desktop/src/shared/types/autofocus.type.ts +++ b/desktop/src/shared/types/autofocus.type.ts @@ -26,7 +26,7 @@ export interface AutoFocusPreference extends Omit { export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { fittingMode: 'HYPERBOLIC', - rSquaredThreshold: 0.7, + rSquaredThreshold: 0.5, initialOffsetSteps: 4, stepSize: 100, totalNumberOfAttempts: 1, diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt index 6a58bb809..6a044aff5 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt @@ -157,9 +157,9 @@ abstract class ASCOMDevice : Device, Resettable { refresh(stopwatch.elapsedSeconds) } - val delayTime = 2000L - elapsedTime + val delayTime = 1500L - elapsedTime - if (delayTime > 1L) { + if (delayTime >= 10L) { sleep(delayTime) } } diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt index c07f7990e..14739c573 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt @@ -29,26 +29,28 @@ data class ASCOMFocuser( @Volatile final override var hasThermometer = false @Volatile final override var temperature = 0.0 + @Volatile private var internalMoving = false + override val snoopedDevices = emptyList() override fun moveFocusIn(steps: Int) { - if (canAbsoluteMove) { - service.move(device.number, position + steps).doRequest() + internalMoving = if (canAbsoluteMove) { + service.move(device.number, position + steps).doRequest { } } else { - service.move(device.number, steps).doRequest() + service.move(device.number, steps).doRequest { } } } override fun moveFocusOut(steps: Int) { - if (canAbsoluteMove) { - service.move(device.number, position - steps).doRequest() + internalMoving = if (canAbsoluteMove) { + service.move(device.number, position - steps).doRequest { } } else { - service.move(device.number, -steps).doRequest() + service.move(device.number, -steps).doRequest { } } } override fun moveFocusTo(steps: Int) { - service.move(device.number, steps).doRequest() + internalMoving = service.move(device.number, steps).doRequest { } } override fun abortFocus() { @@ -74,6 +76,7 @@ data class ASCOMFocuser( super.reset() moving = false + internalMoving = false position = 0 canAbsoluteMove = false canRelativeMove = false @@ -124,9 +127,10 @@ data class ASCOMFocuser( private fun processMoving() { service.isMoving(device.number).doRequest { - if (it.value != moving) { - moving = it.value + val value = it.value || internalMoving + if (value != moving) { + moving = value sender.fireOnEventReceived(FocuserMovingChanged(this)) } } @@ -136,8 +140,11 @@ data class ASCOMFocuser( service.position(device.number).doRequest { if (it.value != position) { position = it.value - sender.fireOnEventReceived(FocuserPositionChanged(this)) + } else if (internalMoving && moving) { + moving = false + internalMoving = false + sender.fireOnEventReceived(FocuserMovingChanged(this)) } } } diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt index 9cb84ed17..26a23f980 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt @@ -19,7 +19,7 @@ class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) { return result } - override fun toString() = "CurvePoint(x=$x, y=$y, weight=$weight)" + override fun toString() = "CurvePoint(x=$x, y=$y)" companion object { diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt index e13c68376..205eb3bed 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt @@ -20,8 +20,11 @@ data object TrendLineFitting : CurveFitting { override fun calculate(points: Collection): Curve { val minimum = points.minBy { it.y } - val left = TrendLine(points.filter { it.x < minimum.x && it.y > minimum.y + 0.1 }) - val right = TrendLine(points.filter { it.x > minimum.x && it.y > minimum.y + 0.1 }) + val minX = minimum.x + val minY = minimum.y + 0.1 + + val left = TrendLine(points.filter { it.x < minX && it.y > minY }) + val right = TrendLine(points.filter { it.x > minX && it.y > minY }) return Curve(left, right, minimum) } diff --git a/nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt b/nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt new file mode 100644 index 000000000..030123a94 --- /dev/null +++ b/nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt @@ -0,0 +1,64 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.shouldBe +import nebulosa.curve.fitting.CurvePoint +import nebulosa.curve.fitting.CurvePoint.Companion.midPoint +import nebulosa.curve.fitting.HyperbolicFitting +import nebulosa.curve.fitting.QuadraticFitting +import nebulosa.curve.fitting.TrendLineFitting +import kotlin.math.roundToInt + +class AutoFocusTest : StringSpec() { + + init { + // The best focus is 8000. + + "near:hyperbolic" { + val points = focusPointsNearBestFocus() + val curve = HyperbolicFitting.calculate(points) + curve.minimum.x.roundToInt() shouldBeExactly 8031 + curve.rSquared shouldBe (0.89 plusOrMinus 1e-2) + } + "near:parabolic" { + val points = focusPointsNearBestFocus() + val curve = QuadraticFitting.calculate(points) + curve.minimum.x.roundToInt() shouldBeExactly 8051 + curve.rSquared shouldBe (0.74 plusOrMinus 1e-2) + } + "near:trendline" { + val points = focusPointsNearBestFocus() + val line = TrendLineFitting.calculate(points) + line.minimum.x.roundToInt() shouldBeExactly 8100 + line.rSquared shouldBe (0.94 plusOrMinus 1e-2) + } + "near:hyperbolic + trendline" { + val points = focusPointsNearBestFocus() + val curve = HyperbolicFitting.calculate(points) + val line = TrendLineFitting.calculate(points) + (curve.minimum midPoint line.intersection).x.roundToInt() shouldBeExactly 7952 + } + "near:parabolic + trendline" { + val points = focusPointsNearBestFocus() + val curve = QuadraticFitting.calculate(points) + val line = TrendLineFitting.calculate(points) + (curve.minimum midPoint line.intersection).x.roundToInt() shouldBeExactly 7962 + } + } + + companion object { + + @JvmStatic + private fun focusPointsNearBestFocus() = listOf( + CurvePoint(10100.0, 13.892408928571431), + CurvePoint(9600.0, 12.879208888888888), + CurvePoint(9100.0, 10.640856213017754), + CurvePoint(8600.0, 6.891483673469387), + CurvePoint(8100.0, 2.9738176470588247), + CurvePoint(7600.0, 5.063299489795917), + CurvePoint(7100.0, 9.326303846153845), + CurvePoint(6600.0, 12.428210576923071), + CurvePoint(6100.0, 13.662644615384618), + ) + } +} From 2e044107a52206671cd0731b0486448e9a4b091c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 31 May 2024 19:21:11 -0300 Subject: [PATCH 30/45] [api][desktop]: Support Auto Focus Successfully tested using ASCOM Sky Simulator --- .../nebulosa/api/autofocus/AutoFocusEvent.kt | 65 +++++++++++++- .../nebulosa/api/autofocus/AutoFocusState.kt | 5 +- .../nebulosa/api/autofocus/AutoFocusTask.kt | 89 ++++++++++++------- desktop/app/main.ts | 2 +- .../app/autofocus/autofocus.component.html | 26 +++++- .../src/app/autofocus/autofocus.component.ts | 30 ++++++- desktop/src/app/guider/guider.component.ts | 1 - desktop/src/shared/pipes/enum.pipe.ts | 13 ++- .../src/shared/services/electron.service.ts | 2 + desktop/src/shared/types/api.types.ts | 2 + desktop/src/shared/types/autofocus.type.ts | 55 +++++++++++- .../alpaca/indi/device/cameras/ASCOMCamera.kt | 15 +++- .../nebulosa/curve/fitting/CurvePoint.kt | 4 + .../curve/fitting/HyperbolicFitting.kt | 8 +- 14 files changed, 267 insertions(+), 50 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt index 0b3441aeb..c88dce623 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt @@ -2,13 +2,74 @@ package nebulosa.api.autofocus import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.messages.MessageEvent -import nebulosa.curve.fitting.CurvePoint +import nebulosa.curve.fitting.* +import nebulosa.nova.almanac.evenlySpacedNumbers data class AutoFocusEvent( @JvmField val state: AutoFocusState = AutoFocusState.IDLE, - @JvmField val focusPoint: CurvePoint = CurvePoint.ZERO, + @JvmField val focusPoint: CurvePoint? = null, + @JvmField val determinedFocusPoint: CurvePoint? = null, + @JvmField val starCount: Int = 0, + @JvmField val starHFD: Double = 0.0, + @JvmField val minX: Double = 0.0, + @JvmField val minY: Double = 0.0, + @JvmField val maxX: Double = 0.0, + @JvmField val maxY: Double = 0.0, + @JvmField val chart: Chart? = null, @JvmField val capture: CameraCaptureEvent? = null, ) : MessageEvent { + data class Chart( + @JvmField val trendLine: Map? = null, + @JvmField val parabolic: Map? = null, + @JvmField val hyperbolic: Map? = null, + ) + override val eventName = "AUTO_FOCUS.ELAPSED" + + companion object { + + @JvmStatic + fun makeChart( + points: List, + trendLine: TrendLineFitting.Curve?, + parabolic: QuadraticFitting.Curve?, + hyperbolic: HyperbolicFitting.Curve? + ) = with(evenlySpacedNumbers(points.first().x, points.last().x, 100)) { + Chart(trendLine?.mapped(this), parabolic?.mapped(this), hyperbolic?.mapped(this)) + } + + @JvmStatic + private fun TrendLineFitting.Curve.mapped(points: DoubleArray) = mapOf( + "left" to left.mapped(points), + "right" to right.mapped(points), + "intersection" to intersection, + "minimum" to minimum, "rSquared" to rSquared, + ) + + @JvmStatic + private fun TrendLine.mapped(points: DoubleArray) = mapOf( + "slope" to slope, "intercept" to intercept, + "rSquared" to rSquared, + "points" to makePoints(points) + ) + + @JvmStatic + private fun QuadraticFitting.Curve.mapped(points: DoubleArray) = mapOf( + "minimum" to minimum, "rSquared" to rSquared, + "points" to makePoints(points) + ) + + @JvmStatic + private fun HyperbolicFitting.Curve.mapped(points: DoubleArray) = mapOf( + "a" to a, "b" to b, "p" to p, + "minimum" to minimum, "rSquared" to rSquared, + "points" to makePoints(points) + ) + + @Suppress("NOTHING_TO_INLINE") + private inline fun Curve.makePoints(points: DoubleArray): List { + return points.map { CurvePoint(it, this(it)) } + } + } } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt index 80f8f4e80..406775746 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt @@ -4,8 +4,11 @@ enum class AutoFocusState { IDLE, MOVING, EXPOSURING, - COMPUTING, + EXPOSURED, + ANALYSING, + ANALYSED, FOCUS_POINT_ADDED, + CURVE_FITTED, FAILED, FINISHED, } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index ef0348e28..c9a7da6db 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -47,11 +47,14 @@ data class AutoFocusTask( private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation) @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null - @Volatile private var parabolicCurve: Lazy? = null - @Volatile private var hyperbolicCurve: Lazy? = null + @Volatile private var parabolicCurve: QuadraticFitting.Curve? = null + @Volatile private var hyperbolicCurve: HyperbolicFitting.Curve? = null @Volatile private var measurementPos = 0 - @Volatile private var focusPoint = CurvePoint.ZERO + @Volatile private var focusPoint: CurvePoint? = null + @Volatile private var starCount = 0 + @Volatile private var starHFD = 0.0 + @Volatile private var determinedFocusPoint: CurvePoint? = null init { cameraCaptureTask.subscribe(this) @@ -94,6 +97,8 @@ data class AutoFocusTask( obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken) + if (cancellationToken.isCancelled) break + var leftCount = trendLineCurve?.left?.points?.size ?: 0 var rightCount = trendLineCurve?.right?.points?.size ?: 0 @@ -138,6 +143,8 @@ data class AutoFocusTask( obtainFocusPoints(1, 1, false, cancellationToken) } + if (cancellationToken.isCancelled) break + leftCount = trendLineCurve!!.left.points.size rightCount = trendLineCurve!!.right.points.size @@ -156,13 +163,15 @@ data class AutoFocusTask( } } while (!cancellationToken.isCancelled && (rightCount + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps)) - if (exited) break + if (exited || cancellationToken.isCancelled) break val finalFocusPoint = determineFinalFocusPoint() val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken) if (!goodAutoFocus) { - if (numberOfAttempts < request.totalNumberOfAttempts) { + if (cancellationToken.isCancelled) { + break + } else if (numberOfAttempts < request.totalNumberOfAttempts) { moveFocuser(initialFocusPosition, cancellationToken, false) LOG.warn("potentially bad auto-focus. Reattempting") reset() @@ -172,8 +181,8 @@ data class AutoFocusTask( exited = true } } else { + determinedFocusPoint = finalFocusPoint LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y) - moveFocuser(finalFocusPoint.x.roundToInt(), cancellationToken, false) break } } @@ -195,16 +204,12 @@ data class AutoFocusTask( } private fun determineFinalFocusPoint(): CurvePoint { - val trendLine by lazy { TrendLineFitting.calculate(focusPoints) } - val hyperbolic by lazy { HyperbolicFitting.calculate(focusPoints) } - val parabolic by lazy { QuadraticFitting.calculate(focusPoints) } - return when (request.fittingMode) { - AutoFocusFittingMode.TRENDLINES -> trendLine.intersection - AutoFocusFittingMode.PARABOLIC -> parabolic.minimum - AutoFocusFittingMode.TREND_PARABOLIC -> trendLine.intersection midPoint parabolic.minimum - AutoFocusFittingMode.HYPERBOLIC -> hyperbolic.minimum - AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLine.intersection midPoint hyperbolic.minimum + AutoFocusFittingMode.TRENDLINES -> trendLineCurve!!.intersection + AutoFocusFittingMode.PARABOLIC -> parabolicCurve!!.minimum + AutoFocusFittingMode.TREND_PARABOLIC -> trendLineCurve!!.intersection midPoint parabolicCurve!!.minimum + AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve!!.minimum + AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLineCurve!!.intersection midPoint trendLineCurve!!.minimum } } @@ -214,15 +219,18 @@ data class AutoFocusTask( override fun accept(event: CameraCaptureEvent) { if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { - sendEvent(AutoFocusState.COMPUTING, capture = event) + sendEvent(AutoFocusState.EXPOSURED, event) + sendEvent(AutoFocusState.ANALYSING) val detectedStars = starDetection.detect(event.savePath!!) - LOG.info("detected ${detectedStars.size} stars") - val measure = detectedStars.measureDetectedStars() - LOG.info("HFD measurement. mean={}", measure) - measurements[measurementPos++] = measure + starCount = detectedStars.size + LOG.info("detected $starCount stars") + starHFD = detectedStars.measureDetectedStars() + LOG.info("HFD measurement. mean={}", starHFD) + measurements[measurementPos++] = starHFD + sendEvent(AutoFocusState.ANALYSED) onNext(event) } else { - sendEvent(AutoFocusState.EXPOSURING, capture = event) + sendEvent(AutoFocusState.EXPOSURING, event) } } @@ -256,19 +264,23 @@ data class AutoFocusTask( val measurement = takeExposure(cancellationToken) + if (cancellationToken.isCancelled) break + LOG.info("HFD measured after exposures. mean={}", measurement) if (remainingSteps-- > 1) { focusPosition = moveFocuser(direction * -stepSize, cancellationToken, true) } + if (cancellationToken.isCancelled) break + // If star measurement is 0, we didn't detect any stars or shapes, // and want this point to be ignored by the fitting as much as possible. if (measurement == 0.0) { LOG.warn("No stars detected in step") } else { focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement) - focusPoints.add(focusPoint) + focusPoints.add(focusPoint!!) focusPoints.sortBy { it.x } LOG.info("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint) @@ -281,16 +293,18 @@ data class AutoFocusTask( } private fun computeCurveFittings() { - with(focusPoints.toList()) { + with(focusPoints) { trendLineCurve = TrendLineFitting.calculate(this) if (size >= 3) { if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { - parabolicCurve = lazy { QuadraticFitting.calculate(this) } + parabolicCurve = QuadraticFitting.calculate(this) } else if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) { - hyperbolicCurve = lazy { HyperbolicFitting.calculate(this) } + hyperbolicCurve = HyperbolicFitting.calculate(this) } } + + sendEvent(AutoFocusState.CURVE_FITTED) } } @@ -301,8 +315,8 @@ data class AutoFocusTask( if (threshold > 0.0) { fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true - fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true - fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true + fun isParabolicBad() = parabolicCurve?.let { it.rSquared < threshold } ?: true + fun isHyperbolicBad() = hyperbolicCurve?.let { it.rSquared < threshold } ?: true val isBad = when (request.fittingMode) { AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() @@ -318,14 +332,16 @@ data class AutoFocusTask( } } - val min = focusPoints.minOf { it.x } - val max = focusPoints.maxOf { it.x } + val min = focusPoints.first().x + val max = focusPoints.last().x - if (focusPoint.x < min || focusPoint.y > max) { + if (focusPoint.x < min || focusPoint.x > max) { LOG.error("determined focus point position is outside of the overall measurement points of the curve") return false } + if (cancellationToken.isCancelled) return false + moveFocuser(focusPoint.x.roundToInt(), cancellationToken, false) val hfd = takeExposure(cancellationToken) @@ -346,9 +362,16 @@ data class AutoFocusTask( return focuser.position } - @Suppress("NOTHING_TO_INLINE") - private inline fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) { - onNext(AutoFocusEvent(state, focusPoint, capture)) + private fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) { + val chart = when (state) { + AutoFocusState.FOCUS_POINT_ADDED -> AutoFocusEvent.makeChart(focusPoints, trendLineCurve, parabolicCurve, hyperbolicCurve) + else -> null + } + + val (minX, minY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[0] + val (maxX, maxY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[focusPoints.lastIndex] + + onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, minX, minY, maxX, maxY, chart, capture)) } override fun reset() { diff --git a/desktop/app/main.ts b/desktop/app/main.ts index f631e27e1..2dada076b 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -565,6 +565,6 @@ function sendToAllWindows(channel: string, data: any, home: boolean = true) { } if (serve) { - console.info(data) + console.info(JSON.stringify(data)) } } diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 41eb277f7..3934065c0 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -6,6 +6,29 @@
+
+ +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
@@ -51,8 +74,7 @@ + styleClass="p-inputtext-sm border-0" (ngModelChange)="savePreference()" />
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index ca7532a3a..21ecc08da 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -1,9 +1,10 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' +import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { AutoFocusPreference, AutoFocusRequest, EMPTY_AUTO_FOCUS_PREFERENCE } from '../../shared/types/autofocus.type' +import { AutoFocusPreference, AutoFocusRequest, AutoFocusState, EMPTY_AUTO_FOCUS_PREFERENCE } from '../../shared/types/autofocus.type' import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types' import { deviceComparator } from '../../shared/utils/comparators' @@ -24,12 +25,18 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { focuser = structuredClone(EMPTY_FOCUSER) running = false + status: AutoFocusState = 'IDLE' + starCount = 0 + starHFD = 0 readonly request: AutoFocusRequest = { ...structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE), capture: structuredClone(EMPTY_CAMERA_START_CAPTURE), } + @ViewChild('cameraExposure') + private readonly cameraExposure!: CameraExposureComponent + constructor( app: AppComponent, private api: ApiService, @@ -98,6 +105,24 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { }) }) + electron.on('AUTO_FOCUS.ELAPSED', event => { + ngZone.run(() => { + this.status = event.state + this.running = event.state !== 'FAILED' && event.state !== 'FINISHED' + + if (event.capture) { + this.cameraExposure.handleCameraCaptureEvent(event.capture, true) + } + + if (event.state === 'FOCUS_POINT_ADDED') { + const chart = event.chart! + } else if (event.state === 'ANALYSED') { + this.starCount = event.starCount + this.starHFD = event.starHFD + } + }) + }) + this.loadPreference() } @@ -115,6 +140,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { if (this.camera.id) { const camera = await this.api.camera(this.camera.id) Object.assign(this.camera, camera) + this.loadPreference() } } diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index 14f72e44f..8c0e79507 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -123,7 +123,6 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { return '' }, label: (context) => { - console.log(context) const barType = context.dataset.type === 'bar' const raType = context.datasetIndex === 0 || context.datasetIndex === 2 const scale = barType ? this.phdDurationScale : 1.0 diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index c84525f23..e2d85cfee 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -1,12 +1,14 @@ import { Pipe, PipeTransform } from '@angular/core' import { DARVState, TPPAState } from '../types/alignment.types' import { Constellation, SatelliteGroupType, SkyObjectType } from '../types/atlas.types' +import { AutoFocusState } from '../types/autofocus.type' import { CameraCaptureState } from '../types/camera.types' +import { FlatWizardState } from '../types/flat-wizard.types' import { GuideState } from '../types/guider.types' import { SCNRProtectionMethod } from '../types/image.types' export type EnumPipeKey = SCNRProtectionMethod | Constellation | SkyObjectType | SatelliteGroupType | - DARVState | TPPAState | GuideState | CameraCaptureState | 'ALL' | string + DARVState | TPPAState | GuideState | CameraCaptureState | FlatWizardState | AutoFocusState | 'ALL' | string @Pipe({ name: 'enum' }) export class EnumPipe implements PipeTransform { @@ -342,7 +344,14 @@ export class EnumPipe implements PipeTransform { 'CAPTURE_STARTED': undefined, 'EXPOSURE_STARTED': undefined, 'EXPOSURE_FINISHED': undefined, - 'CAPTURE_FINISHED': undefined + 'CAPTURE_FINISHED': undefined, + // Auto Focus. + 'CAPTURED': 'Captured', + 'MOVING': 'Moving', + 'EXPOSURED': 'Exposured', + 'ANALYSING': 'Analysing', + 'ANALYSED': 'Analysed', + 'FOCUS_POINT_ADDED': 'Focus point added', } transform(value: EnumPipeKey) { diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 8c7cbea52..6c343e2d9 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -22,6 +22,7 @@ import { Mount } from '../types/mount.types' import { Rotator } from '../types/rotator.types' import { SequencerEvent } from '../types/sequencer.types' import { FilterWheel, WheelRenamed } from '../types/wheel.types' +import { AutoFocusEvent } from '../types/autofocus.type' type EventMappedType = { 'DEVICE.PROPERTY_CHANGED': INDIMessageEvent @@ -74,6 +75,7 @@ type EventMappedType = { 'WINDOW.CLOSE': CloseWindow 'WHEEL.RENAMED': WheelRenamed 'ROI.SELECTED': ROISelected + 'AUTO_FOCUS.ELAPSED': AutoFocusEvent } @Injectable({ providedIn: 'root' }) diff --git a/desktop/src/shared/types/api.types.ts b/desktop/src/shared/types/api.types.ts index 65733134a..56dd57faa 100644 --- a/desktop/src/shared/types/api.types.ts +++ b/desktop/src/shared/types/api.types.ts @@ -29,6 +29,8 @@ export const API_EVENT_TYPES = [ 'GUIDER.CONNECTED', 'GUIDER.DISCONNECTED', 'GUIDER.UPDATED', 'GUIDER.STEPPED', 'GUIDER.MESSAGE_RECEIVED', // Polar Alignment. 'DARV_ALIGNMENT.ELAPSED', + // Auto Focus. + 'AUTO_FOCUS.ELAPSED', ] as const export type ApiEventType = (typeof API_EVENT_TYPES)[number] diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts index 5f728901a..42111d56b 100644 --- a/desktop/src/shared/types/autofocus.type.ts +++ b/desktop/src/shared/types/autofocus.type.ts @@ -1,6 +1,9 @@ -import { CameraStartCapture } from './camera.types' +import { Point } from 'electron' +import { CameraCaptureEvent, CameraStartCapture } from './camera.types' import { EMPTY_STAR_DETECTION_OPTIONS, StarDetectionOptions } from './settings.types' +export type AutoFocusState = 'IDLE' | 'MOVING' | 'EXPOSURING' | 'EXPOSURED' | 'ANALYSING' | 'ANALYSED' | 'FOCUS_POINT_ADDED' | 'FAILED' | 'FINISHED' + export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC' export type BacklashCompensationMode = 'NONE' | 'ABSOLUTE' | 'OVERSHOOT' @@ -37,3 +40,53 @@ export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { }, starDetector: EMPTY_STAR_DETECTION_OPTIONS, } + +export interface Curve { + minimum: Point + rSquared: number +} + +export interface Plottable { + points: Point[] +} + +export interface HyperbolicCurve extends Curve, Plottable { + a: number + b: number + p: number +} + +export interface ParabolicCurve extends Curve, Plottable { +} + +export interface Line extends Plottable { + slope: number + intercept: number + rSquared: number +} + +export interface TrendLineCurve extends Curve { + left: Line + right: Line + intersection: Point +} + +export interface Chart { + trendLine?: TrendLineCurve + parabolic?: ParabolicCurve + hyperbolic?: HyperbolicCurve +} + +export interface AutoFocusEvent { + state: AutoFocusState + focusPoint?: Point + determinedFocusPoint?: Point + starCount: number + starHFD: number + minX: number + maxX: number + minY: number + maxY: number + chart?: Chart + capture?: CameraCaptureEvent +} diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt index 14c3fb6a6..84d26fa14 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt @@ -15,6 +15,8 @@ import nebulosa.image.format.HeaderCard import nebulosa.indi.device.Device import nebulosa.indi.device.camera.* import nebulosa.indi.device.camera.Camera.Companion.NANO_TO_SECONDS +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.guide.GuideOutputPulsingChanged import nebulosa.indi.device.mount.Mount import nebulosa.indi.protocol.INDIProtocol @@ -679,10 +681,21 @@ data class ASCOMCamera( header.add(FitsKeyword.EQUINOX, 2000) } + val focuser = snoopedDevices.firstOrNull { it is Focuser } as? Focuser + + focuser?.also { + header.add(FitsKeyword.FOCUSPOS, it.position) + } + + val wheel = snoopedDevices.firstOrNull { it is FilterWheel } as? FilterWheel + + wheel?.also { + header.add(FitsKeyword.FILTER, it.names.getOrNull(it.position) ?: "Filter #${it.position}") + } + fitsKeywords.forEach(header::add) val hdu = BasicImageHdu(width, height, numberOfChannels, header, data) - val image = Fits() image.add(hdu) diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt index 26a23f980..54cca627f 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt @@ -4,6 +4,10 @@ import org.apache.commons.math3.fitting.WeightedObservedPoint class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) { + operator fun component1() = x + + operator fun component2() = y + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CurvePoint) return false diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt index d845d404e..d711430d1 100644 --- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt +++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt @@ -7,11 +7,11 @@ import kotlin.math.* data object HyperbolicFitting : CurveFitting { data class Curve( - private val a: Double, - private val b: Double, - private val p: Double, + @JvmField val a: Double, + @JvmField val b: Double, + @JvmField val p: Double, override val minimum: CurvePoint, - private val points: Collection, + @JvmField val points: Collection, ) : FittedCurve { override val rSquared by lazy { RSquared.calculate(points, this) } From 64ac7ec334caf28650c7f8c474011b6b020094e2 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 31 May 2024 21:39:43 -0300 Subject: [PATCH 31/45] [desktop]: Minor changes --- desktop/src/app/atlas/atlas.component.html | 30 ++++++------- .../app/autofocus/autofocus.component.html | 6 +-- .../app/calculator/calculator.component.ts | 10 +++++ .../calculator/formula/formula.component.html | 7 ++- .../calibration/calibration.component.html | 2 +- desktop/src/app/camera/camera.component.html | 2 +- .../filterwheel/filterwheel.component.html | 2 +- .../flat-wizard/flat-wizard.component.html | 2 +- .../src/app/focuser/focuser.component.html | 4 +- desktop/src/app/guider/guider.component.html | 14 +++--- desktop/src/app/image/image.component.html | 44 +++++++++---------- .../property/indi-property.component.html | 11 ++--- desktop/src/app/mount/mount.component.html | 22 +++++----- .../src/app/rotator/rotator.component.html | 4 +- .../app/sequencer/sequencer.component.html | 2 +- desktop/src/shared/types/calculator.types.ts | 1 + 16 files changed, 85 insertions(+), 78 deletions(-) diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index 6bbcf5f94..db73c7c19 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -59,7 +59,7 @@
- @@ -140,7 +140,7 @@
- +
@@ -182,7 +182,7 @@
- +
@@ -228,74 +228,74 @@
- +
- +
- +
- +
- +
- +
-
- +
- +
- +
- +
- +
diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 3934065c0..e4d1b67ee 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -12,19 +12,19 @@
- +
- +
- +
diff --git a/desktop/src/app/calculator/calculator.component.ts b/desktop/src/app/calculator/calculator.component.ts index d67c42912..aa166ade6 100644 --- a/desktop/src/app/calculator/calculator.component.ts +++ b/desktop/src/app/calculator/calculator.component.ts @@ -22,6 +22,7 @@ export class CalculatorComponent { { label: 'Aperture', suffix: 'mm', + min: 1, }, { label: 'Focal Ratio', @@ -49,10 +50,12 @@ export class CalculatorComponent { { label: 'Focal Length', suffix: 'mm', + min: 1, }, { label: 'Aperture', suffix: 'mm', + min: 1, }, ], result: { @@ -76,6 +79,7 @@ export class CalculatorComponent { { label: 'Aperture', suffix: 'mm', + min: 1, }, ], result: { @@ -99,6 +103,7 @@ export class CalculatorComponent { { label: 'Aperture', suffix: 'mm', + min: 1, }, ], result: { @@ -122,6 +127,7 @@ export class CalculatorComponent { { label: 'Aperture', suffix: 'mm', + min: 1, }, ], result: { @@ -144,10 +150,12 @@ export class CalculatorComponent { { label: 'Larger Aperture', suffix: 'mm', + min: 1, }, { label: 'Smaller Aperture', suffix: 'mm', + min: 1, }, ], result: { @@ -171,10 +179,12 @@ export class CalculatorComponent { { label: 'Pixel Size', suffix: 'µm', + min: 1, }, { label: 'Focal Length', suffix: 'mm', + min: 1, }, ], result: { diff --git a/desktop/src/app/calculator/formula/formula.component.html b/desktop/src/app/calculator/formula/formula.component.html index 6d4083c3f..8b47fdc2b 100644 --- a/desktop/src/app/calculator/formula/formula.component.html +++ b/desktop/src/app/calculator/formula/formula.component.html @@ -12,7 +12,7 @@ + [min]="item.min ?? 0" [showButtons]="true" styleClass="border-0 p-inputtext-sm" locale="en" scrollableNumber />
@@ -26,9 +26,8 @@
{{ formula.result.prefix }} - + {{ formula.result.suffix }} diff --git a/desktop/src/app/calibration/calibration.component.html b/desktop/src/app/calibration/calibration.component.html index acfded5e3..a9f783024 100644 --- a/desktop/src/app/calibration/calibration.component.html +++ b/desktop/src/app/calibration/calibration.component.html @@ -52,7 +52,7 @@
- +
diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 2127b8875..c082be6b0 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -2,7 +2,7 @@
- +
- +
- diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html index 385b82a5d..4283b74eb 100644 --- a/desktop/src/app/focuser/focuser.component.html +++ b/desktop/src/app/focuser/focuser.component.html @@ -2,7 +2,7 @@
- +
- diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 9202e47e7..2d8551e46 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -5,7 +5,7 @@
- @@ -35,40 +35,40 @@
-
-
-
- +
- +
- +
diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index b9872f8ce..dbb15221c 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -94,46 +94,46 @@
- +
-
-
-
- +
- +
- @@ -191,38 +191,38 @@
- +
- +
- +
- +
-
- +
@@ -348,55 +348,55 @@
- +
-
-
-
-
-
-
- @@ -581,7 +581,7 @@
- +
diff --git a/desktop/src/app/indi/property/indi-property.component.html b/desktop/src/app/indi/property/indi-property.component.html index 458bb5a0e..7b5c9c3f6 100644 --- a/desktop/src/app/indi/property/indi-property.component.html +++ b/desktop/src/app/indi/property/indi-property.component.html @@ -25,14 +25,13 @@
- +
- +
@@ -48,15 +47,13 @@
- +
- +
diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index 769ee72a2..471ac72fc 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -2,7 +2,7 @@
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
diff --git a/desktop/src/app/rotator/rotator.component.html b/desktop/src/app/rotator/rotator.component.html index 17e1b1e6f..b7cb53b0b 100644 --- a/desktop/src/app/rotator/rotator.component.html +++ b/desktop/src/app/rotator/rotator.component.html @@ -2,7 +2,7 @@
- +
- diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index afbb1ab77..556df7628 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -29,7 +29,7 @@ [positionTop]="8"> - diff --git a/desktop/src/shared/types/calculator.types.ts b/desktop/src/shared/types/calculator.types.ts index b82d5da25..ca490510f 100644 --- a/desktop/src/shared/types/calculator.types.ts +++ b/desktop/src/shared/types/calculator.types.ts @@ -6,6 +6,7 @@ export interface CalculatorOperand { value?: number minFractionDigits?: number maxFractionDigits?: number + min?: number } export interface CalculatorFormula { From b8deb6eb2d0bd2553f2baa19f0fe514aaa450ae1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 03:12:27 +0000 Subject: [PATCH 32/45] [api]: Bump com.github.oshi:oshi-core from 6.6.0 to 6.6.1 Bumps [com.github.oshi:oshi-core](https://github.com/oshi/oshi) from 6.6.0 to 6.6.1. - [Release notes](https://github.com/oshi/oshi/releases) - [Changelog](https://github.com/oshi/oshi/blob/master/CHANGELOG.md) - [Commits](https://github.com/oshi/oshi/compare/oshi-parent-6.6.0...oshi-parent-6.6.1) --- updated-dependencies: - dependency-name: com.github.oshi:oshi-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 44067acf1..a19b5eb89 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,7 +31,7 @@ dependencyResolutionManagement { library("apache-codec", "commons-codec:commons-codec:1.17.0") library("apache-collections", "org.apache.commons:commons-collections4:4.4") library("apache-numbers-complex", "org.apache.commons:commons-numbers-complex:1.1") - library("oshi", "com.github.oshi:oshi-core:6.6.0") + library("oshi", "com.github.oshi:oshi-core:6.6.1") library("jna", "net.java.dev.jna:jna:5.14.0") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.9.0") library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.9.0") From 62e47de50f0f221f304b88bcdb0448d746f17dfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 03:12:03 +0000 Subject: [PATCH 33/45] [api]: Bump the netty group with 2 updates Bumps the netty group with 2 updates: [io.netty:netty-transport](https://github.com/netty/netty) and [io.netty:netty-codec](https://github.com/netty/netty). Updates `io.netty:netty-transport` from 4.1.109.Final to 4.1.110.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.109.Final...netty-4.1.110.Final) Updates `io.netty:netty-codec` from 4.1.109.Final to 4.1.110.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.109.Final...netty-4.1.110.Final) --- updated-dependencies: - dependency-name: io.netty:netty-transport dependency-type: direct:production update-type: version-update:semver-patch dependency-group: netty - dependency-name: io.netty:netty-codec dependency-type: direct:production update-type: version-update:semver-patch dependency-group: netty ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index a19b5eb89..daf2ac638 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,8 +23,8 @@ dependencyResolutionManagement { library("rx", "io.reactivex.rxjava3:rxjava:3.1.8") library("logback", "ch.qos.logback:logback-classic:1.5.6") library("eventbus", "org.greenrobot:eventbus-java:3.3.1") - library("netty-transport", "io.netty:netty-transport:4.1.109.Final") - library("netty-codec", "io.netty:netty-codec:4.1.109.Final") + library("netty-transport", "io.netty:netty-transport:4.1.110.Final") + library("netty-codec", "io.netty:netty-codec:4.1.110.Final") library("xml", "com.fasterxml:aalto-xml:1.3.2") library("csv", "de.siegmar:fastcsv:3.1.0") library("apache-lang3", "org.apache.commons:commons-lang3:3.14.0") From 48dd2583a8bc9330c81bbca09c721b7b1ad67e76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 03:12:15 +0000 Subject: [PATCH 34/45] [api]: Bump org.springframework.boot from 3.2.5 to 3.3.0 Bumps org.springframework.boot from 3.2.5 to 3.3.0. --- updated-dependencies: - dependency-name: org.springframework.boot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 8832af464..a3d6fd6ae 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -2,7 +2,7 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar plugins { kotlin("jvm") - id("org.springframework.boot") version "3.2.5" + id("org.springframework.boot") version "3.3.0" id("io.spring.dependency-management") version "1.1.5" kotlin("plugin.spring") kotlin("kapt") From 54315774e74aaab57b01755fb4a4051e80fbb38c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 03:12:23 +0000 Subject: [PATCH 35/45] [api]: Bump org.springframework:spring-context-indexer Bumps [org.springframework:spring-context-indexer](https://github.com/spring-projects/spring-framework) from 6.1.7 to 6.1.8. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.7...v6.1.8) --- updated-dependencies: - dependency-name: org.springframework:spring-context-indexer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index a3d6fd6ae..b86cc2805 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-undertow") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - kapt("org.springframework:spring-context-indexer:6.1.7") + kapt("org.springframework:spring-context-indexer:6.1.8") testImplementation(project(":nebulosa-astrobin-api")) testImplementation(project(":nebulosa-skycatalog-stellarium")) testImplementation(project(":nebulosa-test")) From edbd10064412ea734992fa06050afaa73d33aaff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 04:00:54 +0000 Subject: [PATCH 36/45] [desktop]: Bump @types/node in /desktop in the types group Bumps the types group in /desktop with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 20.12.12 to 20.13.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: types ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 23c11b95c..67d4444ad 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -43,7 +43,7 @@ "@angular/compiler-cli": "17.3.9", "@angular/language-service": "17.3.9", "@types/leaflet": "1.9.12", - "@types/node": "20.12.12", + "@types/node": "20.13.0", "@types/uuid": "9.0.8", "electron": "30.0.6", "electron-builder": "24.13.3", @@ -4476,9 +4476,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "version": "20.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz", + "integrity": "sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/desktop/package.json b/desktop/package.json index e97e2dbd6..c9d103b92 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -65,7 +65,7 @@ "@angular/compiler-cli": "17.3.9", "@angular/language-service": "17.3.9", "@types/leaflet": "1.9.12", - "@types/node": "20.12.12", + "@types/node": "20.13.0", "@types/uuid": "9.0.8", "electron": "30.0.6", "electron-builder": "24.13.3", From 6baa05d54f6bf23f0eb083e31de0c311ad625b4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 10:44:45 +0000 Subject: [PATCH 37/45] [desktop]: Bump electron from 30.0.6 to 30.0.9 in /desktop Bumps [electron](https://github.com/electron/electron) from 30.0.6 to 30.0.9. - [Release notes](https://github.com/electron/electron/releases) - [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md) - [Commits](https://github.com/electron/electron/compare/v30.0.6...v30.0.9) --- updated-dependencies: - dependency-name: electron dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 67d4444ad..9953ec849 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -45,7 +45,7 @@ "@types/leaflet": "1.9.12", "@types/node": "20.13.0", "@types/uuid": "9.0.8", - "electron": "30.0.6", + "electron": "30.0.9", "electron-builder": "24.13.3", "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", @@ -7738,9 +7738,9 @@ } }, "node_modules/electron": { - "version": "30.0.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-30.0.6.tgz", - "integrity": "sha512-PkhEPFdpYcTzjAO3gMHZ+map7g2+xCrMDedo/L1i0ir2BRXvAB93IkTJX497U6Srb/09r2cFt+k20VPNVCdw3Q==", + "version": "30.0.9", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.0.9.tgz", + "integrity": "sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow==", "dev": true, "hasInstallScript": true, "dependencies": { diff --git a/desktop/package.json b/desktop/package.json index c9d103b92..22fd4d39e 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -67,7 +67,7 @@ "@types/leaflet": "1.9.12", "@types/node": "20.13.0", "@types/uuid": "9.0.8", - "electron": "30.0.6", + "electron": "30.0.9", "electron-builder": "24.13.3", "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", From 4be3e5790e876b58b3c1e410c49463a897e9b0f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 04:01:19 +0000 Subject: [PATCH 38/45] [desktop]: Bump primeng from 17.17.0 to 17.18.0 in /desktop Bumps [primeng](https://github.com/primefaces/primeng) from 17.17.0 to 17.18.0. - [Release notes](https://github.com/primefaces/primeng/releases) - [Changelog](https://github.com/primefaces/primeng/blob/master/CHANGELOG.md) - [Commits](https://github.com/primefaces/primeng/compare/17.17.0...17.18.0) --- updated-dependencies: - dependency-name: primeng dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 14 +++++++------- desktop/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 9953ec849..ebc817120 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -30,7 +30,7 @@ "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "7.0.0", - "primeng": "17.17.0", + "primeng": "17.18.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", @@ -13228,16 +13228,16 @@ "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==" }, "node_modules/primeng": { - "version": "17.17.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.17.0.tgz", - "integrity": "sha512-+lIfG2nVve5GJQXGBDi2YeVabg6E9RmG67LDw9Ol8XvMWuHwJXQAGfO+AKPhPPzFSdb1j2v44uJemuNcJLXUiw==", + "version": "17.18.0", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.18.0.tgz", + "integrity": "sha512-EcvU/0Ex9QoBR6g6db9fDTCTAmzokW70TV5Oroy2gdvXRr3eqlflnOBoArQsmxTaw1oxSsu68YVj3RvcKYWhTg==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": "^17.0.0", - "@angular/core": "^17.0.0", - "@angular/forms": "^17.0.0", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", "rxjs": "^6.0.0 || ^7.8.1", "zone.js": "~0.14.0" } diff --git a/desktop/package.json b/desktop/package.json index 22fd4d39e..74b1501d0 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -52,7 +52,7 @@ "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "7.0.0", - "primeng": "17.17.0", + "primeng": "17.18.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", From 45db58de839f2b1128c6d159c2d01b65998f400d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 10:47:31 +0000 Subject: [PATCH 39/45] [desktop]: Bump node-polyfill-webpack-plugin in /desktop Bumps [node-polyfill-webpack-plugin](https://github.com/Richienb/node-polyfill-webpack-plugin) from 3.0.0 to 4.0.0. - [Release notes](https://github.com/Richienb/node-polyfill-webpack-plugin/releases) - [Commits](https://github.com/Richienb/node-polyfill-webpack-plugin/compare/v3.0.0...v4.0.0) --- updated-dependencies: - dependency-name: node-polyfill-webpack-plugin dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 24 ++++++++++++------------ desktop/package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index ebc817120..1ecffeb10 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -47,7 +47,7 @@ "@types/uuid": "9.0.8", "electron": "30.0.9", "electron-builder": "24.13.3", - "node-polyfill-webpack-plugin": "3.0.0", + "node-polyfill-webpack-plugin": "4.0.0", "npm-run-all": "4.1.5", "ts-node": "10.9.2", "typescript": "5.4.5", @@ -7643,12 +7643,12 @@ } }, "node_modules/domain-browser": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz", - "integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-5.7.0.tgz", + "integrity": "sha512-edTFu0M/7wO1pXY6GDxVNVW086uqwWYIHP98txhcPyV995X21JIH2DtYp33sQJOupYoXKe9RwTw2Ya2vWaquTQ==", "dev": true, "engines": { - "node": ">=10" + "node": ">=4" }, "funding": { "url": "https://bevry.me/fund" @@ -11911,9 +11911,9 @@ } }, "node_modules/node-polyfill-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-QpG496dDBiaelQZu9wDcVvpLbtk7h9Ctz693RaUMZBgl8DUoFToO90ZTLKq57gP7rwKqYtGbMBXkcEgLSag2jQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-WLk77vLpbcpmTekRj6s6vYxk30XoyaY5MDZ4+9g8OaKoG3Ij+TjOqhpQjVUlfDZBPBgpNATDltaQkzuXSnnkwg==", "dev": true, "dependencies": { "assert": "^2.1.0", @@ -11922,21 +11922,21 @@ "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.12.0", - "domain-browser": "^4.22.0", + "domain-browser": "^5.7.0", "events": "^3.3.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", - "punycode": "^2.3.0", + "punycode": "^2.3.1", "querystring-es3": "^0.2.1", - "readable-stream": "^4.4.2", + "readable-stream": "^4.5.2", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.3.0", "timers-browserify": "^2.0.12", "tty-browserify": "^0.0.1", - "type-fest": "^4.4.0", + "type-fest": "^4.18.2", "url": "^0.11.3", "util": "^0.12.5", "vm-browserify": "^1.1.2" diff --git a/desktop/package.json b/desktop/package.json index 74b1501d0..0466564d7 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -69,7 +69,7 @@ "@types/uuid": "9.0.8", "electron": "30.0.9", "electron-builder": "24.13.3", - "node-polyfill-webpack-plugin": "3.0.0", + "node-polyfill-webpack-plugin": "4.0.0", "npm-run-all": "4.1.5", "ts-node": "10.9.2", "typescript": "5.4.5", From de81b7aa31a081aa7ae00e27acdd3c3ecf2a78fb Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 1 Jun 2024 15:55:10 -0300 Subject: [PATCH 40/45] [api][desktop]: Support Auto Focus --- .../nebulosa/api/autofocus/AutoFocusEvent.kt | 67 +---- .../AutoFocusEventChartSerializer.kt | 115 +++++++++ .../nebulosa/api/autofocus/AutoFocusState.kt | 1 - .../nebulosa/api/autofocus/AutoFocusTask.kt | 28 +- .../api/autofocus/CurvePointSerializer.kt | 21 ++ api/src/test/kotlin/APITest.kt | 8 +- desktop/README.md | 4 + desktop/auto-focus.png | Bin 0 -> 33231 bytes .../app/autofocus/autofocus.component.html | 134 +++++----- .../src/app/autofocus/autofocus.component.ts | 240 +++++++++++++++++- desktop/src/app/guider/guider.component.html | 23 +- desktop/src/app/guider/guider.component.ts | 14 +- desktop/src/shared/pipes/enum.pipe.ts | 2 +- .../shared/services/browser-window.service.ts | 4 +- desktop/src/shared/types/autofocus.type.ts | 15 +- desktop/src/shared/utils/comparators.ts | 1 + 16 files changed, 514 insertions(+), 163 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt create mode 100644 desktop/auto-focus.png diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt index c88dce623..334f44d59 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt @@ -2,8 +2,10 @@ package nebulosa.api.autofocus import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.messages.MessageEvent -import nebulosa.curve.fitting.* -import nebulosa.nova.almanac.evenlySpacedNumbers +import nebulosa.curve.fitting.CurvePoint +import nebulosa.curve.fitting.HyperbolicFitting +import nebulosa.curve.fitting.QuadraticFitting +import nebulosa.curve.fitting.TrendLineFitting data class AutoFocusEvent( @JvmField val state: AutoFocusState = AutoFocusState.IDLE, @@ -11,65 +13,20 @@ data class AutoFocusEvent( @JvmField val determinedFocusPoint: CurvePoint? = null, @JvmField val starCount: Int = 0, @JvmField val starHFD: Double = 0.0, - @JvmField val minX: Double = 0.0, - @JvmField val minY: Double = 0.0, - @JvmField val maxX: Double = 0.0, - @JvmField val maxY: Double = 0.0, @JvmField val chart: Chart? = null, @JvmField val capture: CameraCaptureEvent? = null, ) : MessageEvent { data class Chart( - @JvmField val trendLine: Map? = null, - @JvmField val parabolic: Map? = null, - @JvmField val hyperbolic: Map? = null, + @JvmField val predictedFocusPoint: CurvePoint? = null, + @JvmField val minX: Double = 0.0, + @JvmField val minY: Double = 0.0, + @JvmField val maxX: Double = 0.0, + @JvmField val maxY: Double = 0.0, + @JvmField val trendLine: TrendLineFitting.Curve? = null, + @JvmField val parabolic: QuadraticFitting.Curve? = null, + @JvmField val hyperbolic: HyperbolicFitting.Curve? = null, ) override val eventName = "AUTO_FOCUS.ELAPSED" - - companion object { - - @JvmStatic - fun makeChart( - points: List, - trendLine: TrendLineFitting.Curve?, - parabolic: QuadraticFitting.Curve?, - hyperbolic: HyperbolicFitting.Curve? - ) = with(evenlySpacedNumbers(points.first().x, points.last().x, 100)) { - Chart(trendLine?.mapped(this), parabolic?.mapped(this), hyperbolic?.mapped(this)) - } - - @JvmStatic - private fun TrendLineFitting.Curve.mapped(points: DoubleArray) = mapOf( - "left" to left.mapped(points), - "right" to right.mapped(points), - "intersection" to intersection, - "minimum" to minimum, "rSquared" to rSquared, - ) - - @JvmStatic - private fun TrendLine.mapped(points: DoubleArray) = mapOf( - "slope" to slope, "intercept" to intercept, - "rSquared" to rSquared, - "points" to makePoints(points) - ) - - @JvmStatic - private fun QuadraticFitting.Curve.mapped(points: DoubleArray) = mapOf( - "minimum" to minimum, "rSquared" to rSquared, - "points" to makePoints(points) - ) - - @JvmStatic - private fun HyperbolicFitting.Curve.mapped(points: DoubleArray) = mapOf( - "a" to a, "b" to b, "p" to p, - "minimum" to minimum, "rSquared" to rSquared, - "points" to makePoints(points) - ) - - @Suppress("NOTHING_TO_INLINE") - private inline fun Curve.makePoints(points: DoubleArray): List { - return points.map { CurvePoint(it, this(it)) } - } - } } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt new file mode 100644 index 000000000..ff36ce31c --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt @@ -0,0 +1,115 @@ +package nebulosa.api.autofocus + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import nebulosa.curve.fitting.* +import nebulosa.nova.almanac.evenlySpacedNumbers +import org.springframework.stereotype.Component +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +@Component +class AutoFocusEventChartSerializer : StdSerializer(AutoFocusEvent.Chart::class.java) { + + override fun serialize(chart: AutoFocusEvent.Chart?, gen: JsonGenerator, provider: SerializerProvider) { + if (chart == null) { + gen.writeNull() + } else { + gen.writeStartObject() + + gen.writePOJOField("predictedFocusPoint", chart.predictedFocusPoint) + gen.writeNumberField("minX", chart.minX) + gen.writeNumberField("minY", chart.minY) + gen.writeNumberField("maxX", chart.maxX) + gen.writeNumberField("maxY", chart.maxY) + + if (chart.trendLine != null || chart.parabolic != null || chart.hyperbolic != null) { + val delta = chart.maxX - chart.minX + val stepSize = max(3, min((delta / 10.0).roundToInt().let { if (it % 2 == 0) it + 1 else it }, 101)) + val points = if (delta <= 0.0) doubleArrayOf(chart.minX) else evenlySpacedNumbers(chart.minX, chart.maxX, stepSize) + chart.trendLine?.serialize(gen, points) + chart.parabolic?.serialize(gen, points) + chart.hyperbolic?.serialize(gen, points) + } + + gen.writeEndObject() + } + } + + companion object { + + @Suppress("NOTHING_TO_INLINE") + private inline fun Double.isRSquaredValid() = isFinite() && this > 0.0 + + private inline fun T?.serializeAsFittedCurve(gen: JsonGenerator, fieldName: String, block: (T) -> Unit = {}) { + if (this != null && rSquared.isRSquaredValid()) { + gen.writeObjectFieldStart(fieldName) + gen.writeNumberField("rSquared", rSquared) + gen.writePOJOField("minimum", minimum) + block(this) + gen.writeEndObject() + } + } + + @JvmStatic + private fun TrendLineFitting.Curve?.serialize(gen: JsonGenerator, points: DoubleArray) { + serializeAsFittedCurve(gen, "trendLine") { + it.left.serialize(gen, "left", points) + it.right.serialize(gen, "right", points) + gen.writePOJOField("intersection", it.intersection) + } + } + + @JvmStatic + private fun TrendLine.serialize(gen: JsonGenerator, fieldName: String, points: DoubleArray) { + gen.writeObjectFieldStart(fieldName) + gen.writeNumberField("slope", slope) + gen.writeNumberField("intercept", intercept) + gen.writeNumberField("rSquared", rSquared) + + if (rSquared.isRSquaredValid()) { + makePoints(gen, points) + } + + gen.writeEndObject() + } + + @JvmStatic + private fun QuadraticFitting.Curve?.serialize(gen: JsonGenerator, points: DoubleArray) { + serializeAsFittedCurve(gen, "parabolic") { + if (it.rSquared.isRSquaredValid()) { + it.makePoints(gen, points) + } + } + } + + @JvmStatic + private fun HyperbolicFitting.Curve?.serialize(gen: JsonGenerator, points: DoubleArray) { + serializeAsFittedCurve(gen, "hyperbolic") { + gen.writeNumberField("a", it.a) + gen.writeNumberField("b", it.b) + gen.writeNumberField("p", it.p) + + if (it.rSquared.isRSquaredValid()) { + it.makePoints(gen, points) + } + } + } + + @JvmStatic + private fun Curve.makePoints(gen: JsonGenerator, points: DoubleArray) { + gen.writeArrayFieldStart("points") + + for (x in points) { + gen.writeStartObject() + gen.writeNumberField("x", x) + gen.writeNumberField("y", this(x)) + gen.writeEndObject() + } + + gen.writeEndArray() + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt index 406775746..e9cc57521 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt @@ -7,7 +7,6 @@ enum class AutoFocusState { EXPOSURED, ANALYSING, ANALYSED, - FOCUS_POINT_ADDED, CURVE_FITTED, FAILED, FINISHED, diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index c9a7da6db..249e5c51d 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -166,9 +166,8 @@ data class AutoFocusTask( if (exited || cancellationToken.isCancelled) break val finalFocusPoint = determineFinalFocusPoint() - val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken) - if (!goodAutoFocus) { + if (finalFocusPoint == null || !validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken)) { if (cancellationToken.isCancelled) { break } else if (numberOfAttempts < request.totalNumberOfAttempts) { @@ -203,13 +202,13 @@ data class AutoFocusTask( LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser) } - private fun determineFinalFocusPoint(): CurvePoint { + private fun determineFinalFocusPoint(): CurvePoint? { return when (request.fittingMode) { AutoFocusFittingMode.TRENDLINES -> trendLineCurve!!.intersection - AutoFocusFittingMode.PARABOLIC -> parabolicCurve!!.minimum - AutoFocusFittingMode.TREND_PARABOLIC -> trendLineCurve!!.intersection midPoint parabolicCurve!!.minimum - AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve!!.minimum - AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLineCurve!!.intersection midPoint trendLineCurve!!.minimum + AutoFocusFittingMode.PARABOLIC -> parabolicCurve?.minimum + AutoFocusFittingMode.TREND_PARABOLIC -> parabolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) + AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve?.minimum + AutoFocusFittingMode.TREND_HYPERBOLIC -> hyperbolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) } } @@ -286,8 +285,6 @@ data class AutoFocusTask( LOG.info("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint) computeCurveFittings() - - sendEvent(AutoFocusState.FOCUS_POINT_ADDED) } } } @@ -364,14 +361,17 @@ data class AutoFocusTask( private fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) { val chart = when (state) { - AutoFocusState.FOCUS_POINT_ADDED -> AutoFocusEvent.makeChart(focusPoints, trendLineCurve, parabolicCurve, hyperbolicCurve) + AutoFocusState.FINISHED, + AutoFocusState.CURVE_FITTED -> { + val predictedFocusPoint = determinedFocusPoint ?: determineFinalFocusPoint() + val (minX, minY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[0] + val (maxX, maxY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[focusPoints.lastIndex] + AutoFocusEvent.Chart(predictedFocusPoint, minX, minY, maxX, maxY, trendLineCurve, parabolicCurve, hyperbolicCurve) + } else -> null } - val (minX, minY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[0] - val (maxX, maxY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[focusPoints.lastIndex] - - onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, minX, minY, maxX, maxY, chart, capture)) + onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, chart, capture)) } override fun reset() { diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt b/api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt new file mode 100644 index 000000000..3d5db1549 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt @@ -0,0 +1,21 @@ +package nebulosa.api.autofocus + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import nebulosa.curve.fitting.CurvePoint +import org.springframework.stereotype.Component + +@Component +class CurvePointSerializer : StdSerializer(CurvePoint::class.java) { + + override fun serialize(point: CurvePoint?, gen: JsonGenerator, provider: SerializerProvider) { + if (point == null) gen.writeNull() + else { + gen.writeStartObject() + gen.writeNumberField("x", point.x) + gen.writeNumberField("y", point.y) + gen.writeEndObject() + } + } +} diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt index 76197a043..a71b9c688 100644 --- a/api/src/test/kotlin/APITest.kt +++ b/api/src/test/kotlin/APITest.kt @@ -59,12 +59,12 @@ class APITest : StringSpec() { // AUTO FOCUS. "Auto Focus Start" { - connect() + connect("192.168.31.153", 11111, ConnectionType.ALPACA) delay(1000) cameraConnect() focuserConnect() delay(1000) - focuserMoveTo(position = 36000) + // focuserMoveTo(position = 8100) delay(2000) autoFocusStart() } @@ -162,9 +162,9 @@ class APITest : StringSpec() { companion object { private const val BASE_URL = "http://localhost:7000" - private const val CAMERA_NAME = "CCD Simulator" + private const val CAMERA_NAME = "Sky Simulator" private const val MOUNT_NAME = "Telescope Simulator" - private const val FOCUSER_NAME = "Focuser Simulator" + private const val FOCUSER_NAME = "ZWO Focuser (1)" @JvmStatic private val EXPOSURE_TIME = Duration.ofSeconds(5) @JvmStatic private val CAPTURES_PATH = Path.of("/home/tiagohm/Git/nebulosa/data/captures") diff --git a/desktop/README.md b/desktop/README.md index b6880a99a..2f0bae907 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -50,6 +50,10 @@ The complete integrated solution for all of your astronomical imaging needs. ![](flat-wizard.png) +## Auto Focus + +![](auto-focus.png) + ## Sequencer ![](sequencer.png) diff --git a/desktop/auto-focus.png b/desktop/auto-focus.png new file mode 100644 index 0000000000000000000000000000000000000000..cea2e19e30528ab03503de78e02e737b41cad61a GIT binary patch literal 33231 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 m04YAr5iThHyNKc&9QGPV!}*16Nw5^R
-
+
@@ -29,74 +29,94 @@
-
-
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
- - - - -
-
-
-
+
+ + +
+
- - + +
-
+
- - +
- - + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
-
-
+ + +
+ +
+
+
{ + return Math.abs(item.parsed.y) - 0.1 > 0.0 + }, + callbacks: { + title: (item) => { + return `${item[0].parsed.x.toFixed(0)}` + }, + label: (item) => { + return `${item.parsed.y.toFixed(1)}` + } + } + }, + zoom: { + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: false, + }, + mode: 'x', + scaleMode: 'xy', + }, + pan: { + enabled: true, + mode: 'xy', + }, + limits: { + x: { + min: 0, + max: 100, + }, + y: { + min: 0, + max: 20, + }, + } + }, + }, + scales: { + y: { + stacked: false, + beginAtZero: true, + min: 0, + max: 20, + ticks: { + autoSkip: true, + count: 5, + callback: (value) => { + return (value as number).toFixed(1).padStart(2, ' ') + } + }, + border: { + display: true, + dash: [2, 4], + }, + grid: { + display: true, + drawTicks: false, + drawOnChartArea: true, + color: '#212121', + } + }, + x: { + type: 'linear', + stacked: false, + min: 0, + max: 100, + border: { + display: true, + dash: [2, 4], + }, + ticks: { + autoSkip: true, + count: 11, + maxRotation: 0, + minRotation: 0, + }, + grid: { + display: true, + drawTicks: false, + color: '#212121', + } + } + } + } + + readonly chartData: ChartData = { + datasets: [ + // TREND LINE (LEFT). + { + type: 'line', + fill: false, + borderColor: '#F44336', + borderWidth: 1, + data: [], + pointRadius: 0, + pointHitRadius: 0, + }, + // TREND LINE (RIGHT). + { + type: 'line', + fill: false, + borderColor: '#F44336', + borderWidth: 1, + data: [], + pointRadius: 0, + pointHitRadius: 0, + }, + // PARABOLIC. + { + type: 'line', + fill: false, + borderColor: '#03A9F4', + borderWidth: 1, + data: [], + pointRadius: 0, + pointHitRadius: 0, + }, + // HYPERBOLIC. + { + type: 'line', + tension: 1, + fill: false, + borderColor: '#4CAF50', + borderWidth: 1, + data: [], + pointRadius: 0, + pointHitRadius: 0, + }, + // FOCUS POINTS. + { + type: 'scatter', + fill: false, + borderColor: '#7E57C2', + borderWidth: 1, + data: [], + pointRadius: 2, + pointHitRadius: 2, + }, + // PREDICTED FOCUS POINT. + { + type: 'scatter', + backgroundColor: '#FFA726', + borderColor: '#FFA726', + borderWidth: 1, + data: [], + pointRadius: 4, + pointHitRadius: 4, + pointStyle: 'cross', + }, + ] + } + constructor( app: AppComponent, private api: ApiService, @@ -114,12 +286,16 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { this.cameraExposure.handleCameraCaptureEvent(event.capture, true) } - if (event.state === 'FOCUS_POINT_ADDED') { - const chart = event.chart! + if (event.state === 'CURVE_FITTED') { + this.focusPoints.push(event.focusPoint!) } else if (event.state === 'ANALYSED') { this.starCount = event.starCount this.starHFD = event.starHFD } + + if (event.chart) { + this.updateChart(event.chart) + } }) }) @@ -161,6 +337,10 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { async start() { await this.openCameraImage() + + this.clearChart() + this.stepSizeForScale = this.request.stepSize + this.request.starDetector = this.preference.starDetectionOptions('ASTAP').get() return this.api.autoFocusStart(this.camera, this.focuser, this.request) } @@ -173,12 +353,64 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { return this.browserWindow.openCameraImage(this.camera, 'ALIGNMENT') } + private updateChart(data: CurveChart) { + if (data.trendLine) { + this.chartData.datasets[0].data = data.trendLine.left.points + this.chartData.datasets[1].data = data.trendLine.right.points + } else { + this.chartData.datasets[0].data = [] + this.chartData.datasets[1].data = [] + } + + if (data.parabolic) { + this.chartData.datasets[2].data = data.parabolic.points + } else { + this.chartData.datasets[2].data = [] + } + + if (data.hyperbolic) { + this.chartData.datasets[3].data = data.hyperbolic.points + } else { + this.chartData.datasets[3].data = [] + } + + this.chartData.datasets[4].data = this.focusPoints + + if (data.predictedFocusPoint) { + this.chartData.datasets[5].data = [data.predictedFocusPoint] + } else { + this.chartData.datasets[5].data = [] + } + + const scales = this.chartOptions.scales! + scales.x!.min = Math.max(0, data.minX - this.stepSizeForScale) + scales.x!.max = data.maxX + this.stepSizeForScale + scales.y!.max = (data.maxY || 19) + 1 + + const zoom = this.chartOptions.plugins!.zoom! + zoom.limits!.x!.min = scales.x!.min + zoom.limits!.x!.max = scales.x!.max + zoom.limits!.y!.max = scales.y!.max + + this.chart?.refresh() + } + + private clearChart() { + this.focusPoints = [] + + for (let i = 0; i < this.chartData.datasets.length; i++) { + this.chartData.datasets[i].data = [] + } + + this.chart?.refresh() + } + private loadPreference() { const preference = this.preference.autoFocusPreference.get() this.request.fittingMode = preference.fittingMode ?? 'HYPERBOLIC' this.request.initialOffsetSteps = preference.initialOffsetSteps ?? 4 - // this.request.rSquaredThreshold + this.request.rSquaredThreshold = preference.rSquaredThreshold ?? 0.5 this.request.stepSize = preference.stepSize ?? 100 this.request.totalNumberOfAttempts = preference.totalNumberOfAttempts ?? 1 this.request.backlashCompensation.mode = preference.backlashCompensation.mode ?? 'NONE' diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 2d8551e46..e5316013e 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -2,7 +2,7 @@
-
+
Host
-
+
@@ -31,8 +31,8 @@
- -
+ +
- +
North East
- -
-
+ +
+
Settle tolerance (px)
-
+
- +
-
+
`${i}`), + labels: Array.from({ length: 100 }), datasets: [ // RA. { @@ -164,12 +164,12 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { }, scales: { y: { - stacked: true, + stacked: false, beginAtZero: false, min: -16, max: 16, ticks: { - autoSkip: false, + autoSkip: true, count: 7, callback: (value) => { return (value as number).toFixed(1).padStart(5, ' ') @@ -187,7 +187,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { } }, x: { - stacked: true, + stacked: false, min: 0, max: 100, border: { @@ -195,11 +195,13 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { dash: [2, 4], }, ticks: { - stepSize: 5.0, + autoSkip: true, + count: 11, maxRotation: 0, minRotation: 0, callback: (value) => { - return (value as number).toFixed(0) + const a = value as number + return (a - Math.trunc(a) > 0) ? undefined : a.toFixed(0) } }, grid: { diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index e2d85cfee..c5135e1fc 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -351,7 +351,7 @@ export class EnumPipe implements PipeTransform { 'EXPOSURED': 'Exposured', 'ANALYSING': 'Analysing', 'ANALYSED': 'Analysed', - 'FOCUS_POINT_ADDED': 'Focus point added', + 'CURVE_FITTED': 'Curve fitted', } transform(value: EnumPipeKey) { diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 041cdc134..3b71ad203 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -61,7 +61,7 @@ export class BrowserWindowService { } openGuider(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'guider', width: 425, height: 438 }) + Object.assign(options, { icon: 'guider', width: 440, height: 455 }) this.openWindow({ ...options, id: 'guider', path: 'guider', data: undefined }) } @@ -105,7 +105,7 @@ export class BrowserWindowService { } openAutoFocus(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'auto-focus', width: 410, height: 370 }) + Object.assign(options, { icon: 'auto-focus', width: 425, height: 420 }) this.openWindow({ ...options, id: 'auto-focus', path: 'auto-focus', data: undefined }) } diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts index 42111d56b..4b84c4c4a 100644 --- a/desktop/src/shared/types/autofocus.type.ts +++ b/desktop/src/shared/types/autofocus.type.ts @@ -2,7 +2,7 @@ import { Point } from 'electron' import { CameraCaptureEvent, CameraStartCapture } from './camera.types' import { EMPTY_STAR_DETECTION_OPTIONS, StarDetectionOptions } from './settings.types' -export type AutoFocusState = 'IDLE' | 'MOVING' | 'EXPOSURING' | 'EXPOSURED' | 'ANALYSING' | 'ANALYSED' | 'FOCUS_POINT_ADDED' | 'FAILED' | 'FINISHED' +export type AutoFocusState = 'IDLE' | 'MOVING' | 'EXPOSURING' | 'EXPOSURED' | 'ANALYSING' | 'ANALYSED' | 'CURVE_FITTED' | 'FAILED' | 'FINISHED' export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC' @@ -71,7 +71,12 @@ export interface TrendLineCurve extends Curve { intersection: Point } -export interface Chart { +export interface CurveChart { + predictedFocusPoint?: Point + minX: number + maxX: number + minY: number + maxY: number trendLine?: TrendLineCurve parabolic?: ParabolicCurve hyperbolic?: HyperbolicCurve @@ -83,10 +88,6 @@ export interface AutoFocusEvent { determinedFocusPoint?: Point starCount: number starHFD: number - minX: number - maxX: number - minY: number - maxY: number - chart?: Chart + chart?: CurveChart capture?: CameraCaptureEvent } diff --git a/desktop/src/shared/utils/comparators.ts b/desktop/src/shared/utils/comparators.ts index edf740cc9..ba83ab618 100644 --- a/desktop/src/shared/utils/comparators.ts +++ b/desktop/src/shared/utils/comparators.ts @@ -5,6 +5,7 @@ export type Comparator = (a: T, b: T) => number export const textComparator: Comparator = (a: string, b: string) => a.localeCompare(b) export const numberComparator: Comparator = (a: number, b: number) => a - b export const deviceComparator: Comparator = (a: Device, b: Device) => textComparator(a.name, b.name) +export const numericTextComparator: Comparator = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }) export function negateComparator(comparator: Comparator): Comparator { return (a, b) => -comparator(a, b) From 174966d90225ea243d3ca711e4e17fc3c251eabb Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 1 Jun 2024 18:15:14 -0300 Subject: [PATCH 41/45] [api][desktop]: Improve event handling --- .../api/alignment/polar/darv/DARVExecutor.kt | 5 +- .../api/alignment/polar/tppa/TPPAExecutor.kt | 5 +- .../api/autofocus/AutoFocusExecutor.kt | 8 ++-- .../api/cameras/CameraCaptureExecutor.kt | 4 +- .../nebulosa/api/cameras/CameraController.kt | 9 +++- ...ameraEventHandler.kt => CameraEventHub.kt} | 30 ++++-------- .../api/cameras/CameraMessageEvent.kt | 7 +-- .../nebulosa/api/cameras/CameraService.kt | 7 ++- .../api/connection/ConnectionEventHandler.kt | 44 ----------------- .../api/connection/ConnectionEventHub.kt | 44 +++++++++++++++++ .../api/connection/ConnectionService.kt | 6 +-- .../nebulosa/api/devices/DeviceEventHub.kt | 47 +++++++++++++++++++ .../DeviceMessageEvent.kt | 3 +- .../api/focusers/FocuserController.kt | 5 ++ ...userEventHandler.kt => FocuserEventHub.kt} | 26 +++------- .../api/focusers/FocuserMessageEvent.kt | 8 ++-- .../nebulosa/api/focusers/FocuserService.kt | 6 ++- .../api/guiding/GuideOutputController.kt | 5 ++ .../api/guiding/GuideOutputEventAware.kt | 9 ++++ ...EventHandler.kt => GuideOutputEventHub.kt} | 42 +++++++---------- .../api/guiding/GuideOutputMessageEvent.kt | 8 ++-- .../api/guiding/GuideOutputService.kt | 6 ++- .../api/guiding/GuiderMessageEvent.kt | 5 +- .../nebulosa/api/indi/INDIMessageEvent.kt | 2 +- .../nebulosa/api/mounts/MountController.kt | 5 ++ ...{MountEventHandler.kt => MountEventHub.kt} | 26 +++------- .../nebulosa/api/mounts/MountMessageEvent.kt | 7 +-- .../nebulosa/api/mounts/MountService.kt | 18 +++++-- .../api/rotators/RotatorController.kt | 5 ++ .../api/rotators/RotatorEventAware.kt | 8 ++++ ...atorEventHandler.kt => RotatorEventHub.kt} | 26 +++------- .../api/rotators/RotatorMessageEvent.kt | 7 +-- .../nebulosa/api/rotators/RotatorService.kt | 6 ++- .../api/sequencer/SequencerExecutor.kt | 11 +++-- .../nebulosa/api/wheels/WheelController.kt | 5 ++ ...{WheelEventHandler.kt => WheelEventHub.kt} | 26 +++------- .../nebulosa/api/wheels/WheelMessageEvent.kt | 8 ++-- .../nebulosa/api/wheels/WheelService.kt | 6 ++- .../api/wizard/flat/FlatWizardExecutor.kt | 5 +- .../src/app/alignment/alignment.component.ts | 27 ++++++++--- .../src/app/autofocus/autofocus.component.ts | 18 ++++++- desktop/src/app/camera/camera.component.ts | 12 ++++- .../app/filterwheel/filterwheel.component.ts | 25 ++++++++-- .../app/flat-wizard/flat-wizard.component.ts | 22 +++++++-- desktop/src/app/focuser/focuser.component.ts | 11 ++++- desktop/src/app/guider/guider.component.html | 3 +- desktop/src/app/guider/guider.component.ts | 16 ++++++- desktop/src/app/mount/mount.component.ts | 16 +++++-- desktop/src/app/rotator/rotator.component.ts | 13 ++++- .../app/sequencer/sequencer.component.html | 11 +++-- .../src/app/sequencer/sequencer.component.ts | 37 +++++++++++++-- desktop/src/shared/services/api.service.ts | 38 ++++++++++++--- desktop/src/shared/services/pinger.service.ts | 32 +++++++++++++ 53 files changed, 513 insertions(+), 278 deletions(-) rename api/src/main/kotlin/nebulosa/api/cameras/{CameraEventHandler.kt => CameraEventHub.kt} (64%) delete mode 100644 api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt create mode 100644 api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHub.kt create mode 100644 api/src/main/kotlin/nebulosa/api/devices/DeviceEventHub.kt rename api/src/main/kotlin/nebulosa/api/{messages => devices}/DeviceMessageEvent.kt (62%) rename api/src/main/kotlin/nebulosa/api/focusers/{FocuserEventHandler.kt => FocuserEventHub.kt} (68%) create mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventAware.kt rename api/src/main/kotlin/nebulosa/api/guiding/{GuideOutputEventHandler.kt => GuideOutputEventHub.kt} (56%) rename api/src/main/kotlin/nebulosa/api/mounts/{MountEventHandler.kt => MountEventHub.kt} (68%) create mode 100644 api/src/main/kotlin/nebulosa/api/rotators/RotatorEventAware.kt rename api/src/main/kotlin/nebulosa/api/rotators/{RotatorEventHandler.kt => RotatorEventHub.kt} (68%) rename api/src/main/kotlin/nebulosa/api/wheels/{WheelEventHandler.kt => WheelEventHub.kt} (68%) create mode 100644 desktop/src/shared/services/pinger.service.ts diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt index 1033e04c2..deedf9e0f 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt @@ -2,6 +2,7 @@ package nebulosa.api.alignment.polar.darv import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.cameras.CameraEventAware import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService import nebulosa.indi.device.camera.Camera @@ -21,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap class DARVExecutor( private val messageService: MessageService, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, -) : Consumer { +) : Consumer, CameraEventAware { private val jobs = ConcurrentHashMap.newKeySet(1) @@ -30,7 +31,7 @@ class DARVExecutor( } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt index c7808faaa..2bc5913e2 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt @@ -2,6 +2,7 @@ package nebulosa.api.alignment.polar.tppa import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.cameras.CameraEventAware import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService import nebulosa.indi.device.camera.Camera @@ -18,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap class TPPAExecutor( private val messageService: MessageService, private val httpClient: OkHttpClient, -) : Consumer { +) : Consumer, CameraEventAware { private val jobs = ConcurrentHashMap.newKeySet(1) @@ -27,7 +28,7 @@ class TPPAExecutor( } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt index bfdd6698d..7225cbc12 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt @@ -2,6 +2,8 @@ package nebulosa.api.autofocus import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.cameras.CameraEventAware +import nebulosa.api.focusers.FocuserEventAware import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService import nebulosa.indi.device.camera.Camera @@ -17,17 +19,17 @@ import java.util.concurrent.ConcurrentHashMap @Subscriber class AutoFocusExecutor( private val messageService: MessageService, -) : Consumer { +) : Consumer, CameraEventAware, FocuserEventAware { private val jobs = ConcurrentHashMap.newKeySet(2) @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFocuserEvent(event: FocuserEvent) { + override fun handleFocuserEvent(event: FocuserEvent) { jobs.find { it.task.focuser === event.device }?.handleFocuserEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index ca75076ef..0c0a83e91 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -18,12 +18,12 @@ class CameraCaptureExecutor( private val messageService: MessageService, private val guider: Guider, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, -) : Consumer { +) : Consumer, CameraEventAware { private val jobs = ConcurrentHashMap.newKeySet(2) @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt index 7e64d19d7..58f4e460b 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt @@ -67,7 +67,12 @@ class CameraController( } @GetMapping("{camera}/capture/status") - fun statusCapture(camera: Camera): CameraCaptureEvent? { - return cameraService.statusCapture(camera) + fun captureStatus(camera: Camera): CameraCaptureEvent? { + return cameraService.captureStatus(camera) + } + + @PutMapping("{camera}/listen") + fun listen(camera: Camera) { + cameraService.listen(camera) } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHub.kt similarity index 64% rename from api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt rename to api/src/main/kotlin/nebulosa/api/cameras/CameraEventHub.kt index 9a0d6988b..8eb839a6e 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHub.kt @@ -1,7 +1,7 @@ package nebulosa.api.cameras -import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.devices.DeviceEventHub import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.camera.Camera @@ -11,45 +11,31 @@ import nebulosa.indi.device.camera.CameraEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component -import java.io.Closeable -import java.util.concurrent.TimeUnit @Component @Subscriber -class CameraEventHandler( +class CameraEventHub( private val messageService: MessageService, -) : Closeable { - - private val throttler = PublishSubject.create() - - init { - throttler - .throttleLast(1000, TimeUnit.MILLISECONDS) - .subscribe { sendUpdate(it.device!!) } - } +) : DeviceEventHub(), CameraEventAware { @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { when (event) { - is PropertyChangedEvent -> throttler.onNext(event) + is PropertyChangedEvent -> onNext(event) is CameraAttached -> sendMessage(CAMERA_ATTACHED, event.device) is CameraDetached -> sendMessage(CAMERA_DETACHED, event.device) } } @Suppress("NOTHING_TO_INLINE") - private inline fun sendMessage(eventName: String, device: Camera) { - messageService.sendMessage(CameraMessageEvent(eventName, device)) + private inline fun sendMessage(eventName: String, camera: Camera) { + messageService.sendMessage(CameraMessageEvent(eventName, camera)) } - fun sendUpdate(device: Camera) { + override fun sendUpdate(device: Camera) { sendMessage(CAMERA_UPDATED, device) } - override fun close() { - throttler.onComplete() - } - companion object { const val CAMERA_UPDATED = "CAMERA.UPDATED" diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt index 4e69fe521..a8fb5ec70 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt @@ -1,9 +1,6 @@ package nebulosa.api.cameras -import nebulosa.api.messages.DeviceMessageEvent +import nebulosa.api.devices.DeviceMessageEvent import nebulosa.indi.device.camera.Camera -data class CameraMessageEvent( - override val eventName: String, - override val device: Camera, -) : DeviceMessageEvent +data class CameraMessageEvent(override val eventName: String, override val device: Camera) : DeviceMessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt index 074b2cdbb..ddd92e97a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt @@ -11,6 +11,7 @@ import kotlin.io.path.isDirectory class CameraService( private val capturesPath: Path, private val cameraCaptureExecutor: CameraCaptureExecutor, + private val cameraEventHub: CameraEventHub, ) { fun connect(camera: Camera) { @@ -47,7 +48,11 @@ class CameraService( cameraCaptureExecutor.stop(camera) } - fun statusCapture(camera: Camera): CameraCaptureEvent? { + fun captureStatus(camera: Camera): CameraCaptureEvent? { return cameraCaptureExecutor.status(camera) } + + fun listen(camera: Camera) { + cameraEventHub.listen(camera) + } } diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt deleted file mode 100644 index e08e9538b..000000000 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt +++ /dev/null @@ -1,44 +0,0 @@ -package nebulosa.api.connection - -import nebulosa.api.cameras.CameraEventHandler -import nebulosa.api.focusers.FocuserEventHandler -import nebulosa.api.guiding.GuideOutputEventHandler -import nebulosa.api.mounts.MountEventHandler -import nebulosa.api.rotators.RotatorEventHandler -import nebulosa.api.wheels.WheelEventHandler -import nebulosa.indi.device.DeviceConnectionEvent -import nebulosa.indi.device.DeviceEvent -import nebulosa.indi.device.DeviceEventHandler -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount -import nebulosa.indi.device.rotator.Rotator -import org.springframework.stereotype.Component - -@Component -class ConnectionEventHandler( - private val cameraEventHandler: CameraEventHandler, - private val mountEventHandler: MountEventHandler, - private val focuserEventHandler: FocuserEventHandler, - private val wheelEventHandler: WheelEventHandler, - private val guideOutputEventHandler: GuideOutputEventHandler, - private val rotatorEventHandler: RotatorEventHandler, -) : DeviceEventHandler.EventReceived { - - @Suppress("CascadeIf") - override fun onEventReceived(event: DeviceEvent<*>) { - if (event is DeviceConnectionEvent) { - val device = event.device ?: return - - if (device is Camera) cameraEventHandler.sendUpdate(device) - else if (device is Mount) mountEventHandler.sendUpdate(device) - else if (device is Focuser) focuserEventHandler.sendUpdate(device) - else if (device is FilterWheel) wheelEventHandler.sendUpdate(device) - else if (device is Rotator) rotatorEventHandler.sendUpdate(device) - - if (device is GuideOutput) guideOutputEventHandler.sendUpdate(device) - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHub.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHub.kt new file mode 100644 index 000000000..7d2ad055b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHub.kt @@ -0,0 +1,44 @@ +package nebulosa.api.connection + +import nebulosa.api.cameras.CameraEventHub +import nebulosa.api.focusers.FocuserEventHub +import nebulosa.api.guiding.GuideOutputEventHub +import nebulosa.api.mounts.MountEventHub +import nebulosa.api.rotators.RotatorEventHub +import nebulosa.api.wheels.WheelEventHub +import nebulosa.indi.device.DeviceConnectionEvent +import nebulosa.indi.device.DeviceEvent +import nebulosa.indi.device.DeviceEventHandler +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.guide.GuideOutput +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator +import org.springframework.stereotype.Component + +@Component +class ConnectionEventHub( + private val cameraEventHub: CameraEventHub, + private val mountEventHub: MountEventHub, + private val focuserEventHub: FocuserEventHub, + private val wheelEventHub: WheelEventHub, + private val guideOutputEventHub: GuideOutputEventHub, + private val rotatorEventHub: RotatorEventHub, +) : DeviceEventHandler.EventReceived { + + @Suppress("CascadeIf") + override fun onEventReceived(event: DeviceEvent<*>) { + if (event is DeviceConnectionEvent) { + val device = event.device ?: return + + if (device is Camera) cameraEventHub.sendUpdate(device) + else if (device is Mount) mountEventHub.sendUpdate(device) + else if (device is Focuser) focuserEventHub.sendUpdate(device) + else if (device is FilterWheel) wheelEventHub.sendUpdate(device) + else if (device is Rotator) rotatorEventHub.sendUpdate(device) + + if (device is GuideOutput) guideOutputEventHub.sendUpdate(device) + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt index 55d135d77..9f8810ef3 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt @@ -26,7 +26,7 @@ import java.io.Closeable @Service class ConnectionService( private val eventBus: EventBus, - private val connectionEventHandler: ConnectionEventHandler, + private val connectionEventHub: ConnectionEventHub, private val alpacaHttpClient: OkHttpClient, private val messageService: MessageService, ) : Closeable { @@ -61,7 +61,7 @@ class ConnectionService( ConnectionType.INDI -> { val client = INDIClient(host, port) client.registerDeviceEventHandler(DeviceEventHandler.EventReceived(eventBus::post)) - client.registerDeviceEventHandler(connectionEventHandler) + client.registerDeviceEventHandler(connectionEventHub) client.registerDeviceEventHandler(DeviceEventHandler.ConnectionClosed { sendConnectionClosedEvent(client) }) client.start() client @@ -69,7 +69,7 @@ class ConnectionService( else -> { val client = AlpacaClient(host, port, alpacaHttpClient) client.registerDeviceEventHandler(DeviceEventHandler.EventReceived(eventBus::post)) - client.registerDeviceEventHandler(connectionEventHandler) + client.registerDeviceEventHandler(connectionEventHub) client.registerDeviceEventHandler(DeviceEventHandler.ConnectionClosed { sendConnectionClosedEvent(client) }) client.discovery() client diff --git a/api/src/main/kotlin/nebulosa/api/devices/DeviceEventHub.kt b/api/src/main/kotlin/nebulosa/api/devices/DeviceEventHub.kt new file mode 100644 index 000000000..34c12f05e --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/devices/DeviceEventHub.kt @@ -0,0 +1,47 @@ +package nebulosa.api.devices + +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.indi.device.Device +import nebulosa.indi.device.DeviceEvent +import nebulosa.log.loggerFor +import java.io.Closeable +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +abstract class DeviceEventHub> : Closeable { + + private val throttler = PublishSubject.create() + private val listenable = ConcurrentHashMap(2) + + init { + throttler + .throttleLast(1000, TimeUnit.MILLISECONDS) + .subscribe { + val device = it.device ?: return@subscribe + val lastTime = listenable[device] ?: return@subscribe + val currentTime = System.currentTimeMillis() + + if (currentTime - lastTime < SEND_UPDATE_INTERVAL) { + sendUpdate(device) + } else { + listenable.remove(device) + LOG.warn("device {} ({}) is no longer listenable", device.name, device.id) + } + } + } + + abstract fun sendUpdate(device: D) + + protected fun onNext(event: E) = throttler.onNext(event) + + fun listen(device: D) = listenable.put(device, System.currentTimeMillis()) != null + + override fun close() = throttler.onComplete() + + companion object { + + const val SEND_UPDATE_INTERVAL = 60000 // 1 min. + + @JvmStatic private val LOG = loggerFor>() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/devices/DeviceMessageEvent.kt similarity index 62% rename from api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt rename to api/src/main/kotlin/nebulosa/api/devices/DeviceMessageEvent.kt index 248354ff7..d20bae1de 100644 --- a/api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/devices/DeviceMessageEvent.kt @@ -1,5 +1,6 @@ -package nebulosa.api.messages +package nebulosa.api.devices +import nebulosa.api.messages.MessageEvent import nebulosa.indi.device.Device interface DeviceMessageEvent : MessageEvent { diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt index 382c9d1a4..ee0347d2a 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt @@ -69,4 +69,9 @@ class FocuserController( ) { focuserService.sync(focuser, steps) } + + @PutMapping("{focuser}/listen") + fun listen(focuser: Focuser) { + focuserService.listen(focuser) + } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHub.kt similarity index 68% rename from api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt rename to api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHub.kt index 91af0802a..7519aee3c 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHub.kt @@ -1,7 +1,7 @@ package nebulosa.api.focusers -import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.devices.DeviceEventHub import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.focuser.Focuser @@ -11,27 +11,17 @@ import nebulosa.indi.device.focuser.FocuserEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component -import java.io.Closeable -import java.util.concurrent.TimeUnit @Component @Subscriber -class FocuserEventHandler( +class FocuserEventHub( private val messageService: MessageService, -) : Closeable { - - private val throttler = PublishSubject.create() - - init { - throttler - .throttleLast(1000, TimeUnit.MILLISECONDS) - .subscribe { sendUpdate(it.device!!) } - } +) : DeviceEventHub(), FocuserEventAware { @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFocuserEvent(event: FocuserEvent) { + override fun handleFocuserEvent(event: FocuserEvent) { when (event) { - is PropertyChangedEvent -> throttler.onNext(event) + is PropertyChangedEvent -> onNext(event) is FocuserAttached -> sendMessage(FOCUSER_ATTACHED, event.device) is FocuserDetached -> sendMessage(FOCUSER_DETACHED, event.device) } @@ -42,14 +32,10 @@ class FocuserEventHandler( messageService.sendMessage(FocuserMessageEvent(eventName, device)) } - fun sendUpdate(device: Focuser) { + override fun sendUpdate(device: Focuser) { sendMessage(FOCUSER_UPDATED, device) } - override fun close() { - throttler.onComplete() - } - companion object { const val FOCUSER_UPDATED = "FOCUSER.UPDATED" diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt index cae010f3f..4a595146b 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt @@ -1,9 +1,7 @@ package nebulosa.api.focusers -import nebulosa.api.messages.DeviceMessageEvent +import nebulosa.api.devices.DeviceMessageEvent import nebulosa.indi.device.focuser.Focuser -data class FocuserMessageEvent( - override val eventName: String, - override val device: Focuser, -) : DeviceMessageEvent +data class FocuserMessageEvent(override val eventName: String, override val device: Focuser) : + DeviceMessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserService.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserService.kt index b0c0553ad..f0084dfdb 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserService.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserService.kt @@ -4,7 +4,7 @@ import nebulosa.indi.device.focuser.Focuser import org.springframework.stereotype.Service @Service -class FocuserService { +class FocuserService(private val focuserEventHub: FocuserEventHub) { fun connect(focuser: Focuser) { focuser.connect() @@ -33,4 +33,8 @@ class FocuserService { fun sync(focuser: Focuser, steps: Int) { focuser.syncFocusTo(steps) } + + fun listen(focuser: Focuser) { + focuserEventHub.listen(focuser) + } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt index d7d0575d1..fd5edb095 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -43,4 +43,9 @@ class GuideOutputController( ) { guideOutputService.pulse(guideOutput, direction, duration) } + + @PutMapping("{guideOutput}/listen") + fun listen(guideOutput: GuideOutput) { + guideOutputService.listen(guideOutput) + } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventAware.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventAware.kt new file mode 100644 index 000000000..f53e0e2f2 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventAware.kt @@ -0,0 +1,9 @@ +package nebulosa.api.guiding + +import nebulosa.indi.device.DeviceEvent +import nebulosa.indi.device.guide.GuideOutput + +fun interface GuideOutputEventAware { + + fun handleGuideOutputEvent(event: DeviceEvent) +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHub.kt similarity index 56% rename from api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt rename to api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHub.kt index daa40c188..0621e1a1e 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHub.kt @@ -1,7 +1,7 @@ package nebulosa.api.guiding -import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.devices.DeviceEventHub import nebulosa.api.messages.MessageService import nebulosa.indi.device.DeviceEvent import nebulosa.indi.device.PropertyChangedEvent @@ -11,32 +11,26 @@ import nebulosa.indi.device.guide.GuideOutputDetached import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component -import java.io.Closeable -import java.util.concurrent.TimeUnit @Component @Subscriber -class GuideOutputEventHandler( +class GuideOutputEventHub( private val messageService: MessageService, -) : Closeable { - - private val throttler = PublishSubject.create>() - - init { - throttler - .throttleLast(1000, TimeUnit.MILLISECONDS) - .subscribe { sendUpdate(it.device!!) } - } +) : DeviceEventHub>(), GuideOutputEventAware { @Subscribe(threadMode = ThreadMode.ASYNC) - fun onGuideOutputEvent(event: DeviceEvent) { - if (event.device!!.canPulseGuide && event is PropertyChangedEvent) { - throttler.onNext(event) - } - - when (event) { - is GuideOutputAttached -> sendMessage(GUIDE_OUTPUT_ATTACHED, event.device) - is GuideOutputDetached -> sendMessage(GUIDE_OUTPUT_DETACHED, event.device) + override fun handleGuideOutputEvent(event: DeviceEvent) { + val device = event.device ?: return + + if (event is PropertyChangedEvent) { + if (device.canPulseGuide) { + onNext(event) + } + } else { + when (event) { + is GuideOutputAttached -> sendMessage(GUIDE_OUTPUT_ATTACHED, event.device) + is GuideOutputDetached -> sendMessage(GUIDE_OUTPUT_DETACHED, event.device) + } } } @@ -45,14 +39,10 @@ class GuideOutputEventHandler( messageService.sendMessage(GuideOutputMessageEvent(eventName, device)) } - fun sendUpdate(device: GuideOutput) { + override fun sendUpdate(device: GuideOutput) { sendMessage(GUIDE_OUTPUT_UPDATED, device) } - override fun close() { - throttler.onComplete() - } - companion object { const val GUIDE_OUTPUT_UPDATED = "GUIDE_OUTPUT.UPDATED" diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt index b19092080..1f372c61f 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt @@ -1,9 +1,7 @@ package nebulosa.api.guiding -import nebulosa.api.messages.DeviceMessageEvent +import nebulosa.api.devices.DeviceMessageEvent import nebulosa.indi.device.guide.GuideOutput -data class GuideOutputMessageEvent( - override val eventName: String, - override val device: GuideOutput, -) : DeviceMessageEvent +data class GuideOutputMessageEvent(override val eventName: String, override val device: GuideOutput) : + DeviceMessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputService.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputService.kt index 4fd7e54cb..c26d9d46a 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputService.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputService.kt @@ -6,7 +6,7 @@ import org.springframework.stereotype.Service import java.time.Duration @Service -class GuideOutputService { +class GuideOutputService(private val guideOutputEventHub: GuideOutputEventHub) { fun connect(guideOutput: GuideOutput) { guideOutput.connect() @@ -26,4 +26,8 @@ class GuideOutputService { } } } + + fun listen(guideOutput: GuideOutput) { + guideOutputEventHub.listen(guideOutput) + } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt index 5cd58eadd..b7612a5d4 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt @@ -2,7 +2,4 @@ package nebulosa.api.guiding import nebulosa.api.messages.MessageEvent -data class GuiderMessageEvent( - override val eventName: String, - val data: Any? = null, -) : MessageEvent +data class GuiderMessageEvent(override val eventName: String, val data: Any? = null) : MessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt index c689d81ab..06c49459b 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.indi -import nebulosa.api.messages.DeviceMessageEvent +import nebulosa.api.devices.DeviceMessageEvent import nebulosa.indi.device.Device import nebulosa.indi.device.DeviceMessageReceived import nebulosa.indi.device.DevicePropertyEvent diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index ee8103c64..948dcdb0b 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -205,4 +205,9 @@ class MountController( fun remoteControlList(mount: Mount): List { return mountService.remoteControlList(mount) } + + @PutMapping("{mount}/listen") + fun listen(mount: Mount) { + mountService.listen(mount) + } } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHub.kt similarity index 68% rename from api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt rename to api/src/main/kotlin/nebulosa/api/mounts/MountEventHub.kt index a2ccaa8a7..f9a7b72b5 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHub.kt @@ -1,7 +1,7 @@ package nebulosa.api.mounts -import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.devices.DeviceEventHub import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.mount.Mount @@ -11,27 +11,17 @@ import nebulosa.indi.device.mount.MountEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component -import java.io.Closeable -import java.util.concurrent.TimeUnit @Component @Subscriber -class MountEventHandler( +class MountEventHub( private val messageService: MessageService, -) : Closeable { - - private val throttler = PublishSubject.create() - - init { - throttler - .throttleLast(1000, TimeUnit.MILLISECONDS) - .subscribe { sendUpdate(it.device!!) } - } +) : DeviceEventHub(), MountEventAware { @Subscribe(threadMode = ThreadMode.ASYNC) - fun onMountEvent(event: MountEvent) { + override fun handleMountEvent(event: MountEvent) { when (event) { - is PropertyChangedEvent -> throttler.onNext(event) + is PropertyChangedEvent -> onNext(event) is MountAttached -> sendMessage(MOUNT_ATTACHED, event.device) is MountDetached -> sendMessage(MOUNT_DETACHED, event.device) } @@ -42,14 +32,10 @@ class MountEventHandler( messageService.sendMessage(MountMessageEvent(eventName, device)) } - fun sendUpdate(device: Mount) { + override fun sendUpdate(device: Mount) { sendMessage(MOUNT_UPDATED, device) } - override fun close() { - throttler.onComplete() - } - companion object { const val MOUNT_UPDATED = "MOUNT.UPDATED" diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt index dc8f68048..20d734325 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt @@ -1,9 +1,6 @@ package nebulosa.api.mounts -import nebulosa.api.messages.DeviceMessageEvent +import nebulosa.api.devices.DeviceMessageEvent import nebulosa.indi.device.mount.Mount -data class MountMessageEvent( - override val eventName: String, - override val device: Mount, -) : DeviceMessageEvent +data class MountMessageEvent(override val eventName: String, override val device: Mount) : DeviceMessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index b21b85a2c..08d2e882f 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -25,18 +25,22 @@ import org.springframework.stereotype.Service import java.nio.file.Path import java.time.LocalDateTime import java.time.OffsetDateTime +import java.util.concurrent.ConcurrentHashMap @Service @Subscriber -class MountService(private val imageBucket: ImageBucket) { +class MountService( + private val imageBucket: ImageBucket, + private val mountEventHub: MountEventHub, +) { - private val site = HashMap(2) + private val sites = ConcurrentHashMap(2) private val remoteControls = ArrayList(2) @Subscribe(threadMode = ThreadMode.ASYNC) fun onMountGeographicCoordinateChanged(event: MountGeographicCoordinateChanged) { val site = Geoid.IERS2010.lonLat(event.device.longitude, event.device.latitude, event.device.elevation) - this.site[event.device] = site + sites[event.device] = site } fun connect(mount: Mount) { @@ -130,7 +134,7 @@ class MountService(private val imageBucket: ImageBucket) { } fun computeLST(mount: Mount): Angle { - return site[mount]!!.lstAt(CurrentTime) + return sites[mount]!!.lstAt(CurrentTime) } fun computeZenithLocation(mount: Mount): ComputedLocation { @@ -176,7 +180,7 @@ class MountService(private val imageBucket: ImageBucket) { ): ComputedLocation { val computedLocation = ComputedLocation() - val center = site[mount]!! + val center = sites[mount]!! val epoch = if (j2000) null else CurrentTime val icrf = ICRF.equatorial(rightAscension, declination, epoch = epoch, center = center) @@ -258,6 +262,10 @@ class MountService(private val imageBucket: ImageBucket) { return remoteControls.filter { it.mount === mount } } + fun listen(mount: Mount) { + mountEventHub.listen(mount) + } + companion object { private const val SIDEREAL_TIME_DIFF = 0.06552777 * PI / 12.0 diff --git a/api/src/main/kotlin/nebulosa/api/rotators/RotatorController.kt b/api/src/main/kotlin/nebulosa/api/rotators/RotatorController.kt index 264353875..01af185f0 100644 --- a/api/src/main/kotlin/nebulosa/api/rotators/RotatorController.kt +++ b/api/src/main/kotlin/nebulosa/api/rotators/RotatorController.kt @@ -66,4 +66,9 @@ class RotatorController( ) { rotatorService.sync(rotator, angle) } + + @PutMapping("{rotator}/listen") + fun listen(rotator: Rotator) { + rotatorService.listen(rotator) + } } diff --git a/api/src/main/kotlin/nebulosa/api/rotators/RotatorEventAware.kt b/api/src/main/kotlin/nebulosa/api/rotators/RotatorEventAware.kt new file mode 100644 index 000000000..d6514a852 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/rotators/RotatorEventAware.kt @@ -0,0 +1,8 @@ +package nebulosa.api.rotators + +import nebulosa.indi.device.rotator.RotatorEvent + +fun interface RotatorEventAware { + + fun handleRotatorEvent(event: RotatorEvent) +} diff --git a/api/src/main/kotlin/nebulosa/api/rotators/RotatorEventHandler.kt b/api/src/main/kotlin/nebulosa/api/rotators/RotatorEventHub.kt similarity index 68% rename from api/src/main/kotlin/nebulosa/api/rotators/RotatorEventHandler.kt rename to api/src/main/kotlin/nebulosa/api/rotators/RotatorEventHub.kt index b4fd46e07..841d42939 100644 --- a/api/src/main/kotlin/nebulosa/api/rotators/RotatorEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/rotators/RotatorEventHub.kt @@ -1,7 +1,7 @@ package nebulosa.api.rotators -import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.devices.DeviceEventHub import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.rotator.Rotator @@ -11,27 +11,17 @@ import nebulosa.indi.device.rotator.RotatorEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component -import java.io.Closeable -import java.util.concurrent.TimeUnit @Component @Subscriber -class RotatorEventHandler( +class RotatorEventHub( private val messageService: MessageService, -) : Closeable { - - private val throttler = PublishSubject.create() - - init { - throttler - .throttleLast(1000, TimeUnit.MILLISECONDS) - .subscribe { sendUpdate(it.device!!) } - } +) : DeviceEventHub(), RotatorEventAware { @Subscribe(threadMode = ThreadMode.ASYNC) - fun onRotatorEvent(event: RotatorEvent) { + override fun handleRotatorEvent(event: RotatorEvent) { when (event) { - is PropertyChangedEvent -> throttler.onNext(event) + is PropertyChangedEvent -> onNext(event) is RotatorAttached -> sendMessage(ROTATOR_ATTACHED, event.device) is RotatorDetached -> sendMessage(ROTATOR_DETACHED, event.device) } @@ -42,14 +32,10 @@ class RotatorEventHandler( messageService.sendMessage(RotatorMessageEvent(eventName, device)) } - fun sendUpdate(device: Rotator) { + override fun sendUpdate(device: Rotator) { sendMessage(ROTATOR_UPDATED, device) } - override fun close() { - throttler.onComplete() - } - companion object { const val ROTATOR_UPDATED = "ROTATOR.UPDATED" diff --git a/api/src/main/kotlin/nebulosa/api/rotators/RotatorMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/rotators/RotatorMessageEvent.kt index cdb79f400..bd6d0db8e 100644 --- a/api/src/main/kotlin/nebulosa/api/rotators/RotatorMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/rotators/RotatorMessageEvent.kt @@ -1,9 +1,6 @@ package nebulosa.api.rotators -import nebulosa.api.messages.DeviceMessageEvent +import nebulosa.api.devices.DeviceMessageEvent import nebulosa.indi.device.rotator.Rotator -data class RotatorMessageEvent( - override val eventName: String, - override val device: Rotator, -) : DeviceMessageEvent +data class RotatorMessageEvent(override val eventName: String, override val device: Rotator) : DeviceMessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/rotators/RotatorService.kt b/api/src/main/kotlin/nebulosa/api/rotators/RotatorService.kt index 80d7537fb..a1831a9e1 100644 --- a/api/src/main/kotlin/nebulosa/api/rotators/RotatorService.kt +++ b/api/src/main/kotlin/nebulosa/api/rotators/RotatorService.kt @@ -4,7 +4,7 @@ import nebulosa.indi.device.rotator.Rotator import org.springframework.stereotype.Service @Service -class RotatorService { +class RotatorService(private val rotatorEventHub: RotatorEventHub) { fun connect(rotator: Rotator) { rotator.connect() @@ -33,4 +33,8 @@ class RotatorService { fun home(rotator: Rotator) { rotator.homeRotator() } + + fun listen(rotator: Rotator) { + rotatorEventHub.listen(rotator) + } } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt index 23465bbb9..4992cff28 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt @@ -2,8 +2,11 @@ package nebulosa.api.sequencer import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.cameras.CameraEventAware +import nebulosa.api.focusers.FocuserEventAware import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService +import nebulosa.api.wheels.WheelEventAware import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent @@ -24,22 +27,22 @@ class SequencerExecutor( private val messageService: MessageService, private val guider: Guider, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, -) : Consumer { +) : Consumer, CameraEventAware, WheelEventAware, FocuserEventAware { private val jobs = ConcurrentHashMap.newKeySet(1) @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFilterWheelEvent(event: FilterWheelEvent) { + override fun handleFilterWheelEvent(event: FilterWheelEvent) { jobs.find { it.task.wheel === event.device }?.handleFilterWheelEvent(event) } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFocuserEvent(event: FocuserEvent) { + override fun handleFocuserEvent(event: FocuserEvent) { // jobs.find { it.task.focuser === event.device }?.handleFocuserEvent(event) } diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt index a8adcc5ee..2c7c9acd9 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt @@ -49,4 +49,9 @@ class WheelController( ) { wheelService.sync(wheel, names.split(",")) } + + @PutMapping("{wheel}/listen") + fun listen(wheel: FilterWheel) { + wheelService.listen(wheel) + } } diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHub.kt similarity index 68% rename from api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt rename to api/src/main/kotlin/nebulosa/api/wheels/WheelEventHub.kt index 1b1e56d02..a6db67264 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHub.kt @@ -1,7 +1,7 @@ package nebulosa.api.wheels -import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.devices.DeviceEventHub import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.filterwheel.FilterWheel @@ -11,27 +11,17 @@ import nebulosa.indi.device.filterwheel.FilterWheelEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component -import java.io.Closeable -import java.util.concurrent.TimeUnit @Component @Subscriber -class WheelEventHandler( +class WheelEventHub( private val messageService: MessageService, -) : Closeable { - - private val throttler = PublishSubject.create() - - init { - throttler - .throttleLast(1000, TimeUnit.MILLISECONDS) - .subscribe { sendUpdate(it.device!!) } - } +) : DeviceEventHub(), WheelEventAware { @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFilterWheelEvent(event: FilterWheelEvent) { + override fun handleFilterWheelEvent(event: FilterWheelEvent) { when (event) { - is PropertyChangedEvent -> throttler.onNext(event) + is PropertyChangedEvent -> onNext(event) is FilterWheelAttached -> sendMessage(WHEEL_ATTACHED, event.device) is FilterWheelDetached -> sendMessage(WHEEL_DETACHED, event.device) } @@ -42,14 +32,10 @@ class WheelEventHandler( messageService.sendMessage(WheelMessageEvent(eventName, device)) } - fun sendUpdate(device: FilterWheel) { + override fun sendUpdate(device: FilterWheel) { sendMessage(WHEEL_UPDATED, device) } - override fun close() { - throttler.onComplete() - } - companion object { const val WHEEL_UPDATED = "WHEEL.UPDATED" diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt index dad365e72..f1856e125 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt @@ -1,9 +1,7 @@ package nebulosa.api.wheels -import nebulosa.api.messages.DeviceMessageEvent +import nebulosa.api.devices.DeviceMessageEvent import nebulosa.indi.device.filterwheel.FilterWheel -data class WheelMessageEvent( - override val eventName: String, - override val device: FilterWheel, -) : DeviceMessageEvent +data class WheelMessageEvent(override val eventName: String, override val device: FilterWheel) : + DeviceMessageEvent diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt index b82576114..fcc199be7 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt @@ -4,7 +4,7 @@ import nebulosa.indi.device.filterwheel.FilterWheel import org.springframework.stereotype.Service @Service -class WheelService { +class WheelService(private val wheelEventHub: WheelEventHub) { fun connect(wheel: FilterWheel) { wheel.connect() @@ -21,4 +21,8 @@ class WheelService { fun sync(wheel: FilterWheel, names: List) { wheel.names(names) } + + fun listen(wheel: FilterWheel) { + wheelEventHub.listen(wheel) + } } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt index 49ac3b688..b3367f165 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt @@ -2,6 +2,7 @@ package nebulosa.api.wizard.flat import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.cameras.CameraEventAware import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService import nebulosa.indi.device.camera.Camera @@ -15,12 +16,12 @@ import java.util.concurrent.ConcurrentHashMap @Subscriber class FlatWizardExecutor( private val messageService: MessageService, -) : Consumer { +) : Consumer, CameraEventAware { private val jobs = ConcurrentHashMap.newKeySet(1) @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + override fun handleCameraEvent(event: CameraEvent) { jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) } diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index 25bbae873..ad4ceb05f 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -3,6 +3,7 @@ import { CameraExposureComponent } from '../../shared/components/camera-exposure import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' import { AlignmentMethod, AlignmentPreference, DARVStart, DARVState, Hemisphere, TPPAStart, TPPAState } from '../../shared/types/alignment.types' import { Angle } from '../../shared/types/atlas.types' @@ -19,7 +20,7 @@ import { CameraComponent } from '../camera/camera.component' templateUrl: './alignment.component.html', styleUrls: ['./alignment.component.scss'], }) -export class AlignmentComponent implements AfterViewInit, OnDestroy { +export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { cameras: Camera[] = [] camera = structuredClone(EMPTY_CAMERA) @@ -78,6 +79,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { private api: ApiService, private browserWindow: BrowserWindowService, private preference: PreferenceService, + private pinger: Pinger, electron: ElectronService, ngZone: NgZone, ) { @@ -222,6 +224,8 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { }) this.loadPreference() + + pinger.register(this, 30000) } async ngAfterViewInit() { @@ -232,15 +236,22 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { @HostListener('window:unload') async ngOnDestroy() { - try { - await this.darvStop() - } finally { - await this.tppaStop() - } + this.pinger.unregister(this) + + this.darvStop() + this.tppaStop() + } + + ping() { + if (this.camera.id) this.api.cameraListen(this.camera) + if (this.mount.id) this.api.mountListen(this.mount) + if (this.guideOutput.id) this.api.guideOutputListen(this.guideOutput) } async cameraChanged() { if (this.camera.id) { + this.ping() + const camera = await this.api.camera(this.camera.id) Object.assign(this.camera, camera) } @@ -248,6 +259,8 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { async mountChanged() { if (this.mount.id) { + this.ping() + const mount = await this.api.mount(this.mount.id) Object.assign(this.mount, mount) this.tppaRequest.stepSpeed = mount.slewRate?.name @@ -256,6 +269,8 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { async guideOutputChanged() { if (this.guideOutput.id) { + this.ping() + const guideOutput = await this.api.guideOutput(this.guideOutput.id) Object.assign(this.guideOutput, guideOutput) } diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index e95c56c50..cec4ab6ab 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -6,6 +6,7 @@ import { CameraExposureComponent } from '../../shared/components/camera-exposure import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' import { AutoFocusPreference, AutoFocusRequest, AutoFocusState, CurveChart, EMPTY_AUTO_FOCUS_PREFERENCE } from '../../shared/types/autofocus.type' import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' @@ -19,7 +20,7 @@ import { CameraComponent } from '../camera/camera.component' templateUrl: './autofocus.component.html', styleUrls: ['./autofocus.component.scss'], }) -export class AutoFocusComponent implements AfterViewInit, OnDestroy { +export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { cameras: Camera[] = [] camera = structuredClone(EMPTY_CAMERA) @@ -214,6 +215,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { private api: ApiService, private browserWindow: BrowserWindowService, private preference: PreferenceService, + private pinger: Pinger, electron: ElectronService, ngZone: NgZone, ) { @@ -300,6 +302,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { }) this.loadPreference() + + pinger.register(this, 30000) } async ngAfterViewInit() { @@ -309,11 +313,19 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { @HostListener('window:unload') async ngOnDestroy() { - await this.stop() + this.pinger.unregister(this) + this.stop() + } + + ping() { + if (this.camera.id) this.api.cameraListen(this.camera) + if (this.focuser.id) this.api.focuserListen(this.focuser) } async cameraChanged() { if (this.camera.id) { + this.ping() + const camera = await this.api.camera(this.camera.id) Object.assign(this.camera, camera) this.loadPreference() @@ -322,6 +334,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy { async focuserChanged() { if (this.focuser.id) { + this.ping() + const focuser = await this.api.focuser(this.focuser.id) Object.assign(this.focuser, focuser) } diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 0df5f366f..2d883b3e4 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -7,6 +7,7 @@ import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' import { Camera, CameraDialogInput, CameraDialogMode, CameraPreference, CameraStartCapture, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureMode, ExposureTimeUnit, FrameType, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { Device } from '../../shared/types/device.types' @@ -22,7 +23,7 @@ import { AppComponent } from '../app.component' templateUrl: './camera.component.html', styleUrls: ['./camera.component.scss'], }) -export class CameraComponent implements AfterContentInit, OnDestroy { +export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { readonly camera = structuredClone(EMPTY_CAMERA) readonly equipment: Equipment = {} @@ -168,6 +169,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { private electron: ElectronService, private preference: PreferenceService, private route: ActivatedRoute, + private pinger: Pinger, ngZone: NgZone, ) { if (app) app.title = 'Camera' @@ -245,6 +247,8 @@ export class CameraComponent implements AfterContentInit, OnDestroy { }) this.cameraModel[1].visible = !app.modal + + pinger.register(this, 30000) } ngAfterContentInit() { @@ -267,11 +271,17 @@ export class CameraComponent implements AfterContentInit, OnDestroy { @HostListener('window:unload') async ngOnDestroy() { + this.pinger.unregister(this) + if (this.mode === 'CAPTURE') { await this.abortCapture() } } + ping() { + this.api.cameraListen(this.camera) + } + private async loadCameraStartCaptureForDialogMode(data?: CameraDialogInput) { if (data) { this.mode = data.mode diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 8a38367ae..18ee4ab06 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -6,6 +6,7 @@ import { Subject, Subscription, debounceTime } from 'rxjs' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' import { CameraStartCapture, EMPTY_CAMERA_START_CAPTURE } from '../../shared/types/camera.types' import { Focuser } from '../../shared/types/focuser.types' @@ -17,7 +18,7 @@ import { AppComponent } from '../app.component' templateUrl: './filterwheel.component.html', styleUrls: ['./filterwheel.component.scss'], }) -export class FilterWheelComponent implements AfterContentInit, OnDestroy { +export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingable { readonly wheel = structuredClone(EMPTY_WHEEL) readonly request = structuredClone(EMPTY_CAMERA_START_CAPTURE) @@ -64,6 +65,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { private electron: ElectronService, private preference: PreferenceService, private route: ActivatedRoute, + private pinger: Pinger, ngZone: NgZone, ) { if (app) app.title = 'Filter Wheel' @@ -132,6 +134,8 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { hotkeys('7', event => { event.preventDefault(); this.moveToPosition(7) }) hotkeys('8', event => { event.preventDefault(); this.moveToPosition(8) }) hotkeys('9', event => { event.preventDefault(); this.moveToPosition(9) }) + + pinger.register(this, 30000) } async ngAfterContentInit() { @@ -158,12 +162,21 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { + this.pinger.unregister(this) this.subscription?.unsubscribe() } + ping() { + this.api.wheelListen(this.wheel) + if (this.focuser) this.api.focuserListen(this.focuser) + } + async wheelChanged(wheel?: FilterWheel) { if (wheel && wheel.id) { wheel = await this.api.wheel(wheel.id) + + this.ping() + Object.assign(this.wheel, wheel) this.loadPreference() @@ -266,9 +279,13 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { } focuserChanged() { - this.focusOffsetMax = this.focuser?.maxPosition ?? 0 - this.focusOffsetMin = -this.focusOffsetMax - this.updateFocusOffset() + if (this.focuser) { + this.ping() + + this.focusOffsetMax = this.focuser.maxPosition + this.focusOffsetMin = -this.focusOffsetMax + this.updateFocusOffset() + } } focusOffsetForFilter(filter: FilterSlot) { diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index c4edc631f..41a887d19 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -3,6 +3,7 @@ import { CameraExposureComponent } from '../../shared/components/camera-exposure import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' @@ -17,7 +18,7 @@ import { CameraComponent } from '../camera/camera.component' templateUrl: './flat-wizard.component.html', styleUrls: ['./flat-wizard.component.scss'], }) -export class FlatWizardComponent implements AfterViewInit, OnDestroy { +export class FlatWizardComponent implements AfterViewInit, OnDestroy, Pingable { cameras: Camera[] = [] camera = structuredClone(EMPTY_CAMERA) @@ -59,6 +60,7 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { private browserWindow: BrowserWindowService, private prime: PrimeService, private preference: PreferenceService, + private pinger: Pinger, ngZone: NgZone, ) { app.title = 'Flat Wizard' @@ -141,6 +143,8 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } }) }) + + pinger.register(this, 30000) } async ngAfterViewInit() { @@ -149,7 +153,15 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } @HostListener('window:unload') - ngOnDestroy() { } + ngOnDestroy() { + this.pinger.unregister(this) + this.stop() + } + + ping() { + if (this.camera.id) this.api.cameraListen(this.camera) + if (this.wheel.id) this.api.wheelListen(this.wheel) + } async showCameraDialog() { if (this.camera.id && await CameraComponent.showAsDialog(this.browserWindow, 'FLAT_WIZARD', this.camera, this.request.capture)) { @@ -159,6 +171,8 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { cameraChanged() { if (this.camera.id) { + this.ping() + const cameraPreference = this.preference.cameraPreference(this.camera).get() this.request.capture = this.preference.cameraStartCaptureForFlatWizard(this.camera).get(cameraPreference) this.updateEntryFromCamera(this.camera) @@ -172,7 +186,9 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } wheelChanged() { - if (this.wheel) { + if (this.wheel.id) { + this.ping() + let filters: FilterSlot[] = [] let filtersChanged = true diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index a914f0567..1c1221264 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router' import hotkeys from 'hotkeys-js' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types' import { AppComponent } from '../app.component' @@ -12,7 +13,7 @@ import { AppComponent } from '../app.component' templateUrl: './focuser.component.html', styleUrls: ['./focuser.component.scss'], }) -export class FocuserComponent implements AfterViewInit, OnDestroy { +export class FocuserComponent implements AfterViewInit, OnDestroy, Pingable { readonly focuser = structuredClone(EMPTY_FOCUSER) @@ -26,6 +27,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { electron: ElectronService, private preference: PreferenceService, private route: ActivatedRoute, + private pinger: Pinger, ngZone: NgZone, ) { app.title = 'Focuser' @@ -59,6 +61,8 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { hotkeys('down', event => { event.preventDefault(); this.stepsRelative = Math.max(0, this.stepsRelative - 1) }) hotkeys('ctrl+up', event => { event.preventDefault(); this.stepsAbsolute = Math.max(0, this.stepsAbsolute - 1) }) hotkeys('ctrl+down', event => { event.preventDefault(); this.stepsAbsolute = Math.min(this.focuser.maxPosition, this.stepsAbsolute + 1) }) + + pinger.register(this, 30000) } async ngAfterViewInit() { @@ -70,9 +74,14 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { + this.pinger.unregister(this) this.abort() } + ping() { + this.api.focuserListen(this.focuser) + } + async focuserChanged(focuser?: Focuser) { if (focuser && focuser.id) { focuser = await this.api.focuser(focuser.id) diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index e5316013e..afdf8c25b 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -138,7 +138,8 @@
- +
diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index f5d8b7386..ac16de440 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -4,6 +4,7 @@ import { ChartData, ChartOptions } from 'chart.js' import { UIChart } from 'primeng/chart' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { GuideDirection, GuideOutput, GuideState, GuideStep, Guider, GuiderHistoryStep, GuiderPlotMode, GuiderYAxisUnit } from '../../shared/types/guider.types' export interface GuiderPreference { @@ -17,7 +18,7 @@ export interface GuiderPreference { templateUrl: './guider.component.html', styleUrls: ['./guider.component.scss'], }) -export class GuiderComponent implements AfterViewInit, OnDestroy { +export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { guideOutputs: GuideOutput[] = [] guideOutput?: GuideOutput @@ -216,6 +217,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { constructor( title: Title, private api: ApiService, + private pinger: Pinger, electron: ElectronService, ngZone: NgZone, ) { @@ -283,6 +285,8 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { this.message = event.data }) }) + + pinger.register(this, 30000) } async ngAfterViewInit() { @@ -303,7 +307,13 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { } @HostListener('window:unload') - ngOnDestroy() { } + ngOnDestroy() { + this.pinger.unregister(this) + } + + ping() { + if (this.guideOutput) this.api.guideOutputListen(this.guideOutput) + } private processGuiderStatus(event: Guider) { this.connected = event.connected @@ -368,6 +378,8 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { async guideOutputChanged() { if (this.guideOutput?.id) { + this.ping() + const guideOutput = await this.api.guideOutput(this.guideOutput.id) Object.assign(this.guideOutput, guideOutput) diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index c853d97cf..604dd02de 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -9,6 +9,7 @@ import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PrimeService } from '../../shared/services/prime.service' import { Angle, ComputedLocation, Constellation, EMPTY_COMPUTED_LOCATION } from '../../shared/types/atlas.types' import { EMPTY_MOUNT, Mount, MountRemoteControlDialog, MountRemoteControlType, MoveDirectionType, PierSide, SlewRate, TargetCoordinateType, TrackMode } from '../../shared/types/mount.types' @@ -30,7 +31,7 @@ export interface MountPreference { templateUrl: './mount.component.html', styleUrls: ['./mount.component.scss'], }) -export class MountComponent implements AfterContentInit, OnDestroy { +export class MountComponent implements AfterContentInit, OnDestroy, Pingable { readonly mount = structuredClone(EMPTY_MOUNT) @@ -223,10 +224,11 @@ export class MountComponent implements AfterContentInit, OnDestroy { private app: AppComponent, private api: ApiService, private browserWindow: BrowserWindowService, - private electron: ElectronService, + electron: ElectronService, private storage: LocalStorageService, private route: ActivatedRoute, private prime: PrimeService, + private pinger: Pinger, ngZone: NgZone, ) { app.title = 'Mount' @@ -277,6 +279,8 @@ export class MountComponent implements AfterContentInit, OnDestroy { hotkeys('e', { keyup: true }, event => { event.preventDefault(); this.moveTo('NE', event.type === 'keydown') }) hotkeys('z', { keyup: true }, event => { event.preventDefault(); this.moveTo('SW', event.type === 'keydown') }) hotkeys('c', { keyup: true }, event => { event.preventDefault(); this.moveTo('SE', event.type === 'keydown') }) + + this.pinger.register(this, 30000) } async ngAfterContentInit() { @@ -288,10 +292,16 @@ export class MountComponent implements AfterContentInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { - this.abort() + this.pinger.unregister(this) this.computeCoordinateSubscriptions .forEach(e => e.unsubscribe()) + + this.abort() + } + + ping() { + this.api.mountListen(this.mount) } async mountChanged(mount?: Mount) { diff --git a/desktop/src/app/rotator/rotator.component.ts b/desktop/src/app/rotator/rotator.component.ts index 20843f897..d9a8215dc 100644 --- a/desktop/src/app/rotator/rotator.component.ts +++ b/desktop/src/app/rotator/rotator.component.ts @@ -2,6 +2,7 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angu import { ActivatedRoute } from '@angular/router' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' import { EMPTY_ROTATOR, Rotator } from '../../shared/types/rotator.types' import { AppComponent } from '../app.component' @@ -11,7 +12,7 @@ import { AppComponent } from '../app.component' templateUrl: './rotator.component.html', styleUrls: ['./rotator.component.scss'], }) -export class RotatorComponent implements AfterViewInit, OnDestroy { +export class RotatorComponent implements AfterViewInit, OnDestroy, Pingable { readonly rotator = structuredClone(EMPTY_ROTATOR) @@ -25,6 +26,7 @@ export class RotatorComponent implements AfterViewInit, OnDestroy { electron: ElectronService, private preference: PreferenceService, private route: ActivatedRoute, + private pinger: Pinger, ngZone: NgZone, ) { app.title = 'Rotator' @@ -45,6 +47,8 @@ export class RotatorComponent implements AfterViewInit, OnDestroy { }) } }) + + pinger.register(this, 30000) } async ngAfterViewInit() { @@ -56,9 +60,14 @@ export class RotatorComponent implements AfterViewInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { + this.pinger.unregister(this) this.abort() } + ping() { + this.api.rotatorListen(this.rotator) + } + async rotatorChanged(rotator?: Rotator) { if (rotator && rotator.id) { rotator = await this.api.rotator(rotator.id) @@ -82,7 +91,7 @@ export class RotatorComponent implements AfterViewInit, OnDestroy { } reverse(enabled: boolean) { - this.api.focuserReverse(this.rotator, enabled) + this.api.rotatorReverse(this.rotator, enabled) } async move() { diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index 556df7628..52c823755 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -136,11 +136,14 @@
- - - + + + + [hasNone]="true" (deviceChange)="focuserChanged()" />
diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index abd8516e1..bde94f520 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -7,6 +7,7 @@ import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' +import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PrimeService } from '../../shared/services/prime.service' import { JsonFile } from '../../shared/types/app.types' import { Camera, CameraCaptureEvent, CameraStartCapture } from '../../shared/types/camera.types' @@ -27,7 +28,7 @@ export const SEQUENCER_PLAN_KEY = 'sequencer.plan' templateUrl: './sequencer.component.html', styleUrls: ['./sequencer.component.scss'], }) -export class SequencerComponent implements AfterContentInit, OnDestroy { +export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable { cameras: Camera[] = [] mounts: Mount[] = [] @@ -124,6 +125,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { private electron: ElectronService, private storage: LocalStorageService, private prime: PrimeService, + private pinger: Pinger, ngZone: NgZone, ) { app.title = 'Sequencer' @@ -235,6 +237,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { for (const p of SEQUENCE_ENTRY_PROPERTIES) { this.availableEntryPropertiesToApply.set(p, true) } + + pinger.register(this, 30000) } async ngAfterContentInit() { @@ -248,13 +252,22 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { // this.route.queryParams.subscribe(e => { }) } + @HostListener('window:unload') + ngOnDestroy() { + this.pinger.unregister(this) + } + + ping() { + if (this.camera) this.api.cameraListen(this.camera) + if (this.mount) this.api.mountListen(this.mount) + if (this.focuser) this.api.focuserListen(this.focuser) + if (this.wheel) this.api.wheelListen(this.wheel) + } + private enableOrDisableTopbarMenu(enable: boolean) { this.app.topMenu.forEach(e => e.disabled = !enable) } - @HostListener('window:unload') - ngOnDestroy() { } - add() { const camera = this.camera ?? this.cameras[0] // const wheel = this.wheel ?? this.wheels[0] @@ -372,6 +385,22 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { } } + cameraChanged() { + this.ping() + } + + mountChanged() { + this.ping() + } + + focuserChanged() { + this.ping() + } + + wheelChanged() { + this.ping() + } + savePlan() { this.plan.camera = this.camera this.plan.mount = this.mount diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 56db681fa..4c4705545 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -71,7 +71,7 @@ export class ApiService { cameraSnoop(camera: Camera, equipment: Equipment) { const { mount, wheel, focuser, rotator } = equipment - const query = this.http.query({ mount: mount?.name, wheel: wheel?.name, focuser: focuser?.name, rotator: rotator?.name }) + const query = this.http.query({ mount: mount?.id, wheel: wheel?.id, focuser: focuser?.id, rotator: rotator?.id }) return this.http.put(`cameras/${camera.id}/snoop?${query}`) } @@ -91,6 +91,10 @@ export class ApiService { return this.http.put(`cameras/${camera.id}/capture/abort`) } + cameraListen(camera: Camera) { + return this.http.put(`cameras/${camera.id}/listen`) + } + // MOUNT mounts() { @@ -186,6 +190,10 @@ export class ApiService { return this.http.put(`mounts/${mount.id}/remote-control/stop?${query}`) } + mountListen(mount: Mount) { + return this.http.put(`mounts/${mount.id}/listen`) + } + // FOCUSER focusers() { @@ -224,6 +232,10 @@ export class ApiService { return this.http.put(`focusers/${focuser.id}/sync?steps=${steps}`) } + focuserListen(focuser: Focuser) { + return this.http.put(`focusers/${focuser.id}/listen`) + } + // FILTER WHEEL wheels() { @@ -250,6 +262,10 @@ export class ApiService { return this.http.put(`wheels/${wheel.id}/sync?names=${names.join(',')}`) } + wheelListen(wheel: FilterWheel) { + return this.http.put(`wheels/${wheel.id}/listen`) + } + // ROTATOR rotators() { @@ -268,7 +284,7 @@ export class ApiService { return this.http.put(`rotators/${rotator.id}/disconnect`) } - focuserReverse(rotator: Rotator, enabled: boolean) { + rotatorReverse(rotator: Rotator, enabled: boolean) { return this.http.put(`rotators/${rotator.id}/reverse?enabled=${enabled}`) } @@ -288,6 +304,10 @@ export class ApiService { return this.http.put(`rotators/${rotator.id}/sync?angle=${angle}`) } + rotatorListen(rotator: Rotator) { + return this.http.put(`rotators/${rotator.id}/listen`) + } + // GUIDE OUTPUT guideOutputs() { @@ -311,6 +331,10 @@ export class ApiService { return this.http.put(`guide-outputs/${guideOutput.id}/pulse?${query}`) } + guideOutputListen(guideOutput: GuideOutput) { + return this.http.put(`guide-outputs/${guideOutput.id}/listen`) + } + // GUIDING guidingConnect(host: string = 'localhost', port: number = 4400) { @@ -369,7 +393,7 @@ export class ApiService { // IMAGE async openImage(path: string, transformation: ImageTransformation, camera?: Camera) { - const query = this.http.query({ path, camera: camera?.name }) + const query = this.http.query({ path, camera: camera?.id }) const response = await this.http.postBlob(`image?${query}`, transformation) const info = JSON.parse(response.headers.get('X-Image-Info')!) as ImageInfo return { info, blob: response.body! } @@ -522,7 +546,7 @@ export class ApiService { } saveImageAs(path: string, save: ImageSaveDialog, camera?: Camera) { - const query = this.http.query({ path, camera: camera?.name }) + const query = this.http.query({ path, camera: camera?.id }) return this.http.put(`image/save-as?${query}`, save) } @@ -619,7 +643,7 @@ export class ApiService { sequencerStart(camera: Camera, plan: SequencePlan) { const body: SequencePlan = { ...plan, mount: undefined, camera: undefined, wheel: undefined, focuser: undefined } - const query = this.http.query({ mount: plan.mount?.name, focuser: plan.focuser?.name, wheel: plan.wheel?.name }) + const query = this.http.query({ mount: plan.mount?.id, focuser: plan.focuser?.id, wheel: plan.wheel?.id }) return this.http.put(`sequencer/${camera.id}/start?${query}`, body) } @@ -650,11 +674,11 @@ export class ApiService { // AUTO FOCUS autoFocusStart(camera: Camera, focuser: Focuser, request: AutoFocusRequest) { - return this.http.put(`auto-focus/${camera.name}/${focuser.name}/start`, request) + return this.http.put(`auto-focus/${camera.id}/${focuser.id}/start`, request) } autoFocusStop(camera: Camera) { - return this.http.put(`auto-focus/${camera.name}/stop`) + return this.http.put(`auto-focus/${camera.id}/stop`) } // PREFERENCE diff --git a/desktop/src/shared/services/pinger.service.ts b/desktop/src/shared/services/pinger.service.ts new file mode 100644 index 000000000..90c4d189f --- /dev/null +++ b/desktop/src/shared/services/pinger.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core' + +export interface Pingable { + + ping(): void +} + +@Injectable({ providedIn: 'root' }) +export class Pinger { + + private readonly pingables = new Map() + + isRegistered(pingable: Pingable) { + return this.pingables.has(pingable) + } + + register(pingable: Pingable, interval: number, initialDelay: number = 1000) { + this.unregister(pingable) + + if (interval > 0) { + if (initialDelay <= 0) pingable.ping() + else if (initialDelay < interval - 1000) setTimeout(() => pingable.ping(), initialDelay) + + this.pingables.set(pingable, setInterval(() => pingable.ping(), interval)) + } + } + + unregister(pingable: Pingable) { + clearInterval(this.pingables.get(pingable)) + this.pingables.delete(pingable) + } +} \ No newline at end of file From 4b8f90821cf0e5443797a59c34b12a41da2b319c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 1 Jun 2024 18:59:52 -0300 Subject: [PATCH 42/45] [desktop]: Upgrade NPM dependencies --- desktop/package-lock.json | 1387 ++++++++++++++++++++++--------------- 1 file changed, 815 insertions(+), 572 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 1ecffeb10..b0629708d 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -105,12 +105,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1703.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", - "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", + "version": "0.1703.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.8.tgz", + "integrity": "sha512-lKxwG4/QABXZvJpqeSIn/kAwnY6MM9HdHZUV+o5o3UiTi+vO8rZApG4CCaITH3Bxebm7Nam7Xbk8RuukC5rq6g==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.7", + "@angular-devkit/core": "17.3.8", "rxjs": "7.8.1" }, "engines": { @@ -248,29 +248,46 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", + "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@angular-devkit/core": "17.3.7", + "rxjs": "7.8.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, "peerDependencies": { - "ajv": "^6.9.1" + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, "node_modules/@angular-devkit/build-angular/node_modules/json-parse-even-better-errors": { @@ -303,6 +320,31 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/@angular-devkit/build-angular/node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/webpack": { "version": "5.90.3", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", @@ -369,7 +411,22 @@ "webpack-dev-server": "^4.0.0" } }, - "node_modules/@angular-devkit/core": { + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", + "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { "version": "17.3.7", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", @@ -396,6 +453,33 @@ } } }, + "node_modules/@angular-devkit/core": { + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.8.tgz", + "integrity": "sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-devkit/schematics": { "version": "17.3.7", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.7.tgz", @@ -414,6 +498,33 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/animations": { "version": "17.3.9", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.9.tgz", @@ -478,6 +589,48 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", + "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/common": { "version": "17.3.9", "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.9.tgz", @@ -682,12 +835,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.6", "picocolors": "^1.0.0" }, "engines": { @@ -695,9 +848,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", + "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -776,25 +929,25 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.6.tgz", + "integrity": "sha512-+wnfqc5uHiMYtvRX7qu80Toef8BXeh4HHR1SPeonGb1SKPniNEd4a/nlaJJMv/OIEYvIVavvo0yR7u10Gqz0Iw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", + "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", + "@babel/compat-data": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -813,19 +966,19 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.6.tgz", + "integrity": "sha512-djsosdPJVZE6Vsw3kk7IPRWethP94WHGOhQTc67SNXE0ZzMhHgALw8iGmYS0TD1bbMM0VDROy43od7/hN6WYcA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", "semver": "^6.3.1" }, "engines": { @@ -835,13 +988,25 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -857,12 +1022,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.6.tgz", + "integrity": "sha512-C875lFBIWWwyv6MHZUG9HmRrlTDgOsLWZfYR0nW69gaKJNe0/Mpxx5r0EID2ZdHQkdUmQo2t0uNckTL08/1BgA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.6", "regexpu-core": "^5.3.1", "semver": "^6.3.1" }, @@ -873,6 +1038,18 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -899,74 +1076,74 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", + "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", + "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", + "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.6.tgz", + "integrity": "sha512-OTsCufZTxDUsv2/eDXanw/mUZHWOxSbEmC3pP8cgjcy5rgeVPWWMStnv274DV60JtHxTk0adT0QrCzC4M9NWGg==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", + "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", "dev": true, "dependencies": { - "@babel/types": "^7.24.0" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", + "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -976,47 +1153,47 @@ } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.6.tgz", + "integrity": "sha512-3SFDJRbx7KuPRl8XDUr8O7GAEB8iGyWPjLKJh/ywP/Iy9WOmEfMrsWbaZpvBu2HSYn4KQygIsz0O7m8y10ncMA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", + "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.6.tgz", + "integrity": "sha512-1Qursq9ArRZPAMOZf/nuzVW8HgJLkTB9y9LfP4lW2MVp4e9WkLJDovfKBxoDcCk6VuzIxyqWHyBoaCtSRP10yg==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-wrap-function": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1025,15 +1202,27 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.6.tgz", + "integrity": "sha512-mRhfPwDqDpba8o1F8ESxsEkJMQkUF8ZIWrAc0FtWhxnjfextxMWxr22RtFizxxSYLjVHDeMgVsRq8BBZR2ikJQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-member-expression-to-functions": "^7.24.6", + "@babel/helper-optimise-call-expression": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1043,24 +1232,24 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", + "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.6.tgz", + "integrity": "sha512-jhbbkK3IUKc4T43WadP96a27oYti9gEf1LdyGSP2rHGH77kwLwfhO7TgwnWvxxQVmke0ImmCSS47vcuxEMGD3Q==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1079,67 +1268,66 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", + "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", - "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.6.tgz", + "integrity": "sha512-f1JLrlw/jbiNfxvdrfBgio/gRBk3yTAEJWirpAkiJG2Hb22E7cEYKHWo0dFPTv/niPovzIdPdEDetrv6tC6gPQ==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.23.0", - "@babel/template": "^7.24.0", - "@babel/types": "^7.24.5" + "@babel/helper-function-name": "^7.24.6", + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", + "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", "dev": true, "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.6", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -1149,9 +1337,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1161,12 +1349,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", - "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.6.tgz", + "integrity": "sha512-iVuhb6poq5ikqRq2XWU6OQ+R5o9wF+r/or9CeUyovgptz0UlnK4/seOQ1Istu/XybYjAhQv1FRSSfHHufIku5Q==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1176,14 +1364,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", - "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.6.tgz", + "integrity": "sha512-c8TER5xMDYzzFcGqOEp9l4hvB7dcbhcGjcLVwxWfe4P5DOafdwjsBJZKsmv+o3aXh7NhopvayQIovHrh2zSRUQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", + "@babel/plugin-transform-optional-chaining": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1193,13 +1381,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", - "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.6.tgz", + "integrity": "sha512-z8zEjYmwBUHN/pCF3NuWBhHQjJCrd33qAi8MgANfMrAvn72k2cImT8VjK9LJFu4ysOLJqhfkYYb3MvwANRUNZQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1284,12 +1472,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", - "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.6.tgz", + "integrity": "sha512-BE6o2BogJKJImTmGpkmOic4V0hlRRxVtzqxiSPa8TIFxyhi4EFjHm08nq1M4STK4RytuLMgnSz0/wfflvGFNOg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1299,12 +1487,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", - "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.6.tgz", + "integrity": "sha512-D+CfsVZousPXIdudSII7RGy52+dYRtbyKAZcvtQKq/NpsivyMVduepzcLqG5pMBugtMdedxdC8Ramdpcne9ZWQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1456,12 +1644,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", - "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.6.tgz", + "integrity": "sha512-jSSSDt4ZidNMggcLx8SaKsbGNEfIl0PHx/4mFEulorE7bpYLbN0d3pDW3eJ7Y5Z3yPhy3L3NaPCYyTUY7TuugQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1506,12 +1694,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", - "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.6.tgz", + "integrity": "sha512-XNW7jolYHW9CwORrZgA/97tL/k05qe/HL0z/qqJq1mdWhwwCM6D4BJBV7wAz9HgFziN5dTOG31znkVIzwxv+vw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1521,12 +1709,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", - "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.6.tgz", + "integrity": "sha512-S/t1Xh4ehW7sGA7c1j/hiOBLnEYCp/c2sEG4ZkL8kI1xX9tW2pqJTCHKtdhe/jHKt8nG0pFCrDHUXd4DvjHS9w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1536,13 +1724,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", - "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.6.tgz", + "integrity": "sha512-j6dZ0Z2Z2slWLR3kt9aOmSIrBvnntWjMDN/TVcMPxhXMLmJVqX605CBRlcGI4b32GMbfifTEsdEjGjiE+j/c3A==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1552,13 +1740,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", - "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.6.tgz", + "integrity": "sha512-1QSRfoPI9RoLRa8Mnakc6v3e0gJxiZQTYrMfLn+mD0sz5+ndSzwymp2hDcYJTyT0MOn0yuWzj8phlIvO72gTHA==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.4", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -1569,18 +1757,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", - "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.6.tgz", + "integrity": "sha512-+fN+NO2gh8JtRmDSOB6gaCVo36ha8kfCW1nMq2Gc0DABln0VcHN4PrALDvF5/diLzIRKptC7z/d7Lp64zk92Fg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", "globals": "^11.1.0" }, "engines": { @@ -1590,26 +1778,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", - "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.6.tgz", + "integrity": "sha512-cRzPobcfRP0ZtuIEkA8QzghoUpSB3X3qSH5W2+FzG+VjWbJXExtx0nbRqwumdBN1x/ot2SlTNQLfBCnPdzp6kg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/template": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/template": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1619,12 +1819,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", - "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.6.tgz", + "integrity": "sha512-YLW6AE5LQpk5npNXL7i/O+U9CE4XsBCuRPgyjl1EICZYKmcitV+ayuuUGMJm2lC1WWjXYszeTnIxF/dq/GhIZQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1634,13 +1834,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", - "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.6.tgz", + "integrity": "sha512-rCXPnSEKvkm/EjzOtLoGvKseK+dS4kZwx1HexO3BtRtgL0fQ34awHn34aeSHuXtZY2F8a1X8xqBBPRtOxDVmcA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1650,12 +1850,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", - "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.6.tgz", + "integrity": "sha512-/8Odwp/aVkZwPFJMllSbawhDAO3UJi65foB00HYnK/uXvvCPm0TAXSByjz1mpRmp0q6oX2SIxpkUOpPFHk7FLA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1665,12 +1865,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", - "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.6.tgz", + "integrity": "sha512-vpq8SSLRTBLOHUZHSnBqVo0AKX3PBaoPs2vVzYVWslXDTDIpwAcCDtfhUcHSQQoYoUvcFPTdC8TZYXu9ZnLT/w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1681,13 +1881,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", - "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.6.tgz", + "integrity": "sha512-EemYpHtmz0lHE7hxxxYEuTYOOBZ43WkDgZ4arQ4r+VX9QHuNZC+WH3wUWmRNvR8ECpTRne29aZV6XO22qpOtdA==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1697,12 +1897,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", - "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.6.tgz", + "integrity": "sha512-inXaTM1SVrIxCkIJ5gqWiozHfFMStuGbGJAxZFBoHcRRdDP0ySLb3jH6JOwmfiinPwyMZqMBX+7NBDCO4z0NSA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1713,13 +1913,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", - "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.6.tgz", + "integrity": "sha512-n3Sf72TnqK4nw/jziSqEl1qaWPbCRw2CziHH+jdRYvw4J6yeCzsj4jdw8hIntOEeDGTmHVe2w4MVL44PN0GMzg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1729,14 +1929,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", - "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.6.tgz", + "integrity": "sha512-sOajCu6V0P1KPljWHKiDq6ymgqB+vfo3isUS4McqW1DZtvSVU2v/wuMhmRmkg3sFoq6GMaUUf8W4WtoSLkOV/Q==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1746,12 +1946,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", - "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.6.tgz", + "integrity": "sha512-Uvgd9p2gUnzYJxVdBLcU0KurF8aVhkmVyMKW4MIY1/BByvs3EBpv45q01o7pRTVmTvtQq5zDlytP3dcUgm7v9w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -1762,12 +1962,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", - "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.6.tgz", + "integrity": "sha512-f2wHfR2HF6yMj+y+/y07+SLqnOSwRp8KYLpQKOzS58XLVlULhXbiYcygfXQxJlMbhII9+yXDwOUFLf60/TL5tw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1777,12 +1977,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", - "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.6.tgz", + "integrity": "sha512-EKaWvnezBCMkRIHxMJSIIylzhqK09YpiJtDbr2wsXTwnO0TxyjMUkaw4RlFIZMIS0iDj0KyIg7H7XCguHu/YDA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -1793,12 +1993,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", - "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.6.tgz", + "integrity": "sha512-9g8iV146szUo5GWgXpRbq/GALTnY+WnNuRTuRHWWFfWGbP9ukRL0aO/jpu9dmOPikclkxnNsjY8/gsWl6bmZJQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1808,13 +2008,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", - "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.6.tgz", + "integrity": "sha512-eAGogjZgcwqAxhyFgqghvoHRr+EYRQPFjUXrTYKBRb5qPnAVxOOglaxc4/byHqjvq/bqO2F3/CGwTHsgKJYHhQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1824,14 +2024,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.6.tgz", + "integrity": "sha512-JEV8l3MHdmmdb7S7Cmx6rbNEjRCgTQMZxllveHO0mx6uiclB0NflCawlQQ6+o5ZrwjUBYPzHm2XoK4wqGVUFuw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1841,15 +2041,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", - "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.6.tgz", + "integrity": "sha512-xg1Z0J5JVYxtpX954XqaaAT6NpAY6LtZXvYFCJmGFJWwtlz2EmJoR8LycFRGNE8dBKizGWkGQZGegtkV8y8s+w==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1859,13 +2059,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", - "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.6.tgz", + "integrity": "sha512-esRCC/KsSEUvrSjv5rFYnjZI6qv4R1e/iHQrqwbZIoRJqk7xCvEUiN7L1XrmW5QSmQe3n1XD88wbgDTWLbVSyg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1875,13 +2075,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.6.tgz", + "integrity": "sha512-6DneiCiu91wm3YiNIGDWZsl6GfTTbspuj/toTEqLh9d4cx50UIzSdg+T96p8DuT7aJOBRhFyaE9ZvTHkXrXr6Q==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1891,12 +2091,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", - "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.6.tgz", + "integrity": "sha512-f8liz9JG2Va8A4J5ZBuaSdwfPqN6axfWRK+y66fjKYbwf9VBLuq4WxtinhJhvp1w6lamKUwLG0slK2RxqFgvHA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1906,12 +2106,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", - "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.6.tgz", + "integrity": "sha512-+QlAiZBMsBK5NqrBWFXCYeXyiU1y7BQ/OYaiPAcQJMomn5Tyg+r5WuVtyEuvTbpV7L25ZSLfE+2E9ywj4FD48A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -1922,12 +2122,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", - "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.6.tgz", + "integrity": "sha512-6voawq8T25Jvvnc4/rXcWZQKKxUNZcKMS8ZNrjxQqoRFernJJKjE3s18Qo6VFaatG5aiX5JV1oPD7DbJhn0a4Q==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -1938,15 +2138,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", - "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.6.tgz", + "integrity": "sha512-OKmi5wiMoRW5Smttne7BwHM8s/fb5JFs+bVGNSeHWzwZkWXWValR1M30jyXo1s/RaqgwwhEC62u4rFH/FBcBPg==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.5" + "@babel/plugin-transform-parameters": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1956,13 +2156,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", - "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.6.tgz", + "integrity": "sha512-N/C76ihFKlZgKfdkEYKtaRUtXZAgK7sOY4h2qrbVbVTXPrKGIi8aww5WGe/+Wmg8onn8sr2ut6FXlsbu/j6JHg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-replace-supers": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1972,12 +2172,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", - "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.6.tgz", + "integrity": "sha512-L5pZ+b3O1mSzJ71HmxSCmTVd03VOT2GXOigug6vDYJzE5awLI7P1g0wFcdmGuwSDSrQ0L2rDOe/hHws8J1rv3w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -1988,13 +2188,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", - "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.6.tgz", + "integrity": "sha512-cHbqF6l1QP11OkYTYQ+hhVx1E017O5ZcSPXk9oODpqhcAD1htsWG2NpHrrhthEO2qZomLK0FXS+u7NfrkF5aOQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -2005,12 +2205,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", - "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.6.tgz", + "integrity": "sha512-ST7guE8vLV+vI70wmAxuZpIKzVjvFX9Qs8bl5w6tN/6gOypPWUmMQL2p7LJz5E63vEGrDhAiYetniJFyBH1RkA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2020,13 +2220,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", - "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.6.tgz", + "integrity": "sha512-T9LtDI0BgwXOzyXrvgLTT8DFjCC/XgWLjflczTLXyvxbnSR/gpv0hbmzlHE/kmh9nOvlygbamLKRo6Op4yB6aw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2036,14 +2236,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", - "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.6.tgz", + "integrity": "sha512-Qu/ypFxCY5NkAnEhCF86Mvg3NSabKsh/TPpBVswEdkGl7+FbsYHy1ziRqJpwGH4thBdQHh8zx+z7vMYmcJ7iaQ==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-annotate-as-pure": "^7.24.6", + "@babel/helper-create-class-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -2053,13 +2253,25 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz", + "integrity": "sha512-DitEzDfOMnd13kZnDqns1ccmftwJTS9DMkyn9pYTxulS7bZxUxpMly3Nf23QQ6NwA4UB8lAqjbqWtyvElEMAkg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", - "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.6.tgz", + "integrity": "sha512-oARaglxhRsN18OYsnPTpb8TcKQWDYNsPNmTnx5++WOAsUJ0cSC/FZVlIJCKvPbU4yn/UXsS0551CFKJhN0CaMw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2069,12 +2281,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", - "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.6.tgz", + "integrity": "sha512-SMDxO95I8WXRtXhTAc8t/NFQUT7VYbIWwJCJgEli9ml4MhqUMh4S6hxgH6SmAC3eAQNWCDJFxcFeEt9w2sDdXg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.6", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2085,12 +2297,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", - "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.6.tgz", + "integrity": "sha512-DcrgFXRRlK64dGE0ZFBPD5egM2uM8mgfrvTMOSB2yKzOtjpGegVYkzh3s1zZg1bBck3nkXiaOamJUqK3Syk+4A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2129,12 +2341,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", - "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.6.tgz", + "integrity": "sha512-xnEUvHSMr9eOWS5Al2YPfc32ten7CXdH7Zwyyk7IqITg4nX61oHj+GxpNvl+y5JHjfN3KXE2IV55wAWowBYMVw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2144,13 +2356,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", - "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.6.tgz", + "integrity": "sha512-h/2j7oIUDjS+ULsIrNZ6/TKG97FgmEk1PXryk/HQq6op4XUUUwif2f69fJrzK0wza2zjCS1xhXmouACaWV5uPA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2160,12 +2372,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", - "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.6.tgz", + "integrity": "sha512-fN8OcTLfGmYv7FnDrsjodYBo1DhPL3Pze/9mIIE2MGCT1KgADYIOD7rEglpLHZj8PZlC/JFX5WcD+85FLAQusw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2175,12 +2387,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", - "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.6.tgz", + "integrity": "sha512-BJbEqJIcKwrqUP+KfUIkxz3q8VzXe2R8Wv8TaNgO1cx+nNavxn/2+H8kp9tgFSOL6wYPPEgFvU6IKS4qoGqhmg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2190,12 +2402,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", - "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.6.tgz", + "integrity": "sha512-IshCXQ+G9JIFJI7bUpxTE/oA2lgVLAIK8q1KdJNoPXOpvRaNjMySGuvLfBw/Xi2/1lLo953uE8hyYSDW3TSYig==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2205,12 +2417,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", - "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.6.tgz", + "integrity": "sha512-bKl3xxcPbkQQo5eX9LjjDpU2xYHeEeNQbOhj0iPvetSzA+Tu9q/o5lujF4Sek60CM6MgYvOS/DJuwGbiEYAnLw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2220,13 +2432,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", - "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.6.tgz", + "integrity": "sha512-8EIgImzVUxy15cZiPii9GvLZwsy7Vxc+8meSlR3cXFmBIl5W5Tn9LGBf7CDKkHj4uVfNXCJB8RsVfnmY61iedA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2236,13 +2448,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", - "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.6.tgz", + "integrity": "sha512-pssN6ExsvxaKU638qcWb81RrvvgZom3jDgU/r5xFZ7TONkZGFf4MhI2ltMb8OcQWhHyxgIavEU+hgqtbKOmsPA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2252,13 +2464,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", - "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.6.tgz", + "integrity": "sha512-quiMsb28oXWIDK0gXLALOJRXLgICLiulqdZGOaPPd0vRT7fQp74NtdADAVu+D8s00C+0Xs0MxVP0VKF/sZEUgw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.6", + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -2403,33 +2615,33 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", + "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", + "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2438,12 +2650,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", + "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5", + "@babel/types": "^7.24.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -2453,25 +2665,25 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", "to-fast-properties": "^2.0.0" }, "engines": { @@ -3693,9 +3905,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.0.tgz", - "integrity": "sha512-1aL4TuVrLS9sf8quCLerU3H9J4vtCtgu8VauYozrmEyU57i/EdKleCnsQ7vpnABIH6c9mnTxcH5sFkO3BlV8wQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.1.tgz", + "integrity": "sha512-uTq5j/UqUzbOaOxVy+osfOhpqOiLfUZ0Ut33UbcyyAPJbZcJsf4Mrsyb8r58FoIFlofw0iOFsuCA/oDK14VDJQ==", "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", @@ -3711,16 +3923,16 @@ } }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -3873,9 +4085,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", "cpu": [ "arm" ], @@ -3886,9 +4098,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", "cpu": [ "arm64" ], @@ -3899,9 +4111,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", "cpu": [ "arm64" ], @@ -3912,9 +4124,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", "cpu": [ "x64" ], @@ -3925,9 +4137,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", "cpu": [ "arm" ], @@ -3938,9 +4150,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", "cpu": [ "arm" ], @@ -3951,9 +4163,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", "cpu": [ "arm64" ], @@ -3964,9 +4176,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", "cpu": [ "arm64" ], @@ -3977,9 +4189,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", "cpu": [ "ppc64" ], @@ -3990,9 +4202,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", "cpu": [ "riscv64" ], @@ -4003,9 +4215,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", "cpu": [ "s390x" ], @@ -4016,9 +4228,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", "cpu": [ "x64" ], @@ -4029,9 +4241,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", "cpu": [ "x64" ], @@ -4042,9 +4254,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", "cpu": [ "arm64" ], @@ -4055,9 +4267,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", "cpu": [ "ia32" ], @@ -4068,9 +4280,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", "cpu": [ "x64" ], @@ -4096,6 +4308,33 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -4392,9 +4631,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", - "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", + "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", "dev": true, "dependencies": { "@types/node": "*", @@ -5739,12 +5978,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -6176,16 +6415,16 @@ } }, "node_modules/cacache/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -6286,9 +6525,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001620", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", - "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", + "version": "1.0.30001625", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", + "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", "dev": true, "funding": [ { @@ -6381,9 +6620,9 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "engines": { "node": ">=6.0" @@ -6719,16 +6958,16 @@ } }, "node_modules/config-file-ts/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -7263,9 +7502,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -8058,9 +8297,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.774", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz", - "integrity": "sha512-132O1XCd7zcTkzS3FgkAzKmnBuNJjK8WjcTtNuoylj7MYbqw5eXehjQ5OK91g0zm7OTKIPeaAG4CPoRfD9M1Mg==", + "version": "1.4.788", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.788.tgz", + "integrity": "sha512-ubp5+Ev/VV8KuRoWnfP2QF2Bg+O2ZFdb49DiiNbz2VmgkIqrnyYaqIOqj8A6K/3p1xV0QcU5hBQ1+BmB6ot1OA==", "dev": true }, "node_modules/elliptic": { @@ -8761,9 +9000,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -9122,6 +9361,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -9835,6 +10075,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { "once": "^1.3.0", @@ -10437,9 +10678,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -11323,12 +11564,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -11471,9 +11712,9 @@ } }, "node_modules/minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -11850,16 +12091,16 @@ } }, "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -11989,9 +12230,9 @@ } }, "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.3.tgz", + "integrity": "sha512-Q08/0IrpvM+NMY9PA2rti9Jb+JejTddwmwmVQGskAlhtcrw1wsRzoR6ode6mR+OAabNa75w/dxedSUY2mlphaQ==", "dev": true, "engines": { "node": ">=16" @@ -13199,9 +13440,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -13527,6 +13768,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "dev": true, "dependencies": { "glob": "^10.2.2", @@ -13552,16 +13794,16 @@ } }, "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", - "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -13942,6 +14184,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -13989,9 +14232,9 @@ "optional": true }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -14004,22 +14247,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", "fsevents": "~2.3.2" } }, @@ -14191,9 +14434,9 @@ } }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true }, "node_modules/schema-utils": { @@ -14831,9 +15074,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "node_modules/spdy": { From d6ec85db7bcee4b51bdd9e1b4e53e7402bbdcac1 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 1 Jun 2024 19:00:16 -0300 Subject: [PATCH 43/45] [ci]: Create Makefile --- Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..f4e9d8e17 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +.PHONY: build install + +build: + ./gradlew api:bootJar + cd desktop && npm run electron:build:deb + +install: + sudo dpkg -i desktop/release/nebulosa_0.1.0_amd64.deb From 6799fbd60e5b579215572b01e39f379a52c2118d Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 1 Jun 2024 19:52:06 -0300 Subject: [PATCH 44/45] [api]: Bump Gradle from 8.7 to 8.8 --- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch delta 34118 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 delta 34176 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 SGW6ptSs!X7{rG*o_g?|vpSEZK diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e7646dead..6f7a6eb33 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 7000b030d4f381eb2cb3fa8e512dad16d7c1f37e Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 1 Jun 2024 22:11:08 -0300 Subject: [PATCH 45/45] [api][desktop]: Improve Star Detection --- .../nebulosa/api/image/ImageTransformation.kt | 30 ++--- .../api/stardetection/StarDetectionOptions.kt | 3 +- desktop/src/app/image/image.component.html | 105 ++++++++++++++++-- desktop/src/app/image/image.component.scss | 8 ++ desktop/src/app/image/image.component.ts | 89 ++++++++++++--- .../src/app/settings/settings.component.html | 13 ++- desktop/src/shared/types/image.types.ts | 23 ++-- desktop/src/shared/types/settings.types.ts | 2 + desktop/src/styles.scss | 14 ++- .../astap/star/detection/AstapStarDetector.kt | 4 +- 10 files changed, 232 insertions(+), 59 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt b/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt index 96d9b80c5..42455df07 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt @@ -4,20 +4,20 @@ import nebulosa.image.algorithms.transformation.ProtectionMethod import nebulosa.image.format.ImageChannel data class ImageTransformation( - val force: Boolean = false, - val calibrationGroup: String? = null, - val debayer: Boolean = true, - val stretch: Stretch = Stretch.EMPTY, - val mirrorHorizontal: Boolean = false, - val mirrorVertical: Boolean = false, - val invert: Boolean = false, - val scnr: SCNR = SCNR.EMPTY, + @JvmField val force: Boolean = false, + @JvmField val calibrationGroup: String? = null, + @JvmField val debayer: Boolean = true, + @JvmField val stretch: Stretch = Stretch.EMPTY, + @JvmField val mirrorHorizontal: Boolean = false, + @JvmField val mirrorVertical: Boolean = false, + @JvmField val invert: Boolean = false, + @JvmField val scnr: SCNR = SCNR.EMPTY, ) { data class SCNR( - val channel: ImageChannel? = ImageChannel.GREEN, - val amount: Float = 0.5f, - val method: ProtectionMethod = ProtectionMethod.AVERAGE_NEUTRAL, + @JvmField val channel: ImageChannel? = ImageChannel.GREEN, + @JvmField val amount: Float = 0.5f, + @JvmField val method: ProtectionMethod = ProtectionMethod.AVERAGE_NEUTRAL, ) { companion object { @@ -27,10 +27,10 @@ data class ImageTransformation( } data class Stretch( - val auto: Boolean = false, - val shadow: Float = 0f, - val highlight: Float = 0.5f, - val midtone: Float = 1f, + @JvmField val auto: Boolean = false, + @JvmField val shadow: Float = 0f, + @JvmField val highlight: Float = 0.5f, + @JvmField val midtone: Float = 1f, ) { companion object { diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt index 5f8aa4940..9d3711755 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt @@ -10,10 +10,11 @@ data class StarDetectionOptions( @JvmField val type: StarDetectorType = StarDetectorType.ASTAP, @JvmField val executablePath: Path? = null, @JvmField val timeout: Duration = Duration.ZERO, + @JvmField val minSNR: Double = 0.0, ) : Supplier> { override fun get() = when (type) { - StarDetectorType.ASTAP -> AstapStarDetector(executablePath!!) + StarDetectorType.ASTAP -> AstapStarDetector(executablePath!!, minSNR) } companion object { diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index dbb15221c..ef6ee2804 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -20,10 +20,11 @@ - - - + + {{ s.hfd.toFixed(1) }} @@ -68,7 +69,7 @@ @@ -251,12 +252,12 @@
    - - @@ -268,7 +269,7 @@
    - @@ -294,7 +295,7 @@
    + [options]="scnrMethods" styleClass="p-inputtext-sm border-0" [autoDisplayFirst]="false">
    {{ item | enum }} @@ -312,7 +313,7 @@
    @@ -415,6 +416,92 @@
    + +
    +
    + + + + +
    +
    + + + + +
    +
    + COMPUTED +
    +
    + + + + +
    +
    + + + + +
    +
    + + + + +
    +
    + + + + +
    +
    + SELECTED +
    +
    + +
    +
    +
    +
    + + + + +
    +
    + + + + +
    +
    + + + + +
    +
    + + + + +
    +
    +
    +
    + + + +
    +
    diff --git a/desktop/src/app/image/image.component.scss b/desktop/src/app/image/image.component.scss index 9d5a0981c..edc3b3799 100644 --- a/desktop/src/app/image/image.component.scss +++ b/desktop/src/app/image/image.component.scss @@ -7,6 +7,14 @@ fill: transparent; stroke-width: 0.25rem; } + + canvas.detected-star { + width: 128px; + height: 128px; + background: #111; + image-rendering: pixelated; + border-radius: 4px; + } } .roi { diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 1f06971c5..4a8dbeaff 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -18,7 +18,7 @@ import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' -import { DEFAULT_FOV, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageDetectStars, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types' +import { DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS, StarDetectionDialog } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @@ -45,6 +45,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { @ViewChild('histogram') private readonly histogram!: HistogramComponent + @ViewChild('detectedStarCanvas') + private readonly detectedStarCanvas!: ElementRef + imageInfo?: ImageInfo private imageURL!: string imageData: ImageData = {} @@ -95,6 +98,27 @@ export class ImageComponent implements AfterViewInit, OnDestroy { useSimbad: false } + readonly starDetection: StarDetectionDialog = { + showDialog: false, + type: 'ASTAP', + minSNR: 0, + visible: false, + stars: [], + computed: { + hfd: 0, + snr: 0, + maxFlux: 0, + minFlux: 0, + }, + selected: { + x: 0, + y: 0, + snr: 0, + hfd: 0, + flux: 0 + }, + } + readonly solver: ImageSolverDialog = { showDialog: false, solving: false, @@ -114,11 +138,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { annotationInfo?: AstronomicalObject & Partial annotationIsVisible = false - readonly detectedStars: ImageDetectStars = { - visible: false, - stars: [] - } - readonly fitsHeaders: ImageFITSHeadersDialog = { showDialog: false, headers: [] @@ -332,16 +351,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { disabled: false, toggleable: false, toggled: false, - command: async () => { - const options = this.preference.starDetectionOptions('ASTAP').get() - this.detectedStars.stars = await this.api.detectStars(this.imageData.path!, options) - this.detectedStars.visible = this.detectedStars.stars.length > 0 - this.detectStarsMenuItem.toggleable = this.detectedStars.visible - this.detectStarsMenuItem.toggled = this.detectedStars.visible + command: () => { + this.starDetection.showDialog = true }, toggle: (event) => { + this.starDetection.visible = event.checked event.originalEvent?.stopImmediatePropagation() - this.detectedStars.visible = event.checked }, } @@ -713,8 +728,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.annotationIsVisible = false this.annotationMenuItem.toggleable = false - this.detectedStars.stars = [] - this.detectedStars.visible = false + this.starDetection.stars = [] + this.starDetection.visible = false this.detectStarsMenuItem.toggleable = false Object.assign(this.solver.solved, EMPTY_IMAGE_SOLVED) @@ -731,6 +746,44 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.computeHistogram() } + async detectStars() { + const options = this.preference.starDetectionOptions(this.starDetection.type).get() + options.minSNR = this.starDetection.minSNR + this.starDetection.stars = await this.api.detectStars(this.imageData.path!, options) + + let hfd = 0 + let snr = 0 + let maxFlux = 0 + let minFlux = 10000000 + + for (const star of this.starDetection.stars) { + hfd += star.hfd + snr += star.snr + minFlux = Math.min(minFlux, star.flux) + maxFlux = Math.max(maxFlux, star.flux) + } + + const starCount = this.starDetection.stars.length + this.starDetection.computed.hfd = starCount > 0 ? hfd / starCount : 0 + this.starDetection.computed.snr = starCount > 0 ? snr / starCount : 0 + this.starDetection.computed.maxFlux = maxFlux + this.starDetection.computed.minFlux = minFlux + + this.savePreference() + + this.starDetection.visible = this.starDetection.stars.length > 0 + this.detectStarsMenuItem.toggleable = this.starDetection.visible + this.detectStarsMenuItem.toggled = this.starDetection.visible + } + + selectDetectedStar(star: DetectedStar) { + Object.assign(this.starDetection.selected, star) + + const canvas = this.detectedStarCanvas.nativeElement + const ctx = canvas.getContext('2d') + ctx?.drawImage(this.image.nativeElement, star.x - 8, star.y - 8, 16, 16, 0, 0, canvas.width, canvas.height) + } + private async loadImage() { if (this.imageData.path) { await this.loadImageFromPath(this.imageData.path) @@ -1112,8 +1165,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { svg, } - console.info(fov.computed) - return true } else { return false @@ -1137,6 +1188,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const preference = this.preference.imagePreference.get() this.solver.radius = preference.solverRadius ?? this.solver.radius this.solver.type = preference.solverType ?? this.solver.types[0] + this.starDetection.type = preference.starDetectionType ?? this.starDetection.type + this.starDetection.minSNR = this.preference.starDetectionOptions(this.starDetection.type).get().minSNR ?? this.starDetection.type + this.fov.fovs = this.preference.imageFOVs.get() this.fov.fovs.forEach(e => { e.enabled = false; e.computed = undefined }) } @@ -1145,6 +1199,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const preference = this.preference.imagePreference.get() preference.solverRadius = this.solver.radius preference.solverType = this.solver.type + preference.starDetectionType = this.starDetection.type this.preference.imagePreference.set(preference) } diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 0931108e9..e2cdc4107 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -9,10 +9,10 @@
    - - + + + size="small" severity="danger" [text]="true" />
    @@ -63,6 +63,13 @@ (ngModelChange)="starDetectors.get(starDetectorType)!.type = $event; save()" [autoDisplayFirst]="false" /> + + + + & { minFlux: number, maxFlux: number } + selected: DetectedStar +} diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index b427182f6..b9226023d 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -24,10 +24,12 @@ export interface StarDetectionOptions { type: StarDetectorType executablePath: string timeout: number + minSNR: number } export const EMPTY_STAR_DETECTION_OPTIONS: StarDetectionOptions = { type: 'ASTAP', executablePath: '', timeout: 600, + minSNR: 0, } diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index b1a1660ff..e2df7238b 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -237,10 +237,16 @@ p-dropdownitem *, height: 3rem; } -.p-dialog .p-dialog-content:has(neb-slide-menu) { - background: #1e1e1e; - color: rgba(255, 255, 255, 0.87); - padding: 1.25rem 1.25rem 1.25rem 1.25rem; +.p-dialog { + .p-dialog-content { + padding-bottom: 0px; + + &:has(neb-slide-menu) { + background: #1e1e1e; + color: rgba(255, 255, 255, 0.87); + padding: 1.25rem 1.25rem 1.25rem 1.25rem; + } + } } .pixelated { diff --git a/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt b/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt index 6c25aa07b..d95dda050 100644 --- a/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt +++ b/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt @@ -13,7 +13,7 @@ import kotlin.io.path.exists import kotlin.io.path.inputStream import kotlin.io.path.nameWithoutExtension -class AstapStarDetector(path: Path) : StarDetector { +class AstapStarDetector(path: Path, private val minSNR: Double = 0.0) : StarDetector { private val executor = ProcessExecutor(path) @@ -22,7 +22,7 @@ class AstapStarDetector(path: Path) : StarDetector { arguments["-f"] = input arguments["-z"] = 0 - arguments["-extract"] = 0 + arguments["-extract"] = minSNR val process = executor.execute(arguments, workingDir = input.parent)