From 4136c2164fd316c96390047e6490eeb1a39b6e04 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 11 Jun 2024 21:20:42 -0300 Subject: [PATCH 1/5] [api]: Improve Siril command line --- .../api/livestacker/LiveStackingRequest.kt | 5 +- api/src/test/kotlin/SirilLiveStackerTest.kt | 28 ----- .../concurrency/latch/CountUpDownLatch.kt | 9 +- .../nebulosa/common/exec/CommandLine.kt | 97 ++++++++++------- .../common/exec/CommandLineListener.kt | 18 ++++ .../nebulosa/common/exec/LineReadListener.kt | 18 ---- .../src/test/kotlin/CommandLineTest.kt | 8 +- .../script/AbstractPixInsightScript.kt | 12 +-- .../pixinsight/script/PixInsightIsRunning.kt | 6 +- nebulosa-siril/build.gradle.kts | 1 + .../main/kotlin/nebulosa/siril/command/Cd.kt | 39 +++++++ .../kotlin/nebulosa/siril/command/Exit.kt | 8 ++ .../nebulosa/siril/command/LiveStack.kt | 57 ++++++++++ .../kotlin/nebulosa/siril/command/Load.kt | 39 +++++++ .../kotlin/nebulosa/siril/command/Requires.kt | 10 ++ .../nebulosa/siril/command/SirilCommand.kt | 11 ++ .../siril/command/SirilCommandLine.kt | 56 ++++++++++ .../kotlin/nebulosa/siril/command/StartLs.kt | 53 +++++++++ .../kotlin/nebulosa/siril/command/StopLs.kt | 11 ++ .../siril/livestacker/SirilLiveStacker.kt | 101 ++++-------------- .../src/test/kotlin/SirilLiveStackerTest.kt | 40 +++++++ 21 files changed, 440 insertions(+), 187 deletions(-) delete mode 100644 api/src/test/kotlin/SirilLiveStackerTest.kt create mode 100644 nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLineListener.kt delete mode 100644 nebulosa-common/src/main/kotlin/nebulosa/common/exec/LineReadListener.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Cd.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/LiveStack.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Load.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommand.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StopLs.kt create mode 100644 nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt diff --git a/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt b/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt index c9cbbb573..f44d606ff 100644 --- a/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/livestacker/LiveStackingRequest.kt @@ -1,7 +1,5 @@ package nebulosa.api.livestacker -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import nebulosa.api.beans.converters.angle.DegreesDeserializer import nebulosa.livestacker.LiveStacker import nebulosa.pixinsight.livestacker.PixInsightLiveStacker import nebulosa.pixinsight.script.PixInsightIsRunning @@ -20,7 +18,6 @@ data class LiveStackingRequest( @JvmField val dark: Path? = null, @JvmField val flat: Path? = null, @JvmField val bias: Path? = null, - @JvmField @field:JsonDeserialize(using = DegreesDeserializer::class) val rotate: Double = 0.0, @JvmField val use32Bits: Boolean = false, @JvmField val slot: Int = 1, ) : Supplier { @@ -29,7 +26,7 @@ data class LiveStackingRequest( val workingDirectory = Files.createTempDirectory("ls-") return when (type) { - LiveStackerType.SIRIL -> SirilLiveStacker(executablePath!!, workingDirectory, dark, flat, rotate, use32Bits) + LiveStackerType.SIRIL -> SirilLiveStacker(executablePath!!, workingDirectory, dark, flat, use32Bits) LiveStackerType.PIXINSIGHT -> { val runner = PixInsightScriptRunner(executablePath!!) diff --git a/api/src/test/kotlin/SirilLiveStackerTest.kt b/api/src/test/kotlin/SirilLiveStackerTest.kt deleted file mode 100644 index 02c1262e0..000000000 --- a/api/src/test/kotlin/SirilLiveStackerTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -import io.kotest.core.annotation.EnabledIf -import io.kotest.core.spec.style.StringSpec -import nebulosa.siril.livestacker.SirilLiveStacker -import nebulosa.test.NonGitHubOnlyCondition -import java.nio.file.Path -import kotlin.io.path.listDirectoryEntries - -@EnabledIf(NonGitHubOnlyCondition::class) -class SirilLiveStackerTest : StringSpec() { - - init { - "live stacking" { - val executablePath = Path.of("siril-cli") - val workingDirectory = Path.of("/home/tiagohm/Git/nebulosa/data/siril") - - val siril = SirilLiveStacker(executablePath, workingDirectory) - siril.start() - - val fitsDir = Path.of("/home/tiagohm/Imagens/Astrophotos/Light/C2023_A3/2024-05-29") - - for (fits in fitsDir.listDirectoryEntries().drop(140).sorted()) { - siril.add(fits) - } - - siril.stop() - } - } -} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/CountUpDownLatch.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/CountUpDownLatch.kt index ffdc616fa..508135ae0 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/CountUpDownLatch.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/CountUpDownLatch.kt @@ -48,15 +48,18 @@ class CountUpDownLatch(initialCount: Int = 0) : Supplier, CancellationL } fun await(n: Int = 0) { - if (n >= 0) sync.acquireSharedInterruptibly(n) + require(n >= 0) { "n must be greater or equal to 0" } + sync.acquireSharedInterruptibly(n) } fun await(timeout: Long, unit: TimeUnit, n: Int = 0): Boolean { - return n >= 0 && sync.tryAcquireSharedNanos(n, unit.toNanos(timeout)) + require(n >= 0) { "n must be greater or equal to 0" } + return sync.tryAcquireSharedNanos(n, unit.toNanos(timeout)) } fun await(timeout: Duration, n: Int = 0): Boolean { - return n >= 0 && sync.tryAcquireSharedNanos(n, timeout.toNanos()) + require(n >= 0) { "n must be greater or equal to 0" } + return sync.tryAcquireSharedNanos(n, timeout.toNanos()) } override fun onCancel(source: CancellationSource) { diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt index 550b722e6..8595325e8 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt @@ -2,6 +2,7 @@ package nebulosa.common.exec import nebulosa.common.concurrency.cancel.CancellationListener import nebulosa.common.concurrency.cancel.CancellationSource +import nebulosa.log.loggerFor import java.io.InputStream import java.io.OutputStream import java.io.PrintStream @@ -17,10 +18,10 @@ inline fun commandLine(action: CommandLine.Builder.() -> Unit): CommandLine { data class CommandLine internal constructor( private val builder: ProcessBuilder, - private val listeners: HashSet, + private val listeners: LinkedHashSet, ) : CompletableFuture(), CancellationListener { - @Volatile private var process: Process? = null + @Volatile private lateinit var process: Process @Volatile private var waiter: ProcessWaiter? = null @Volatile private var inputReader: StreamLineReader? = null @Volatile private var errorReader: StreamLineReader? = null @@ -29,63 +30,65 @@ data class CommandLine internal constructor( get() = builder.command() val pid - get() = process?.pid() ?: -1L + get() = process.pid() val exitCode - get() = process?.takeIf { !it.isAlive }?.exitValue() ?: -1 + get() = process.takeIf { !it.isAlive }?.exitValue() ?: -1 + + val isRunning + get() = ::process.isInitialized && process.isAlive val writer = PrintStream(object : OutputStream() { override fun write(b: Int) { - process?.outputStream?.write(b) + process.outputStream?.write(b) } override fun write(b: ByteArray) { - process?.outputStream?.write(b) + process.outputStream?.write(b) } override fun write(b: ByteArray, off: Int, len: Int) { - process?.outputStream?.write(b, off, len) + process.outputStream?.write(b, off, len) } override fun flush() { - process?.outputStream?.flush() + process.outputStream?.flush() } override fun close() { - process?.outputStream?.close() + process.outputStream?.close() } }, true) - fun registerLineReadListener(listener: LineReadListener) { - listeners.add(listener) + fun registerCommandLineListener(listener: CommandLineListener) { + synchronized(listeners) { listeners.add(listener) } } - fun unregisterLineReadListener(listener: LineReadListener) { - listeners.remove(listener) + fun unregisterCommandLineListener(listener: CommandLineListener) { + synchronized(listeners) { listeners.remove(listener) } } @Synchronized fun start(timeout: Duration = Duration.ZERO): CommandLine { - if (process == null) { - process = try { - builder.start() - } catch (e: Throwable) { - completeExceptionally(e) - return this - } + require(!::process.isInitialized) { "process has already executed" } + + process = try { + builder.start() + } catch (e: Throwable) { + completeExceptionally(e) + listeners.forEach { it.onExit(-1, e) } + return this + } - if (listeners.isNotEmpty()) { - inputReader = StreamLineReader(process!!.inputStream, false) - inputReader!!.start() + inputReader = StreamLineReader(process.inputStream, false) + inputReader!!.start() - errorReader = StreamLineReader(process!!.errorStream, true) - errorReader!!.start() - } + errorReader = StreamLineReader(process.errorStream, true) + errorReader!!.start() - waiter = ProcessWaiter(process!!, timeout.toMillis()) - waiter!!.start() - } + waiter = ProcessWaiter(process, timeout.toMillis()) + waiter!!.start() return this } @@ -101,9 +104,8 @@ data class CommandLine internal constructor( errorReader?.interrupt() errorReader = null - process?.destroyForcibly() - process?.waitFor() - process = null + process.destroyForcibly() + process.waitFor() } fun get(timeout: Duration): Int { @@ -140,7 +142,14 @@ data class CommandLine internal constructor( process.waitFor() } - complete(process.exitValue()) + with(process.exitValue()) { + complete(this) + synchronized(listeners) { listeners.forEach { it.onExit(this, null) } } + } + + waiter = null + inputReader = null + errorReader = null } } } @@ -161,10 +170,15 @@ data class CommandLine internal constructor( try { while (true) { val line = reader.readLine() ?: break - if (isError) listeners.forEach { it.onErrorRead(line) } - else listeners.forEach { it.onInputRead(line) } + + synchronized(listeners) { + listeners.forEach { it.onLineRead(line) } + } } - } catch (ignored: Throwable) { + } catch (e: InterruptedException) { + LOG.error("command line interrupted") + } catch (e: Throwable) { + LOG.error("command line failed", e) } finally { completable.complete(Unit) reader.close() @@ -182,7 +196,7 @@ data class CommandLine internal constructor( private val environment by lazy { builder.environment() } private val arguments = mutableMapOf() private var executable = "" - private val listeners = HashSet(1) + private val listeners = LinkedHashSet(1) fun executablePath(path: Path) = executable("$path") @@ -208,9 +222,9 @@ data class CommandLine internal constructor( fun workingDirectory(path: Path): Unit = run { builder.directory(path.toFile()) } - fun registerLineReadListener(listener: LineReadListener) = listeners.add(listener) + fun registerCommandLineListener(listener: CommandLineListener) = listeners.add(listener) - fun unregisterLineReadListener(listener: LineReadListener) = listeners.remove(listener) + fun unregisterCommandLineListener(listener: CommandLineListener) = listeners.remove(listener) override fun get(): CommandLine { val args = ArrayList(1 + arguments.size * 2) @@ -229,4 +243,9 @@ data class CommandLine internal constructor( return CommandLine(builder, listeners) } } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } } diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLineListener.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLineListener.kt new file mode 100644 index 000000000..9b32a8827 --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLineListener.kt @@ -0,0 +1,18 @@ +package nebulosa.common.exec + +interface CommandLineListener { + + fun onLineRead(line: String) = Unit + + fun onExit(exitCode: Int, exception: Throwable?) = Unit + + fun interface OnLineRead : CommandLineListener { + + override fun onLineRead(line: String) + } + + fun interface OnExit : CommandLineListener { + + override fun onExit(exitCode: Int, exception: Throwable?) + } +} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/LineReadListener.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/LineReadListener.kt deleted file mode 100644 index f836b4d91..000000000 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/LineReadListener.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nebulosa.common.exec - -interface LineReadListener { - - fun onInputRead(line: String) - - fun onErrorRead(line: String) - - fun interface OnInput : LineReadListener { - - override fun onErrorRead(line: String) = Unit - } - - fun interface OnError : LineReadListener { - - override fun onInputRead(line: String) = Unit - } -} diff --git a/nebulosa-common/src/test/kotlin/CommandLineTest.kt b/nebulosa-common/src/test/kotlin/CommandLineTest.kt index cfc0f6593..89cea34bf 100644 --- a/nebulosa-common/src/test/kotlin/CommandLineTest.kt +++ b/nebulosa-common/src/test/kotlin/CommandLineTest.kt @@ -6,7 +6,7 @@ import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.ints.shouldNotBeExactly import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual import io.kotest.matchers.longs.shouldBeLessThan -import nebulosa.common.exec.LineReadListener +import nebulosa.common.exec.CommandLineListener import nebulosa.common.exec.commandLine import nebulosa.test.NonGitHubOnlyCondition import java.nio.file.Path @@ -51,9 +51,9 @@ class CommandLineTest : StringSpec() { } shouldBeGreaterThanOrEqual 2000 shouldBeLessThan 10000 } "ls" { - val lineReadListener = object : LineReadListener.OnInput, ArrayList() { + val lineReadListener = object : CommandLineListener.OnLineRead, ArrayList(64) { - override fun onInputRead(line: String) { + override fun onLineRead(line: String) { add(line) } } @@ -61,7 +61,7 @@ class CommandLineTest : StringSpec() { val cmd = commandLine { executable("ls") workingDirectory(Path.of("../")) - registerLineReadListener(lineReadListener) + registerCommandLineListener(lineReadListener) } cmd.start().get() shouldBeExactly 0 diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt index 57468b766..ad5554d39 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/AbstractPixInsightScript.kt @@ -3,7 +3,7 @@ package nebulosa.pixinsight.script import com.fasterxml.jackson.module.kotlin.jsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import nebulosa.common.exec.CommandLine -import nebulosa.common.exec.LineReadListener +import nebulosa.common.exec.CommandLineListener import nebulosa.common.json.PathDeserializer import nebulosa.common.json.PathSerializer import nebulosa.log.loggerFor @@ -11,11 +11,11 @@ import org.apache.commons.codec.binary.Hex import java.nio.file.Path import java.util.concurrent.CompletableFuture -abstract class AbstractPixInsightScript : PixInsightScript, LineReadListener, CompletableFuture() { +abstract class AbstractPixInsightScript : PixInsightScript, CommandLineListener, CompletableFuture() { - override fun onInputRead(line: String) = Unit + override fun onLineRead(line: String) = Unit - override fun onErrorRead(line: String) = Unit + override fun onExit(exitCode: Int, exception: Throwable?) = Unit protected open fun beforeRun() = Unit @@ -36,11 +36,11 @@ abstract class AbstractPixInsightScript : PixInsightScript, LineReadListen else if (exception != null) completeExceptionally(exception) else complete(processOnComplete(exitCode).also { LOG.info("script processed. output={}", it) }) } finally { - commandLine.unregisterLineReadListener(this) + commandLine.unregisterCommandLineListener(this) } } - commandLine.registerLineReadListener(this) + commandLine.registerCommandLineListener(this) beforeRun() commandLine.start() } diff --git a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightIsRunning.kt b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightIsRunning.kt index d1a987ae9..4ec4b78ad 100644 --- a/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightIsRunning.kt +++ b/nebulosa-pixinsight/src/main/kotlin/nebulosa/pixinsight/script/PixInsightIsRunning.kt @@ -11,11 +11,7 @@ data class PixInsightIsRunning(private val slot: Int) : AbstractPixInsightScript private val slotCrashed = "The requested application instance #$slot has crashed" private val yieldedExecutionInstance = "$YIELDED_EXECUTION_INSTANCE$slot" - override fun onInputRead(line: String) { - processLine(line) - } - - override fun onErrorRead(line: String) { + override fun onLineRead(line: String) { processLine(line) } diff --git a/nebulosa-siril/build.gradle.kts b/nebulosa-siril/build.gradle.kts index 5df6ca750..85461be19 100644 --- a/nebulosa-siril/build.gradle.kts +++ b/nebulosa-siril/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { api(project(":nebulosa-math")) api(project(":nebulosa-livestacker")) implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-test")) } publishing { diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Cd.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Cd.kt new file mode 100644 index 000000000..f99dba012 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Cd.kt @@ -0,0 +1,39 @@ +package nebulosa.siril.command + +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.common.exec.CommandLineListener +import nebulosa.siril.command.SirilCommand.Companion.SCRIPT_EXECUTION_FAILED +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Sets the new current working [directory]. + */ +data class Cd(@JvmField val directory: Path) : SirilCommand, CommandLineListener { + + private val latch = CountUpDownLatch(1) + private val success = AtomicBoolean() + + override fun onLineRead(line: String) { + if (line.startsWith(SCRIPT_EXECUTION_FAILED)) { + success.set(false) + } else if (line.startsWith("log: Setting CWD")) { + success.set(true) + } else { + return + } + + latch.reset() + } + + override fun write(commandLine: SirilCommandLine): Boolean { + return try { + commandLine.registerCommandLineListener(this) + commandLine.write("cd $directory") + latch.await(30, TimeUnit.SECONDS) && success.get() + } finally { + commandLine.unregisterCommandLineListener(this) + } + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt new file mode 100644 index 000000000..2d0141c87 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt @@ -0,0 +1,8 @@ +package nebulosa.siril.command + +data object Exit : SirilCommand { + + override fun write(commandLine: SirilCommandLine) { + commandLine.write("exit") + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/LiveStack.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/LiveStack.kt new file mode 100644 index 000000000..e880af8e1 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/LiveStack.kt @@ -0,0 +1,57 @@ +package nebulosa.siril.command + +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.common.exec.CommandLineListener +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Processes the provided [path] for live stacking. + */ +data class LiveStack(@JvmField val path: Path) : SirilCommand, CommandLineListener { + + private val latch = CountUpDownLatch(1) + private val success = AtomicBoolean() + + override fun onLineRead(line: String) { + if (SUCCESSFUL_LOGS.any { line.startsWith(it, true) }) { + success.set(true) + } else if (FAILED_LOGS.any { line.contains(it, true) }) { + success.set(false) + } else { + return + } + + latch.reset() + } + + override fun onExit(exitCode: Int, exception: Throwable?) { + latch.reset() + } + + override fun write(commandLine: SirilCommandLine): Boolean { + return try { + commandLine.registerCommandLineListener(this) + commandLine.write("livestack \"$path\"") + latch.await(60, TimeUnit.SECONDS) + } finally { + commandLine.unregisterCommandLineListener(this) + } + } + + companion object { + + @JvmStatic private val SUCCESSFUL_LOGS = arrayOf( + "log: Waiting for second image", + "log: Stacked image", + "log: Live stacking waiting for files" + ) + + @JvmStatic private val FAILED_LOGS = arrayOf( + "Not enough stars", + "Sequence processing partially succeeded", + "Script execution failed", + ) + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Load.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Load.kt new file mode 100644 index 000000000..ebcc84b36 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Load.kt @@ -0,0 +1,39 @@ +package nebulosa.siril.command + +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.common.exec.CommandLineListener +import nebulosa.siril.command.SirilCommand.Companion.SCRIPT_EXECUTION_FAILED +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Loads the image at [path]. + */ +data class Load(@JvmField val path: Path) : SirilCommand, CommandLineListener { + + private val latch = CountUpDownLatch(1) + private val success = AtomicBoolean() + + override fun onLineRead(line: String) { + if (line.startsWith("log: Reading FITS")) { + success.set(true) + } else if (line.startsWith("log: Error opening image") || line.startsWith(SCRIPT_EXECUTION_FAILED)) { + success.set(false) + } else { + return + } + + latch.reset() + } + + override fun write(commandLine: SirilCommandLine): Boolean { + return try { + commandLine.registerCommandLineListener(this) + commandLine.write("load $path") + latch.await(30, TimeUnit.SECONDS) && success.get() + } finally { + commandLine.unregisterCommandLineListener(this) + } + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt new file mode 100644 index 000000000..11971c36b --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt @@ -0,0 +1,10 @@ +package nebulosa.siril.command + +data object Requires : SirilCommand { + + const val MIN_VERSION = "1.0.0" + + override fun write(commandLine: SirilCommandLine) { + commandLine.write("requires $MIN_VERSION") + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommand.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommand.kt new file mode 100644 index 000000000..a7b25f10d --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommand.kt @@ -0,0 +1,11 @@ +package nebulosa.siril.command + +sealed interface SirilCommand { + + fun write(commandLine: SirilCommandLine): T + + companion object { + + internal const val SCRIPT_EXECUTION_FAILED = "log: Script execution failed" + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt new file mode 100644 index 000000000..b104137f1 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt @@ -0,0 +1,56 @@ +package nebulosa.siril.command + +import nebulosa.common.concurrency.cancel.CancellationListener +import nebulosa.common.concurrency.cancel.CancellationSource +import nebulosa.common.exec.CommandLineListener +import nebulosa.common.exec.commandLine +import java.io.Closeable +import java.nio.file.Path + +data class SirilCommandLine(private val executablePath: Path) : Runnable, CancellationListener, Closeable { + + private val commandLine = commandLine { + executablePath(executablePath) + putArg("-s", "-") + } + + val isRunning + get() = commandLine.isRunning + + val pid + get() = commandLine.pid + + fun registerCommandLineListener(listener: CommandLineListener) { + commandLine.registerCommandLineListener(listener) + } + + fun unregisterCommandLineListener(listener: CommandLineListener) { + commandLine.unregisterCommandLineListener(listener) + } + + override fun run() { + if (!commandLine.isRunning) { + commandLine.start() + execute(Requires) + } + } + + internal fun write(command: String) { + if (commandLine.isRunning) { + commandLine.writer.println(command) + } + } + + fun execute(command: SirilCommand): T { + return command.write(this) + } + + override fun onCancel(source: CancellationSource) { + close() + } + + override fun close() { + execute(Exit) + commandLine.stop() + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt new file mode 100644 index 000000000..716b47754 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StartLs.kt @@ -0,0 +1,53 @@ +package nebulosa.siril.command + +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.common.exec.CommandLineListener +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile + +/** + * Initializes a livestacking session. + */ +data class StartLs( + @JvmField val dark: Path? = null, + @JvmField val flat: Path? = null, + @JvmField val use32Bits: Boolean = false, +) : SirilCommand, CommandLineListener { + + private val command by lazy { + buildString(256) { + append("start_ls") + if (dark != null && dark.exists() && dark.isRegularFile()) append(" \"-dark=$dark\"") + if (flat != null && flat.exists() && flat.isRegularFile()) append(" \"-flat=$flat\"") + if (use32Bits) append(" -32bits") + } + } + + private val latch = CountUpDownLatch(1) + private val success = AtomicBoolean() + + override fun onLineRead(line: String) { + if (line.startsWith("log: Live stacking waiting for files")) { + success.set(true) + } else if (line.startsWith(SirilCommand.SCRIPT_EXECUTION_FAILED)) { + success.set(false) + } else { + return + } + + latch.reset() + } + + override fun write(commandLine: SirilCommandLine): Boolean { + return try { + commandLine.registerCommandLineListener(this) + commandLine.write(command) + latch.await(30, TimeUnit.SECONDS) && success.get() + } finally { + commandLine.unregisterCommandLineListener(this) + } + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StopLs.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StopLs.kt new file mode 100644 index 000000000..03298c976 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/StopLs.kt @@ -0,0 +1,11 @@ +package nebulosa.siril.command + +/** + * Stops the live stacking session. + */ +data object StopLs : SirilCommand { + + override fun write(commandLine: SirilCommandLine) { + commandLine.write("stop_ls") + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt index fa20816da..2a0cb7ac5 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt @@ -1,15 +1,11 @@ package nebulosa.siril.livestacker import nebulosa.common.concurrency.latch.CountUpDownLatch -import nebulosa.common.exec.CommandLine -import nebulosa.common.exec.LineReadListener -import nebulosa.common.exec.commandLine +import nebulosa.common.exec.CommandLineListener import nebulosa.livestacker.LiveStacker -import nebulosa.log.debug import nebulosa.log.loggerFor -import nebulosa.math.Angle +import nebulosa.siril.command.* import java.nio.file.Path -import java.util.concurrent.atomic.AtomicBoolean import kotlin.io.path.deleteIfExists import kotlin.io.path.isSymbolicLink import kotlin.io.path.listDirectoryEntries @@ -20,75 +16,44 @@ data class SirilLiveStacker( private val workingDirectory: Path, private val dark: Path? = null, private val flat: Path? = null, - private val rotate: Angle = 0.0, private val use32Bits: Boolean = false, -) : LiveStacker, LineReadListener { - - @Volatile private var commandLine: CommandLine? = null +) : LiveStacker, CommandLineListener { + private val commandLine = SirilCommandLine(executablePath) private val waitForStacking = CountUpDownLatch() - private val failed = AtomicBoolean() override val isRunning - get() = commandLine != null && !commandLine!!.isDone + get() = commandLine.isRunning override val isStacking get() = !waitForStacking.get() @Synchronized override fun start() { - if (commandLine == null) { - commandLine = commandLine { - executablePath(executablePath) - putArg("-s", "-") - registerLineReadListener(this@SirilLiveStacker) - } - - commandLine!!.whenComplete { exitCode, e -> - LOG.info("live stacking finished. exitCode={}", exitCode, e) - commandLine = null - } + if (!commandLine.isRunning) { + commandLine.registerCommandLineListener(this) + commandLine.run() - commandLine!!.start() + LOG.info("live stacking started. pid={}", commandLine.pid) - LOG.info("live stacking started. pid={}", commandLine!!.pid) - - commandLine!!.writer.println(REQUIRES_COMMAND) - commandLine!!.writer.println("$CD_COMMAND $workingDirectory") - commandLine!!.writer.println(buildString(256) { - append(START_LS_COMMAND) - if (dark != null) append(" \"-dark=$dark\"") - if (flat != null) append(" \"-flat=$flat\"") - if (rotate != 0.0) append(" -rotate=$rotate") - if (use32Bits) append(" -32bits") - }) + check(commandLine.execute(Cd(workingDirectory))) { "failed to run cd command" } + check(commandLine.execute(StartLs(dark, flat, use32Bits))) { "failed to start livestacking" } } } @Synchronized override fun add(path: Path): Path? { - failed.set(false) - - try { - waitForStacking.countUp() - commandLine?.writer?.println("$LS_COMMAND \"$path\"") - waitForStacking.await() - } catch (e: Throwable) { - LOG.error("failed to add path", e) - return null + return if (commandLine.isRunning && commandLine.execute(LiveStack(path))) { + Path.of("$workingDirectory", "live_stack_00001.fit") + } else { + null } - - return if (failed.get()) null - else Path.of("$workingDirectory", "live_stack_00001.fit") } @Synchronized override fun stop() { waitForStacking.reset() - - commandLine?.writer?.println(STOP_LS_COMMAND) - commandLine?.stop() - commandLine = null + commandLine.execute(StopLs) } override fun close() { @@ -96,43 +61,19 @@ data class SirilLiveStacker( workingDirectory.deleteStackingFiles() } - override fun onInputRead(line: String) { - LOG.debug { line } - - if (SUCCESSFUL_LOGS.any { line.contains(it, true) }) { - waitForStacking.reset() - } else if (FAILED_LOGS.any { line.contains(it, true) }) { - failed.set(true) - waitForStacking.reset() - } + override fun onLineRead(line: String) { + // LOG.debug { line } + LOG.info(line) } - override fun onErrorRead(line: String) { - LOG.debug { line } - failed.set(true) - waitForStacking.reset() + override fun onExit(exitCode: Int, exception: Throwable?) { + LOG.info("live stacking finished. exitCode={}", exitCode, exception) } companion object { - private const val REQUIRES_COMMAND = "requires 1.0.0" - private const val CD_COMMAND = "cd" - private const val START_LS_COMMAND = "start_ls" - private const val LS_COMMAND = "livestack" - private const val STOP_LS_COMMAND = "stop_ls" - @JvmStatic private val LOG = loggerFor() - @JvmStatic private val SUCCESSFUL_LOGS = arrayOf( - "Waiting for second image", - "Stacked image", - ) - - @JvmStatic private val FAILED_LOGS = arrayOf( - "Not enough stars", - "Sequence processing partially succeeded", - ) - @JvmStatic private val LIVE_STACK_FIT_REGEX = Regex("live_stack_\\d+.fit") @JvmStatic private val LIVE_STACK_SEQ_REGEX = Regex("live_stack_\\d*.seq") diff --git a/nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt b/nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt new file mode 100644 index 000000000..ddf62d505 --- /dev/null +++ b/nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt @@ -0,0 +1,40 @@ +import io.kotest.core.annotation.EnabledIf +import io.kotest.engine.spec.tempdir +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import nebulosa.siril.livestacker.SirilLiveStacker +import nebulosa.test.AbstractFitsAndXisfTest +import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Path +import kotlin.io.path.copyTo +import kotlin.io.path.listDirectoryEntries + +@EnabledIf(NonGitHubOnlyCondition::class) +class SirilLiveStackerTest : AbstractFitsAndXisfTest() { + + init { + "live stacking" { + val executablePath = Path.of("siril-cli") + val workingDirectory = Path.of("/home/tiagohm/Git/nebulosa/data/siril") + + SirilLiveStacker(executablePath, workingDirectory).use { + it.start() + + val fitsDir = tempdir().toPath() + + PI_01_LIGHT.copyTo(Path.of("$fitsDir", "01.fits")) + PI_02_LIGHT.copyTo(Path.of("$fitsDir", "02.fits")) + PI_03_LIGHT.copyTo(Path.of("$fitsDir", "03.fits")) + PI_04_LIGHT.copyTo(Path.of("$fitsDir", "04.fits")) + + for (fits in fitsDir.listDirectoryEntries().shouldHaveSize(4).sorted()) { + it.add(fits).shouldNotBeNull() + } + + workingDirectory.listDirectoryEntries().shouldHaveSize(5) + } + + workingDirectory.listDirectoryEntries().shouldHaveSize(1) + } + } +} From 17f5b9fecb1d439fb1961e737cff195c630b4b33 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 11 Jun 2024 22:42:09 -0300 Subject: [PATCH 2/5] [api]: Implement Siril Plate Solver --- .../LocalAstrometryNetPlateSolver.kt | 8 +- .../nebulosa/common/exec/CommandLine.kt | 2 + .../main/kotlin/nebulosa/fits/FitsHeader.kt | 22 ++- .../nebulosa/image/format/AbstractHeader.kt | 17 +- .../kotlin/nebulosa/image/format/Header.kt | 18 +- .../nebulosa/image/format/WritableHeader.kt | 6 +- .../nebulosa/platesolver/PlateSolution.kt | 9 +- nebulosa-siril/build.gradle.kts | 1 + .../nebulosa/siril/command/DumpHeader.kt | 58 ++++++ .../kotlin/nebulosa/siril/command/Exit.kt | 3 + .../nebulosa/siril/command/PlateSolve.kt | 178 ++++++++++++++++++ .../kotlin/nebulosa/siril/command/Requires.kt | 3 + .../siril/command/SirilCommandLine.kt | 8 + .../siril/livestacker/SirilLiveStacker.kt | 14 +- .../siril/platesolver/SirilPlateSolver.kt | 33 ++++ .../src/test/kotlin/SirilLiveStackerTest.kt | 25 ++- 16 files changed, 370 insertions(+), 35 deletions(-) create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/DumpHeader.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt create mode 100644 nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt diff --git a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt index 7b6f0dedf..4965e93d3 100644 --- a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt +++ b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt @@ -1,7 +1,7 @@ package nebulosa.astrometrynet.platesolver import nebulosa.common.concurrency.cancel.CancellationToken -import nebulosa.common.exec.LineReadListener +import nebulosa.common.exec.CommandLineListener import nebulosa.common.exec.commandLine import nebulosa.image.Image import nebulosa.log.loggerFor @@ -61,7 +61,7 @@ data class LocalAstrometryNetPlateSolver(private val executablePath: Path) : Pla try { cancellationToken.listen(cmd) - cmd.registerLineReadListener(solution) + cmd.registerCommandLineListener(solution) cmd.start() LOG.info("astrometry.net exited. code={}", cmd.get()) return solution.get() @@ -74,14 +74,14 @@ data class LocalAstrometryNetPlateSolver(private val executablePath: Path) : Pla } } - private class PlateSolutionLineReader : LineReadListener.OnInput, Supplier { + private class PlateSolutionLineReader : CommandLineListener.OnLineRead, Supplier { @Volatile private var fieldCenter: DoubleArray? = null @Volatile private var fieldRotation: Angle = 0.0 @Volatile private var pixelScale: Angle = 0.0 @Volatile private var fieldSize: DoubleArray? = null - override fun onInputRead(line: String) { + override fun onLineRead(line: String) { fieldCenter(line)?.also { fieldCenter = it } ?: fieldRotation(line)?.also { fieldRotation = it } ?: pixelScale(line)?.also { pixelScale = it } diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt index 8595325e8..0a27196d5 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/exec/CommandLine.kt @@ -106,6 +106,8 @@ data class CommandLine internal constructor( process.destroyForcibly() process.waitFor() + + listeners.clear() } fun get(timeout: Duration): Int { diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHeader.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHeader.kt index 9e0b83afd..6be7004d0 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHeader.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHeader.kt @@ -7,6 +7,7 @@ import nebulosa.io.SeekableSource import nebulosa.io.source import nebulosa.log.loggerFor import java.util.* +import java.util.function.Predicate open class FitsHeader : AbstractHeader { @@ -54,15 +55,6 @@ open class FitsHeader : AbstractHeader { FitsHeaderCard.create(key, value, comment).also(::add) } - override fun add(card: HeaderCard) { - if (!card.isKeyValuePair) cards.add(card) - else { - val index = cards.indexOfFirst { it.key == card.key } - if (index >= 0) cards[index] = card - else cards.add(card) - } - } - open class ReadOnly : FitsHeader { constructor() : super(LinkedList()) @@ -87,10 +79,20 @@ open class FitsHeader : AbstractHeader { final override fun add(key: String, value: String, comment: String) = Unit - final override fun add(card: HeaderCard) = Unit + final override fun add(element: HeaderCard) = false final override fun addAll(cards: Iterable) = Unit + final override fun addAll(elements: Collection) = false + + final override fun removeIf(filter: Predicate) = false + + final override fun remove(element: HeaderCard) = false + + final override fun removeAll(elements: Collection) = false + + final override fun retainAll(elements: Collection) = false + final override fun delete(key: HeaderKey) = false final override fun delete(key: String) = false diff --git a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt index 3bf66aa24..8fdb541a9 100644 --- a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt +++ b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt @@ -4,7 +4,7 @@ import java.io.Serializable import java.util.* abstract class AbstractHeader protected constructor(@JvmField protected val cards: LinkedList) : - Header, Collection by cards, Serializable { + Header, MutableCollection by cards, Serializable { constructor() : this(LinkedList()) @@ -12,8 +12,19 @@ abstract class AbstractHeader protected constructor(@JvmField protected val card abstract fun readOnly(): Header - override fun clear() { - cards.clear() + override fun add(element: HeaderCard): Boolean { + return if (!element.isKeyValuePair) { + cards.add(element) + } else { + val index = cards.indexOfFirst { it.key == element.key } + + if (index >= 0) { + cards[index] = element + true + } else { + cards.add(element) + } + } } override fun delete(key: String): Boolean { diff --git a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/Header.kt b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/Header.kt index e8fd2a4f0..f396737e4 100644 --- a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/Header.kt +++ b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/Header.kt @@ -1,10 +1,12 @@ package nebulosa.image.format +import java.util.function.Predicate + interface Header : ReadableHeader, WritableHeader, Cloneable { public override fun clone(): Header - data object Empty : Header, Iterator { + data object Empty : Header, MutableIterator { override fun clone() = this @@ -20,6 +22,12 @@ interface Header : ReadableHeader, WritableHeader, Cloneable { override fun clear() = Unit + override fun remove(element: HeaderCard) = false + + override fun removeAll(elements: Collection) = false + + override fun retainAll(elements: Collection) = false + override fun add(key: String, value: Boolean, comment: String) = Unit override fun add(key: String, value: Int, comment: String) = Unit @@ -28,12 +36,18 @@ interface Header : ReadableHeader, WritableHeader, Cloneable { override fun add(key: String, value: String, comment: String) = Unit - override fun add(card: HeaderCard) = Unit + override fun add(element: HeaderCard) = false + + override fun addAll(elements: Collection) = false override fun delete(key: String) = false override fun hasNext() = false override fun next() = TODO("Unsupported operation") + + override fun remove() = Unit + + override fun removeIf(filter: Predicate) = false } } diff --git a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt index aaf1a1078..4a6f4c017 100644 --- a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt +++ b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt @@ -1,8 +1,6 @@ package nebulosa.image.format -interface WritableHeader { - - fun clear() +interface WritableHeader : MutableCollection { fun add(key: HeaderKey, value: Boolean) = add(key.key, value, key.comment) @@ -20,8 +18,6 @@ interface WritableHeader { fun add(key: String, value: String, comment: String = "") - fun add(card: HeaderCard) - fun addAll(cards: Iterable) = cards.forEach(::add) fun delete(key: HeaderKey) = delete(key.key) diff --git a/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt index 00718a98e..04a6d68df 100644 --- a/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt +++ b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt @@ -7,10 +7,7 @@ import nebulosa.image.format.ReadableHeader import nebulosa.log.loggerFor import nebulosa.math.* import nebulosa.wcs.computeCdMatrix -import kotlin.math.abs -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.hypot +import kotlin.math.* data class PlateSolution( @JvmField val solved: Boolean = false, @@ -22,8 +19,8 @@ data class PlateSolution( @JvmField val height: Angle = 0.0, @JvmField val parity: Parity = Parity.NORMAL, @JvmField val radius: Angle = hypot(width, height).rad / 2.0, - @JvmField val widthInPixels: Double = width / scale, - @JvmField val heightInPixels: Double = height / scale, + @JvmField val widthInPixels: Double = truncate(width / scale), + @JvmField val heightInPixels: Double = truncate(height / scale), private val header: Collection = emptyList(), ) : FitsHeader.ReadOnly(header) { diff --git a/nebulosa-siril/build.gradle.kts b/nebulosa-siril/build.gradle.kts index 85461be19..848424aef 100644 --- a/nebulosa-siril/build.gradle.kts +++ b/nebulosa-siril/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { api(project(":nebulosa-common")) api(project(":nebulosa-math")) api(project(":nebulosa-livestacker")) + api(project(":nebulosa-platesolver")) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/DumpHeader.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/DumpHeader.kt new file mode 100644 index 000000000..a17992ba3 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/DumpHeader.kt @@ -0,0 +1,58 @@ +package nebulosa.siril.command + +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.common.exec.CommandLineListener +import nebulosa.fits.FitsHeader +import nebulosa.fits.FitsHeaderCard +import nebulosa.image.format.Header +import nebulosa.log.debug +import nebulosa.log.loggerFor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +data class DumpHeader(private val header: Header = FitsHeader()) : SirilCommand
, CommandLineListener { + + private val latch = CountUpDownLatch(1) + private val started = AtomicBoolean() + private val finished = AtomicBoolean() + + override fun onLineRead(line: String) { + if (finished.get()) return + + if (started.get()) { + val card = line.replaceFirst("log: ", "").trimStart() + + try { + with(FitsHeaderCard.from(card)) { + header.add(this) + + LOG.debug { line } + + if (key == "END") { + finished.set(true) + latch.reset() + } + } + } catch (ignored: Throwable) { + } + } else if (line.contains("FITS header for currently loaded image", true)) { + started.set(true) + } + } + + override fun write(commandLine: SirilCommandLine): Header { + return try { + commandLine.registerCommandLineListener(this) + commandLine.write("dumpheader") + latch.await(15, TimeUnit.SECONDS) + header + } finally { + commandLine.unregisterCommandLineListener(this) + } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt index 2d0141c87..ab5f724e6 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Exit.kt @@ -1,5 +1,8 @@ package nebulosa.siril.command +/** + * Quits the application. + */ data object Exit : SirilCommand { override fun write(commandLine: SirilCommandLine) { diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt new file mode 100644 index 000000000..b26333d63 --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt @@ -0,0 +1,178 @@ +package nebulosa.siril.command + +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.common.exec.CommandLineListener +import nebulosa.log.debug +import nebulosa.log.loggerFor +import nebulosa.math.* +import nebulosa.platesolver.Parity +import nebulosa.platesolver.PlateSolution +import java.nio.file.Path +import java.time.Duration +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Plate solves the loaded image. + * + * ```txt + * log: Up is +164.28 deg ClockWise wrt. N + * log: Resolution: 1.368 arcsec/px + * log: Focal length: 1395.85 mm + * log: Pixel size: 9.26 µm + * log: Field of view: 47' 15.23" x 32' 9.38" + * log: Saved focal length 1395.85 and pixel size 9.26 as default values + * log: Image center: alpha: 13h25m38s, delta: -43°00'52" + * ``` + */ +data class PlateSolve( + @JvmField val path: Path, + @JvmField val focalLength: Double = 0.0, + @JvmField val useCenterCoordinates: Boolean = false, + @JvmField val rightAscension: Angle = 0.0, val declination: Angle = 0.0, + @JvmField val downsampleFactor: Int = 0, + @JvmField val timeout: Duration = Duration.ZERO, +) : SirilCommand, CommandLineListener { + + private val command by lazy { + buildString(256) { + append("platesolve") + if (useCenterCoordinates) append(" ${rightAscension.toHours},${declination.toDegrees}") + append(" -platesolve -noflip") + if (focalLength > 0.0) append(" -focal=$focalLength") + if (downsampleFactor > 0) append(" -downscale=$downsampleFactor") + } + } + + private val latch = CountUpDownLatch(1) + private val success = AtomicBoolean() + private val exited = AtomicBoolean() + + @Volatile private var orientation: Angle = 0.0 + @Volatile private var parity = Parity.NORMAL + @Volatile private var resolution: Angle = 0.0 + @Volatile private var fovWidth: Angle = 0.0 + @Volatile private var fovHeight: Angle = 0.0 + @Volatile private var imageCenterRA: Angle = 0.0 + @Volatile private var imageCenterDEC: Angle = 0.0 + + override fun onLineRead(line: String) { + LOG.debug { line } + + if (line.matchesOrientation() || line.matchesUndeterminatedOrientation() || + line.matchesResolution() || line.matchesFOV() + ) { + return + } + + if (line.matchesImageCenter()) { + success.set(true) + latch.reset() + } + } + + override fun onExit(exitCode: Int, exception: Throwable?) { + LOG.info("plate solver finished. exitCode={}", exitCode, exception) + exited.set(true) + latch.reset() + } + + override fun write(commandLine: SirilCommandLine): PlateSolution { + if (commandLine.execute(Load(path))) { + LOG.info("plate solver started. pid={}", commandLine.pid) + + try { + commandLine.registerCommandLineListener(this) + commandLine.write(command) + + if (!latch.await(maxOf(timeout, MIN_TIMEOUT)) || exited.get() || !success.get()) { + return PlateSolution.NO_SOLUTION + } + } finally { + commandLine.unregisterCommandLineListener(this) + } + + if (success.get()) { + return PlateSolution(true, orientation, resolution, imageCenterRA, imageCenterDEC, fovWidth, fovHeight, parity) + } + } else { + LOG.error("failed to load $path") + } + + return PlateSolution.NO_SOLUTION + } + + private fun String.matchesOrientation(): Boolean { + val m = ORIENTATION_REGEX.matchEntire(this) ?: return false + orientation = m.groupValues[1].toDouble().deg + parity = if ("flipped" in this) Parity.FLIPPED else Parity.NORMAL + return true + } + + private fun String.matchesUndeterminatedOrientation(): Boolean { + return if (startsWith(UNDETERMINED_ORIENTATION_REGEX)) { + orientation = 0.0 + true + } else { + false + } + } + + private fun String.matchesResolution(): Boolean { + val m = RESOLUTION_REGEX.matchEntire(this) ?: return false + resolution = m.groupValues[1].toDouble().arcsec + return true + } + + private fun String.matchesFOV(): Boolean { + val m = FOV_REGEX.matchEntire(this) ?: return false + val (_, width, height) = m.groupValues + fovWidth = width.parseFovInDHMS() + fovHeight = height.parseFovInDHMS() + return true + } + + private fun String.matchesImageCenter(): Boolean { + val m = IMAGE_CENTER_REGEX.matchEntire(this) ?: return false + val (_, alpha, delta) = m.groupValues + imageCenterRA = alpha.hours + imageCenterDEC = delta.deg + return true + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + @JvmStatic private val MIN_TIMEOUT = Duration.ofSeconds(30) + + private const val INT_REGEX = "\\d+" + private const val SIGNED_INT_REGEX = "[-+]?$INT_REGEX" + private const val FLOAT_REGEX = "$SIGNED_INT_REGEX(?:\\.$INT_REGEX)?" + + // https://gitlab.com/free-astro/siril/-/blob/master/src/algos/astrometry_solver.c + + @JvmStatic private val ORIENTATION_REGEX = "log: Up is ($FLOAT_REGEX) deg ClockWise wrt. N( \\(flipped\\))?".toRegex() + private const val UNDETERMINED_ORIENTATION_REGEX = "log: Up position wrt. N is undetermined" + @JvmStatic private val RESOLUTION_REGEX = "log: Resolution:\\s*($FLOAT_REGEX) arcsec/px".toRegex() + @JvmStatic private val FOV_REGEX = "log: Field of view:\\s*(.*) x (.*)".toRegex() + @JvmStatic private val IMAGE_CENTER_REGEX = "log: Image center: alpha: (.*), delta: (.*)".toRegex() + + @JvmStatic + private fun String.parseFovInDHMS(): Angle { + val parts = split(" ") + + var value = 0.0 + + for (part in parts) { + if (part.endsWith('d')) { + value = part.substring(0, part.length - 1).toDouble() + } else if (part.endsWith('m') || part.endsWith('\'')) { + value += part.substring(0, part.length - 1).toDouble() / 60.0 + } else if (part.endsWith('s') || part.endsWith('"')) { + value += part.substring(0, part.length - 1).toDouble() / 3600.0 + } + } + + return value.deg + } + } +} diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt index 11971c36b..4dafe6c17 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/Requires.kt @@ -1,5 +1,8 @@ package nebulosa.siril.command +/** + * Returns an error if the version of Siril is older than the one passed in argument. + */ data object Requires : SirilCommand { const val MIN_VERSION = "1.0.0" diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt index b104137f1..23d873705 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/SirilCommandLine.kt @@ -4,6 +4,8 @@ import nebulosa.common.concurrency.cancel.CancellationListener import nebulosa.common.concurrency.cancel.CancellationSource import nebulosa.common.exec.CommandLineListener import nebulosa.common.exec.commandLine +import nebulosa.log.debug +import nebulosa.log.loggerFor import java.io.Closeable import java.nio.file.Path @@ -37,6 +39,7 @@ data class SirilCommandLine(private val executablePath: Path) : Runnable, Cancel internal fun write(command: String) { if (commandLine.isRunning) { + LOG.debug { "writing command: $command" } commandLine.writer.println(command) } } @@ -53,4 +56,9 @@ data class SirilCommandLine(private val executablePath: Path) : Runnable, Cancel execute(Exit) commandLine.stop() } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } } diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt index 2a0cb7ac5..3e1a89708 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/livestacker/SirilLiveStacker.kt @@ -3,6 +3,7 @@ package nebulosa.siril.livestacker import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.common.exec.CommandLineListener import nebulosa.livestacker.LiveStacker +import nebulosa.log.debug import nebulosa.log.loggerFor import nebulosa.siril.command.* import java.nio.file.Path @@ -36,8 +37,13 @@ data class SirilLiveStacker( LOG.info("live stacking started. pid={}", commandLine.pid) - check(commandLine.execute(Cd(workingDirectory))) { "failed to run cd command" } - check(commandLine.execute(StartLs(dark, flat, use32Bits))) { "failed to start livestacking" } + try { + check(commandLine.execute(Cd(workingDirectory))) { "failed to run cd command" } + check(commandLine.execute(StartLs(dark, flat, use32Bits))) { "failed to start livestacking" } + } catch (e: Throwable) { + commandLine.close() + throw e + } } } @@ -54,6 +60,7 @@ data class SirilLiveStacker( override fun stop() { waitForStacking.reset() commandLine.execute(StopLs) + commandLine.close() } override fun close() { @@ -62,8 +69,7 @@ data class SirilLiveStacker( } override fun onLineRead(line: String) { - // LOG.debug { line } - LOG.info(line) + LOG.debug { line } } override fun onExit(exitCode: Int, exception: Throwable?) { diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt new file mode 100644 index 000000000..53807a75f --- /dev/null +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt @@ -0,0 +1,33 @@ +package nebulosa.siril.platesolver + +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.image.Image +import nebulosa.math.Angle +import nebulosa.math.toDegrees +import nebulosa.platesolver.PlateSolution +import nebulosa.platesolver.PlateSolver +import nebulosa.siril.command.PlateSolve +import nebulosa.siril.command.SirilCommandLine +import java.nio.file.Path +import java.time.Duration + +data class SirilPlateSolver(private val executablePath: Path) : PlateSolver { + + override fun solve( + path: Path?, image: Image?, + centerRA: Angle, centerDEC: Angle, radius: Angle, + downsampleFactor: Int, timeout: Duration?, cancellationToken: CancellationToken + ): PlateSolution { + val commandLine = SirilCommandLine(executablePath) + + return try { + commandLine.run() + cancellationToken.listen(commandLine) + val useCenterCoordinates = radius > 0.0 && radius.toDegrees >= 0.1 && centerRA.isFinite() && centerDEC.isFinite() + commandLine.execute(PlateSolve(path!!, 0.0, useCenterCoordinates, centerRA, centerDEC, downsampleFactor, timeout ?: Duration.ZERO)) + } finally { + cancellationToken.unlisten(commandLine) + commandLine.close() + } + } +} diff --git a/nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt b/nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt index ddf62d505..1bc2cc7b1 100644 --- a/nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt +++ b/nebulosa-siril/src/test/kotlin/SirilLiveStackerTest.kt @@ -1,8 +1,15 @@ import io.kotest.core.annotation.EnabledIf import io.kotest.engine.spec.tempdir +import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.doubles.shouldBeExactly import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import nebulosa.math.* +import nebulosa.platesolver.Parity import nebulosa.siril.livestacker.SirilLiveStacker +import nebulosa.siril.platesolver.SirilPlateSolver import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition import java.nio.file.Path @@ -13,8 +20,9 @@ import kotlin.io.path.listDirectoryEntries class SirilLiveStackerTest : AbstractFitsAndXisfTest() { init { + val executablePath = Path.of("siril-cli") + "live stacking" { - val executablePath = Path.of("siril-cli") val workingDirectory = Path.of("/home/tiagohm/Git/nebulosa/data/siril") SirilLiveStacker(executablePath, workingDirectory).use { @@ -36,5 +44,20 @@ class SirilLiveStackerTest : AbstractFitsAndXisfTest() { workingDirectory.listDirectoryEntries().shouldHaveSize(1) } + "plate solver" { + val solver = SirilPlateSolver(executablePath) + val solution = solver.solve(PI_01_LIGHT, null) + + solution.solved.shouldBeTrue() + solution.orientation.toDegrees shouldBe (-90.02 plusOrMinus 1e-2) + solution.rightAscension.formatHMS() shouldBe "00h06m46.0s" + solution.declination.formatSignedDMS() shouldBe "+089°51'42.0\"" + solution.scale.toArcsec shouldBe (3.575 plusOrMinus 1e-3) + solution.width.formatDMS() shouldBe "001°16'16.3\"" + solution.height.formatDMS() shouldBe "001°01'01.1\"" + solution.parity shouldBe Parity.FLIPPED + solution.widthInPixels shouldBeExactly 1280.0 + solution.heightInPixels shouldBeExactly 1024.0 + } } } From 0b76e71fd134edf4d57c852168194a572a32fb4a Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 12 Jun 2024 20:30:18 -0300 Subject: [PATCH 3/5] [api][desktop]: Support Siril Plate Solver --- .../api/platesolver/PlateSolverRequest.kt | 4 +++ .../api/platesolver/PlateSolverService.kt | 3 +- .../api/platesolver/PlateSolverType.kt | 1 + desktop/src/app/image/image.component.html | 35 ++++++++++++++----- desktop/src/app/image/image.component.ts | 29 +++++++++++++-- .../src/app/settings/settings.component.html | 2 +- .../src/app/settings/settings.component.ts | 2 ++ desktop/src/shared/pipes/dropdown-options.ts | 2 +- desktop/src/shared/types/image.types.ts | 4 +++ desktop/src/shared/types/settings.types.ts | 6 ++-- .../astap/platesolver/AstapPlateSolver.kt | 4 +-- .../LibAstrometryNetPlateSolver.kt | 2 +- .../LocalAstrometryNetPlateSolver.kt | 4 +-- .../NovaAstrometryNetPlateSolver.kt | 4 +-- .../nebulosa/platesolver/PlateSolver.kt | 2 +- .../nebulosa/siril/command/PlateSolve.kt | 4 ++- .../siril/platesolver/SirilPlateSolver.kt | 10 ++++-- 17 files changed, 89 insertions(+), 29 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt index e671615f7..dfd73f7d0 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt @@ -4,6 +4,7 @@ import nebulosa.astap.platesolver.AstapPlateSolver import nebulosa.astrometrynet.nova.NovaAstrometryNetService import nebulosa.astrometrynet.platesolver.LocalAstrometryNetPlateSolver import nebulosa.astrometrynet.platesolver.NovaAstrometryNetPlateSolver +import nebulosa.siril.platesolver.SirilPlateSolver import okhttp3.OkHttpClient import org.hibernate.validator.constraints.time.DurationMax import org.hibernate.validator.constraints.time.DurationMin @@ -16,6 +17,8 @@ data class PlateSolverRequest( @JvmField val type: PlateSolverType = PlateSolverType.ASTROMETRY_NET_ONLINE, @JvmField val executablePath: Path? = null, @JvmField val downsampleFactor: Int = 0, + @JvmField val focalLength: Double = 0.0, + @JvmField val pixelSize: Double = 0.0, @JvmField val apiUrl: String = "", @JvmField val apiKey: String = "", @field:DurationMin(seconds = 0) @field:DurationMax(minutes = 5) @field:DurationUnit(ChronoUnit.SECONDS) @@ -31,6 +34,7 @@ data class PlateSolverRequest( val service = NOVA_ASTROMETRY_NET_CACHE.getOrPut(key) { NovaAstrometryNetService(apiUrl, httpClient) } NovaAstrometryNetPlateSolver(service, apiKey) } + PlateSolverType.SIRIL -> SirilPlateSolver(executablePath!!, focalLength, pixelSize) } } diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt index 553b60193..aa7fbb45c 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt @@ -26,6 +26,5 @@ class PlateSolverService( fun solve( options: PlateSolverRequest, path: Path, centerRA: Angle = 0.0, centerDEC: Angle = 0.0, radius: Angle = 0.0, - ) = options.get(httpClient) - .solve(path, null, centerRA, centerDEC, radius, 1, options.timeout.takeIf { it.toSeconds() > 0 }) + ) = options.get(httpClient).solve(path, null, centerRA, centerDEC, radius, options.downsampleFactor, options.timeout) } diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverType.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverType.kt index 57df173dc..3c85a9a86 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverType.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverType.kt @@ -4,4 +4,5 @@ enum class PlateSolverType { ASTAP, ASTROMETRY_NET, ASTROMETRY_NET_ONLINE, + SIRIL, } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index c372fd401..3e0ee793c 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -165,34 +165,52 @@
+ [autoDisplayFirst]="false" appendTo="body" /> - +
- +
- +
-
+
- +
+ @if (solver.type === 'SIRIL') { +
+ + + + +
+
+ + + + +
+ }
@@ -247,7 +265,8 @@
- +
diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 2ebedbb75..6d4eda6a4 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, EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' -import { AnnotationInfoDialog, 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, StarDetectionDialog } from '../../shared/types/image.types' +import { AnnotationInfoDialog, DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FITSHeaderItem, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, 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' @@ -130,12 +130,14 @@ export class ImageComponent implements AfterViewInit, OnDestroy { readonly solver: ImageSolverDialog = { showDialog: false, running: false, + type: 'ASTAP', blind: true, centerRA: '', centerDEC: '', radius: 4, + focalLength: 0, + pixelSize: 0, solved: structuredClone(EMPTY_IMAGE_SOLVED), - type: 'ASTAP' } crossHair = false @@ -459,6 +461,10 @@ export class ImageComponent implements AfterViewInit, OnDestroy { return (this.showLiveStackedImage && this.imageData.liveStackedPath) || this.imageData.path } + get canPlateSolve() { + return this.solver.type !== 'SIRIL' || (this.solver.focalLength > 0 && this.solver.pixelSize > 0) + } + constructor( private app: AppComponent, private route: ActivatedRoute, @@ -879,6 +885,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.fitsHeaders.headers = info.headers + this.retrieveInfoFromImageHeaders(info.headers) + if (this.imageURL) window.URL.revokeObjectURL(this.imageURL) this.imageURL = window.URL.createObjectURL(blob) image.src = this.imageURL @@ -891,6 +899,21 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.retrieveCoordinateInterpolation() } + private retrieveInfoFromImageHeaders(headers: FITSHeaderItem[]) { + const imagePreference = this.preference.imagePreference.get() + + for (const item of headers) { + if (item.name === 'FOCALLEN') { + this.solver.focalLength = parseFloat(item.value) + } else if (item.name === 'XPIXSZ') { + this.solver.pixelSize = parseFloat(item.value) + } + } + + this.solver.focalLength ||= imagePreference.solverFocalLength || 0 + this.solver.pixelSize ||= imagePreference.solverPixelSize || 0 + } + imageClicked(event: MouseEvent, contextMenu: boolean) { this.imageMouseX = event.offsetX this.imageMouseY = event.offsetY @@ -1254,6 +1277,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const preference = this.preference.imagePreference.get() preference.solverRadius = this.solver.radius preference.solverType = this.solver.type + preference.solverPixelSize = this.solver.pixelSize + preference.solverFocalLength = this.solver.focalLength preference.starDetectionType = this.starDetection.type preference.starDetectionMinSNR = this.starDetection.minSNR this.preference.imagePreference.set(preference) diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 38c8a9dd8..e3ba8e467 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -53,7 +53,7 @@ -
+
diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 25ffcd1c6..3c3fb95a1 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -60,6 +60,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.plateSolvers.set('ASTAP', preference.plateSolverRequest('ASTAP').get()) this.plateSolvers.set('ASTROMETRY_NET_ONLINE', preference.plateSolverRequest('ASTROMETRY_NET_ONLINE').get()) + this.plateSolvers.set('SIRIL', preference.plateSolverRequest('SIRIL').get()) this.starDetectors.set('ASTAP', preference.starDetectionRequest('ASTAP').get()) this.starDetectors.set('PIXINSIGHT', preference.starDetectionRequest('PIXINSIGHT').get()) @@ -130,6 +131,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { save() { this.preference.plateSolverRequest('ASTAP').set(this.plateSolvers.get('ASTAP')) this.preference.plateSolverRequest('ASTROMETRY_NET_ONLINE').set(this.plateSolvers.get('ASTROMETRY_NET_ONLINE')) + this.preference.plateSolverRequest('SIRIL').set(this.plateSolvers.get('SIRIL')) this.preference.starDetectionRequest('ASTAP').set(this.starDetectors.get('ASTAP')) this.preference.starDetectionRequest('PIXINSIGHT').set(this.starDetectors.get('PIXINSIGHT')) diff --git a/desktop/src/shared/pipes/dropdown-options.ts b/desktop/src/shared/pipes/dropdown-options.ts index 915938d42..29c1e10ad 100644 --- a/desktop/src/shared/pipes/dropdown-options.ts +++ b/desktop/src/shared/pipes/dropdown-options.ts @@ -19,7 +19,7 @@ export class DropdownOptionsPipe implements PipeTransform { transform(type: DropdownOptionType): DropdownOptionReturnType | undefined { switch (type) { case 'STAR_DETECTOR': return ['ASTAP', 'PIXINSIGHT'] - case 'PLATE_SOLVER': return ['ASTAP', 'ASTROMETRY_NET_ONLINE'] + case 'PLATE_SOLVER': return ['ASTAP', 'ASTROMETRY_NET_ONLINE', 'SIRIL'] case 'AUTO_FOCUS_FITTING_MODE': return ['TRENDLINES', 'PARABOLIC', 'TREND_PARABOLIC', 'HYPERBOLIC', 'TREND_HYPERBOLIC'] case 'AUTO_FOCUS_BACKLASH_COMPENSATION_MODE': return ['NONE', 'ABSOLUTE', 'OVERSHOOT'] case 'LIVE_STACKER': return ['SIRIL', 'PIXINSIGHT'] diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index d4533e86d..a28a2a2b6 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -114,6 +114,8 @@ export interface ImageStatistics { export interface ImagePreference { solverRadius?: number solverType?: PlateSolverType + solverFocalLength?: number + solverPixelSize?: number savePath?: string starDetectionType?: StarDetectorType starDetectionMinSNR?: number @@ -215,6 +217,8 @@ export interface ImageSolverDialog { centerRA: Angle centerDEC: Angle radius: number + focalLength: number + pixelSize: number readonly solved: ImageSolved type: PlateSolverType } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index c048dda1d..85f5c84a1 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -1,4 +1,4 @@ -export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP' +export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP' | 'SIRIL' export interface PlateSolverOptions { type: PlateSolverType @@ -15,7 +15,7 @@ export const EMPTY_PLATE_SOLVER_OPTIONS: PlateSolverOptions = { downsampleFactor: 0, apiUrl: 'https://nova.astrometry.net/', apiKey: '', - timeout: 600, + timeout: 300, } export type StarDetectorType = 'ASTAP' | 'PIXINSIGHT' @@ -31,7 +31,7 @@ export interface StarDetectionOptions { export const EMPTY_STAR_DETECTION_OPTIONS: StarDetectionOptions = { type: 'ASTAP', executablePath: '', - timeout: 600, + timeout: 300, minSNR: 0, slot: 1, } diff --git a/nebulosa-astap/src/main/kotlin/nebulosa/astap/platesolver/AstapPlateSolver.kt b/nebulosa-astap/src/main/kotlin/nebulosa/astap/platesolver/AstapPlateSolver.kt index 1972f5e6a..65781b721 100644 --- a/nebulosa-astap/src/main/kotlin/nebulosa/astap/platesolver/AstapPlateSolver.kt +++ b/nebulosa-astap/src/main/kotlin/nebulosa/astap/platesolver/AstapPlateSolver.kt @@ -30,7 +30,7 @@ data class AstapPlateSolver(private val executablePath: Path) : PlateSolver { override fun solve( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, - downsampleFactor: Int, timeout: Duration?, + downsampleFactor: Int, timeout: Duration, cancellationToken: CancellationToken, ): PlateSolution { requireNotNull(path) { "path is required" } @@ -61,7 +61,7 @@ data class AstapPlateSolver(private val executablePath: Path) : PlateSolver { LOG.info("ASTAP solving. command={}", cmd.command) try { - val timeoutOrDefault = timeout?.takeIf { it.toSeconds() > 0 } ?: Duration.ofMinutes(5) + val timeoutOrDefault = timeout.takeIf { it.toSeconds() > 0 } ?: Duration.ofMinutes(5) cancellationToken.listen(cmd) cmd.start(timeoutOrDefault) diff --git a/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/platesolver/LibAstrometryNetPlateSolver.kt b/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/platesolver/LibAstrometryNetPlateSolver.kt index de341240b..37919f1cc 100644 --- a/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/platesolver/LibAstrometryNetPlateSolver.kt +++ b/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/platesolver/LibAstrometryNetPlateSolver.kt @@ -13,7 +13,7 @@ data class LibAstrometryNetPlateSolver(private val solver: LibAstrometryNet) : P override fun solve( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, - downsampleFactor: Int, timeout: Duration?, + downsampleFactor: Int, timeout: Duration, cancellationToken: CancellationToken, ): PlateSolution { return PlateSolution.NO_SOLUTION diff --git a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt index 4965e93d3..2cdda406a 100644 --- a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt +++ b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/LocalAstrometryNetPlateSolver.kt @@ -23,7 +23,7 @@ data class LocalAstrometryNetPlateSolver(private val executablePath: Path) : Pla override fun solve( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, - downsampleFactor: Int, timeout: Duration?, + downsampleFactor: Int, timeout: Duration, cancellationToken: CancellationToken, ): PlateSolution { requireNotNull(path) { "path is required" } @@ -39,7 +39,7 @@ data class LocalAstrometryNetPlateSolver(private val executablePath: Path) : Pla putArg("--dir", outFolder) - putArg("--cpulimit", timeout?.takeIf { it.toSeconds() > 0 }?.toSeconds() ?: 300) + putArg("--cpulimit", timeout.takeIf { it.toSeconds() > 0 }?.toSeconds() ?: 300) putArg("--scale-units", "degwidth") putArg("--guess-scale") putArg("--crpix-center") diff --git a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/NovaAstrometryNetPlateSolver.kt b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/NovaAstrometryNetPlateSolver.kt index 37a8fe03e..0719e5d0b 100644 --- a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/NovaAstrometryNetPlateSolver.kt +++ b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/platesolver/NovaAstrometryNetPlateSolver.kt @@ -43,7 +43,7 @@ data class NovaAstrometryNetPlateSolver( override fun solve( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, - downsampleFactor: Int, timeout: Duration?, + downsampleFactor: Int, timeout: Duration, cancellationToken: CancellationToken, ): PlateSolution { renewSession() @@ -69,7 +69,7 @@ data class NovaAstrometryNetPlateSolver( throw PlateSolverException(submission.errorMessage) } - var timeLeft = timeout?.takeIf { it.toSeconds() > 0 }?.toMillis() ?: 300000L + var timeLeft = timeout.takeIf { it.toSeconds() > 0 }?.toMillis() ?: 300000L while (timeLeft >= 0L && !cancellationToken.isCancelled) { val startTime = System.currentTimeMillis() diff --git a/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolver.kt b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolver.kt index 543751dee..b1672ca58 100644 --- a/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolver.kt +++ b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolver.kt @@ -11,7 +11,7 @@ interface PlateSolver { fun solve( path: Path?, image: Image?, centerRA: Angle = 0.0, centerDEC: Angle = 0.0, radius: Angle = 0.0, - downsampleFactor: Int = 0, timeout: Duration? = null, + downsampleFactor: Int = 0, timeout: Duration = Duration.ZERO, cancellationToken: CancellationToken = CancellationToken.NONE, ): PlateSolution } diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt index b26333d63..df72d7bd3 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/command/PlateSolve.kt @@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicBoolean data class PlateSolve( @JvmField val path: Path, @JvmField val focalLength: Double = 0.0, + @JvmField val pixelSize: Double = 0.0, @JvmField val useCenterCoordinates: Boolean = false, @JvmField val rightAscension: Angle = 0.0, val declination: Angle = 0.0, @JvmField val downsampleFactor: Int = 0, @@ -39,7 +40,8 @@ data class PlateSolve( if (useCenterCoordinates) append(" ${rightAscension.toHours},${declination.toDegrees}") append(" -platesolve -noflip") if (focalLength > 0.0) append(" -focal=$focalLength") - if (downsampleFactor > 0) append(" -downscale=$downsampleFactor") + if (pixelSize > 0.0) append(" -pixelsize=$pixelSize") + if (downsampleFactor > 1) append(" -downscale") } } diff --git a/nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt b/nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt index 53807a75f..9af5bd07c 100644 --- a/nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt +++ b/nebulosa-siril/src/main/kotlin/nebulosa/siril/platesolver/SirilPlateSolver.kt @@ -11,12 +11,16 @@ import nebulosa.siril.command.SirilCommandLine import java.nio.file.Path import java.time.Duration -data class SirilPlateSolver(private val executablePath: Path) : PlateSolver { +data class SirilPlateSolver( + private val executablePath: Path, + private val focalLength: Double = 0.0, + private val pixelSize: Double = 0.0, +) : PlateSolver { override fun solve( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, - downsampleFactor: Int, timeout: Duration?, cancellationToken: CancellationToken + downsampleFactor: Int, timeout: Duration, cancellationToken: CancellationToken ): PlateSolution { val commandLine = SirilCommandLine(executablePath) @@ -24,7 +28,7 @@ data class SirilPlateSolver(private val executablePath: Path) : PlateSolver { commandLine.run() cancellationToken.listen(commandLine) val useCenterCoordinates = radius > 0.0 && radius.toDegrees >= 0.1 && centerRA.isFinite() && centerDEC.isFinite() - commandLine.execute(PlateSolve(path!!, 0.0, useCenterCoordinates, centerRA, centerDEC, downsampleFactor, timeout ?: Duration.ZERO)) + commandLine.execute(PlateSolve(path!!, focalLength, pixelSize, useCenterCoordinates, centerRA, centerDEC, downsampleFactor, timeout)) } finally { cancellationToken.unlisten(commandLine) commandLine.close() From b760e97cd8e921f4f96dabce94439867ddadd399 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 12 Jun 2024 21:12:47 -0300 Subject: [PATCH 4/5] [api]: Fix unit tests --- .../nebulosa/image/format/AbstractHeader.kt | 38 ++++++++++++++++++- .../nebulosa/image/format/WritableHeader.kt | 2 + 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt index 8fdb541a9..2181ae75a 100644 --- a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt +++ b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/AbstractHeader.kt @@ -3,8 +3,7 @@ package nebulosa.image.format import java.io.Serializable import java.util.* -abstract class AbstractHeader protected constructor(@JvmField protected val cards: LinkedList) : - Header, MutableCollection by cards, Serializable { +abstract class AbstractHeader protected constructor(@JvmField protected val cards: LinkedList) : Header, Serializable { constructor() : this(LinkedList()) @@ -27,6 +26,41 @@ abstract class AbstractHeader protected constructor(@JvmField protected val card } } + override val size + get() = cards.size + + override fun contains(element: HeaderCard): Boolean { + return element in cards + } + + override fun containsAll(elements: Collection): Boolean { + return cards.containsAll(elements) + } + + override fun isEmpty(): Boolean { + return cards.isEmpty() + } + + override fun iterator(): MutableIterator { + return cards.iterator() + } + + override fun clear() { + cards.clear() + } + + override fun remove(element: HeaderCard): Boolean { + return cards.remove(element) + } + + override fun removeAll(elements: Collection): Boolean { + return cards.removeAll(elements.toSet()) + } + + override fun retainAll(elements: Collection): Boolean { + return cards.retainAll(elements.toSet()) + } + override fun delete(key: String): Boolean { return cards.removeIf { it.key == key } } diff --git a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt index 4a6f4c017..abcc50f2b 100644 --- a/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt +++ b/nebulosa-image-format/src/main/kotlin/nebulosa/image/format/WritableHeader.kt @@ -20,6 +20,8 @@ interface WritableHeader : MutableCollection { fun addAll(cards: Iterable) = cards.forEach(::add) + override fun addAll(elements: Collection) = elements.fold(false) { a, b -> add(b) || a } + fun delete(key: HeaderKey) = delete(key.key) fun delete(key: String): Boolean From 51a77500997fd91cee55f5f26d28338895bb986b Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 12 Jun 2024 21:44:01 -0300 Subject: [PATCH 5/5] [api]: Fix compilation error --- .../kotlin/nebulosa/watney/platesolver/WatneyPlateSolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nebulosa-watney/src/main/kotlin/nebulosa/watney/platesolver/WatneyPlateSolver.kt b/nebulosa-watney/src/main/kotlin/nebulosa/watney/platesolver/WatneyPlateSolver.kt index 5ac7d9b9f..6f651db3c 100644 --- a/nebulosa-watney/src/main/kotlin/nebulosa/watney/platesolver/WatneyPlateSolver.kt +++ b/nebulosa-watney/src/main/kotlin/nebulosa/watney/platesolver/WatneyPlateSolver.kt @@ -43,7 +43,7 @@ data class WatneyPlateSolver( override fun solve( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, - downsampleFactor: Int, timeout: Duration?, + downsampleFactor: Int, timeout: Duration, cancellationToken: CancellationToken, ): PlateSolution { val image = image ?: path!!.fits().use(Image::open)