diff --git a/LICENSE b/LICENSE index b6f689142..19ff7ef8f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022-2023 Tiago Melo +Copyright (c) 2022-2024 Tiago Melo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5b7b05d37..af52ae372 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,19 @@ [![Active Development](https://img.shields.io/badge/Maintenance%20Level-Actively%20Developed-brightgreen.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d) [![CI](https://github.com/tiagohm/nebulosa/actions/workflows/ci.yml/badge.svg)](https://github.com/tiagohm/nebulosa/actions/workflows/ci.yml) [![CodeFactor](https://www.codefactor.io/repository/github/tiagohm/nebulosa/badge/main)](https://www.codefactor.io/repository/github/tiagohm/nebulosa/overview/main) + +The complete integrated solution for all of your astronomical imaging needs. + +## Building + +### Pre-requisites + +* Java 17 +* Node 20.9.0 or newer + +### Steps + +* `./gradlew api:bootJar` +* `cd desktop` +* `npm i` +* `npm run electron:build` diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 4362e05cf..b57cf5fb6 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.2" + id("org.springframework.boot") version "3.2.3" id("io.spring.dependency-management") version "1.1.4" kotlin("plugin.spring") kotlin("kapt") @@ -10,8 +10,10 @@ plugins { } dependencies { + implementation(project(":nebulosa-alignment")) implementation(project(":nebulosa-astap")) implementation(project(":nebulosa-astrometrynet")) + implementation(project(":nebulosa-alpaca-indi")) implementation(project(":nebulosa-batch-processing")) implementation(project(":nebulosa-common")) implementation(project(":nebulosa-guiding-phd2")) @@ -43,7 +45,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.3") + kapt("org.springframework:spring-context-indexer:6.1.4") testImplementation(project(":nebulosa-skycatalog-stellarium")) testImplementation(project(":nebulosa-test")) } @@ -60,6 +62,6 @@ tasks.withType { kapt { arguments { arg("objectbox.modelPath", "$projectDir/schemas/objectbox.json") - arg("objectbox.myObjectBoxPackage", "nebulosa.api.entities") + arg("objectbox.myObjectBoxPackage", "nebulosa.api.database") } } diff --git a/api/src/main/kotlin/nebulosa/api/README.md b/api/src/main/kotlin/nebulosa/api/README.md index 19a28ce23..7a6187f39 100644 --- a/api/src/main/kotlin/nebulosa/api/README.md +++ b/api/src/main/kotlin/nebulosa/api/README.md @@ -71,7 +71,7 @@ URL: `localhost:{PORT}/ws` ```json5 { "camera": {}, - "state": "EXPOSURING", + "state": "CAPTURE_STARTED|EXPOSURE_STARTED|EXPOSURING|WAITING|SETTLING|EXPOSURE_FINISHED|CAPTURE_FINISHED", "exposureAmount": 0, "exposureCount": 0, "captureElapsedTime": 0, @@ -152,7 +152,7 @@ URL: `localhost:{PORT}/ws` "canRelativeMove": false, "canAbort": false, "canReverse": false, - "reverse": false, + "reversed": false, "canSync": false, "hasBacklash": false, "maxPosition": 0, @@ -181,16 +181,35 @@ URL: `localhost:{PORT}/ws` ### DARV Polar Alignment -#### DARV_ALIGNMENT.ELAPSED +#### DARV.ELAPSED ```json5 { - "camera": {}, - "guideOutput": {}, + "id": "", "remainingTime": 0, "progress": 0.0, "direction": "EAST", - "state": "FORWARD" + "state": "FORWARD|BACKWARD" +} +``` + +### Three Point Polar Alignment + +#### TPPA.ELAPSED + +```json5 +{ + "id": "", + "elapsedTime": 0, + "stepCount": 0, + "state": "SLEWING|SOLVING|SOLVED|COMPUTED|FAILED|FINISHED", + "rightAscension": "00h00m00s", + "declination": "00d00m00s", + "azimuthError": "00d00m00s", + "altitudeError": "00d00m00s", + "totalError": "00d00m00s", + "azimuthErrorDirection": "", + "altitudeErrorDirection": "" } ``` @@ -200,23 +219,46 @@ URL: `localhost:{PORT}/ws` ```json5 { + "state": "EXPOSURING|CAPTURED|FAILED", "exposureTime": 0, + "savedPath": "", + // CAMERA.CAPTURE_ELAPSED + "capture": {}, + "message": "" +} +``` + +### Sequencer + +#### SEQUENCER.ELAPSED + +```json5 +{ + "id": 0, + "elapsedTime": 0, + "remainingTime": 0, + "progress": 0.0, + // CAMERA.CAPTURE_ELAPSED "capture": {} } ``` -#### FLAT_WIZARD.FRAME_CAPTURED +### INDI + +#### DEVICE.PROPERTY_CHANGED, DEVICE.PROPERTY_DELETED ```json5 { - "exposureTime": 0, - "savedPath": "" + "device": {}, + "property": {} } ``` -#### FLAT_WIZARD.FAILED +#### DEVICE.MESSAGE_RECEIVED ```json5 { + "device": {}, + "message": "" } ``` diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt index 8d5f5b676..a9ac9310e 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt @@ -1,13 +1,12 @@ package nebulosa.api.alignment.polar import nebulosa.api.alignment.polar.darv.DARVStartRequest +import nebulosa.api.alignment.polar.tppa.TPPAStartRequest import nebulosa.api.beans.converters.indi.DeviceOrEntityParam import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput -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 nebulosa.indi.device.mount.Mount +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("polar-alignment") @@ -19,12 +18,31 @@ class PolarAlignmentController( fun darvStart( @DeviceOrEntityParam camera: Camera, @DeviceOrEntityParam guideOutput: GuideOutput, @RequestBody body: DARVStartRequest, - ) { - polarAlignmentService.darvStart(camera, guideOutput, body) + ) = polarAlignmentService.darvStart(camera, guideOutput, body) + + @PutMapping("darv/{id}/stop") + fun darvStop(@PathVariable id: String) { + polarAlignmentService.darvStop(id) + } + + @PutMapping("tppa/{camera}/{mount}/start") + fun tppaStart( + @DeviceOrEntityParam camera: Camera, @DeviceOrEntityParam mount: Mount, + @RequestBody body: TPPAStartRequest, + ) = polarAlignmentService.tppaStart(camera, mount, body) + + @PutMapping("tppa/{id}/stop") + fun tppaStop(@PathVariable id: String) { + polarAlignmentService.tppaStop(id) + } + + @PutMapping("tppa/{id}/pause") + fun tppaPause(@PathVariable id: String) { + polarAlignmentService.tppaPause(id) } - @PutMapping("darv/{camera}/{guideOutput}/stop") - fun darvStop(@DeviceOrEntityParam camera: Camera, @DeviceOrEntityParam guideOutput: GuideOutput) { - polarAlignmentService.darvStop(camera, guideOutput) + @PutMapping("tppa/{id}/unpause") + fun tppaUnpause(@PathVariable id: String) { + polarAlignmentService.tppaUnpause(id) } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt index 44e703b19..1c43fa76e 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt @@ -2,22 +2,44 @@ package nebulosa.api.alignment.polar import nebulosa.api.alignment.polar.darv.DARVExecutor import nebulosa.api.alignment.polar.darv.DARVStartRequest +import nebulosa.api.alignment.polar.tppa.TPPAExecutor +import nebulosa.api.alignment.polar.tppa.TPPAStartRequest import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput +import nebulosa.indi.device.mount.Mount import org.springframework.stereotype.Service @Service class PolarAlignmentService( private val darvExecutor: DARVExecutor, + private val tppaExecutor: TPPAExecutor, ) { - fun darvStart(camera: Camera, guideOutput: GuideOutput, darvStartRequest: DARVStartRequest) { + fun darvStart(camera: Camera, guideOutput: GuideOutput, darvStartRequest: DARVStartRequest): String { check(camera.connected) { "camera not connected" } check(guideOutput.connected) { "guide output not connected" } - darvExecutor.execute(darvStartRequest.copy(camera = camera, guideOutput = guideOutput)) + return darvExecutor.execute(camera, guideOutput, darvStartRequest) } - fun darvStop(camera: Camera, guideOutput: GuideOutput) { - darvExecutor.stop(camera, guideOutput) + fun darvStop(id: String) { + darvExecutor.stop(id) + } + + fun tppaStart(camera: Camera, mount: Mount, tppaStartRequest: TPPAStartRequest): String { + check(camera.connected) { "camera not connected" } + check(mount.connected) { "mount not connected" } + return tppaExecutor.execute(camera, mount, tppaStartRequest) + } + + fun tppaStop(id: String) { + tppaExecutor.stop(id) + } + + fun tppaPause(id: String) { + tppaExecutor.pause(id) + } + + fun tppaUnpause(id: String) { + tppaExecutor.unpause(id) } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt index 588ae7f14..2b768a6c1 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt @@ -2,15 +2,11 @@ package nebulosa.api.alignment.polar.darv import nebulosa.api.messages.MessageEvent import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput import java.time.Duration sealed interface DARVEvent : MessageEvent { - val camera: Camera - - val guideOutput: GuideOutput + val id: String val remainingTime: Duration @@ -21,5 +17,43 @@ sealed interface DARVEvent : MessageEvent { val state: DARVState override val eventName - get() = "DARV_ALIGNMENT.ELAPSED" + get() = "DARV.ELAPSED" + + data class Started( + override val id: String, + override val remainingTime: Duration, + override val direction: GuideDirection, + ) : DARVEvent { + + override val progress = 0.0 + override val state = DARVState.INITIAL_PAUSE + } + + data class Finished( + override val id: String, + ) : DARVEvent { + + override val remainingTime = Duration.ZERO!! + override val progress = 0.0 + override val state = DARVState.IDLE + override val direction = null + } + + data class InitialPauseElapsed( + override val id: String, + override val remainingTime: Duration, + override val progress: Double, + ) : DARVEvent { + + override val state = DARVState.INITIAL_PAUSE + override val direction = null + } + + data class GuidePulseElapsed( + override val id: String, + override val remainingTime: Duration, + override val progress: Double, + override val direction: GuideDirection, + override val state: DARVState, + ) : MessageEvent, DARVEvent } 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 a71e22e04..dd6f5b7aa 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 @@ -1,69 +1,33 @@ package nebulosa.api.alignment.polar.darv -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService -import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobExecutor import nebulosa.batch.processing.JobLauncher import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput -import nebulosa.log.debug +import nebulosa.log.info import nebulosa.log.loggerFor import org.springframework.stereotype.Component -import java.util.* /** * @see Reference */ @Component class DARVExecutor( - private val jobLauncher: JobLauncher, + override val jobLauncher: JobLauncher, private val messageService: MessageService, -) : Consumer { - - private val jobExecutions = LinkedList() +) : JobExecutor() { @Synchronized - fun execute(request: DARVStartRequest) { - val camera = requireNotNull(request.camera) - val guideOutput = requireNotNull(request.guideOutput) - - check(!isRunning(camera, guideOutput)) { "DARV job is already running" } - - LOG.debug { "starting DARV. request=%s".format(request) } - - with(DARVJob(request)) { - subscribe(this@DARVExecutor) - val jobExecution = jobLauncher.launch(this) - jobExecutions.add(jobExecution) - } - } - - fun findJobExecution(camera: Camera, guideOutput: GuideOutput): JobExecution? { - for (i in jobExecutions.indices.reversed()) { - val jobExecution = jobExecutions[i] - val job = jobExecution.job as DARVJob - - if (!jobExecution.isDone && job.camera === camera && job.guideOutput === guideOutput) { - return jobExecution - } - } + fun execute(camera: Camera, guideOutput: GuideOutput, request: DARVStartRequest): String { + check(findJobExecutionWithAny(camera, guideOutput) == null) { "DARV job is already running" } - return null - } - - @Synchronized - fun stop(camera: Camera, guideOutput: GuideOutput) { - val jobExecution = findJobExecution(camera, guideOutput) ?: return - jobLauncher.stop(jobExecution) - } - - fun isRunning(camera: Camera, guideOutput: GuideOutput): Boolean { - return findJobExecution(camera, guideOutput) != null - } + LOG.info { "starting DARV. camera=$camera, guideOutput=$guideOutput, request=$request" } - override fun accept(event: MessageEvent) { - messageService.sendMessage(event) + val darvJob = DARVJob(camera, guideOutput, request) + darvJob.subscribe(messageService::sendMessage) + register(jobLauncher.launch(darvJob)) + return darvJob.id } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt deleted file mode 100644 index 0748956e8..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt +++ /dev/null @@ -1,16 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import java.time.Duration - -data class DARVFinished( - override val camera: Camera, - override val guideOutput: GuideOutput, -) : DARVEvent { - - override val remainingTime = Duration.ZERO!! - override val progress = 0.0 - override val state = DARVState.IDLE - override val direction = null -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt deleted file mode 100644 index 580cb0afc..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt +++ /dev/null @@ -1,16 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.api.messages.MessageEvent -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import java.time.Duration - -data class DARVGuidePulseElapsed( - override val camera: Camera, - override val guideOutput: GuideOutput, - override val remainingTime: Duration, - override val progress: Double, - override val direction: GuideDirection, - override val state: DARVState, -) : MessageEvent, DARVEvent diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt deleted file mode 100644 index cc17a87a1..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt +++ /dev/null @@ -1,16 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import java.time.Duration - -data class DARVInitialPauseElapsed( - override val camera: Camera, - override val guideOutput: GuideOutput, - override val remainingTime: Duration, - override val progress: Double, -) : DARVEvent { - - override val state = DARVState.INITIAL_PAUSE - override val direction = null -} 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 0361a5a9d..04a268396 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,7 +1,10 @@ package nebulosa.api.alignment.polar.darv import io.reactivex.rxjava3.subjects.PublishSubject -import nebulosa.api.cameras.* +import nebulosa.api.cameras.AutoSubFolderMode +import nebulosa.api.cameras.CameraCaptureListener +import nebulosa.api.cameras.CameraExposureFinished +import nebulosa.api.cameras.CameraExposureStep import nebulosa.api.guiding.GuidePulseListener import nebulosa.api.guiding.GuidePulseRequest import nebulosa.api.guiding.GuidePulseStep @@ -11,21 +14,22 @@ import nebulosa.batch.processing.ExecutionContext.Companion.getDouble import nebulosa.batch.processing.ExecutionContext.Companion.getDuration import nebulosa.batch.processing.delay.DelayStep import nebulosa.batch.processing.delay.DelayStepListener +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.guide.GuideOutput import java.nio.file.Files import java.time.Duration data class DARVJob( - val request: DARVStartRequest, + @JvmField val camera: Camera, + @JvmField val guideOutput: GuideOutput, + @JvmField val request: DARVStartRequest, ) : SimpleJob(), PublishSubscribe, CameraCaptureListener, GuidePulseListener, DelayStepListener { - @JvmField val camera = requireNotNull(request.camera) - @JvmField val guideOutput = requireNotNull(request.guideOutput) @JvmField val direction = if (request.reversed) request.direction.reversed else request.direction - @JvmField val cameraRequest = (request.capture ?: CameraStartCaptureRequest()).copy( - camera = camera, - exposureTime = request.exposureTime + request.initialPause, + @JvmField val cameraRequest = request.capture.copy( + exposureTime = request.capture.exposureTime + request.capture.exposureDelay, savePath = Files.createTempDirectory("darv"), exposureAmount = 1, exposureDelay = Duration.ZERO, frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF @@ -34,31 +38,31 @@ data class DARVJob( override val subject = PublishSubject.create() init { - val cameraExposureStep = CameraExposureStep(cameraRequest) + val cameraExposureStep = CameraExposureStep(camera, cameraRequest) cameraExposureStep.registerCameraCaptureListener(this) - val initialPauseDelayStep = DelayStep(request.initialPause) + val initialPauseDelayStep = DelayStep(request.capture.exposureDelay) initialPauseDelayStep.registerDelayStepListener(this) - val guidePulseDuration = request.exposureTime.dividedBy(2L) - val forwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction, guidePulseDuration) - val forwardGuidePulseStep = GuidePulseStep(forwardGuidePulseRequest) + val guidePulseDuration = request.capture.exposureTime.dividedBy(2L) + val forwardGuidePulseRequest = GuidePulseRequest(direction, guidePulseDuration) + val forwardGuidePulseStep = GuidePulseStep(guideOutput, forwardGuidePulseRequest) forwardGuidePulseStep.registerGuidePulseListener(this) - val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) - val backwardGuidePulseStep = GuidePulseStep(backwardGuidePulseRequest) + val backwardGuidePulseRequest = GuidePulseRequest(direction.reversed, guidePulseDuration) + val backwardGuidePulseStep = GuidePulseStep(guideOutput, backwardGuidePulseRequest) backwardGuidePulseStep.registerGuidePulseListener(this) val guideFlow = SimpleFlowStep(initialPauseDelayStep, forwardGuidePulseStep, backwardGuidePulseStep) - add(SimpleSplitStep(cameraExposureStep, guideFlow)) + register(SimpleSplitStep(cameraExposureStep, guideFlow)) } override fun beforeJob(jobExecution: JobExecution) { - onNext(DARVStarted(camera, guideOutput, request.initialPause, direction)) + onNext(DARVEvent.Started(id, request.capture.exposureDelay, direction)) } override fun afterJob(jobExecution: JobExecution) { - onNext(DARVFinished(camera, guideOutput)) + onNext(DARVEvent.Finished(id)) } override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { @@ -70,12 +74,16 @@ data class DARVJob( val remainingTime = stepExecution.context.getDuration(DelayStep.REMAINING_TIME) val progress = stepExecution.context.getDouble(DelayStep.PROGRESS) val state = if (direction == this.direction) DARVState.FORWARD else DARVState.BACKWARD - onNext(DARVGuidePulseElapsed(camera, guideOutput, remainingTime, progress, direction, state)) + onNext(DARVEvent.GuidePulseElapsed(id, remainingTime, progress, direction, state)) } override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { val remainingTime = stepExecution.context.getDuration(DelayStep.REMAINING_TIME) val progress = stepExecution.context.getDouble(DelayStep.PROGRESS) - onNext(DARVInitialPauseElapsed(camera, guideOutput, remainingTime, progress)) + onNext(DARVEvent.InitialPauseElapsed(id, remainingTime, progress)) + } + + override fun contains(data: Any): Boolean { + return data === camera || data === guideOutput || super.contains(data) } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt index 44efa3684..53b99114f 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt @@ -1,23 +1,10 @@ package nebulosa.api.alignment.polar.darv -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonIgnoreProperties import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.hibernate.validator.constraints.time.DurationMax -import org.hibernate.validator.constraints.time.DurationMin -import org.springframework.boot.convert.DurationUnit -import java.time.Duration -import java.time.temporal.ChronoUnit data class DARVStartRequest( - @JsonIgnoreProperties("camera", "focuser", "wheel") val capture: CameraStartCaptureRequest? = null, - @JsonIgnore val camera: Camera? = null, - @JsonIgnore val guideOutput: GuideOutput? = null, - @field:DurationMin(seconds = 1) @field:DurationMax(seconds = 600) @field:DurationUnit(ChronoUnit.SECONDS) val exposureTime: Duration = Duration.ZERO, - @field:DurationMin(seconds = 1) @field:DurationMax(seconds = 60) @field:DurationUnit(ChronoUnit.SECONDS) val initialPause: Duration = Duration.ZERO, + val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, val direction: GuideDirection = GuideDirection.NORTH, val reversed: Boolean = false, ) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt deleted file mode 100644 index 066e6f704..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt +++ /dev/null @@ -1,17 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import java.time.Duration - -data class DARVStarted( - override val camera: Camera, - override val guideOutput: GuideOutput, - override val remainingTime: Duration, - override val direction: GuideDirection, -) : DARVEvent { - - override val progress = 0.0 - override val state = DARVState.INITIAL_PAUSE -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAEvent.kt new file mode 100644 index 000000000..c16deed1b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAEvent.kt @@ -0,0 +1,116 @@ +package nebulosa.api.alignment.polar.tppa + +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import nebulosa.api.beans.converters.angle.DeclinationSerializer +import nebulosa.api.beans.converters.angle.RightAscensionSerializer +import nebulosa.api.messages.MessageEvent +import nebulosa.math.Angle +import java.time.Duration +import kotlin.math.hypot + +sealed interface TPPAEvent : MessageEvent { + + val id: String + + val state: TPPAState + + val stepCount: Int + + val elapsedTime: Duration + + val rightAscension: Angle + get() = 0.0 + + val declination: Angle + get() = 0.0 + + val azimuthError: Angle + get() = 0.0 + + val altitudeError: Angle + get() = 0.0 + + val totalError: Angle + get() = 0.0 + + val azimuthErrorDirection: String + get() = "" + + val altitudeErrorDirection: String + get() = "" + + override val eventName + get() = "TPPA.ELAPSED" + + data class Slewing( + override val id: String, + override val stepCount: Int, + override val elapsedTime: Duration, + @field:JsonSerialize(using = RightAscensionSerializer::class) override val rightAscension: Angle, + @field:JsonSerialize(using = DeclinationSerializer::class) override val declination: Angle, + ) : TPPAEvent { + + override val state = TPPAState.SLEWING + } + + data class Solving( + override val id: String, + override val stepCount: Int, + override val elapsedTime: Duration, + ) : TPPAEvent { + + override val state = TPPAState.SOLVING + } + + data class Solved( + override val id: String, + override val stepCount: Int, + override val elapsedTime: Duration, + @field:JsonSerialize(using = RightAscensionSerializer::class) override val rightAscension: Angle, + @field:JsonSerialize(using = DeclinationSerializer::class) override val declination: Angle, + ) : TPPAEvent { + + override val state = TPPAState.SOLVED + } + + data class Paused( + override val id: String, + override val stepCount: Int, + override val elapsedTime: Duration, + ) : TPPAEvent { + + override val state = TPPAState.PAUSED + } + + data class Computed( + override val id: String, + override val stepCount: Int, + override val elapsedTime: Duration, + @field:JsonSerialize(using = DeclinationSerializer::class) override val azimuthError: Angle, + @field:JsonSerialize(using = DeclinationSerializer::class) override val altitudeError: Angle, + override val azimuthErrorDirection: String, + override val altitudeErrorDirection: String, + ) : TPPAEvent { + + @JsonSerialize(using = DeclinationSerializer::class) override val totalError = hypot(azimuthError, altitudeError) + override val state = TPPAState.COMPUTED + } + + data class Failed( + override val id: String, + override val stepCount: Int, + override val elapsedTime: Duration, + ) : TPPAEvent { + + override val state = TPPAState.FAILED + } + + data class Finished( + override val id: String, + ) : TPPAEvent { + + override val stepCount = 0 + override val elapsedTime: Duration = Duration.ZERO + override val state = TPPAState.FINISHED + } +} 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 new file mode 100644 index 000000000..5c941ebf5 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt @@ -0,0 +1,47 @@ +package nebulosa.api.alignment.polar.tppa + +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.cameras.CameraExposureFinished +import nebulosa.api.messages.MessageEvent +import nebulosa.api.messages.MessageService +import nebulosa.api.solver.PlateSolverService +import nebulosa.batch.processing.JobExecutor +import nebulosa.batch.processing.JobLauncher +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.mount.Mount +import nebulosa.log.info +import nebulosa.log.loggerFor +import org.springframework.stereotype.Component + +@Component +class TPPAExecutor( + override val jobLauncher: JobLauncher, + private val messageService: MessageService, + private val plateSolverService: PlateSolverService, +) : JobExecutor(), Consumer { + + @Synchronized + fun execute(camera: Camera, mount: Mount, request: TPPAStartRequest): String { + check(findJobExecutionWithAny(camera, mount) == null) { "TPPA job is already running" } + + LOG.info { "starting TPPA. camera=$camera, mount=$mount, request=$request" } + + val solver = plateSolverService.solverFor(request.plateSolver) + + val tppaJob = TPPAJob(camera, request, solver, mount) + tppaJob.subscribe(this) + register(jobLauncher.launch(tppaJob)) + return tppaJob.id + } + + override fun accept(event: MessageEvent) { + if (event is TPPAEvent || event is CameraExposureFinished) { + messageService.sendMessage(event) + } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} 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 new file mode 100644 index 000000000..821a281c9 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt @@ -0,0 +1,94 @@ +package nebulosa.api.alignment.polar.tppa + +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.api.cameras.AutoSubFolderMode +import nebulosa.api.cameras.CameraCaptureEventHandler +import nebulosa.api.cameras.CameraCaptureListener +import nebulosa.api.messages.MessageEvent +import nebulosa.batch.processing.PublishSubscribe +import nebulosa.batch.processing.SimpleJob +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.mount.Mount +import nebulosa.math.Angle +import nebulosa.plate.solving.PlateSolver +import java.nio.file.Files +import java.time.Duration + +data class TPPAJob( + @JvmField val camera: Camera, + @JvmField val request: TPPAStartRequest, + @JvmField val solver: PlateSolver, + @JvmField val mount: Mount? = null, + @JvmField val longitude: Angle = mount!!.longitude, + @JvmField val latitude: Angle = mount!!.latitude, +) : SimpleJob(), PublishSubscribe, CameraCaptureListener, TPPAListener { + + @JvmField val cameraRequest = request.capture.copy( + savePath = Files.createTempDirectory("tppa"), + exposureAmount = 1, exposureDelay = Duration.ZERO, + exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), + frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF + ) + + override val subject = PublishSubject.create() + + private val cameraCaptureEventHandler = CameraCaptureEventHandler(this) + private val tppaStep = TPPAStep(camera, solver, request, mount, longitude, latitude, cameraRequest) + + init { + tppaStep.registerCameraCaptureListener(cameraCaptureEventHandler) + tppaStep.registerTPPAListener(this) + + register(tppaStep) + } + + override fun slewStarted(step: TPPAStep, rightAscension: Angle, declination: Angle) { + onNext(TPPAEvent.Slewing(id, step.stepCount, step.elapsedTime, rightAscension, declination)) + } + + override fun solverStarted(step: TPPAStep) { + onNext(TPPAEvent.Solving(id, step.stepCount, step.elapsedTime)) + } + + override fun solverFinished(step: TPPAStep, rightAscension: Angle, declination: Angle) { + onNext(TPPAEvent.Solved(id, step.stepCount, step.elapsedTime, rightAscension, declination)) + } + + override fun polarAlignmentPaused(step: TPPAStep) { + onNext(TPPAEvent.Paused(id, step.stepCount, step.elapsedTime)) + } + + override fun polarAlignmentComputed(step: TPPAStep, azimuth: Angle, altitude: Angle) { + val azimuthErrorDirection = when { + azimuth > 0 -> if (latitude > 0) "๐Ÿ ” Move LEFT/WEST" else "๐Ÿ ” Move LEFT/EAST" + azimuth < 0 -> if (latitude > 0) "Move RIGHT/EAST ๐Ÿ –" else "Move RIGHT/WEST ๐Ÿ –" + else -> "" + } + + val altitudeErrorDirection = when { + altitude > 0 -> if (latitude > 0) "๐Ÿ — Move DOWN" else "Move UP ๐Ÿ •" + altitude < 0 -> if (latitude > 0) "Move UP ๐Ÿ •" else "๐Ÿ — Move DOWN" + else -> "" + } + + onNext(TPPAEvent.Computed(id, step.stepCount, step.elapsedTime, azimuth, altitude, azimuthErrorDirection, altitudeErrorDirection)) + } + + override fun solverFailed(step: TPPAStep) { + onNext(TPPAEvent.Failed(id, step.stepCount, step.elapsedTime)) + } + + override fun polarAlignmentFinished(step: TPPAStep, aborted: Boolean) { + onNext(TPPAEvent.Finished(id)) + } + + override fun contains(data: Any): Boolean { + return data === camera || data === mount || super.contains(data) + } + + companion object { + + @JvmStatic private val MIN_EXPOSURE_TIME: Duration = Duration.ofSeconds(1L) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAListener.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAListener.kt new file mode 100644 index 000000000..a14e666c2 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAListener.kt @@ -0,0 +1,20 @@ +package nebulosa.api.alignment.polar.tppa + +import nebulosa.math.Angle + +interface TPPAListener { + + fun slewStarted(step: TPPAStep, rightAscension: Angle, declination: Angle) + + fun solverStarted(step: TPPAStep) + + fun solverFinished(step: TPPAStep, rightAscension: Angle, declination: Angle) + + fun polarAlignmentPaused(step: TPPAStep) + + fun polarAlignmentComputed(step: TPPAStep, azimuth: Angle, altitude: Angle) + + fun solverFailed(step: TPPAStep) + + fun polarAlignmentFinished(step: TPPAStep, aborted: Boolean) +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStartRequest.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStartRequest.kt new file mode 100644 index 000000000..26b4d0efd --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStartRequest.kt @@ -0,0 +1,17 @@ +package nebulosa.api.alignment.polar.tppa + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.solver.PlateSolverOptions + +data class TPPAStartRequest( + @JsonIgnoreProperties("camera", "focuser", "wheel") val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, + @field:NotNull @Valid val plateSolver: PlateSolverOptions = PlateSolverOptions.EMPTY, + val startFromCurrentPosition: Boolean = true, + val eastDirection: Boolean = true, + val compensateRefraction: Boolean = false, + val stopTrackingWhenDone: Boolean = true, + val stepDistance: Double = 10.0, // degrees +) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAState.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAState.kt new file mode 100644 index 000000000..62f507b39 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAState.kt @@ -0,0 +1,11 @@ +package nebulosa.api.alignment.polar.tppa + +enum class TPPAState { + SLEWING, + SOLVING, + SOLVED, + PAUSED, + COMPUTED, + FAILED, + FINISHED, +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStep.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStep.kt new file mode 100644 index 000000000..02dd5ead7 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStep.kt @@ -0,0 +1,188 @@ +package nebulosa.api.alignment.polar.tppa + +import nebulosa.alignment.polar.point.three.ThreePointPolarAlignment +import nebulosa.alignment.polar.point.three.ThreePointPolarAlignmentResult +import nebulosa.api.cameras.CameraCaptureListener +import nebulosa.api.cameras.CameraExposureStep +import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.mounts.MountSlewStep +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.common.concurrency.latch.Pauseable +import nebulosa.common.time.Stopwatch +import nebulosa.fits.fits +import nebulosa.imaging.Image +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.mount.Mount +import nebulosa.log.debug +import nebulosa.log.loggerFor +import nebulosa.math.Angle +import nebulosa.math.deg +import nebulosa.plate.solving.PlateSolver + +data class TPPAStep( + @JvmField val camera: Camera, + private val solver: PlateSolver, + private val request: TPPAStartRequest, + @JvmField val mount: Mount? = null, + private val longitude: Angle = mount!!.longitude, + private val latitude: Angle = mount!!.latitude, + private val cameraRequest: CameraStartCaptureRequest = request.capture, +) : Step, Pauseable { + + private val cameraExposureStep = CameraExposureStep(camera, cameraRequest) + private val alignment = ThreePointPolarAlignment(solver, longitude, latitude) + private val listeners = LinkedHashSet() + private val stopwatch = Stopwatch() + private val stepDistances = DoubleArray(2) { if (request.eastDirection) request.stepDistance else -request.stepDistance } + + @Volatile private var image: Image? = null + @Volatile private var mountSlewStep: MountSlewStep? = null + @Volatile private var noSolutionAttempts = 0 + @Volatile private var stepExecution: StepExecution? = null + + val stepCount + get() = alignment.state + + val elapsedTime + get() = stopwatch.elapsed + + fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return cameraExposureStep.registerCameraCaptureListener(listener) + } + + fun unregisterCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return cameraExposureStep.unregisterCameraCaptureListener(listener) + } + + fun registerTPPAListener(listener: TPPAListener): Boolean { + return listeners.add(listener) + } + + fun unregisterTPPAListener(listener: TPPAListener): Boolean { + return listeners.remove(listener) + } + + override fun beforeJob(jobExecution: JobExecution) { + cameraExposureStep.beforeJob(jobExecution) + mount?.tracking(true) + } + + override fun afterJob(jobExecution: JobExecution) { + cameraExposureStep.afterJob(jobExecution) + + if (mount != null && request.stopTrackingWhenDone) { + mount.tracking(false) + } + + stopwatch.stop() + + listeners.forEach { it.polarAlignmentFinished(this, jobExecution.cancellationToken.isCancelled) } + } + + override fun execute(stepExecution: StepExecution): StepResult { + val cancellationToken = stepExecution.jobExecution.cancellationToken + + if (cancellationToken.isCancelled) return StepResult.FINISHED + + LOG.debug { "executing TPPA. camera=$camera, mount=$mount, state=${alignment.state}" } + + this.stepExecution = stepExecution + + if (cancellationToken.isPaused) { + listeners.forEach { it.polarAlignmentPaused(this) } + cancellationToken.waitIfPaused() + } + + if (cancellationToken.isCancelled) return StepResult.FINISHED + + stopwatch.start() + + // Mount slew step. + if (mount != null) { + if (alignment.state in 1..2 && stepDistances[alignment.state - 1] != 0.0) { + val step = MountSlewStep(mount, mount.rightAscension + stepDistances[alignment.state - 1].deg, mount.declination) + mountSlewStep = step + listeners.forEach { it.slewStarted(this, step.rightAscension, step.declination) } + step.executeSingle(stepExecution) + stepDistances[alignment.state - 1] = 0.0 + } + } + + if (cancellationToken.isCancelled) return StepResult.FINISHED + + listeners.forEach { it.solverStarted(this) } + + // Camera capture step. + cameraExposureStep.execute(stepExecution) + + if (!cancellationToken.isCancelled) { + val savedPath = cameraExposureStep.savedPath ?: return StepResult.FINISHED + image = savedPath.fits().use { image?.load(it, false) ?: Image.open(it, false) } + + val radius = if (mount == null) 0.0 else ThreePointPolarAlignment.DEFAULT_RADIUS + + // Polar alignment step. + val result = alignment.align( + savedPath, image!!, mount?.rightAscension ?: 0.0, mount?.declination ?: 0.0, radius, + request.compensateRefraction, cancellationToken + ) + + LOG.info("alignment completed. result=$result, cancelled={}", cancellationToken.isCancelled) + + if (cancellationToken.isCancelled) return StepResult.FINISHED + + when (result) { + is ThreePointPolarAlignmentResult.NeedMoreMeasurement -> { + noSolutionAttempts = 0 + listeners.forEach { it.solverFinished(this, result.rightAscension, result.declination) } + return StepResult.CONTINUABLE + } + is ThreePointPolarAlignmentResult.NoPlateSolution -> { + noSolutionAttempts++ + + return if (noSolutionAttempts < 10) { + listeners.forEach { it.solverFailed(this) } + StepResult.CONTINUABLE + } else { + StepResult.FINISHED + } + } + is ThreePointPolarAlignmentResult.Measured -> { + noSolutionAttempts = 0 + + listeners.forEach { + it.solverFinished(this, result.rightAscension, result.declination) + it.polarAlignmentComputed(this, result.azimuth, result.altitude) + } + + return StepResult.CONTINUABLE + } + } + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + mountSlewStep?.stop(mayInterruptIfRunning) + cameraExposureStep.stop(mayInterruptIfRunning) + } + + override val isPaused + get() = stepExecution?.jobExecution?.cancellationToken?.isPaused ?: false + + override fun pause() { + stopwatch.stop() + } + + override fun unpause() { + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt b/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt index 0f3ccc328..0bd8e7b5c 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/BodyPosition.kt @@ -14,6 +14,7 @@ import nebulosa.math.deg import nebulosa.nova.astrometry.Constellation import nebulosa.nova.position.GeographicPosition import nebulosa.skycatalog.SkyObject +import nebulosa.time.CurrentTime data class BodyPosition( @field:JsonSerialize(using = RightAscensionSerializer::class) val rightAscensionJ2000: Angle, diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt index b4aba41e4..bdaaf93bf 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt @@ -2,7 +2,7 @@ package nebulosa.api.atlas import io.objectbox.annotation.Entity import io.objectbox.annotation.Id -import nebulosa.api.entities.BoxEntity +import nebulosa.api.database.BoxEntity @Entity data class SatelliteEntity( diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt index 1d61fd45d..c43c00416 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt @@ -1,5 +1,7 @@ package nebulosa.api.atlas +import nebulosa.api.messages.MessageService +import nebulosa.api.notifications.NotificationEvent import nebulosa.api.preferences.PreferenceService import nebulosa.log.loggerFor import okhttp3.OkHttpClient @@ -14,8 +16,15 @@ class SatelliteUpdateTask( private val httpClient: OkHttpClient, private val preferenceService: PreferenceService, private val satelliteRepository: SatelliteRepository, + private val messageService: MessageService, ) : Runnable { + data class UpdateFinished(val numberOfSatellites: Int) : NotificationEvent { + + override val type = "SATELLITE.UPDATE_FINISHED" + override val body = "%d satellites was updated".format(numberOfSatellites) + } + @Scheduled(fixedDelay = UPDATE_INTERVAL, timeUnit = TimeUnit.MILLISECONDS) override fun run() { checkIsOutOfDateAndUpdate() @@ -57,6 +66,7 @@ class SatelliteUpdateTask( return satelliteRepository .save(data.values) .also { LOG.info("{} satellites updated", it.size) } + .also { messageService.sendMessage(UpdateFinished(it.size)) } .isNotEmpty() } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseReader.kt b/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseReader.kt index 45aabb327..5c6b182bb 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseReader.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseReader.kt @@ -15,7 +15,6 @@ import java.io.Closeable class SimbadDatabaseReader(source: Source) : Iterator, Closeable { private val buffer = if (source is BufferedSource) source else source.gzip().buffer() - private val time = CurrentTime.utc override fun hasNext() = !buffer.exhausted() @@ -33,7 +32,7 @@ class SimbadDatabaseReader(source: Source) : Iterator, Closeable { val radialVelocity = buffer.readFloat().toDouble().kms val redshift = 0.0 // buffer.readDouble() // val constellation = Constellation.entries[buffer.readByte().toInt() and 0xFF] - val constellation = SkyObject.constellationFor(rightAscension, declination, time) + val constellation = SkyObject.constellationFor(rightAscension, declination) return SimbadEntity(id, name, type, rightAscension, declination, magnitude, pmRA, pmDEC, parallax, radialVelocity, redshift, constellation) } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntity.kt b/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntity.kt index 8051a9a4b..8fe232465 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntity.kt @@ -4,9 +4,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore import io.objectbox.annotation.Convert import io.objectbox.annotation.Entity import io.objectbox.annotation.Id -import nebulosa.api.beans.converters.ConstellationPropertyConverter -import nebulosa.api.beans.converters.SkyObjectTypePropertyConverter -import nebulosa.api.entities.BoxEntity +import nebulosa.api.beans.converters.database.ConstellationPropertyConverter +import nebulosa.api.beans.converters.database.SkyObjectTypePropertyConverter +import nebulosa.api.database.BoxEntity import nebulosa.math.Angle import nebulosa.math.Velocity import nebulosa.nova.astrometry.Body diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntityRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntityRepository.kt index eda339c8b..54040bec7 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntityRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntityRepository.kt @@ -32,10 +32,8 @@ class SimbadEntityRepository(@Qualifier("simbadEntityBox") override val box: Box if (type != null) it.equal(SimbadEntity_.type, type.ordinal) if (constellation != null) it.equal(SimbadEntity_.constellation, constellation.ordinal) - if (name != null && name.trim().trim('%').isNotEmpty()) { - if (name.startsWith("%") == name.endsWith("%")) it.contains(SimbadEntity_.name, name.replace("%", ""), CASE_INSENSITIVE) - else if (name.endsWith("%")) it.startsWith(SimbadEntity_.name, name.replace("%", ""), CASE_INSENSITIVE) - else if (name.startsWith("%")) it.endsWith(SimbadEntity_.name, name.replace("%", ""), CASE_INSENSITIVE) + if (!name.isNullOrBlank()) { + it.contains(SimbadEntity_.name, name, CASE_INSENSITIVE) } if (useFilter) it.filter(object : QueryFilter { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt index a7a55a9ab..8eced252c 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt @@ -1,5 +1,7 @@ package nebulosa.api.atlas +import nebulosa.api.messages.MessageService +import nebulosa.api.notifications.NotificationEvent import nebulosa.api.preferences.PreferenceService import nebulosa.log.loggerFor import okhttp3.OkHttpClient @@ -14,8 +16,21 @@ class SkyAtlasUpdateTask( private val httpClient: OkHttpClient, private val simbadEntityRepository: SimbadEntityRepository, private val preferenceService: PreferenceService, + private val messageService: MessageService, ) : Runnable { + data object UpdateStarted : NotificationEvent { + + override val type = "SKY_ATLAS.UPDATE_STARTED" + override val body = "Sky Atlas database is being updated" + } + + data object UpdateFinished : NotificationEvent { + + override val type = "SKY_ATLAS.UPDATE_FINISHED" + override val body = "Sky Atlas database was updated" + } + @Scheduled(fixedDelay = Long.MAX_VALUE, timeUnit = TimeUnit.SECONDS) override fun run() { var request = Request.Builder().get().url(VERSION_URL).build() @@ -25,7 +40,9 @@ class SkyAtlasUpdateTask( val newestVersion = response.body!!.string() if (newestVersion != preferenceService.getText(VERSION_KEY) || simbadEntityRepository.isEmpty()) { - LOG.info("Sky Atlas is out of date. Downloading...") + LOG.info("Sky Atlas database is out of date. Downloading...") + + messageService.sendMessage(UpdateStarted) var finished = false @@ -52,8 +69,11 @@ class SkyAtlasUpdateTask( } preferenceService.putText(VERSION_KEY, newestVersion) + messageService.sendMessage(UpdateFinished) + + LOG.info("Sky Atlas database was updated. version={}, size={}", newestVersion, simbadEntityRepository.size) } else { - LOG.info("Sky Atlas is up to date. version={}, size={}", newestVersion, simbadEntityRepository.size) + LOG.info("Sky Atlas database is up to date. version={}, size={}", newestVersion, simbadEntityRepository.size) } } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/BodyEphemerisProvider.kt b/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/BodyEphemerisProvider.kt index dac5f3ced..e2c9cb715 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/BodyEphemerisProvider.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/BodyEphemerisProvider.kt @@ -37,7 +37,7 @@ class BodyEphemerisProvider : CachedEphemerisProvider() { val astrometric = site.at(utc).observe(target) val (az, alt) = astrometric.horizontal() val (ra, dec) = astrometric.equatorialAtDate() - val (raJ2000, decJ2000) = astrometric.equatorialJ2000() + val (raJ2000, decJ2000) = astrometric.equatorial() val element = HorizonsElement(time) element[HorizonsQuantity.ASTROMETRIC_RA] = "${raJ2000.normalized.toDegrees}" 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 9cf868f73..8ce437070 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -9,7 +9,7 @@ import io.objectbox.BoxStore import nebulosa.api.atlas.SatelliteEntity import nebulosa.api.atlas.SimbadEntity import nebulosa.api.calibration.CalibrationFrameEntity -import nebulosa.api.entities.MyObjectBox +import nebulosa.api.database.MyObjectBox import nebulosa.api.locations.LocationEntity import nebulosa.api.preferences.PreferenceEntity import nebulosa.batch.processing.AsyncJobLauncher @@ -19,9 +19,13 @@ import nebulosa.guiding.Guider import nebulosa.guiding.phd2.PHD2Guider import nebulosa.hips2fits.Hips2FitsService import nebulosa.horizons.HorizonsService +import nebulosa.imaging.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 @@ -90,10 +94,22 @@ class BeanConfiguration { fun cache(cachePath: Path) = Cache(cachePath.toFile(), MAX_CACHE_SIZE) @Bean - fun httpClient(connectionPool: ConnectionPool, cache: Cache) = OkHttpClient.Builder() + fun httpLogger() = HttpLoggingInterceptor.Logger { OKHTTP_LOGGER.info(it) } + + @Bean + fun httpClient(connectionPool: ConnectionPool, cache: Cache, httpLogger: HttpLoggingInterceptor.Logger) = OkHttpClient.Builder() .connectionPool(connectionPool) .cache(cache) - .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) + .addInterceptor(HttpLoggingInterceptor(httpLogger).setLevel(HttpLoggingInterceptor.Level.BASIC)) + .readTimeout(60L, TimeUnit.SECONDS) + .writeTimeout(60L, TimeUnit.SECONDS) + .connectTimeout(60L, TimeUnit.SECONDS) + .callTimeout(60L, TimeUnit.SECONDS) + .build() + + @Bean + fun alpacaHttpClient(connectionPool: ConnectionPool) = OkHttpClient.Builder() + .connectionPool(connectionPool) .readTimeout(60L, TimeUnit.SECONDS) .writeTimeout(60L, TimeUnit.SECONDS) .connectTimeout(60L, TimeUnit.SECONDS) @@ -139,6 +155,10 @@ class BeanConfiguration { @Bean fun asyncJobLauncher(threadPoolTaskExecutor: ThreadPoolTaskExecutor) = AsyncJobLauncher(threadPoolTaskExecutor) + @Bean + @Primary + fun watneyStarDetector(): StarDetector = WatneyStarDetector(computeHFD = true) + @Bean @Primary fun boxStore(dataPath: Path) = MyObjectBox.builder() @@ -199,5 +219,7 @@ class BeanConfiguration { companion object { const val MAX_CACHE_SIZE = 1024L * 1024L * 32L // 32MB + + @JvmStatic private val OKHTTP_LOGGER = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/ConstellationPropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/database/ConstellationPropertyConverter.kt similarity index 91% rename from api/src/main/kotlin/nebulosa/api/beans/converters/ConstellationPropertyConverter.kt rename to api/src/main/kotlin/nebulosa/api/beans/converters/database/ConstellationPropertyConverter.kt index fade4e0fc..e1454963b 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/ConstellationPropertyConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/database/ConstellationPropertyConverter.kt @@ -1,4 +1,4 @@ -package nebulosa.api.beans.converters +package nebulosa.api.beans.converters.database import io.objectbox.converter.PropertyConverter import nebulosa.nova.astrometry.Constellation diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/FrameTypePropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/database/FrameTypePropertyConverter.kt similarity index 90% rename from api/src/main/kotlin/nebulosa/api/beans/converters/FrameTypePropertyConverter.kt rename to api/src/main/kotlin/nebulosa/api/beans/converters/database/FrameTypePropertyConverter.kt index e9689f070..e160266ce 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/FrameTypePropertyConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/database/FrameTypePropertyConverter.kt @@ -1,4 +1,4 @@ -package nebulosa.api.beans.converters +package nebulosa.api.beans.converters.database import io.objectbox.converter.PropertyConverter import nebulosa.indi.device.camera.FrameType diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/SkyObjectTypePropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/database/SkyObjectTypePropertyConverter.kt similarity index 91% rename from api/src/main/kotlin/nebulosa/api/beans/converters/SkyObjectTypePropertyConverter.kt rename to api/src/main/kotlin/nebulosa/api/beans/converters/database/SkyObjectTypePropertyConverter.kt index 29487c552..537f4ce88 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/SkyObjectTypePropertyConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/database/SkyObjectTypePropertyConverter.kt @@ -1,4 +1,4 @@ -package nebulosa.api.beans.converters +package nebulosa.api.beans.converters.database import io.objectbox.converter.PropertyConverter import nebulosa.skycatalog.SkyObjectType diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/DeviceDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/indi/DeviceDeserializer.kt index fc2f56a62..11a3bf16a 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/DeviceDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/indi/DeviceDeserializer.kt @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.node.TextNode -sealed class DeviceDeserializer(type: Class) : StdDeserializer(type) { +abstract class DeviceDeserializer(type: Class) : StdDeserializer(type) { protected abstract fun deviceFor(name: String): T? diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt index 4e7753fc4..9bf5519b2 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt @@ -1,8 +1,8 @@ package nebulosa.api.calibration import io.objectbox.annotation.* -import nebulosa.api.entities.BoxEntity -import nebulosa.api.beans.converters.FrameTypePropertyConverter +import nebulosa.api.beans.converters.database.FrameTypePropertyConverter +import nebulosa.api.database.BoxEntity import nebulosa.indi.device.camera.FrameType @Entity diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index 2bf8047c2..a217cac8e 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -5,7 +5,6 @@ import nebulosa.imaging.Image import nebulosa.imaging.algorithms.transformation.correction.BiasSubtraction import nebulosa.imaging.algorithms.transformation.correction.DarkSubtraction import nebulosa.imaging.algorithms.transformation.correction.FlatCorrection -import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType import nebulosa.log.loggerFor import org.springframework.stereotype.Service @@ -32,10 +31,10 @@ class CalibrationFrameService( return if (darkFrame != null || biasFrame != null || flatFrame != null) { var transformedImage = if (createNew) image.clone() else image - var calibrationImage = Image(transformedImage.width, transformedImage.height, Header(), transformedImage.mono) + var calibrationImage = Image(transformedImage.width, transformedImage.height, Header.EMPTY, transformedImage.mono) if (biasFrame != null) { - calibrationImage = Fits(biasFrame.path!!).also(Fits::read).use(calibrationImage::load)!! + calibrationImage = biasFrame.path!!.fits().use(calibrationImage::load)!! transformedImage = transformedImage.transform(BiasSubtraction(calibrationImage)) LOG.info("bias frame subtraction applied. frame={}", biasFrame) } else { @@ -46,7 +45,7 @@ class CalibrationFrameService( } if (darkFrame != null) { - calibrationImage = Fits(darkFrame.path!!).also(Fits::read).use(calibrationImage::load)!! + calibrationImage = darkFrame.path!!.fits().use(calibrationImage::load)!! transformedImage = transformedImage.transform(DarkSubtraction(calibrationImage)) LOG.info("dark frame subtraction applied. frame={}", darkFrame) } else { @@ -57,7 +56,7 @@ class CalibrationFrameService( } if (flatFrame != null) { - calibrationImage = Fits(flatFrame.path!!).also(Fits::read).use(calibrationImage::load)!! + calibrationImage = flatFrame.path!!.fits().use(calibrationImage::load)!! transformedImage = transformedImage.transform(FlatCorrection(calibrationImage)) LOG.info("flat frame correction applied. frame={}", flatFrame) } else { @@ -98,7 +97,7 @@ class CalibrationFrameService( calibrationFrameRepository.delete(camera, "$file") try { - Fits(file).also(Fits::read).use { fits -> + file.fits().use { fits -> val (header) = fits.filterIsInstance().firstOrNull() ?: return@use val frameType = header.frameType?.takeIf { it != FrameType.LIGHT } ?: return@use diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt similarity index 90% rename from api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt rename to api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt index fbf54a8c9..1e9121050 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt @@ -6,7 +6,7 @@ import nebulosa.indi.device.camera.Camera import java.nio.file.Path import java.time.Duration -sealed interface CameraCaptureEvent : MessageEvent, JobExecutionEvent { +sealed interface CameraCaptureElapsed : MessageEvent, JobExecutionEvent { val camera: Camera diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index 0eb132a04..7d0b97eb8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -1,65 +1,35 @@ package nebulosa.api.cameras -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService -import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobExecutor import nebulosa.batch.processing.JobLauncher import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera -import nebulosa.log.debug +import nebulosa.log.info import nebulosa.log.loggerFor import org.springframework.stereotype.Component -import java.util.* @Component class CameraCaptureExecutor( private val messageService: MessageService, private val guider: Guider, - private val jobLauncher: JobLauncher, -) : Consumer { - - private val jobExecutions = LinkedList() - - @Synchronized - fun execute(request: CameraStartCaptureRequest) { - val camera = requireNotNull(request.camera) + override val jobLauncher: JobLauncher, +) : JobExecutor() { + fun execute(camera: Camera, request: CameraStartCaptureRequest): String { check(camera.connected) { "camera is not connected" } - check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } - - LOG.debug { "starting camera capture. request=$request" } - - val cameraCaptureJob = CameraCaptureJob(request, guider) - cameraCaptureJob.subscribe(this) - jobExecutions.add(jobLauncher.launch(cameraCaptureJob)) - } + check(findJobExecutionWithAny(camera) == null) { "Camera Capture job is already running" } - fun findJobExecution(camera: Camera): JobExecution? { - for (i in jobExecutions.indices.reversed()) { - val jobExecution = jobExecutions[i] - val job = jobExecution.job as CameraCaptureJob + LOG.info { "starting camera capture. camera=$camera, request=$request" } - if (!jobExecution.isDone && job.camera === camera) { - return jobExecution - } - } - - return null + val cameraCaptureJob = CameraCaptureJob(camera, request, guider) + cameraCaptureJob.subscribe(messageService::sendMessage) + register(jobLauncher.launch(cameraCaptureJob)) + return cameraCaptureJob.id } - @Synchronized fun stop(camera: Camera) { - val jobExecution = findJobExecution(camera) ?: return - jobLauncher.stop(jobExecution) - } - - fun isCapturing(camera: Camera): Boolean { - return findJobExecution(camera) != null - } - - override fun accept(event: MessageEvent) { - messageService.sendMessage(event) + findJobExecutionWithAny(camera)?.also { jobLauncher.stop(it) } } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt index 101155307..522733948 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt @@ -11,7 +11,7 @@ data class CameraCaptureFinished( override val exposureAmount: Int, override val captureElapsedTime: Duration, val aborted: Boolean, -) : CameraCaptureEvent { +) : CameraCaptureElapsed { override val exposureCount = exposureAmount override val captureProgress = 1.0 diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt index 62908f063..5e6cb8c51 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt @@ -16,7 +16,7 @@ data class CameraCaptureIsWaiting( override val waitProgress: Double, override val waitRemainingTime: Duration, override val state: CameraCaptureState = CameraCaptureState.WAITING, -) : CameraCaptureEvent { +) : CameraCaptureElapsed { override val exposureProgress = 1.0 override val exposureRemainingTime = Duration.ZERO!! diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt index ba130ce39..9f0337f2f 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -9,21 +9,21 @@ import nebulosa.batch.processing.SimpleJob import nebulosa.batch.processing.SimpleSplitStep import nebulosa.batch.processing.delay.DelayStep import nebulosa.guiding.Guider +import nebulosa.indi.device.camera.Camera data class CameraCaptureJob( + @JvmField val camera: Camera, @JvmField val request: CameraStartCaptureRequest, @JvmField val guider: Guider, ) : SimpleJob(), PublishSubscribe { private val cameraCaptureEventHandler = CameraCaptureEventHandler(this) - @JvmField val camera = requireNotNull(request.camera) - override val subject = PublishSubject.create() init { - val cameraExposureStep = if (request.isLoop) CameraLoopExposureStep(request) - else CameraExposureStep(request) + val cameraExposureStep = if (request.isLoop) CameraLoopExposureStep(camera, request) + else CameraExposureStep(camera, request) if (cameraExposureStep is CameraExposureStep) { val ditherStep = DitherAfterExposureStep(request.dither, guider) @@ -34,18 +34,22 @@ data class CameraCaptureJob( waitForSettleStep.registerWaitForSettleListener(cameraExposureStep) cameraDelayStep.registerDelayStepListener(cameraExposureStep) - add(waitForSettleStep) - add(cameraExposureStep) + register(waitForSettleStep) + register(cameraExposureStep) repeat(request.exposureAmount - 1) { - add(delayAndWaitForSettleStep) - add(cameraExposureStep) - add(ditherStep) + register(delayAndWaitForSettleStep) + register(cameraExposureStep) + register(ditherStep) } } else { - add(cameraExposureStep) + register(cameraExposureStep) } cameraExposureStep.registerCameraCaptureListener(cameraCaptureEventHandler) } + + override fun contains(data: Any): Boolean { + return data === camera || super.contains(data) + } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt index ea5217906..d17db6ad0 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt @@ -11,7 +11,7 @@ data class CameraCaptureStarted( override val exposureAmount: Int, override val captureRemainingTime: Duration, override val exposureRemainingTime: Duration, -) : CameraCaptureEvent { +) : CameraCaptureElapsed { override val exposureCount = 1 override val captureElapsedTime = Duration.ZERO!! diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt index 05b50d9a7..b62e6ee42 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt @@ -34,11 +34,6 @@ class CameraController( cameraService.disconnect(camera) } - @GetMapping("{camera}/capturing") - fun isCapturing(@DeviceOrEntityParam camera: Camera): Boolean { - return cameraService.isCapturing(camera) - } - @PutMapping("{camera}/cooler") fun cooler( @DeviceOrEntityParam camera: Camera, @@ -59,9 +54,7 @@ class CameraController( fun startCapture( @DeviceOrEntityParam camera: Camera, @RequestBody body: CameraStartCaptureRequest, - ) { - cameraService.startCapture(camera, body) - } + ) = cameraService.startCapture(camera, body) @PutMapping("{camera}/capture/abort") fun abortCapture(@DeviceOrEntityParam camera: Camera) { diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/CameraDeserializer.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraDeserializer.kt similarity index 84% rename from api/src/main/kotlin/nebulosa/api/beans/converters/indi/CameraDeserializer.kt rename to api/src/main/kotlin/nebulosa/api/cameras/CameraDeserializer.kt index 4fd540c58..bbac37188 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/CameraDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraDeserializer.kt @@ -1,5 +1,6 @@ -package nebulosa.api.beans.converters.indi +package nebulosa.api.cameras +import nebulosa.api.beans.converters.indi.DeviceDeserializer import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.camera.Camera import org.springframework.beans.factory.annotation.Autowired diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt index 5073bec96..705e84ab7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt @@ -15,7 +15,7 @@ data class CameraExposureElapsed( override val captureRemainingTime: Duration, override val exposureProgress: Double, override val exposureRemainingTime: Duration, -) : CameraExposureEvent { +) : CameraCaptureElapsed { override val state = CameraCaptureState.EXPOSURING override val waitProgress = 0.0 diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt deleted file mode 100644 index b2b004111..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.api.cameras - -sealed interface CameraExposureEvent : CameraCaptureEvent diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt index bd691428a..98a7cfe50 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt @@ -15,7 +15,7 @@ data class CameraExposureFinished( override val captureProgress: Double, override val captureRemainingTime: Duration, override val savePath: Path, -) : CameraExposureEvent { +) : CameraCaptureElapsed { override val state = CameraCaptureState.EXPOSURE_FINISHED override val exposureProgress = 0.0 diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt index 44d9601fa..f53797f7f 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt @@ -14,7 +14,7 @@ data class CameraExposureStarted( override val captureProgress: Double, override val captureRemainingTime: Duration, override val exposureRemainingTime: Duration, -) : CameraExposureEvent { +) : CameraCaptureElapsed { override val state = CameraCaptureState.EXPOSURE_STARTED override val exposureProgress = 0.0 diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt index 9e1c2bacf..b55358c11 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt @@ -4,16 +4,19 @@ import nebulosa.api.guiding.WaitForSettleListener import nebulosa.api.guiding.WaitForSettleStep import nebulosa.batch.processing.ExecutionContext import nebulosa.batch.processing.ExecutionContext.Companion.getDuration +import nebulosa.batch.processing.ExecutionContext.Companion.getInt import nebulosa.batch.processing.JobExecution import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult import nebulosa.batch.processing.delay.DelayStep import nebulosa.batch.processing.delay.DelayStepListener -import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.fits.Fits import nebulosa.indi.device.camera.* import nebulosa.io.transferAndClose import nebulosa.log.debug import nebulosa.log.loggerFor +import okio.sink import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -25,9 +28,11 @@ import java.time.format.DateTimeFormatter import kotlin.io.path.createParentDirectories import kotlin.io.path.outputStream -data class CameraExposureStep(override val request: CameraStartCaptureRequest) : CameraStartCaptureStep, DelayStepListener, WaitForSettleListener { - - @JvmField val camera = requireNotNull(request.camera) +data class CameraExposureStep( + override val camera: Camera, + override val request: CameraStartCaptureRequest, + private val virtualLoop: Boolean = false, +) : CameraStartCaptureStep, DelayStepListener, WaitForSettleListener { @JvmField val exposureTime = request.exposureTime @JvmField val exposureAmount = request.exposureAmount @@ -61,7 +66,7 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : if (event.device === camera) { when (event) { is CameraFrameCaptured -> { - save(event.fits) + save(event.stream, event.fits) } is CameraExposureAborted, is CameraExposureFailed, @@ -81,7 +86,8 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : } override fun beforeJob(jobExecution: JobExecution) { - camera.enableBlob() + exposureCount = jobExecution.context.getInt(EXPOSURE_COUNT, exposureCount) + captureElapsedTime = jobExecution.context.getDuration(CAPTURE_ELAPSED_TIME, captureElapsedTime) jobExecution.context.populateExecutionContext(Duration.ZERO, estimatedCaptureTime, 0.0) listeners.forEach { it.onCaptureStarted(this, jobExecution) } } @@ -94,17 +100,20 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : } override fun execute(stepExecution: StepExecution): StepResult { - this.stepExecution = stepExecution - EventBus.getDefault().register(this) - executeCapture(stepExecution) - EventBus.getDefault().unregister(this) + if (request.isLoop || estimatedCaptureTime > Duration.ZERO) { + this.stepExecution = stepExecution + EventBus.getDefault().register(this) + executeCapture(stepExecution) + EventBus.getDefault().unregister(this) + } + return StepResult.FINISHED } override fun stop(mayInterruptIfRunning: Boolean) { LOG.info("stopping camera exposure. camera={}", camera) camera.abortCapture() - camera.disableBlob() + // camera.disableBlob() aborted = true latch.reset() } @@ -126,12 +135,14 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : private fun executeCapture(stepExecution: StepExecution) { if (camera.connected && !aborted) { synchronized(camera) { - LOG.debug { "camera exposure started. estimatedCaptureTime=$estimatedCaptureTime, request=$request" } + LOG.debug { "camera exposure started. estimatedCaptureTime=$estimatedCaptureTime, request=$request, context=${stepExecution.context}" } latch.countUp() stepExecution.context[EXPOSURE_COUNT] = ++exposureCount + camera.enableBlob() + listeners.forEach { it.onExposureStarted(this, stepExecution) } if (request.width > 0 && request.height > 0) { @@ -148,20 +159,30 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : latch.await() captureElapsedTime += exposureTime + stepExecution.context[CAPTURE_ELAPSED_TIME] = captureElapsedTime - LOG.debug { "camera exposure finished. aborted=$aborted, camera=$camera" } + LOG.debug { "camera exposure finished. aborted=$aborted, camera=$camera, context=${stepExecution.context}" } } + } else { + LOG.warn("camera not connected or aborted. aborted=$aborted, camera=$camera") } } - private fun save(stream: InputStream) { + private fun save(stream: InputStream?, fits: Fits?) { try { - savedPath = request.makeSavePath() + savedPath = request.makeSavePath(camera) LOG.info("saving FITS. path={}", savedPath) savedPath!!.createParentDirectories() - stream.transferAndClose(savedPath!!.outputStream()) + + if (stream != null) { + stream.transferAndClose(savedPath!!.outputStream()) + } else if (fits != null) { + savedPath!!.outputStream().use { fits.writeTo(it.sink()) } + } else { + return + } listeners.forEach { it.onExposureFinished(this, stepExecution!!) } } catch (e: Throwable) { @@ -182,7 +203,7 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : var captureRemainingTime = Duration.ZERO var captureProgress = 0.0 - if (!request.isLoop) { + if (!request.isLoop && !virtualLoop) { captureRemainingTime = if (estimatedCaptureTime > captureElapsedTime) estimatedCaptureTime - captureElapsedTime else Duration.ZERO captureProgress = (estimatedCaptureTime - captureRemainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() } @@ -209,14 +230,14 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : @JvmStatic private val DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd.HHmmssSSS") @JvmStatic - fun CameraStartCaptureRequest.makeSavePath(autoSave: Boolean = this.autoSave): Path { + fun CameraStartCaptureRequest.makeSavePath(camera: Camera, autoSave: Boolean = this.autoSave): Path { return if (autoSave) { val now = LocalDateTime.now() val savePath = autoSubFolderMode.pathFor(savePath!!, now) val fileName = "%s-%s.fits".format(now.format(DATE_TIME_FORMAT), frameType) Path.of("$savePath", fileName) } else { - val fileName = "%s.fits".format(camera!!.name) + val fileName = "%s.fits".format(camera.name) Path.of("$savePath", fileName) } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt index 0fe19c6c7..bc6f968cb 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt @@ -4,12 +4,14 @@ import nebulosa.batch.processing.JobExecution import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult import nebulosa.batch.processing.delay.DelayStep +import nebulosa.indi.device.camera.Camera data class CameraLoopExposureStep( + override val camera: Camera, override val request: CameraStartCaptureRequest, ) : CameraStartCaptureStep { - private val cameraExposureStep = CameraExposureStep(request) + private val cameraExposureStep = CameraExposureStep(camera, request) private val delayStep = DelayStep(request.exposureDelay) override val savedPath diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt index 162ca36df..0a01653b8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt @@ -12,6 +12,8 @@ class CameraSerializer(private val capturesPath: Path) : StdSerializer(C override fun serialize(value: Camera, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("sender", value.sender.id) + gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) gen.writeBooleanField("connected", value.connected) gen.writeBooleanField("exposuring", value.exposuring) diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt index e61cd8135..987940b1a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt @@ -20,10 +20,6 @@ class CameraService( camera.disconnect() } - fun isCapturing(camera: Camera): Boolean { - return cameraCaptureExecutor.isCapturing(camera) - } - fun setpointTemperature(camera: Camera, temperature: Double) { camera.temperature(temperature) } @@ -33,17 +29,16 @@ class CameraService( } @Synchronized - fun startCapture(camera: Camera, request: CameraStartCaptureRequest) { - if (isCapturing(camera)) return - + fun startCapture(camera: Camera, request: CameraStartCaptureRequest): String { val savePath = request.savePath ?.takeIf { "$it".isNotBlank() && it.exists() && it.isDirectory() } ?: Path.of("$capturesPath", camera.name, request.frameType.name) - cameraCaptureExecutor - .execute(request.copy(camera = camera, savePath = savePath)) + return cameraCaptureExecutor + .execute(camera, request.copy(savePath = savePath)) } + @Synchronized fun abortCapture(camera: Camera) { cameraCaptureExecutor.stop(camera) } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt index 60c1cf3d4..9482918b4 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt @@ -6,10 +6,7 @@ import jakarta.validation.constraints.Positive import jakarta.validation.constraints.PositiveOrZero import nebulosa.api.beans.converters.time.DurationDeserializer import nebulosa.api.guiding.DitherAfterExposureRequest -import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.indi.device.focuser.Focuser import org.hibernate.validator.constraints.Range import org.hibernate.validator.constraints.time.DurationMax import org.hibernate.validator.constraints.time.DurationMin @@ -21,7 +18,6 @@ import java.time.temporal.ChronoUnit data class CameraStartCaptureRequest( val enabled: Boolean = true, // Capture. - val camera: Camera? = null, @field:DurationMin(nanos = 1000L) @field:DurationMax(minutes = 60L) val exposureTime: Duration = Duration.ZERO, @field:Range(min = 0L, max = 1000L) val exposureAmount: Int = 1, // 0 = looping @field:JsonDeserialize(using = DurationDeserializer::class) @field:DurationUnit(ChronoUnit.SECONDS) @@ -41,14 +37,17 @@ data class CameraStartCaptureRequest( val autoSubFolderMode: AutoSubFolderMode = AutoSubFolderMode.OFF, @field:Valid val dither: DitherAfterExposureRequest = DitherAfterExposureRequest.DISABLED, // Filter Wheel. - val wheel: FilterWheel? = null, val filterPosition: Int = 0, val shutterPosition: Int = 0, // Focuser. - val focuser: Focuser? = null, val focusOffset: Int = 0, ) { inline val isLoop get() = exposureAmount <= 0 + + companion object { + + @JvmStatic val EMPTY = CameraStartCaptureRequest() + } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt index 0110edce6..86c6e67ab 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt @@ -1,10 +1,13 @@ package nebulosa.api.cameras import nebulosa.batch.processing.Step +import nebulosa.indi.device.camera.Camera import java.nio.file.Path sealed interface CameraStartCaptureStep : Step { + val camera: Camera + val request: CameraStartCaptureRequest val savedPath: Path? diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionClosedWithClient.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionClosedWithClient.kt new file mode 100644 index 000000000..ed98c0613 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionClosedWithClient.kt @@ -0,0 +1,8 @@ +package nebulosa.api.connection + +import nebulosa.api.messages.MessageEvent + +data class ConnectionClosedWithClient(@JvmField val id: String) : MessageEvent { + + override val eventName = "CONNECTION.CLOSED" +} diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt index 51ea35ad2..0bd82df5a 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt @@ -3,8 +3,10 @@ package nebulosa.api.connection import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import org.hibernate.validator.constraints.Range +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* +@Validated @RestController @RequestMapping("connection") class ConnectionController( @@ -15,17 +17,21 @@ class ConnectionController( fun connect( @RequestParam @Valid @NotBlank host: String, @RequestParam @Valid @Range(min = 1, max = 65535) port: Int, - ) { - connectionService.connect(host, port) - } + @RequestParam(required = false, defaultValue = "INDI") type: ConnectionType, + ) = connectionService.connect(host, port, type) - @DeleteMapping - fun disconnect() { - connectionService.disconnect() + @DeleteMapping("{id}") + fun disconnect(@PathVariable id: String) { + connectionService.disconnect(id) } @GetMapping - fun connectionStatus(): Boolean { - return connectionService.connectionStatus() + fun connectionStatuses(): List { + return connectionService.connectionStatuses() + } + + @GetMapping("{id}") + fun connectionStatus(@PathVariable id: String): ConnectionStatus? { + return connectionService.connectionStatus(id) } } diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt index 1719d5d97..715c1def3 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionEventHandler.kt @@ -5,7 +5,7 @@ import nebulosa.api.focusers.FocuserEventHandler import nebulosa.api.guiding.GuideOutputEventHandler import nebulosa.api.mounts.MountEventHandler import nebulosa.api.wheels.WheelEventHandler -import nebulosa.indi.device.ConnectionEvent +import nebulosa.indi.device.DeviceConnectionEvent import nebulosa.indi.device.DeviceEvent import nebulosa.indi.device.DeviceEventHandler import nebulosa.indi.device.camera.Camera @@ -22,11 +22,11 @@ class ConnectionEventHandler( private val focuserEventHandler: FocuserEventHandler, private val wheelEventHandler: WheelEventHandler, private val guideOutputEventHandler: GuideOutputEventHandler, -) : DeviceEventHandler { +) : DeviceEventHandler.EventReceived { @Suppress("CascadeIf") override fun onEventReceived(event: DeviceEvent<*>) { - if (event is ConnectionEvent) { + if (event is DeviceConnectionEvent) { val device = event.device ?: return if (device is Camera) cameraEventHandler.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 4213b2c90..2485b2e72 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt @@ -1,8 +1,12 @@ package nebulosa.api.connection -import nebulosa.indi.client.DefaultINDIClient +import nebulosa.alpaca.indi.client.AlpacaClient +import nebulosa.api.messages.MessageService import nebulosa.indi.client.INDIClient +import nebulosa.indi.client.connection.INDISocketConnection import nebulosa.indi.device.Device +import nebulosa.indi.device.DeviceEventHandler +import nebulosa.indi.device.INDIDeviceProvider import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.focuser.Focuser @@ -12,6 +16,7 @@ import nebulosa.indi.device.mount.Mount import nebulosa.indi.device.thermometer.Thermometer import nebulosa.log.error import nebulosa.log.loggerFor +import okhttp3.OkHttpClient import org.greenrobot.eventbus.EventBus import org.springframework.stereotype.Service import org.springframework.web.server.ServerErrorException @@ -21,25 +26,58 @@ import java.io.Closeable class ConnectionService( private val eventBus: EventBus, private val connectionEventHandler: ConnectionEventHandler, + private val alpacaHttpClient: OkHttpClient, + private val messageService: MessageService, ) : Closeable { - @Volatile private var client: INDIClient? = null + private val providers = LinkedHashMap() - fun connectionStatus(): Boolean { - return client != null + fun connectionStatuses(): List { + return providers.keys.map { connectionStatus(it)!! } } - @Synchronized - fun connect(host: String, port: Int) { - try { - disconnect() + fun connectionStatus(id: String): ConnectionStatus? { + when (val client = providers[id]) { + is INDIClient -> { + when (val connection = client.connection) { + is INDISocketConnection -> { + return ConnectionStatus(id, ConnectionType.INDI, connection.host, connection.port) + } + } + } + is AlpacaClient -> { + return ConnectionStatus(id, ConnectionType.INDI, client.host, client.port) + } + } - val client = DefaultINDIClient(host, port) - client.registerDeviceEventHandler(eventBus::post) - client.registerDeviceEventHandler(connectionEventHandler) - client.start() + return null + } - this.client = client + @Synchronized + fun connect(host: String, port: Int, type: ConnectionType): String { + try { + val provider = when (type) { + ConnectionType.INDI -> { + val client = INDIClient(host, port) + client.registerDeviceEventHandler(DeviceEventHandler.EventReceived(eventBus::post)) + client.registerDeviceEventHandler(connectionEventHandler) + client.registerDeviceEventHandler(DeviceEventHandler.ConnectionClosed { sendConnectionClosedEvent(client) }) + client.start() + client + } + else -> { + val client = AlpacaClient(host, port, alpacaHttpClient) + client.registerDeviceEventHandler(DeviceEventHandler.EventReceived(eventBus::post)) + client.registerDeviceEventHandler(connectionEventHandler) + client.registerDeviceEventHandler(DeviceEventHandler.ConnectionClosed { sendConnectionClosedEvent(client) }) + client.discovery() + client + } + } + + providers[provider.id] = provider + + return provider.id } catch (e: Throwable) { LOG.error(e) @@ -48,69 +86,136 @@ class ConnectionService( } @Synchronized - fun disconnect() { - runCatching { client?.close() } - client = null + fun disconnect(id: String) { + providers[id]?.close() + providers.remove(id) + } + + fun disconnectAll() { + providers.forEach { it.value.close() } + providers.clear() + } + + private fun sendConnectionClosedEvent(provider: INDIDeviceProvider) { + LOG.info("client connection was closed. id={}", provider.id) + providers.remove(provider.id) + messageService.sendMessage(ConnectionClosedWithClient(provider.id)) } override fun close() { - disconnect() + disconnectAll() + } + + fun cameras(id: String): List { + return providers[id]?.cameras() ?: emptyList() + } + + fun mounts(id: String): List { + return providers[id]?.mounts() ?: emptyList() + } + + fun focusers(id: String): List { + return providers[id]?.focusers() ?: emptyList() + } + + fun wheels(id: String): List { + return providers[id]?.wheels() ?: emptyList() + } + + fun gpss(id: String): List { + return providers[id]?.gps() ?: emptyList() + } + + fun guideOutputs(id: String): List { + return providers[id]?.guideOutputs() ?: emptyList() + } + + fun thermometers(id: String): List { + return providers[id]?.thermometers() ?: emptyList() } fun cameras(): List { - return client?.cameras() ?: emptyList() + return providers.values.flatMap { it.cameras() } } fun mounts(): List { - return client?.mounts() ?: emptyList() + return providers.values.flatMap { it.mounts() } } fun focusers(): List { - return client?.focusers() ?: emptyList() + return providers.values.flatMap { it.focusers() } } fun wheels(): List { - return client?.wheels() ?: emptyList() + return providers.values.flatMap { it.wheels() } } - fun gps(): List { - return client?.gps() ?: emptyList() + fun gpss(): List { + return providers.values.flatMap { it.gps() } } fun guideOutputs(): List { - return client?.guideOutputs() ?: emptyList() + return providers.values.flatMap { it.guideOutputs() } } fun thermometers(): List { - return client?.thermometers() ?: emptyList() + return providers.values.flatMap { it.thermometers() } + } + + fun camera(id: String, name: String): Camera? { + return providers[id]?.camera(name) + } + + fun mount(id: String, name: String): Mount? { + return providers[id]?.mount(name) + } + + fun focuser(id: String, name: String): Focuser? { + return providers[id]?.focuser(name) + } + + fun wheel(id: String, name: String): FilterWheel? { + return providers[id]?.wheel(name) + } + + fun gps(id: String, name: String): GPS? { + return providers[id]?.gps(name) + } + + fun guideOutput(id: String, name: String): GuideOutput? { + return providers[id]?.guideOutput(name) + } + + fun thermometer(id: String, name: String): Thermometer? { + return providers[id]?.thermometer(name) } fun camera(name: String): Camera? { - return client?.camera(name) + return providers.firstNotNullOfOrNull { it.value.camera(name) } } fun mount(name: String): Mount? { - return client?.mount(name) + return providers.firstNotNullOfOrNull { it.value.mount(name) } } fun focuser(name: String): Focuser? { - return client?.focuser(name) + return providers.firstNotNullOfOrNull { it.value.focuser(name) } } fun wheel(name: String): FilterWheel? { - return client?.wheel(name) + return providers.firstNotNullOfOrNull { it.value.wheel(name) } } fun gps(name: String): GPS? { - return client?.gps(name) + return providers.firstNotNullOfOrNull { it.value.gps(name) } } fun guideOutput(name: String): GuideOutput? { - return client?.guideOutput(name) + return providers.firstNotNullOfOrNull { it.value.guideOutput(name) } } fun thermometer(name: String): Thermometer? { - return client?.thermometer(name) + return providers.firstNotNullOfOrNull { it.value.thermometer(name) } } fun device(name: String): Device? { @@ -119,6 +224,8 @@ class ConnectionService( ?: focuser(name) ?: wheel(name) ?: guideOutput(name) + ?: gps(name) + ?: thermometer(name) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionStatus.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionStatus.kt new file mode 100644 index 000000000..5d3802bc3 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionStatus.kt @@ -0,0 +1,7 @@ +package nebulosa.api.connection + +data class ConnectionStatus( + val id: String, + val type: ConnectionType, + val host: String, val port: Int, +) diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionType.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionType.kt new file mode 100644 index 000000000..be788c578 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionType.kt @@ -0,0 +1,6 @@ +package nebulosa.api.connection + +enum class ConnectionType { + INDI, + ALPACA, +} diff --git a/api/src/main/kotlin/nebulosa/api/entities/BoxEntity.kt b/api/src/main/kotlin/nebulosa/api/database/BoxEntity.kt similarity index 58% rename from api/src/main/kotlin/nebulosa/api/entities/BoxEntity.kt rename to api/src/main/kotlin/nebulosa/api/database/BoxEntity.kt index e66206f40..16091348c 100644 --- a/api/src/main/kotlin/nebulosa/api/entities/BoxEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/database/BoxEntity.kt @@ -1,4 +1,4 @@ -package nebulosa.api.entities +package nebulosa.api.database interface BoxEntity { diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocusOffsetStep.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocusOffsetStep.kt index 563854f54..78b44d61e 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocusOffsetStep.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocusOffsetStep.kt @@ -4,7 +4,7 @@ import nebulosa.api.wheels.WheelStep import nebulosa.batch.processing.Step import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult -import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserEvent import nebulosa.indi.device.focuser.FocuserMoveFailed diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/FocuserDeserializer.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt similarity index 84% rename from api/src/main/kotlin/nebulosa/api/beans/converters/indi/FocuserDeserializer.kt rename to api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt index 1e66a9665..7bb27a1bf 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/FocuserDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt @@ -1,5 +1,6 @@ -package nebulosa.api.beans.converters.indi +package nebulosa.api.focusers +import nebulosa.api.beans.converters.indi.DeviceDeserializer import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.focuser.Focuser import org.springframework.beans.factory.annotation.Autowired diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt index aa0295dff..ab76e2024 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt @@ -11,6 +11,8 @@ class FocuserSerializer : StdSerializer(Focuser::class.java) { override fun serialize(value: Focuser, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("sender", value.sender.id) + gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) gen.writeBooleanField("connected", value.connected) gen.writeBooleanField("moving", value.moving) @@ -19,7 +21,7 @@ class FocuserSerializer : StdSerializer(Focuser::class.java) { gen.writeBooleanField("canRelativeMove", value.canRelativeMove) gen.writeBooleanField("canAbort", value.canAbort) gen.writeBooleanField("canReverse", value.canReverse) - gen.writeBooleanField("reverse", value.reverse) + gen.writeBooleanField("reversed", value.reversed) gen.writeBooleanField("canSync", value.canSync) gen.writeBooleanField("hasBacklash", value.hasBacklash) gen.writeNumberField("maxPosition", value.maxPosition) diff --git a/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt b/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt index 35bc92e43..b9dd4ac30 100644 --- a/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt +++ b/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt @@ -8,18 +8,18 @@ import nebulosa.api.image.ImageService import nebulosa.math.deg import nebulosa.math.hours import org.hibernate.validator.constraints.Range -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 java.nio.file.Path +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("framing") class FramingController( private val imageService: ImageService, + private val framingService: FramingService, ) { + @GetMapping("hips-surveys") + fun hipsSurveys() = framingService.availableHipsSurveys + @PutMapping fun frame( @RequestParam @Valid @NotBlank rightAscension: String, @@ -28,8 +28,6 @@ class FramingController( @RequestParam(required = false, defaultValue = "720") @Valid @Range(min = 1, max = 4320) height: Int, @RequestParam(required = false, defaultValue = "1.0") @Valid @Positive @Max(90) fov: Double, @RequestParam(required = false, defaultValue = "0.0") rotation: Double, - @RequestParam(required = false, defaultValue = "CDS_P_DSS2_COLOR") hipsSurvey: HipsSurveyType, - ): Path { - return imageService.frame(rightAscension.hours, declination.deg, width, height, fov.deg, rotation.deg, hipsSurvey) - } + @RequestParam(required = false, defaultValue = "CDS/P/DSS2/COLOR") hipsSurvey: String, + ) = imageService.frame(rightAscension.hours, declination.deg, width, height, fov.deg, rotation.deg, hipsSurvey) } diff --git a/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt b/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt index cb4b24bbb..3c78ca249 100644 --- a/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt +++ b/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt @@ -1,6 +1,6 @@ package nebulosa.api.framing -import nebulosa.fits.Fits +import nebulosa.fits.fits import nebulosa.hips2fits.FormatOutputType import nebulosa.hips2fits.Hips2FitsService import nebulosa.imaging.Image @@ -16,25 +16,28 @@ import kotlin.io.path.outputStream @Service class FramingService(private val hips2FitsService: Hips2FitsService) { + val availableHipsSurveys by lazy { hips2FitsService.availableSurveys().execute().body()!!.sorted() } + @Synchronized fun frame( rightAscension: Angle, declination: Angle, width: Int, height: Int, fov: Angle, rotation: Angle = 0.0, - hipsSurveyType: HipsSurveyType = HipsSurveyType.CDS_P_DSS2_COLOR, + id: String = "CDS/P/DSS2/COLOR", ): Triple? { val responseBody = hips2FitsService.query( - hipsSurveyType.hipsSurvey, - rightAscension, declination, - width, height, - rotation, fov, + id, rightAscension, declination, + width, height, rotation, fov, format = FormatOutputType.FITS, ).execute().body() ?: return null responseBody.use { it.byteStream().transferAndCloseOutput(DEFAULT_PATH.outputStream()) } - val image = Fits(DEFAULT_PATH).also(Fits::read).use(Image::open) + + val image = DEFAULT_PATH.fits().use(Image::open) val solution = PlateSolution.from(image.header) + LOG.info("framing file loaded. calibration={}", solution) + return Triple(image, solution, DEFAULT_PATH) } diff --git a/api/src/main/kotlin/nebulosa/api/framing/HipsSurveyType.kt b/api/src/main/kotlin/nebulosa/api/framing/HipsSurveyType.kt deleted file mode 100644 index c90dc8dc7..000000000 --- a/api/src/main/kotlin/nebulosa/api/framing/HipsSurveyType.kt +++ /dev/null @@ -1,68 +0,0 @@ -package nebulosa.api.framing - -import nebulosa.hips2fits.HipsSurvey - -enum class HipsSurveyType(val hipsSurvey: HipsSurvey) { - CDS_P_DSS2_NIR("CDS/P/DSS2/NIR", "Image/Optical/DSS", "equatorial", "Optical", 16, 2.236E-4, 0.9955), - CDS_P_DSS2_BLUE("CDS/P/DSS2/blue", "Image/Optical/DSS", "equatorial", "Optical", 16, 2.236E-4, 0.9972), - CDS_P_DSS2_COLOR("CDS/P/DSS2/color", "Image/Optical/DSS", "equatorial", "Optical", 0, 2.236E-4, 1.0), - CDS_P_DSS2_RED("CDS/P/DSS2/red", "Image/Optical/DSS", "equatorial", "Optical", 16, 2.236E-4, 1.0), - FZU_CZ_P_CTA_FRAM_SURVEY_B("fzu.cz/P/CTA-FRAM/survey/B", "Image/Optical/CTA-FRAM", "equatorial", "Optical", -64, 0.003579, 1.0), - FZU_CZ_P_CTA_FRAM_SURVEY_R("fzu.cz/P/CTA-FRAM/survey/R", "Image/Optical/CTA-FRAM", "equatorial", "Optical", -64, 0.003579, 1.0), - FZU_CZ_P_CTA_FRAM_SURVEY_V("fzu.cz/P/CTA-FRAM/survey/V", "Image/Optical/CTA-FRAM", "equatorial", "Optical", -64, 0.003579, 1.0), - FZU_CZ_P_CTA_FRAM_SURVEY_COLOR("fzu.cz/P/CTA-FRAM/survey/color", "Image/Optical/CTA-FRAM", "equatorial", "Optical", 0, 0.003579, 1.0), - CDS_P_2MASS_H("CDS/P/2MASS/H", "Image/Infrared/2MASS", "equatorial", "Infrared", -32, 2.236E-4, 1.0), - CDS_P_2MASS_J("CDS/P/2MASS/J", "Image/Infrared/2MASS", "equatorial", "Infrared", -32, 2.236E-4, 1.0), - CDS_P_2MASS_K("CDS/P/2MASS/K", "Image/Infrared/2MASS", "equatorial", "Infrared", -32, 2.236E-4, 1.0), - CDS_P_2MASS_COLOR("CDS/P/2MASS/color", "Image/Infrared/2MASS", "equatorial", "Infrared", 0, 2.236E-4, 1.0), - CDS_P_AKARI_FIS_COLOR("CDS/P/AKARI/FIS/Color", "Image/Infrared/AKARI-FIS", "equatorial", "Infrared", 0, 0.003579, 1.0), - CDS_P_AKARI_FIS_N160("CDS/P/AKARI/FIS/N160", "Image/Infrared/AKARI-FIS", "equatorial", "Infrared", -32, 0.003579, 1.0), - CDS_P_AKARI_FIS_N60("CDS/P/AKARI/FIS/N60", "Image/Infrared/AKARI-FIS", "equatorial", "Infrared", -32, 0.003579, 1.0), - CDS_P_AKARI_FIS_WIDEL("CDS/P/AKARI/FIS/WideL", "Image/Infrared/AKARI-FIS", "equatorial", "Infrared", -32, 0.003579, 1.0), - CDS_P_AKARI_FIS_WIDES("CDS/P/AKARI/FIS/WideS", "Image/Infrared/AKARI-FIS", "equatorial", "Infrared", -32, 0.003579, 1.0), - CDS_P_NEOWISER_COLOR("CDS/P/NEOWISER/Color", "Image/Infrared/WISE/NEOWISER", "equatorial", "Infrared", 0, 4.473E-4, 1.0), - CDS_P_NEOWISER_W1("CDS/P/NEOWISER/W1", "Image/Infrared/WISE/NEOWISER", "equatorial", "Infrared", -32, 4.473E-4, 1.0), - CDS_P_NEOWISER_W2("CDS/P/NEOWISER/W2", "Image/Infrared/WISE/NEOWISER", "equatorial", "Infrared", -32, 4.473E-4, 1.0), - CDS_P_WISE_WSSA_12UM("CDS/P/WISE/WSSA/12um", "Image/Infrared/WISE/WSSA", "equatorial", "Infrared", -32, 8.946E-4, 1.0), - CDS_P_ALLWISE_W1("CDS/P/allWISE/W1", "Image/Infrared/WISE", "equatorial", "Infrared", -32, 4.473E-4, 1.0), - CDS_P_ALLWISE_W2("CDS/P/allWISE/W2", "Image/Infrared/WISE", "equatorial", "Infrared", -32, 4.473E-4, 1.0), - CDS_P_ALLWISE_W3("CDS/P/allWISE/W3", "Image/Infrared/WISE", "equatorial", "Infrared", -32, 4.473E-4, 0.9999), - CDS_P_ALLWISE_W4("CDS/P/allWISE/W4", "Image/Infrared/WISE", "equatorial", "Infrared", -32, 4.473E-4, 1.0), - CDS_P_ALLWISE_COLOR("CDS/P/allWISE/color", "Image/Infrared/WISE", "equatorial", "Infrared", 0, 4.473E-4, 1.0), - CDS_P_UNWISE_W1("CDS/P/unWISE/W1", "Image/Infrared/WISE/unWISE", "equatorial", "Infrared", -32, 0.229, 1.0), - CDS_P_UNWISE_W2("CDS/P/unWISE/W2", "Image/Infrared/WISE/unWISE", "equatorial", "Infrared", -32, 0.229, 1.0), - CDS_P_UNWISE_COLOR_W2_W1W2_W1("CDS/P/unWISE/color-W2-W1W2-W1", "Image/Infrared/WISE/unWISE", "equatorial", "Infrared", -32, 4.473E-4, 1.0), - CDS_P_RASS("CDS/P/RASS", "Image/X/ROSAT", "equatorial", "X-ray", 16, 0.007157, 1.0), - JAXA_P_ASCA_GIS("JAXA/P/ASCA_GIS", "Image/X/ASCA", "equatorial", "X-ray", 0, 0.001789, 1.0), - JAXA_P_ASCA_SIS("JAXA/P/ASCA_SIS", "Image/X/ASCA", "equatorial", "X-ray", 0, 0.001789, 1.0), - JAXA_P_MAXI_GSC("JAXA/P/MAXI-GSC", "Image/X/MAXI", "equatorial", "X-ray", 0, 0.001789, 1.0), - JAXA_P_MAXI_SSC("JAXA/P/MAXI-SSC", "Image/X/MAXI", "equatorial", "X-ray", 0, 0.1145, 1.0), - JAXA_P_SUZAKU("JAXA/P/SUZAKU", "Image/X", "equatorial", "X-ray", 0, 0.001789, 1.0), - JAXA_P_SWIFT_BAT_FLUX("JAXA/P/SWIFT_BAT_FLUX", "Image/X", "equatorial", "X-ray", 0, 0.001789, 1.0), - CDS_P_EGRET_DIF_100_150("CDS/P/EGRET/Dif/100-150", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_1000_2000("CDS/P/EGRET/Dif/1000-2000", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_150_300("CDS/P/EGRET/Dif/150-300", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_2000_4000("CDS/P/EGRET/Dif/2000-4000", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_30_50("CDS/P/EGRET/Dif/30-50", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_300_500("CDS/P/EGRET/Dif/300-500", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_4000_10000("CDS/P/EGRET/Dif/4000-10000", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_50_70("CDS/P/EGRET/Dif/50-70", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_500_1000("CDS/P/EGRET/Dif/500-1000", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_DIF_70_100("CDS/P/EGRET/Dif/70-100", "Image/Gamma-ray/EGRET/Diffuse", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_INF100("CDS/P/EGRET/inf100", "Image/Gamma-ray/EGRET", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_EGRET_SUP100("CDS/P/EGRET/sup100", "Image/Gamma-ray/EGRET", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_FERMI_3("CDS/P/Fermi/3", "Image/Gamma-ray", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_FERMI_4("CDS/P/Fermi/4", "Image/Gamma-ray", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_FERMI_5("CDS/P/Fermi/5", "Image/Gamma-ray", "equatorial", "Gamma-ray", -32, 0.01431, 1.0), - CDS_P_FERMI_COLOR("CDS/P/Fermi/color", "Image/Gamma-ray", "equatorial", "Gamma-ray", 0, 0.01431, 1.0); - - constructor( - id: String, - category: String, - frame: String, - regime: String, - bitPix: Int, - pixelScale: Double, - skyFraction: Double, - ) : this(HipsSurvey(id, category, frame, regime, bitPix, pixelScale, skyFraction)) -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt index 073e66c72..62e7972bb 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt @@ -3,7 +3,7 @@ package nebulosa.api.guiding import nebulosa.batch.processing.Step import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult -import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.guiding.GuideState import nebulosa.guiding.Guider import nebulosa.guiding.GuiderListener diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputDeserializer.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputDeserializer.kt new file mode 100644 index 000000000..6ede536fb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputDeserializer.kt @@ -0,0 +1,16 @@ +package nebulosa.api.guiding + +import nebulosa.api.beans.converters.indi.DeviceDeserializer +import nebulosa.api.connection.ConnectionService +import nebulosa.indi.device.guide.GuideOutput +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Lazy +import org.springframework.stereotype.Component + +@Component +class GuideOutputDeserializer : DeviceDeserializer(GuideOutput::class.java) { + + @Autowired @Lazy private lateinit var connectionService: ConnectionService + + override fun deviceFor(name: String) = connectionService.guideOutput(name) +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputSerializer.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputSerializer.kt new file mode 100644 index 000000000..1d9c8d878 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputSerializer.kt @@ -0,0 +1,22 @@ +package nebulosa.api.guiding + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import nebulosa.indi.device.guide.GuideOutput +import org.springframework.stereotype.Component + +@Component +class GuideOutputSerializer : StdSerializer(GuideOutput::class.java) { + + override fun serialize(value: GuideOutput, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeStartObject() + gen.writeStringField("sender", value.sender.id) + gen.writeStringField("id", value.id) + gen.writeStringField("name", value.name) + gen.writeBooleanField("connected", value.connected) + gen.writeBooleanField("canPulseGuide", value.canPulseGuide) + gen.writeBooleanField("pulseGuiding", value.pulseGuiding) + gen.writeEndObject() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseRequest.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseRequest.kt index 3badd2dbe..3b6d2134a 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseRequest.kt @@ -1,12 +1,9 @@ package nebulosa.api.guiding -import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.guide.GuideOutput import java.time.Duration data class GuidePulseRequest( - @JsonIgnore val guideOutput: GuideOutput? = null, val direction: GuideDirection, val duration: Duration, ) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt index 0894f392b..56fcc0fc8 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt @@ -10,7 +10,10 @@ import nebulosa.guiding.GuideDirection import nebulosa.indi.device.guide.GuideOutput import java.time.Duration -data class GuidePulseStep(@JvmField val request: GuidePulseRequest) : Step, DelayStepListener { +data class GuidePulseStep( + @JvmField val guideOutput: GuideOutput, + @JvmField val request: GuidePulseRequest, +) : Step, DelayStepListener { private val listeners = LinkedHashSet() private val delayStep = DelayStep(request.duration) @@ -28,8 +31,6 @@ data class GuidePulseStep(@JvmField val request: GuidePulseRequest) : Step, Dela } override fun execute(stepExecution: StepExecution): StepResult { - val guideOutput = requireNotNull(request.guideOutput) - if (guideOutput.pulseGuide(request.duration, request.direction)) { delayStep.execute(stepExecution) } @@ -38,11 +39,11 @@ data class GuidePulseStep(@JvmField val request: GuidePulseRequest) : Step, Dela } override fun afterJob(jobExecution: JobExecution) { - request.guideOutput?.stop() + guideOutput.stop() } override fun stop(mayInterruptIfRunning: Boolean) { - request.guideOutput?.stop() + guideOutput.stop() delayStep.stop() } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideStepHistory.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideStepHistory.kt index 720306b58..e85b1732f 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideStepHistory.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideStepHistory.kt @@ -1,6 +1,6 @@ package nebulosa.api.guiding -import nebulosa.common.concurrency.Incrementer +import nebulosa.common.concurrency.atomic.Incrementer import nebulosa.guiding.GuideStep import java.util.* diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt index 7c8d7d124..145810a0f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt @@ -1,16 +1,17 @@ package nebulosa.api.image import nebulosa.math.Angle +import nebulosa.math.Point2D import nebulosa.skycatalog.DeepSkyObject import nebulosa.skycatalog.SkyObject data class ImageAnnotation( - val x: Double, - val y: Double, + override val x: Double, + override val y: Double, val star: DeepSkyObject? = null, val dso: DeepSkyObject? = null, val minorPlanet: SkyObject? = null, -) { +) : Point2D { internal data class MinorPlanet( override val id: Long = 0L, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt index 82c31c173..cc73e3e7f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt @@ -1,6 +1,6 @@ package nebulosa.api.image -import nebulosa.fits.Fits +import nebulosa.fits.fits import nebulosa.imaging.Image import nebulosa.plate.solving.PlateSolution import org.springframework.stereotype.Component @@ -27,7 +27,7 @@ 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 - val image = Fits(path).also(Fits::read).use { openedImage?.first?.load(it) ?: Image.open(it, debayer) } + val image = path.fits().use { openedImage?.first?.load(it) ?: Image.open(it, debayer) } put(path, image, solution ?: PlateSolution.from(image.header)) return image } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt b/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt index d2ebe8e1c..83a6aa217 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt @@ -14,7 +14,7 @@ data class ImageInfo( val stretchShadow: Float = 0.0f, val stretchHighlight: Float = 1.0f, val stretchMidtone: Float = 0.5f, @field:JsonSerialize(using = RightAscensionSerializer::class) val rightAscension: Double? = null, @field:JsonSerialize(using = DeclinationSerializer::class) val declination: Double? = null, - val solved: Boolean = false, + val solved: ImageSolved? = null, val headers: List = emptyList(), val camera: Camera? = null, @JsonIgnoreProperties("histogram") val statistics: Statistics.Data? = null, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index a7e289c48..35abce005 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -6,8 +6,8 @@ import nebulosa.api.atlas.SimbadEntityRepository import nebulosa.api.calibration.CalibrationFrameService import nebulosa.api.connection.ConnectionService import nebulosa.api.framing.FramingService -import nebulosa.api.framing.HipsSurveyType import nebulosa.fits.* +import nebulosa.imaging.Image import nebulosa.imaging.ImageChannel import nebulosa.imaging.algorithms.computation.Histogram import nebulosa.imaging.algorithms.computation.Statistics @@ -16,17 +16,22 @@ import nebulosa.indi.device.camera.Camera import nebulosa.io.transferAndClose import nebulosa.log.loggerFor import nebulosa.math.* +import nebulosa.nova.astrometry.VSOP87E +import nebulosa.nova.position.Barycentric import nebulosa.sbd.SmallBodyDatabaseService import nebulosa.skycatalog.ClassificationType import nebulosa.star.detection.ImageStar -import nebulosa.watney.star.detection.WatneyStarDetector -import nebulosa.wcs.WCSException +import nebulosa.star.detection.StarDetector +import nebulosa.time.TimeYMDHMS +import nebulosa.time.UTC import nebulosa.wcs.WCS +import nebulosa.wcs.WCSException import org.springframework.http.HttpStatus import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Service import org.springframework.web.server.ResponseStatusException import java.nio.file.Path +import java.time.LocalDateTime import java.util.* import java.util.concurrent.CompletableFuture import javax.imageio.ImageIO @@ -44,6 +49,7 @@ class ImageService( private val imageBucket: ImageBucket, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, private val connectionService: ConnectionService, + private val starDetector: StarDetector, ) { @Synchronized @@ -61,8 +67,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 @@ -101,7 +107,7 @@ class ImageService( stretchParams.shadow, stretchParams.highlight, stretchParams.midtone, transformedImage.header.rightAscension.takeIf { it.isFinite() }, transformedImage.header.declination.takeIf { it.isFinite() }, - imageBucket[path]?.second != null, + imageBucket[path]?.second?.let(::ImageSolved), transformedImage.header.mapNotNull { if (it.isCommentStyle) null else ImageHeaderItem(it.key, it.value) }, instrument?.let(connectionService::camera), statistics, @@ -141,9 +147,9 @@ class ImageService( val annotations = Vector() val tasks = ArrayList>() - val dateTime = image.header.observationDate + val dateTime = image.header.observationDate ?: LocalDateTime.now() - if (minorPlanets && dateTime != null) { + if (minorPlanets) { threadPoolTaskExecutor.submitCompletable { val latitude = image.header.latitude ?: 0.0 val longitude = image.header.longitude ?: 0.0 @@ -181,9 +187,9 @@ class ImageService( .also(tasks::add) } - // val barycentric = VSOP87E.EARTH.at(UTC(TimeYMDHMS(dateTime))) - if (starsAndDSOs) { + val barycentric = VSOP87E.EARTH.at(UTC(TimeYMDHMS(dateTime))) + threadPoolTaskExecutor.submitCompletable { LOG.info("finding star/DSO annotations. dateTime={}, calibration={}", dateTime, calibration) @@ -192,7 +198,8 @@ class ImageService( var count = 0 for (entry in catalog) { - val (x, y) = wcs.skyToPix(entry.rightAscensionJ2000, entry.declinationJ2000) + val astrometric = barycentric.observe(entry).equatorial() + val (x, y) = wcs.skyToPix(astrometric.longitude.normalized, astrometric.latitude) val annotation = if (entry.type.classification == ClassificationType.STAR) ImageAnnotation(x, y, star = entry) else ImageAnnotation(x, y, dso = entry) annotations.add(annotation) @@ -230,10 +237,10 @@ class ImageService( fun frame( rightAscension: Angle, declination: Angle, width: Int, height: Int, fov: Angle, - rotation: Angle = 0.0, hipsSurveyType: HipsSurveyType = HipsSurveyType.CDS_P_DSS2_COLOR, + rotation: Angle = 0.0, id: String = "CDS/P/DSS2/COLOR", ): Path { val (image, calibration, path) = framingService - .frame(rightAscension, declination, width, height, fov, rotation, hipsSurveyType)!! + .frame(rightAscension, declination, width, height, fov, rotation, id)!! imageBucket.put(path, image, calibration) return path } @@ -276,7 +283,7 @@ class ImageService( fun detectStars(path: Path): List { val (image) = imageBucket[path] ?: return emptyList() - return WATNEY_STAR_DETECTOR.detect(image) + return starDetector.detect(image) } fun histogram(path: Path, bitLength: Int = 16): IntArray { @@ -287,7 +294,6 @@ class ImageService( companion object { @JvmStatic private val LOG = loggerFor() - @JvmStatic private val WATNEY_STAR_DETECTOR = WatneyStarDetector(computeHFD = true) private const val COORDINATE_INTERPOLATION_DELTA = 24 } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt b/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt index 3d3992184..d9ab40126 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageSolved.kt @@ -16,8 +16,8 @@ data class ImageSolved( constructor(solution: PlateSolution) : this( solution.orientation.toDegrees, solution.scale.toArcsec, - solution.rightAscension.format(AngleFormatter.HMS), - solution.declination.format(AngleFormatter.SIGNED_DMS), + solution.rightAscension.formatHMS(), + solution.declination.formatSignedDMS(), solution.width.toArcmin, solution.height.toArcmin, solution.radius.toDegrees, ) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt index 3a8412201..f69a95d6a 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt @@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.* @RequestMapping("indi") class INDIController( private val indiService: INDIService, - private val indiEventHandler: INDIEventHandler, ) { @GetMapping("{device}/properties") @@ -28,7 +27,7 @@ class INDIController( @GetMapping("{device}/log") fun log(@DeviceOrEntityParam device: Device): List { - return device.messages + return synchronized(device.messages) { device.messages } } @GetMapping("log") @@ -39,12 +38,12 @@ class INDIController( @Synchronized @PutMapping("listener/{device}/start") fun startListening(device: Device) { - indiEventHandler.canSendEvents.add(device) + indiService.registerDeviceToSendMessage(device) } @Synchronized @PutMapping("listener/{device}/stop") fun stopListening(device: Device) { - indiEventHandler.canSendEvents.remove(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 26effc19a..77855d479 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt @@ -14,37 +14,41 @@ class INDIEventHandler( private val messageService: MessageService, ) : LinkedList() { - val canSendEvents = HashSet() + private val canSendEvents = HashSet() @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) - } - - sendINDIMessageReceived(event) - } + is DeviceMessageReceived -> if (event.device == null) addFirst(event.message) + else sendINDIMessageReceived(event) + is DeviceDetached<*> -> unregisterDevice(event.device) } } + fun registerDevice(device: Device) { + canSendEvents.add(device.id) + } + + fun unregisterDevice(device: Device) { + canSendEvents.remove(device.id) + } + fun sendINDIPropertyChanged(event: DevicePropertyEvent) { - if (event.device in canSendEvents) { + if (event.device.id in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_CHANGED, event)) } } fun sendINDIPropertyDeleted(event: DevicePropertyEvent) { - if (event.device in canSendEvents) { + if (event.device.id in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_DELETED, event)) } } fun sendINDIMessageReceived(event: DeviceMessageReceived) { - if (event.device in canSendEvents) { + if (event.device != null && event.device!!.id in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_MESSAGE_RECEIVED, event)) } } diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/INDIPropertySerializer.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIPropertySerializer.kt similarity index 93% rename from api/src/main/kotlin/nebulosa/api/beans/converters/indi/INDIPropertySerializer.kt rename to api/src/main/kotlin/nebulosa/api/indi/INDIPropertySerializer.kt index fb536be46..9115b7b85 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/INDIPropertySerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIPropertySerializer.kt @@ -1,4 +1,4 @@ -package nebulosa.api.beans.converters.indi +package nebulosa.api.indi import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/INDIPropertyVectorSerializer.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIPropertyVectorSerializer.kt similarity index 96% rename from api/src/main/kotlin/nebulosa/api/beans/converters/indi/INDIPropertyVectorSerializer.kt rename to api/src/main/kotlin/nebulosa/api/indi/INDIPropertyVectorSerializer.kt index 3d773ad48..069753059 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/INDIPropertyVectorSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIPropertyVectorSerializer.kt @@ -1,4 +1,4 @@ -package nebulosa.api.beans.converters.indi +package nebulosa.api.indi import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt index 775518c39..e66b2ef2c 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt @@ -10,6 +10,14 @@ class INDIService( private val indiEventHandler: INDIEventHandler, ) { + fun registerDeviceToSendMessage(device: Device) { + indiEventHandler.registerDevice(device) + } + + fun unregisterDeviceToSendMessage(device: Device) { + indiEventHandler.unregisterDevice(device) + } + fun messages(): List { return indiEventHandler } diff --git a/api/src/main/kotlin/nebulosa/api/locations/LocationEntity.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationEntity.kt index 0a0072626..be1bcc29a 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationEntity.kt @@ -3,7 +3,7 @@ package nebulosa.api.locations import io.objectbox.annotation.Entity import io.objectbox.annotation.Id import jakarta.validation.constraints.NotBlank -import nebulosa.api.entities.BoxEntity +import nebulosa.api.database.BoxEntity import nebulosa.math.deg import nebulosa.math.m import nebulosa.nova.position.GeographicPosition diff --git a/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt b/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt index af5f4b90d..0a4206e7c 100644 --- a/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt @@ -38,7 +38,7 @@ class MessageService( fun sendMessage(event: MessageEvent) { if (connected.get()) { simpleMessageTemplate.convertAndSend(EVENT_NAME, event) - } else { + } else if (event is QueueableEvent) { LOG.debug { "queueing message. event=$event" } messageQueue.offer(event) } diff --git a/api/src/main/kotlin/nebulosa/api/messages/QueueableEvent.kt b/api/src/main/kotlin/nebulosa/api/messages/QueueableEvent.kt new file mode 100644 index 000000000..c1a9bfb8a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/messages/QueueableEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.api.messages + +interface QueueableEvent diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/MountDeserializer.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt similarity index 84% rename from api/src/main/kotlin/nebulosa/api/beans/converters/indi/MountDeserializer.kt rename to api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt index 37a716c4e..edfacb29e 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/MountDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt @@ -1,5 +1,6 @@ -package nebulosa.api.beans.converters.indi +package nebulosa.api.mounts +import nebulosa.api.beans.converters.indi.DeviceDeserializer import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.mount.Mount import org.springframework.beans.factory.annotation.Autowired diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt index 8fab6ee19..9fecfbc99 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt @@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.ser.std.StdSerializer import nebulosa.indi.device.mount.Mount -import nebulosa.math.AngleFormatter -import nebulosa.math.format +import nebulosa.math.formatHMS +import nebulosa.math.formatSignedDMS import nebulosa.math.toDegrees import nebulosa.math.toMeters import org.springframework.stereotype.Component @@ -16,6 +16,8 @@ class MountSerializer : StdSerializer(Mount::class.java) { override fun serialize(value: Mount, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("sender", value.sender.id) + gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) gen.writeBooleanField("connected", value.connected) gen.writeBooleanField("slewing", value.slewing) @@ -32,8 +34,8 @@ class MountSerializer : StdSerializer(Mount::class.java) { gen.writeStringField("pierSide", value.pierSide.name) gen.writeNumberField("guideRateWE", value.guideRateWE) gen.writeNumberField("guideRateNS", value.guideRateNS) - gen.writeStringField("rightAscension", value.rightAscension.format(AngleFormatter.HMS)) - gen.writeStringField("declination", value.declination.format(AngleFormatter.SIGNED_DMS)) + gen.writeStringField("rightAscension", value.rightAscension.formatHMS()) + gen.writeStringField("declination", value.declination.formatSignedDMS()) gen.writeBooleanField("canPulseGuide", value.canPulseGuide) gen.writeBooleanField("pulseGuiding", value.pulseGuiding) gen.writeBooleanField("canPark", value.canPark) diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index 81cc6f13c..c97723dbe 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -1,6 +1,5 @@ package nebulosa.api.mounts -import nebulosa.api.atlas.CurrentTime import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.image.ImageBucket import nebulosa.constants.PI @@ -16,6 +15,9 @@ import nebulosa.nova.frame.Ecliptic 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 @@ -168,15 +170,14 @@ class MountService(private val imageBucket: ImageBucket) { val computedLocation = ComputedLocation() val center = site[mount]!! - val time = CurrentTime - val epoch = if (j2000) null else time + val epoch = if (j2000) null else CurrentTime - val icrf = ICRF.equatorial(rightAscension, declination, time = time, epoch = epoch, center = center) + val icrf = ICRF.equatorial(rightAscension, declination, epoch = epoch, center = center) computedLocation.constellation = Constellation.find(icrf) if (j2000) { if (equatorial) { - val raDec = icrf.equatorialAtDate() + val raDec = icrf.equatorialAtEpoch(CurrentTime) computedLocation.rightAscension = raDec.longitude.normalized computedLocation.declination = raDec.latitude } @@ -185,7 +186,7 @@ class MountService(private val imageBucket: ImageBucket) { computedLocation.declinationJ2000 = declination } else { if (equatorial) { - val raDec = icrf.equatorialJ2000() + val raDec = icrf.equatorial() computedLocation.rightAscensionJ2000 = raDec.longitude.normalized computedLocation.declinationJ2000 = raDec.latitude } @@ -223,8 +224,8 @@ class MountService(private val imageBucket: ImageBucket) { val raOffset = mount.rightAscension - calibratedRA val decOffset = mount.declination - calibratedDEC LOG.info( - "pointing mount adjusted. ra={}, dec={}, dx={}, dy={}", rightAscension.format(AngleFormatter.HMS), - declination.format(AngleFormatter.SIGNED_DMS), raOffset.format(AngleFormatter.HMS), decOffset.format(AngleFormatter.SIGNED_DMS) + "pointing mount adjusted. ra={}, dec={}, dx={}, dy={}", rightAscension.formatHMS(), + declination.formatSignedDMS(), raOffset.formatHMS(), decOffset.formatSignedDMS() ) goTo(mount, rightAscension + raOffset, declination + decOffset, true) } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewStep.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewStep.kt new file mode 100644 index 000000000..69991f3be --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewStep.kt @@ -0,0 +1,94 @@ +package nebulosa.api.mounts + +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.mount.MountEvent +import nebulosa.indi.device.mount.MountSlewFailed +import nebulosa.indi.device.mount.MountSlewingChanged +import nebulosa.log.loggerFor +import nebulosa.math.Angle +import nebulosa.math.formatHMS +import nebulosa.math.formatSignedDMS +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.time.Duration + +data class MountSlewStep( + val mount: Mount, + val rightAscension: Angle, val declination: Angle, + val j2000: Boolean = false, val goTo: Boolean = true, +) : Step { + + private val latch = CountUpDownLatch() + private val settleDelayStep = DelayStep(SETTLE_DURATION) + + @Volatile private var initialRA = mount.rightAscension + @Volatile private var initialDEC = mount.declination + + @Subscribe(threadMode = ThreadMode.ASYNC) + fun onMountEvent(event: MountEvent) { + if (event.device === mount) { + if (event is MountSlewingChanged) { + if (!mount.slewing && (mount.rightAscension != initialRA || mount.declination != initialDEC)) { + latch.reset() + } + } else if (event is MountSlewFailed) { + LOG.warn("failed to slew mount. mount={}", mount) + latch.reset() + } + } + } + + override fun execute(stepExecution: StepExecution): StepResult { + if (mount.connected && !mount.parked && !mount.parking && !mount.slewing && + rightAscension.isFinite() && declination.isFinite() && + (mount.rightAscension != rightAscension || mount.declination != declination) + ) { + EventBus.getDefault().register(this) + + latch.countUp() + + LOG.info("moving mount. mount={}, ra={}, dec={}", mount, rightAscension.formatHMS(), declination.formatSignedDMS()) + + initialRA = mount.rightAscension + initialDEC = mount.declination + + if (j2000) { + if (goTo) mount.goToJ2000(rightAscension, declination) + else mount.slewToJ2000(rightAscension, declination) + } else { + if (goTo) mount.goTo(rightAscension, declination) + else mount.slewTo(rightAscension, declination) + } + + latch.await() + + LOG.info("mount moved. mount={}", mount) + + settleDelayStep.execute(stepExecution) + + EventBus.getDefault().unregister(this) + } else { + LOG.warn("cannot move mount. mount={}", mount) + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + mount.abortMotion() + latch.reset() + settleDelayStep.stop(mayInterruptIfRunning) + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + @JvmStatic private val SETTLE_DURATION: Duration = Duration.ofSeconds(5) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt b/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt index 13813967c..266876056 100644 --- a/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt @@ -1,8 +1,9 @@ package nebulosa.api.notifications import nebulosa.api.messages.MessageEvent +import nebulosa.api.messages.QueueableEvent -interface NotificationEvent : MessageEvent { +interface NotificationEvent : MessageEvent, QueueableEvent { enum class Severity { INFO, diff --git a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceEntity.kt b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceEntity.kt index 84570c502..2087fcbcc 100644 --- a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceEntity.kt @@ -4,7 +4,7 @@ import io.objectbox.annotation.ConflictStrategy import io.objectbox.annotation.Entity import io.objectbox.annotation.Id import io.objectbox.annotation.Unique -import nebulosa.api.entities.BoxEntity +import nebulosa.api.database.BoxEntity @Entity data class PreferenceEntity( diff --git a/api/src/main/kotlin/nebulosa/api/repositories/BoxRepository.kt b/api/src/main/kotlin/nebulosa/api/repositories/BoxRepository.kt index b098f4be6..98223b3c4 100644 --- a/api/src/main/kotlin/nebulosa/api/repositories/BoxRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/repositories/BoxRepository.kt @@ -1,7 +1,7 @@ package nebulosa.api.repositories import io.objectbox.Box -import nebulosa.api.entities.BoxEntity +import nebulosa.api.database.BoxEntity abstract class BoxRepository : Collection { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt index 8b46bc006..579ff2f82 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt @@ -1,6 +1,8 @@ package nebulosa.api.sequencer import jakarta.validation.Valid +import nebulosa.api.beans.converters.indi.DeviceOrEntityParam +import nebulosa.indi.device.camera.Camera import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -12,13 +14,14 @@ class SequencerController( private val sequencerService: SequencerService, ) { - @PutMapping("start") - fun startSequencer(@RequestBody @Valid body: SequencePlanRequest) { - sequencerService.startSequencer(body) - } + @PutMapping("{camera}/start") + fun startSequencer( + @DeviceOrEntityParam camera: Camera, + @RequestBody @Valid body: SequencePlanRequest, + ) = sequencerService.start(camera, body) - @PutMapping("stop") - fun stopSequencer() { - sequencerService.stopSequencer() + @PutMapping("{camera}/stop") + fun stopSequencer(@DeviceOrEntityParam camera: Camera) { + sequencerService.stop(camera) } } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerElapsed.kt similarity index 69% rename from api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt rename to api/src/main/kotlin/nebulosa/api/sequencer/SequencerElapsed.kt index 8b00f412f..63d1ce089 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerElapsed.kt @@ -1,15 +1,15 @@ package nebulosa.api.sequencer -import nebulosa.api.cameras.CameraCaptureEvent +import nebulosa.api.cameras.CameraCaptureElapsed import nebulosa.api.messages.MessageEvent import java.time.Duration -data class SequencerEvent( +data class SequencerElapsed( val id: Int, val elapsedTime: Duration, val remainingTime: Duration, val progress: Double, - val capture: CameraCaptureEvent? = null, + val capture: CameraCaptureElapsed? = null, ) : MessageEvent { override val eventName = "SEQUENCER.ELAPSED" diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt index 6a4ea0ebd..f4af5f833 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt @@ -1,53 +1,40 @@ package nebulosa.api.sequencer -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService -import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobExecutor import nebulosa.batch.processing.JobLauncher import nebulosa.guiding.Guider -import nebulosa.log.debug +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.focuser.Focuser +import nebulosa.log.info import nebulosa.log.loggerFor import org.springframework.stereotype.Component -import java.util.* @Component class SequencerExecutor( private val messageService: MessageService, private val guider: Guider, - private val jobLauncher: JobLauncher, -) : Consumer { + override val jobLauncher: JobLauncher, +) : JobExecutor() { - private val jobExecutions = LinkedList() + fun execute( + camera: Camera, request: SequencePlanRequest, + wheel: FilterWheel? = null, focuser: Focuser? = null, + ): String { + check(findJobExecutionWithAny(camera) == null) { "job is already running" } - @Synchronized - fun execute(request: SequencePlanRequest) { - check(!isRunning()) { "job is already running" } + LOG.info { "starting sequencer. camera=$camera, wheel=$wheel, focuser=$focuser, request=$request" } - LOG.debug { "starting sequencer. request=%s".format(request) } - - val sequencerJob = SequencerJob(request, guider) - sequencerJob.subscribe(this) + val sequencerJob = SequencerJob(camera, request, guider, wheel, focuser) + sequencerJob.subscribe(messageService::sendMessage) sequencerJob.initialize() - jobExecutions.add(jobLauncher.launch(sequencerJob)) - } - - fun findJobExecution(): JobExecution? { - return jobExecutions.lastOrNull { !it.isDone } - } - - fun isRunning(): Boolean { - return findJobExecution() != null - } - - @Synchronized - fun stop() { - val jobExecution = findJobExecution() ?: return - jobLauncher.stop(jobExecution) + register(jobLauncher.launch(sequencerJob)) + return sequencerJob.id } - override fun accept(event: MessageEvent) { - messageService.sendMessage(event) + fun stop(camera: Camera) { + findJobExecutionWithAny(camera)?.also { jobLauncher.stop(it) } } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt index ccdcbc357..1f0bdb4fc 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt @@ -14,7 +14,10 @@ import nebulosa.batch.processing.ExecutionContext.Companion.getInt import nebulosa.batch.processing.delay.DelayStep import nebulosa.batch.processing.delay.DelayStepListener import nebulosa.guiding.Guider +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.focuser.Focuser import java.time.Duration import java.time.LocalDateTime @@ -23,8 +26,11 @@ import java.time.LocalDateTime // https://nighttime-imaging.eu/docs/master/site/sequencer/advanced/advanced/ data class SequencerJob( + @JvmField val camera: Camera, @JvmField val plan: SequencePlanRequest, @JvmField val guider: Guider, + @JvmField val wheel: FilterWheel? = null, + @JvmField val focuser: Focuser? = null, ) : SimpleJob(), PublishSubscribe, DelayStepListener { private val cameraCaptureEventHandler = CameraCaptureEventHandler(this) @@ -40,7 +46,7 @@ data class SequencerJob( val initialDelayStep = DelayStep(plan.initialDelay) initialDelayStep.registerDelayStepListener(this) - add(initialDelayStep) + register(initialDelayStep) val waitForSettleStep = WaitForSettleStep(guider) @@ -63,7 +69,7 @@ data class SequencerJob( if (plan.captureMode == SequenceCaptureMode.FULLY || usedEntries.size == 1) { for (i in usedEntries.indices) { val request = mapRequest(usedEntries[i]) - val cameraExposureStep = CameraExposureStep(request) + val cameraExposureStep = CameraExposureStep(camera, request) val delayStep = DelayStep(request.exposureDelay) delayStep.registerDelayStepListener(cameraExposureStep) val delayAndWaitForSettleStep = SimpleSplitStep(waitForSettleStep, delayStep) @@ -72,21 +78,21 @@ data class SequencerJob( val focusStep = request.focusStep() var estimatedCaptureTimeForEntry = Duration.ZERO - add(SequenceIdStep(plan.entries.indexOf(usedEntries[i]) + 1)) + register(SequenceIdStep(plan.entries.indexOf(usedEntries[i]) + 1)) repeat(request.exposureAmount) { if (i == 0 && it == 0) { - add(waitForSettleStep) + register(waitForSettleStep) } else { - add(delayAndWaitForSettleStep) + register(delayAndWaitForSettleStep) estimatedCaptureTime += request.exposureDelay estimatedCaptureTimeForEntry += request.exposureDelay } - wheelStep?.also(::add) - focusStep?.also(::add) - add(cameraExposureStep) - add(ditherStep) + wheelStep?.also(::register) + focusStep?.also(::register) + register(cameraExposureStep) + register(ditherStep) estimatedCaptureTime += request.exposureTime estimatedCaptureTimeForEntry += request.exposureTime @@ -101,7 +107,7 @@ data class SequencerJob( val count = IntArray(requests.size) val delaySteps = requests.map { DelayStep(it.exposureDelay) } val ditherSteps = requests.map { DitherAfterExposureStep(it.dither, guider) } - val cameraExposureSteps = requests.map { CameraExposureStep(it) } + val cameraExposureSteps = requests.map { CameraExposureStep(camera, it) } delaySteps.indices.forEach { delaySteps[it].registerDelayStepListener(cameraExposureSteps[it]) } val delayAndWaitForSettleSteps = requests.indices.map { SimpleSplitStep(waitForSettleStep, delaySteps[it]) } val wheelSteps = requests.map { it.wheelStep() } @@ -115,20 +121,20 @@ data class SequencerJob( val request = requests[i] if (count[i] < request.exposureAmount) { - add(sequenceIdSteps[i]) + register(sequenceIdSteps[i]) if (i == 0 && count[i] == 0) { - add(waitForSettleStep) + register(waitForSettleStep) } else { - add(delayAndWaitForSettleSteps[i]) + register(delayAndWaitForSettleSteps[i]) estimatedCaptureTime += delaySteps[i].duration estimatedCaptureTimeForEntry[i] += delaySteps[i].duration } - wheelSteps[i]?.also(::add) - focusSteps[i]?.also(::add) - add(cameraExposureSteps[i]) - add(ditherSteps[i]) + wheelSteps[i]?.also(::register) + focusSteps[i]?.also(::register) + register(cameraExposureSteps[i]) + register(ditherSteps[i]) estimatedCaptureTime += cameraExposureSteps[i].exposureTime estimatedCaptureTimeForEntry[i] += cameraExposureSteps[i].exposureTime @@ -152,11 +158,11 @@ data class SequencerJob( override fun afterJob(jobExecution: JobExecution) { val id = jobExecution.context.getInt(SequenceIdStep.ID) - super.onNext(SequencerEvent(id, estimatedCaptureTime, Duration.ZERO, 1.0)) + super.onNext(SequencerElapsed(id, estimatedCaptureTime, Duration.ZERO, 1.0)) } override fun onNext(event: MessageEvent) { - if (event is CameraCaptureEvent) { + if (event is CameraCaptureElapsed) { val context = event.jobExecution.context val id = context.getInt(SequenceIdStep.ID) @@ -172,7 +178,7 @@ data class SequencerJob( val progress = elapsedTime.toMillis() / estimatedCaptureTime.toMillis().toDouble() - super.onNext(SequencerEvent(id, elapsedTime, estimatedCaptureTime - elapsedTime, progress, event)) + super.onNext(SequencerElapsed(id, elapsedTime, estimatedCaptureTime - elapsedTime, progress, event)) } if (event is CameraExposureFinished) { @@ -186,7 +192,11 @@ data class SequencerJob( val elapsedTime = step.duration - remainingTime val progress = elapsedTime.toMillis() / estimatedCaptureTime.toMillis().toDouble() - super.onNext(SequencerEvent(0, elapsedTime, estimatedCaptureTime - elapsedTime, progress)) + super.onNext(SequencerElapsed(0, elapsedTime, estimatedCaptureTime - elapsedTime, progress)) + } + + override fun contains(data: Any): Boolean { + return data === camera || data === focuser || data === wheel || super.contains(data) } private data class SequenceIdStep(private val id: Int) : Step { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt index ae95aad24..b3840bcfe 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt @@ -1,5 +1,6 @@ package nebulosa.api.sequencer +import nebulosa.indi.device.camera.Camera import org.springframework.stereotype.Service import java.nio.file.Path import kotlin.io.path.exists @@ -12,18 +13,17 @@ class SequencerService( ) { @Synchronized - fun startSequencer(request: SequencePlanRequest) { - if (sequencerExecutor.isRunning()) return - + fun start(camera: Camera, request: SequencePlanRequest): String { val savePath = request.savePath ?.takeIf { "$it".isNotBlank() && it.exists() && it.isDirectory() } ?: Path.of("$sequencesPath", (System.currentTimeMillis() / 1000).toString()) - sequencerExecutor - .execute(request.copy(savePath = savePath)) + return sequencerExecutor + .execute(camera, request.copy(savePath = savePath)) } - fun stopSequencer() { - sequencerExecutor.stop() + @Synchronized + fun stop(camera: Camera) { + sequencerExecutor.stop(camera) } } diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt index deb7ab00b..22daca0b9 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt @@ -14,4 +14,10 @@ data class PlateSolverOptions( val apiUrl: String = "", val apiKey: String = "", @field:DurationMin(seconds = 0) @field:DurationMax(minutes = 5) @field:DurationUnit(ChronoUnit.SECONDS) val timeout: Duration = Duration.ZERO, -) +) { + + companion object { + + @JvmStatic val EMPTY = PlateSolverOptions() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt index 3384ac90e..89e0ba376 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt @@ -7,6 +7,7 @@ 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 @@ -26,24 +27,26 @@ 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, - ) = with(options) { - val plateSolver = 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) - } - } - - plateSolver - .solve(path, null, centerRA, centerDEC, radius, 1, options.timeout.takeIf { it.toSeconds() > 0 }) - } + ) = solverFor(options) + .solve(path, null, centerRA, centerDEC, radius, 1, options.timeout.takeIf { it.toSeconds() > 0 }) companion object { diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/WheelDeserializer.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt similarity index 85% rename from api/src/main/kotlin/nebulosa/api/beans/converters/indi/WheelDeserializer.kt rename to api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt index ab626b6af..35809545c 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/indi/WheelDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt @@ -1,5 +1,6 @@ -package nebulosa.api.beans.converters.indi +package nebulosa.api.wheels +import nebulosa.api.beans.converters.indi.DeviceDeserializer import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.filterwheel.FilterWheel import org.springframework.beans.factory.annotation.Autowired diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt index 35c8da51c..31510ba08 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt @@ -11,6 +11,8 @@ class WheelSerializer : StdSerializer(FilterWheel::class.java) { override fun serialize(value: FilterWheel, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("sender", value.sender.id) + gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) gen.writeBooleanField("connected", value.connected) gen.writeNumberField("count", value.count) diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt index 7a54d2f62..b82576114 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelService.kt @@ -18,7 +18,7 @@ class WheelService { wheel.moveTo(steps) } - fun sync(wheel: FilterWheel, filterNames: List) { - wheel.syncNames(filterNames) + fun sync(wheel: FilterWheel, names: List) { + wheel.names(names) } } diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelStep.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelStep.kt index 59aed5181..caeefa4aa 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelStep.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelStep.kt @@ -3,7 +3,7 @@ package nebulosa.api.wheels import nebulosa.batch.processing.Step import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult -import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelEvent import nebulosa.indi.device.filterwheel.FilterWheelMoveFailed diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardElapsed.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardElapsed.kt index 948fdd336..f60a71466 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardElapsed.kt @@ -1,13 +1,22 @@ package nebulosa.api.wizard.flat -import nebulosa.api.cameras.CameraCaptureEvent +import nebulosa.api.cameras.CameraCaptureElapsed import nebulosa.api.messages.MessageEvent +import java.nio.file.Path import java.time.Duration -data class FlatWizardElapsed( - val exposureTime: Duration, - val capture: CameraCaptureEvent, -) : MessageEvent { +sealed interface FlatWizardElapsed : MessageEvent { - override val eventName = "FLAT_WIZARD.ELAPSED" + val state: FlatWizardState + + val exposureTime: Duration + + val capture: CameraCaptureElapsed? + + val savedPath: Path? + + val message: String + + override val eventName + get() = "FLAT_WIZARD.ELAPSED" } 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 d0db460ec..e79c4038b 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt @@ -1,63 +1,33 @@ package nebulosa.api.wizard.flat -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService -import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobExecutor import nebulosa.batch.processing.JobLauncher import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.log.debug +import nebulosa.log.info import nebulosa.log.loggerFor import org.springframework.stereotype.Component -import java.util.* @Component class FlatWizardExecutor( private val messageService: MessageService, - private val jobLauncher: JobLauncher, -) : Consumer { - - private val jobExecutions = LinkedList() - - fun execute(request: FlatWizardRequest) { - val camera = requireNotNull(request.captureRequest.camera) + override val jobLauncher: JobLauncher, +) : JobExecutor() { + fun execute(camera: Camera, request: FlatWizardRequest): String { check(camera.connected) { "camera is not connected" } - check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } - - LOG.debug { "starting flat wizard capture. request=$request" } - - val flatWizardJob = FlatWizardJob(request) - flatWizardJob.subscribe(this) - jobExecutions.add(jobLauncher.launch(flatWizardJob)) - } + check(findJobExecutionWithAny(camera) == null) { "job is already running for camera: [${camera.name}]" } - fun findJobExecution(camera: Camera): JobExecution? { - for (i in jobExecutions.indices.reversed()) { - val jobExecution = jobExecutions[i] - val job = jobExecution.job as FlatWizardJob + LOG.info { "starting flat wizard capture. camera=$camera, request=$request" } - if (!jobExecution.isDone && job.camera === camera) { - return jobExecution - } - } - - return null + val flatWizardJob = FlatWizardJob(camera, request) + flatWizardJob.subscribe(messageService::sendMessage) + register(jobLauncher.launch(flatWizardJob)) + return flatWizardJob.id } - @Synchronized fun stop(camera: Camera) { - val jobExecution = findJobExecution(camera) ?: return - jobLauncher.stop(jobExecution) - } - - fun isCapturing(camera: Camera, wheel: FilterWheel? = null): Boolean { - return findJobExecution(camera) != null - } - - override fun accept(event: MessageEvent) { - messageService.sendMessage(event) + findJobExecutionWithAny(camera)?.also { jobLauncher.stop(it) } } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFailed.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFailed.kt index 5f1afc8f3..6a862e6df 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFailed.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFailed.kt @@ -1,8 +1,12 @@ package nebulosa.api.wizard.flat -import nebulosa.api.messages.MessageEvent +import java.time.Duration -object FlatWizardFailed : MessageEvent { +data object FlatWizardFailed : FlatWizardElapsed { - override val eventName = "FLAT_WIZARD.FAILED" + override val state = FlatWizardState.FAILED + override val exposureTime: Duration = Duration.ZERO + override val capture = null + override val savedPath = null + override val message = "" } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFrameCaptured.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFrameCaptured.kt index 96b030659..565df85f6 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFrameCaptured.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardFrameCaptured.kt @@ -1,13 +1,14 @@ package nebulosa.api.wizard.flat -import nebulosa.api.messages.MessageEvent import java.nio.file.Path import java.time.Duration data class FlatWizardFrameCaptured( - val savedPath: Path, - val exposureTime: Duration, -) : MessageEvent { + override val exposureTime: Duration, + override val savedPath: Path, +) : FlatWizardElapsed { - override val eventName = "FLAT_WIZARD.FRAME_CAPTURED" + override val state = FlatWizardState.CAPTURED + override val capture = null + override val message = "" } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardIsExposuring.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardIsExposuring.kt new file mode 100644 index 000000000..a412c1289 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardIsExposuring.kt @@ -0,0 +1,14 @@ +package nebulosa.api.wizard.flat + +import nebulosa.api.cameras.CameraCaptureElapsed +import java.time.Duration + +data class FlatWizardIsExposuring( + override val exposureTime: Duration, + override val capture: CameraCaptureElapsed, +) : FlatWizardElapsed { + + override val state = FlatWizardState.EXPOSURING + override val savedPath = null + override val message = "" +} 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 b6b04f472..100b6c041 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt @@ -1,33 +1,36 @@ package nebulosa.api.wizard.flat import io.reactivex.rxjava3.subjects.PublishSubject -import nebulosa.api.cameras.CameraCaptureEvent +import nebulosa.api.cameras.CameraCaptureElapsed import nebulosa.api.cameras.CameraCaptureEventHandler import nebulosa.api.cameras.CameraExposureFinished import nebulosa.api.messages.MessageEvent import nebulosa.batch.processing.PublishSubscribe import nebulosa.batch.processing.SimpleJob +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.filterwheel.FilterWheel import java.nio.file.Path import java.time.Duration -data class FlatWizardJob(@JvmField val request: FlatWizardRequest) : SimpleJob(), PublishSubscribe, FlatWizardExecutionListener { - - @JvmField val camera = request.captureRequest.camera - @JvmField val wheel = request.captureRequest.wheel +data class FlatWizardJob( + @JvmField val camera: Camera, + @JvmField val request: FlatWizardRequest, + @JvmField val wheel: FilterWheel? = null, +) : SimpleJob(), PublishSubscribe, FlatWizardExecutionListener { private val cameraCaptureEventHandler = CameraCaptureEventHandler(this) - private val step = FlatWizardStep(request) + private val step = FlatWizardStep(camera, request) override val subject = PublishSubject.create() init { step.registerCameraCaptureListener(cameraCaptureEventHandler) step.registerFlatWizardExecutionListener(this) - add(step) + register(step) } override fun onFlatCaptured(step: FlatWizardStep, savedPath: Path, duration: Duration) { - super.onNext(FlatWizardFrameCaptured(savedPath, duration)) + super.onNext(FlatWizardFrameCaptured(duration, savedPath)) } override fun onFlatFailed(step: FlatWizardStep) { @@ -35,12 +38,17 @@ data class FlatWizardJob(@JvmField val request: FlatWizardRequest) : SimpleJob() } override fun onNext(event: MessageEvent) { - if (event is CameraCaptureEvent) { - super.onNext(FlatWizardElapsed(step.exposureTime, event)) - } + if (event is CameraCaptureElapsed) { + super.onNext(FlatWizardIsExposuring(step.exposureTime, event)) - if (event is CameraExposureFinished) { - super.onNext(event) + // Notify Camera window to retrieve new image. + if (event is CameraExposureFinished) { + super.onNext(event) + } } } + + override fun contains(data: Any): Boolean { + return data === camera || data === wheel || super.contains(data) + } } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardService.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardService.kt index ec0ec35be..a175a48d1 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardService.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardService.kt @@ -18,7 +18,8 @@ class FlatWizardService( ?.takeIf { "$it".isNotBlank() && it.exists() && it.isDirectory() } ?: Path.of("$capturesPath", camera.name, "FLAT") - flatWizardExecutor.execute(request.copy(captureRequest = request.captureRequest.copy(camera = camera, savePath = savePath))) + flatWizardExecutor + .execute(camera, request.copy(captureRequest = request.captureRequest.copy(savePath = savePath))) } @Synchronized diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardState.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardState.kt new file mode 100644 index 000000000..06bc8ec4a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardState.kt @@ -0,0 +1,7 @@ +package nebulosa.api.wizard.flat + +enum class FlatWizardState { + EXPOSURING, + CAPTURED, + FAILED, +} diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardStep.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardStep.kt index 2f9215ba0..c57d04d34 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardStep.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardStep.kt @@ -7,9 +7,10 @@ import nebulosa.api.cameras.CameraExposureStep.Companion.makeSavePath import nebulosa.batch.processing.Step import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult -import nebulosa.fits.Fits +import nebulosa.fits.fits import nebulosa.imaging.Image import nebulosa.imaging.algorithms.computation.Statistics +import nebulosa.indi.device.camera.Camera import nebulosa.io.transferAndClose import org.slf4j.LoggerFactory import java.time.Duration @@ -19,6 +20,7 @@ import kotlin.io.path.inputStream import kotlin.io.path.outputStream data class FlatWizardStep( + @JvmField val camera: Camera, @JvmField val request: FlatWizardRequest, ) : Step { @@ -57,6 +59,8 @@ data class FlatWizardStep( } override fun execute(stepExecution: StepExecution): StepResult { + if (stopped) return StepResult.FINISHED + val delta = exposureMax.toMillis() - exposureMin.toMillis() if (delta < 10) { @@ -68,6 +72,7 @@ data class FlatWizardStep( exposureTime = (exposureMax + exposureMin).dividedBy(2L) val cameraExposureStep = CameraExposureStep( + camera, request.captureRequest.copy( exposureTime = exposureTime, exposureAmount = 1, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF, @@ -76,22 +81,18 @@ data class FlatWizardStep( this.cameraExposureStep.set(cameraExposureStep) cameraCaptureListeners.forEach(cameraExposureStep::registerCameraCaptureListener) - cameraExposureStep.beforeJob(stepExecution.jobExecution) - cameraExposureStep.execute(stepExecution) - cameraExposureStep.afterJob(stepExecution.jobExecution) + cameraExposureStep.executeSingle(stepExecution) val savedPath = cameraExposureStep.savedPath if (!stopped && savedPath != null) { - image = Fits(savedPath).also(Fits::read).use { fits -> - image?.load(fits, false) ?: Image.open(fits, false) - } + image = savedPath.fits().use { image?.load(it, false) ?: Image.open(it, false) } val statistics = STATISTICS.compute(image!!) LOG.info("flat frame captured. duration={}, statistics={}", exposureTime, statistics) if (statistics.mean in meanRange) { - val path = request.captureRequest.makeSavePath(true) + val path = request.captureRequest.makeSavePath(camera, true) savedPath.inputStream().transferAndClose(path.outputStream()) savedPath.deleteIfExists() LOG.info("Found an optimal exposure time. exposure={}, path={}", exposureTime, path) diff --git a/api/src/test/kotlin/SimbadDatabaseGenerator.kt b/api/src/test/kotlin/SimbadDatabaseGenerator.kt index 8f194f059..c960c968d 100644 --- a/api/src/test/kotlin/SimbadDatabaseGenerator.kt +++ b/api/src/test/kotlin/SimbadDatabaseGenerator.kt @@ -18,10 +18,7 @@ import okio.source import org.slf4j.LoggerFactory import java.io.InputStreamReader import java.nio.file.Path -import java.util.concurrent.Callable -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit +import java.util.concurrent.* import kotlin.io.path.createDirectories import kotlin.io.path.deleteRecursively import kotlin.math.min @@ -47,25 +44,31 @@ object SimbadDatabaseGenerator { .commentCharacter('#') .commentStrategy(CommentStrategy.SKIP) - @JvmStatic private val CALDWELL = resource("caldwell.csv")!! + @JvmStatic private val MELOTTE = resource("MELOTTE.csv")!! + .use { stream -> + CSV_READER.ofCsvRecord(InputStreamReader(stream, Charsets.UTF_8)) + .associate { it.getField(1) to it.getField(0) } + } + + @JvmStatic private val CALDWELL = resource("CALDWELL.csv")!! .use { stream -> CSV_READER.ofCsvRecord(InputStreamReader(stream, Charsets.UTF_8)) .associate { it.getField(1).ifEmpty { it.getField(2) } to it.getField(0) } } - @JvmStatic private val BENNETT = resource("bennett.csv")!! + @JvmStatic private val BENNETT = resource("BENNETT.csv")!! .use { stream -> CSV_READER.ofCsvRecord(InputStreamReader(stream, Charsets.UTF_8)) .associate { it.getField(1) to it.getField(0) } } - @JvmStatic private val DUNLOP = resource("dunlop.csv")!! + @JvmStatic private val DUNLOP = resource("DUNLOP.csv")!! .use { stream -> CSV_READER.ofCsvRecord(InputStreamReader(stream, Charsets.UTF_8)) .associate { it.getField(1) to it.getField(0) } } - @JvmStatic private val HERSHEL = resource("hershel.csv")!! + @JvmStatic private val HERSHEL = resource("HERSHEL.csv")!! .use { stream -> CSV_READER.ofCsvRecord(InputStreamReader(stream, Charsets.UTF_8)) .associate { it.getField(1) to it.getField(0) } @@ -96,7 +99,8 @@ object SimbadDatabaseGenerator { @JvmStatic private val MAG_H = FLUX_TABLE.column("H") @JvmStatic private val MAG_K = FLUX_TABLE.column("K") - @JvmStatic private val STELLARIUM_NAMES = Nebula.namesFor(Path.of("data", "names.dat").source()).toMutableList().also { it.reverse() } + @JvmStatic private val STELLARIUM_NAMES = Path.of("data", "names.dat").source().use(Nebula::namesFor).toMutableList() + @JvmStatic private val ENTITY_IDS = ConcurrentHashMap.newKeySet(64000) @JvmStatic fun SimbadEntity.generateNames(): Boolean { @@ -128,6 +132,9 @@ object SimbadDatabaseGenerator { } for (name in names) { + if (name in MELOTTE) { + moreNames.add("Mel ${MELOTTE[name]}") + } if (name in CALDWELL) { moreNames.add("C ${CALDWELL[name]}") } @@ -145,14 +152,14 @@ object SimbadDatabaseGenerator { name = buildString { var i = 0 - moreNames.forEach { + names.forEach { if (i > 0) append(SkyObject.NAME_SEPARATOR) append(it) i++ } - names.forEach { n -> - if (moreNames.none { it.equals(n, true) }) { + moreNames.forEach { n -> + if (names.none { it.equals(n, true) }) { if (i > 0) append(SkyObject.NAME_SEPARATOR) append(n) i++ @@ -265,6 +272,7 @@ object SimbadDatabaseGenerator { if (entity.generateNames()) { entities.add(entity) writeCount++ + ENTITY_IDS.add(entity.id) } } @@ -321,6 +329,7 @@ object SimbadDatabaseGenerator { try { val rows = SIMBAD_SERVICE.query(query).execute().body().takeIf { !it.isNullOrEmpty() } ?: return entities ids = LongArray(rows.size) { rows[it].getField("oidref").toLong() } + ids = ids.filter { it !in ENTITY_IDS }.toLongArray() break } catch (e: Throwable) { log.error("Failed to retrieve IDs. attempt=${attempt++}, query=$query", e) @@ -329,7 +338,10 @@ object SimbadDatabaseGenerator { } } - if (ids.isEmpty()) break + if (ids.isEmpty()) { + log.info("no IDs") + break + } lastID = ids.last() diff --git a/api/src/test/resources/bennett.csv b/api/src/test/resources/BENNETT.csv similarity index 100% rename from api/src/test/resources/bennett.csv rename to api/src/test/resources/BENNETT.csv diff --git a/api/src/test/resources/caldwell.csv b/api/src/test/resources/CALDWELL.csv similarity index 100% rename from api/src/test/resources/caldwell.csv rename to api/src/test/resources/CALDWELL.csv diff --git a/api/src/test/resources/dunlop.csv b/api/src/test/resources/DUNLOP.csv similarity index 100% rename from api/src/test/resources/dunlop.csv rename to api/src/test/resources/DUNLOP.csv diff --git a/api/src/test/resources/hershel.csv b/api/src/test/resources/HERSHEL.csv similarity index 100% rename from api/src/test/resources/hershel.csv rename to api/src/test/resources/HERSHEL.csv diff --git a/api/src/test/resources/MELOTTE.csv b/api/src/test/resources/MELOTTE.csv new file mode 100644 index 000000000..54423f7c2 --- /dev/null +++ b/api/src/test/resources/MELOTTE.csv @@ -0,0 +1,247 @@ +# Source: https://en.wikipedia.org/wiki/Melotte_catalogue +ID,NGC +1,NGC 104 +2,NGC 188 +3,NGC 288 +4,NGC 362 +5,NGC 371 +6,NGC 436 +7,NGC 457 +8,NGC 581 +9,NGC 654 +10,NGC 659 +11,NGC 663 +12,NGC 752 +13,NGC 869 +14,NGC 884 +15,IC 1805 +16,NGC 1027 +17,NGC 1039 +18,NGC 1245 +19,NGC 1261 +#20, +21,NGC 1342 +#22, +23,NGC 1528 +24,IC 361 +#25, +26,NGC 1647 +27,NGC 1664 +28,NGC 1746 +29,NGC 1807 +30,NGC 1851 +#31, +32,NGC 1857 +33,NGC 1893 +34,NGC 1904 +35,NGC 1907 +36,NGC 1912 +37,NGC 1960 +38,NGC 2099 +39,NGC 2126 +40,NGC 2158 +41,NGC 2168 +42,NGC 2192 +43,NGC 2194 +44,NGC 2204 +45,NGC 2215 +46,NGC 2243 +47,NGC 2244 +48,NGC 2259 +49,NGC 2264 +50,NGC 2266 +51,NGC 2281 +52,NGC 2287 +53,NGC 2298 +54,NGC 2301 +55,NGC 2304 +56,NGC 2309 +57,NGC 2314 +58,NGC 2323 +59,NGC 2324 +60,NGC 2335 +61,NGC 2345 +62,NGC 2353 +63,NGC 2355 +64,NGC 2360 +65,NGC 2362 +#66, +67,NGC 2421 +68,NGC 2422 +69,NGC 2420 +70,NGC 2423 +#71, +#72, +73,NGC 2432 +74,NGC 2439 +75,NGC 2437 +76,NGC 2447 +77,NGC 2455 +78,NGC 2477 +79,NGC 2489 +80,NGC 2506 +81,NGC 2509 +82,NGC 2516 +83,NGC 2539 +84,NGC 2547 +85,NGC 2548 +86,NGC 2567 +87,NGC 2627 +88,NGC 2632 +89,NGC 2635 +90,NGC 2658 +91,NGC 2659 +92,NGC 2660 +93,NGC 2670 +94,NGC 2682 +95,NGC 2808 +96,NGC 2818 +97,IC 2488 +98,NGC 3114 +99,NGC 3201 +100,NGC 3293 +#101, +102,IC 2602 +103,NGC 3532 +104,IC 2714 +#105, +106,NGC 3680 +107,NGC 3766 +108,NGC 3960 +109,NGC 4103 +110,NGC 4349 +#111, +112,NGC 4372 +113,NGC 4590 +114,NGC 4755 +115,NGC 4833 +116,NGC 4852 +117,NGC 5024 +118,NGC 5139 +119,NGC 5272 +120,NGC 5281 +121,NGC 5286 +122,NGC 5316 +123,NGC 5460 +124,NGC 5466 +125,NGC 5617 +126,NGC 5634 +127,NGC 5662 +128,NGC 5715 +129,IC 4499 +130,NGC 5822 +131,NGC 5823 +132,NGC 5897 +133,NGC 5904 +134,NGC 5927 +135,NGC 5946 +136,NGC 5986 +137,NGC 5999 +138,NGC 6005 +139,NGC 6025 +140,NGC 6067 +141,NGC 6087 +142,NGC 6093 +143,NGC 6101 +144,NGC 6121 +145,NGC 6124 +146,NGC 6134 +147,NGC 6144 +148,NGC 6171 +149,NGC 6192 +150,NGC 6205 +151,NGC 6218 +152,NGC 6222 +153,NGC 6231 +154,NGC 6235 +155,NGC 6242 +156,NGC 6253 +157,NGC 6254 +158,NGC 6259 +159,NGC 6266 +160,NGC 6273 +161,NGC 6281 +162,NGC 6284 +163,NGC 6287 +164,NGC 6293 +165,NGC 6304 +166,NGC 6318 +167,NGC 6333 +168,NGC 6341 +169,IC 4651 +170,NGC 6352 +171,NGC 6356 +172,NGC 6362 +173,NGC 6366 +174,NGC 6388 +175,NGC 6402 +176,NGC 6397 +177,NGC 6400 +178,NGC 6405 +179,IC 4665 +180,NGC 6441 +181,NGC 6451 +182,NGC 6469 +183,NGC 6475 +184,NGC 6494 +185,NGC 6496 +#186, +187,NGC 6520 +188,NGC 6531 +189,NGC 6535 +190,NGC 6539 +191,NGC 6541 +192,NGC 6544 +193,NGC 6553 +194,NGC 6558 +195,NGC 6569 +196,NGC 6584 +197,NGC 6603 +198,NGC 6611 +199,NGC 6624 +200,NGC 6626 +201,NGC 6633 +202,NGC 6637 +203,NGC 6642 +204,IC 4725 +205,NGC 6645 +206,NGC 6649 +207,NGC 6652 +208,NGC 6656 +209,NGC 6664 +210,IC 4756 +211,NGC 6681 +212,NGC 6694 +213,NGC 6705 +214,NGC 6709 +215,NGC 6712 +216,NGC 6715 +217,NGC 6723 +218,NGC 6752 +219,NGC 6760 +220,NGC 6779 +221,NGC 6809 +222,NGC 6811 +223,NGC 6819 +224,NGC 6830 +225,NGC 6834 +226,NGC 6838 +#227, +228,NGC 6864 +229,NGC 6866 +230,NGC 6934 +231,NGC 6939 +232,NGC 6940 +233,NGC 6981 +234,NGC 7078 +235,NGC 7089 +236,NGC 7092 +237,NGC 7099 +238,NGC 7209 +239,IC 1434 +240,NGC 7243 +241,NGC 7245 +242,NGC 7492 +243,NGC 7654 +244,NGC 7762 +245,NGC 7789 diff --git a/build.gradle.kts b/build.gradle.kts index 2aaef195b..1074c3d77 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,10 +5,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0-Beta3") - classpath("org.jetbrains.kotlin:kotlin-allopen:2.0.0-Beta3") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0-Beta4") + classpath("org.jetbrains.kotlin:kotlin-allopen:2.0.0-Beta4") classpath("com.adarshr:gradle-test-logger-plugin:4.0.0") - classpath("io.objectbox:objectbox-gradle-plugin:3.7.1") + classpath("io.objectbox:objectbox-gradle-plugin:3.8.0") } repositories { diff --git a/desktop/README.md b/desktop/README.md index 113adf9fb..054c3dc07 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -40,7 +40,7 @@ The complete integrated solution for all of your astronomical imaging needs. ## Alignment -![](alignment.darv.png) +![](alignment.png) ## Flat Wizard @@ -54,6 +54,10 @@ The complete integrated solution for all of your astronomical imaging needs. ![](indi.png) +## Calculator + +![](calculator.png) + ## Settings ![](settings.png) diff --git a/desktop/alignment.darv.png b/desktop/alignment.darv.png deleted file mode 100644 index ad58ed722..000000000 Binary files a/desktop/alignment.darv.png and /dev/null differ diff --git a/desktop/alignment.png b/desktop/alignment.png new file mode 100644 index 000000000..02e17291f Binary files /dev/null and b/desktop/alignment.png differ diff --git a/desktop/app/main.ts b/desktop/app/main.ts index 610fe5ba7..4561e7691 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -16,7 +16,7 @@ const browserWindows = new Map() const modalWindows = new Map void }>() let api: ChildProcessWithoutNullStreams | null = null let apiPort = 7000 -let wsClient: Client +let webSocket: Client const args = process.argv.slice(1) const serve = args.some(e => e === '--serve') @@ -29,10 +29,10 @@ function createMainWindow() { createWindow({ id: 'home', path: 'home', data: undefined }) - wsClient = new Client({ + webSocket = new Client({ brokerURL: `ws://localhost:${apiPort}/ws`, onConnect: () => { - wsClient.subscribe('NEBULOSA.EVENT', message => { + webSocket.subscribe('NEBULOSA.EVENT', message => { const event = JSON.parse(message.body) as MessageEvent if (event.eventName) { @@ -59,7 +59,7 @@ function createMainWindow() { }, }) - wsClient.activate() + webSocket.activate() } function createWindow(options: OpenWindow, parent?: BrowserWindow) { @@ -74,13 +74,13 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { return window } - const size = screen.getPrimaryDisplay().workAreaSize + const screenSize = screen.getPrimaryDisplay().workAreaSize function computeWidth(value: number | string) { if (typeof value === 'number') { return value } else if (value.endsWith('%')) { - return parseFloat(value.substring(0, value.length - 1)) * size.width / 100 + return parseFloat(value.substring(0, value.length - 1)) * screenSize.width / 100 } else { return parseFloat(value) } @@ -92,7 +92,7 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { if (typeof value === 'number') { return value } else if (value.endsWith('%')) { - return parseFloat(value.substring(0, value.length - 1)) * size.height / 100 + return parseFloat(value.substring(0, value.length - 1)) * screenSize.height / 100 } else if (value.endsWith('w')) { return parseFloat(value.substring(0, value.length - 1)) * width } else { @@ -100,17 +100,23 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { } } - const height = options.height ? Math.trunc(computeHeight(options.height)) : 420 + const height = options.height ? Math.trunc(computeHeight(options.height)) : 416 const resizable = options.resizable ?? false const icon = options.icon ?? 'nebulosa' const data = encodeURIComponent(JSON.stringify(options.data || {})) - const position = !options.modal ? store.get(`window.${options.id}.position`, undefined) as { x: number, y: number } | undefined : undefined + const savedPos = !options.modal ? store.get(`window.${options.id}.position`, undefined) as { x: number, y: number } | undefined : undefined + const savedSize = !options.modal && options.resizable ? store.get(`window.${options.id}.size`, undefined) as { width: number, height: number } | undefined : undefined - if (position) { - position.x = Math.max(0, Math.min(position.x, size.width)) - position.y = Math.max(0, Math.min(position.y, size.height)) + if (savedPos) { + savedPos.x = Math.max(0, Math.min(savedPos.x, screenSize.width)) + savedPos.y = Math.max(0, Math.min(savedPos.y, screenSize.height)) + } + + if (savedSize) { + savedSize.width = Math.max(0, Math.min(savedSize.width, screenSize.width)) + savedSize.height = Math.max(0, Math.min(savedSize.height, screenSize.height)) } window = new BrowserWindow({ @@ -118,9 +124,10 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { frame: false, modal: options.modal, parent, - width, height, - x: position?.x ?? undefined, - y: position?.y ?? undefined, + width: savedSize?.width || width, + height: savedSize?.height || height, + x: savedPos?.x ?? undefined, + y: savedPos?.y ?? undefined, resizable: serve || resizable, autoHideMenuBar: true, icon: path.join(__dirname, serve ? `../src/assets/icons/${icon}.png` : `assets/icons/${icon}.png`), @@ -128,13 +135,13 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { nodeIntegration: true, allowRunningInsecureContent: serve, contextIsolation: false, - additionalArguments: [`--port=${apiPort}`, `--id=${options.id}`, `--modal=${options.modal ?? false}`], + additionalArguments: [`--port=${apiPort}`, `--options=${Buffer.from(JSON.stringify(options)).toString('base64')}`], preload: path.join(__dirname, 'preload.js'), devTools: serve, }, }) - if (!position) { + if (!savedPos) { window.center() } @@ -161,6 +168,15 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { } }) + if (!serve && window.isResizable() && options.autoResizable !== false) { + window.on('resized', () => { + if (window) { + const [width, height] = window.getSize() + store.set(`window.${options.id}.size`, { width, height }) + } + }) + } + window.on('close', () => { const homeWindow = browserWindows.get('home') @@ -413,6 +429,20 @@ try { return window?.isMaximized() ?? false }) + ipcMain.handle('WINDOW.RESIZE', (event, data: number) => { + const window = findWindowById(event.sender.id)?.window + + if (!window || (!serve && window.isResizable())) return false + + const size = window.getSize() + const maxHeight = screen.getPrimaryDisplay().workAreaSize.height + const height = Math.max(0, Math.min(data, maxHeight)) + window.setSize(size[0], height) + console.info('window resized:', size[0], height) + + return true + }) + ipcMain.handle('WINDOW.CLOSE', (event, data: CloseWindow) => { if (data.id) { for (const [key, value] of browserWindows) { diff --git a/desktop/app/preload.js b/desktop/app/preload.js index 6a80a8492..290851dd0 100644 --- a/desktop/app/preload.js +++ b/desktop/app/preload.js @@ -4,4 +4,4 @@ function argWith(name) { window.apiPort = parseInt(argWith('port')) window.id = argWith('id') -window.modal = argWith('modal') === 'true' +window.options = JSON.parse(Buffer.from(argWith('options'), 'base64').toString('utf-8')) diff --git a/desktop/calculator.png b/desktop/calculator.png new file mode 100644 index 000000000..306e4aba1 Binary files /dev/null and b/desktop/calculator.png differ diff --git a/desktop/camera.png b/desktop/camera.png index 19e2ab0f2..dda83d68e 100644 Binary files a/desktop/camera.png and b/desktop/camera.png differ diff --git a/desktop/filter-wheel.png b/desktop/filter-wheel.png index 4b1f8c4c0..0918caf9a 100644 Binary files a/desktop/filter-wheel.png and b/desktop/filter-wheel.png differ diff --git a/desktop/flat-wizard.png b/desktop/flat-wizard.png index bb6261704..cba1563d6 100644 Binary files a/desktop/flat-wizard.png and b/desktop/flat-wizard.png differ diff --git a/desktop/focuser.png b/desktop/focuser.png index 9f62829a5..98a5db553 100644 Binary files a/desktop/focuser.png and b/desktop/focuser.png differ diff --git a/desktop/framing.png b/desktop/framing.png index e0f96b053..a76aca2ff 100644 Binary files a/desktop/framing.png and b/desktop/framing.png differ diff --git a/desktop/guider.png b/desktop/guider.png index 294fa1e2e..6c2bf9829 100644 Binary files a/desktop/guider.png and b/desktop/guider.png differ diff --git a/desktop/home.png b/desktop/home.png index 1ecd054fd..62ecbd8b8 100644 Binary files a/desktop/home.png and b/desktop/home.png differ diff --git a/desktop/mount.png b/desktop/mount.png index 3e749a068..6ed630e77 100644 Binary files a/desktop/mount.png and b/desktop/mount.png differ diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 5973cda2d..630425501 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,48 +10,49 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "17.1.2", - "@angular/cdk": "17.1.2", - "@angular/common": "17.1.2", - "@angular/compiler": "17.1.2", - "@angular/core": "17.1.2", - "@angular/forms": "17.1.2", - "@angular/language-service": "17.1.2", - "@angular/platform-browser": "17.1.2", - "@angular/platform-browser-dynamic": "17.1.2", - "@angular/router": "17.1.2", + "@angular/animations": "17.2.3", + "@angular/cdk": "17.2.1", + "@angular/common": "17.2.3", + "@angular/compiler": "17.2.3", + "@angular/core": "17.2.3", + "@angular/forms": "17.2.3", + "@angular/language-service": "17.2.3", + "@angular/platform-browser": "17.2.3", + "@angular/platform-browser-dynamic": "17.2.3", + "@angular/router": "17.2.3", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.4.47", - "chart.js": "4.4.1", + "chart.js": "4.4.2", "chartjs-plugin-zoom": "2.0.1", + "hotkeys-js": "3.13.7", "interactjs": "1.10.26", "leaflet": "1.9.4", "moment": "2.30.1", "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "17.4.0", + "primeng": "17.9.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", - "zone.js": "0.14.3" + "zone.js": "0.14.4" }, "devDependencies": { - "@angular-builders/custom-webpack": "17.0.0", - "@angular-devkit/build-angular": "17.1.2", - "@angular/cli": "17.1.2", - "@angular/compiler-cli": "17.1.2", + "@angular-builders/custom-webpack": "17.0.1", + "@angular-devkit/build-angular": "17.2.2", + "@angular/cli": "17.2.2", + "@angular/compiler-cli": "17.2.3", "@types/leaflet": "1.9.8", - "@types/node": "20.11.14", + "@types/node": "20.11.24", "@types/uuid": "9.0.8", - "electron": "28.2.1", - "electron-builder": "24.9.1", + "electron": "29.1.0", + "electron-builder": "24.12.0", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", "ts-node": "10.9.2", - "typescript": "5.2.2", + "typescript": "5.3.3", "wait-on": "7.2.0" }, "engines": { @@ -71,18 +72,31 @@ "node": ">=6.0.0" } }, + "node_modules/@angular-builders/common": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-1.0.1.tgz", + "integrity": "sha512-qPgTjz3ISdGIY+vOIiUzpZRXwchdL/HEhCRzM2QKdqz/c5AB06X9wKhvXezabtzpYSq4lN9fliPYCntqimefFw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "^17.1.0", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + } + }, "node_modules/@angular-builders/custom-webpack": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-17.0.0.tgz", - "integrity": "sha512-gKZKRzCE4cbDYyQLu1G/2CkAFbMd0oF07jMxX+jOTADzDeOy9mPOeBaFO60oWgeknrhXf31rynho55LGrHStkg==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-17.0.1.tgz", + "integrity": "sha512-wRmCy8B+/SPv10Ufy2WqDhU68UGxF6fPPGu2ZeBRqzh10axvdfyD20a4v8xITfAaraOJb/MA4qsUs0x96QQCCQ==", "dev": true, "dependencies": { + "@angular-builders/common": "1.0.1", "@angular-devkit/architect": ">=0.1700.0 < 0.1800.0", "@angular-devkit/build-angular": "^17.0.0", "@angular-devkit/core": "^17.0.0", "lodash": "^4.17.15", - "ts-node": "^10.0.0", - "tsconfig-paths": "^4.1.0", "webpack-merge": "^5.7.3" }, "engines": { @@ -93,12 +107,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1701.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1701.2.tgz", - "integrity": "sha512-g3gn5Ht6r9bCeFeAYF+HboZB8IvgvqqdeOnaWNaXJLI0ymEkpbqRdqrHGuVKHJV7JOMNXC7GPJEctBC6SXxOxA==", + "version": "0.1702.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1702.2.tgz", + "integrity": "sha512-qBvif8/NquFUqVQgs4U+8wXh/rQZv+YlYwg6eDZly1bIaTd/k9spko/seTtNT1OpK/Be+GLo5IbiQ7i2SON3iQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.1.2", + "@angular-devkit/core": "17.2.2", "rxjs": "7.8.1" }, "engines": { @@ -108,71 +122,70 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.1.2.tgz", - "integrity": "sha512-QIDTP+TjiCKCYRZYb8to4ymvIV1Djcfd5c17VdgMGhRqIQAAK1V4f4A1njdhGYOrgsLajZQAnKvFfk2ZMeI37A==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.2.2.tgz", + "integrity": "sha512-K55xBiWBfxD4wmxLR2viOPbBryOk6YaZeNr72IMkp1yIrIy1BES6LDJi7R9fDW7+TprqZdM4B91Tkc+BCwYQzQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1701.2", - "@angular-devkit/build-webpack": "0.1701.2", - "@angular-devkit/core": "17.1.2", - "@babel/core": "7.23.7", + "@angular-devkit/architect": "0.1702.2", + "@angular-devkit/build-webpack": "0.1702.2", + "@angular-devkit/core": "17.2.2", + "@babel/core": "7.23.9", "@babel/generator": "7.23.6", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.7", + "@babel/plugin-transform-async-generator-functions": "7.23.9", "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.23.7", - "@babel/preset-env": "7.23.7", - "@babel/runtime": "7.23.7", + "@babel/plugin-transform-runtime": "7.23.9", + "@babel/preset-env": "7.23.9", + "@babel/runtime": "7.23.9", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.1.2", - "@vitejs/plugin-basic-ssl": "1.0.2", + "@ngtools/webpack": "17.2.2", + "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", - "autoprefixer": "10.4.16", + "autoprefixer": "10.4.17", "babel-loader": "9.1.3", "babel-plugin-istanbul": "6.1.1", "browserslist": "^4.21.5", "copy-webpack-plugin": "11.0.0", "critters": "0.0.20", - "css-loader": "6.8.1", - "esbuild-wasm": "0.19.11", + "css-loader": "6.10.0", + "esbuild-wasm": "0.20.0", "fast-glob": "3.3.2", "http-proxy-middleware": "2.0.6", "https-proxy-agent": "7.0.2", - "inquirer": "9.2.12", - "jsonc-parser": "3.2.0", + "inquirer": "9.2.14", + "jsonc-parser": "3.2.1", "karma-source-map-support": "1.4.0", "less": "4.2.0", "less-loader": "11.1.0", "license-webpack-plugin": "4.0.2", "loader-utils": "3.2.1", - "magic-string": "0.30.5", - "mini-css-extract-plugin": "2.7.6", + "magic-string": "0.30.7", + "mini-css-extract-plugin": "2.8.0", "mrmime": "2.0.0", "open": "8.4.2", "ora": "5.4.1", "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "3.0.1", - "piscina": "4.2.1", - "postcss": "8.4.33", - "postcss-loader": "7.3.4", + "picomatch": "4.0.1", + "piscina": "4.3.1", + "postcss": "8.4.35", + "postcss-loader": "8.1.0", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.69.7", - "sass-loader": "13.3.3", - "semver": "7.5.4", + "sass": "1.70.0", + "sass-loader": "14.1.0", + "semver": "7.6.0", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.26.0", - "text-table": "0.2.0", + "terser": "5.27.0", "tree-kill": "1.2.2", "tslib": "2.6.2", - "undici": "6.2.1", + "undici": "6.6.2", "vite": "5.0.12", "watchpack": "2.4.0", - "webpack": "5.89.0", + "webpack": "5.90.1", "webpack-dev-middleware": "6.1.1", "webpack-dev-server": "4.15.1", "webpack-merge": "5.10.0", @@ -184,7 +197,7 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.19.11" + "esbuild": "0.20.0" }, "peerDependencies": { "@angular/compiler-cli": "^17.0.0", @@ -293,19 +306,19 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", + "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -319,7 +332,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -340,12 +353,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1701.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1701.2.tgz", - "integrity": "sha512-LqfSO5iTbiYByDadUET/8uIun8vSHMEdtoxiil/kdZ5T0NG0p7K8QqUMnWgg6suwO6kFfYJkMiS8Dq3Y/ONUNQ==", + "version": "0.1702.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1702.2.tgz", + "integrity": "sha512-+c7rHD2Se1VD9i9uPEYHqhq8hTnsUAn5LfeJCLS8g7FU8T42tDSC/k1qWxHp7d99kf7ecg2BvYcZDlYaBUnl3A==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1701.2", + "@angular-devkit/architect": "0.1702.2", "rxjs": "7.8.1" }, "engines": { @@ -359,15 +372,15 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.2.tgz", - "integrity": "sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.2.2.tgz", + "integrity": "sha512-bKMi6bBkEeN4a3qTxCykhrAvE0ESHhKO38Qh1bN/8QSyvKVAEyVAVls5W9IN5GKRHvXgEn9aw+DSzRnPpy9nyw==", "dev": true, "dependencies": { "ajv": "8.12.0", "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", "rxjs": "7.8.1", "source-map": "0.7.4" }, @@ -386,14 +399,14 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.1.2.tgz", - "integrity": "sha512-8S9RuM8olFN/gwN+mjbuF1CwHX61f0i59EGXz9tXLnKRUTjsRR+8vVMTAmX0dvVAT5fJTG/T69X+HX7FeumdqA==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.2.2.tgz", + "integrity": "sha512-t6dBhHvto9BEIo+Kew0+YyIS3TV1SEd4MActUk+zF4NNQyJ8wRUHL+8glUKB6ZWPyCTYSinJ+QKn/3yytELTHg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.1.2", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.5", + "@angular-devkit/core": "17.2.2", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.7", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -404,9 +417,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.1.2.tgz", - "integrity": "sha512-ZsHa/zoWBOZdispjcNgXCoF9MAtc6Zyzc/QFUjtOFI9vigOI8tWP6GY1Wfeg4cyL+R3uDGYBgMrdr8l84VfuKg==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.2.3.tgz", + "integrity": "sha512-eQcN6hC/dXISEYC/TjRuQJgfdZieBROBlXrS+BxRbsy9T4/QeKxChC3yiNxTmdxl5mvjLKvQTXHR8X0AWc07/Q==", "dependencies": { "tslib": "^2.3.0" }, @@ -414,13 +427,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.1.2" + "@angular/core": "17.2.3" } }, "node_modules/@angular/cdk": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.1.2.tgz", - "integrity": "sha512-eu9D60RQv213qi7oh6ae9Z+d6+AG/aqi0y70Ag9BjwqTiatDiYvSySxswxYYKdzPp0hx0ZUTGi16LqtT6pyj6Q==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.2.1.tgz", + "integrity": "sha512-9cWV9MyWnpImns/WQApgoQBKblXA9Zx2CpCkDNipRgx9RyvGrvCLjpEfwQI4HjpPAQDI1trsbeJKihzgz4tFgw==", "dependencies": { "tslib": "^2.3.0" }, @@ -434,27 +447,27 @@ } }, "node_modules/@angular/cli": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.1.2.tgz", - "integrity": "sha512-U1W6XZNrfeRkXW2fO3AU25rRttqZahVkhzcK3lAtJ8+lSrStCOF7x1gz6tmFZFte1fNHQrXqD0yIDkd8H2/cvw==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.2.2.tgz", + "integrity": "sha512-cGGOnOTjU1bHBAU+5LMR1vfjUSmIY204pUcRAHu6xq1Qp8jm0Wf1lYOG1KrzpDezKa8d0WZe6FIVlxsCZRRYSw==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1701.2", - "@angular-devkit/core": "17.1.2", - "@angular-devkit/schematics": "17.1.2", - "@schematics/angular": "17.1.2", + "@angular-devkit/architect": "0.1702.2", + "@angular-devkit/core": "17.2.2", + "@angular-devkit/schematics": "17.2.2", + "@schematics/angular": "17.2.2", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", - "inquirer": "9.2.12", - "jsonc-parser": "3.2.0", + "inquirer": "9.2.14", + "jsonc-parser": "3.2.1", "npm-package-arg": "11.0.1", "npm-pick-manifest": "9.0.0", "open": "8.4.2", "ora": "5.4.1", - "pacote": "17.0.5", + "pacote": "17.0.6", "resolve": "1.22.8", - "semver": "7.5.4", + "semver": "7.6.0", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -468,9 +481,9 @@ } }, "node_modules/@angular/common": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.1.2.tgz", - "integrity": "sha512-y/wD+zuPaPgK3dB80Q63qBtuu5TuryKuUgjWrOmrguBWV9oiJRhKQrcp1gVw9vVrowmbDBKGtPMS622Q4oxOWQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.2.3.tgz", + "integrity": "sha512-XR3rWS4W7/+RknyJMUUo9E81mSeyUznpclqTZ+Hy7+i4Naeso0qcRaIyr6JJmB5UGvlnfT1MlH9Fj78Dc80NEw==", "dependencies": { "tslib": "^2.3.0" }, @@ -478,14 +491,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.1.2", + "@angular/core": "17.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.1.2.tgz", - "integrity": "sha512-1vJuQRM5V01nC6qsLvBKrHVZXpzbK0YKubwVQUXCSfDNZBcDFak3SQcwU4C2t880rU3ZvFDB1UWfk7CKn5w9Kw==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.2.3.tgz", + "integrity": "sha512-U2okLZ+4ipD5zTv32pMp+RsrM3kkP0XneSsIMPRpYZZfKgfnGLIwkRx6FoVoBwByugng6lBG/PiIe8DhRU/HFg==", "dependencies": { "tslib": "^2.3.0" }, @@ -493,7 +506,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.1.2" + "@angular/core": "17.2.3" }, "peerDependenciesMeta": { "@angular/core": { @@ -502,16 +515,16 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.1.2.tgz", - "integrity": "sha512-4P4ttCe4IF9yq7bxCDxbVW7purN7qV0nqofP5Tth1xCsgIJeGmOMMQJN5RJCZNrAPMkvMv39eV878sgcDjbpOA==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.2.3.tgz", + "integrity": "sha512-mATybangypneXwO270VQeIw3N0avzc2Lpvdb8nm9WZYj23AcTUzpUUKOn63HtJdwMT5J2GjkyZFSRXisiPmpkA==", "dev": true, "dependencies": { - "@babel/core": "7.23.2", + "@babel/core": "7.23.9", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.1.2", + "reflect-metadata": "^0.2.0", "semver": "^7.0.0", "tslib": "^2.3.0", "yargs": "^17.2.1" @@ -525,59 +538,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.1.2", + "@angular/compiler": "17.2.3", "typescript": ">=5.2 <5.4" } }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@angular/core": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.1.2.tgz", - "integrity": "sha512-0M787BZVgYSVogHCUzo/dFrT56TgfQoEsOQngHMpyERJZv6dycXZlRdHc6TzvHUa+Uu/MNjn/RclBR8063bdWA==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.2.3.tgz", + "integrity": "sha512-DU+RdUB4E4I489R2P2hOrgkCDJNXlVaTzYixpgeDnuldCIYM0MatEzjor9DYNL3EDCayHF+M4HlVOcn6T/IVPQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -590,9 +558,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.1.2.tgz", - "integrity": "sha512-n1WsZAL2IVOB6ocROKR6CFOR14PIC9RGAB41SwTfPhJeBM1kjW48bXY0sw97TasxM4mWJKGCmFXu0jQwkoeSpQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.2.3.tgz", + "integrity": "sha512-v+/6pimht808F5XpmVTNV4/109s+A7m3nadQP97qvIDsrtwrPPZR7cST+DRioG2C41VwtjXM0HVbIon/3ydo6A==", "dependencies": { "tslib": "^2.3.0" }, @@ -600,24 +568,24 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.1.2", - "@angular/core": "17.1.2", - "@angular/platform-browser": "17.1.2", + "@angular/common": "17.2.3", + "@angular/core": "17.2.3", + "@angular/platform-browser": "17.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.1.2.tgz", - "integrity": "sha512-EqmbDT696a1KC04l5I4dilf86IJnj0jPxw8OXI9dlSQhsWYp8Egkc5+C0Hd7wmuHt/BeqSuMSJfk7DhfzKbx1w==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.2.3.tgz", + "integrity": "sha512-H4LUs2Ftdlk1iqHqC7jRcbHmnNRy53OUlBYNkjRkTsthOI4WqsiSqAp5Frrni3erBqpZ2ik86cbMEyEXcfjRhw==", "engines": { "node": "^18.13.0 || >=20.9.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.1.2.tgz", - "integrity": "sha512-unfpA5OLnqDmDb/oAQR2t2iROpOg02qwZayxyFg4MUZdDdnghPCfX77L2sr6oVVa7OJfKYFlmwmBXX1H3zjcXA==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.2.3.tgz", + "integrity": "sha512-bFi+H8avyCjwSBy+zpOKmqx852MRH8fkuZa4XgwKCPJRay8BfSCjHdtIo3eokUNPMu9JsyXM7HYKIfzLu5y6LA==", "dependencies": { "tslib": "^2.3.0" }, @@ -625,9 +593,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.1.2", - "@angular/common": "17.1.2", - "@angular/core": "17.1.2" + "@angular/animations": "17.2.3", + "@angular/common": "17.2.3", + "@angular/core": "17.2.3" }, "peerDependenciesMeta": { "@angular/animations": { @@ -636,9 +604,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.1.2.tgz", - "integrity": "sha512-xiWVDHbA+owDhKo5SAnzZtawA1ktGthlCl3YTI+vmkJpF6axkYOqR7YL+aEQX/y/5GSK+oR+03SgAnYcpOwKlQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.2.3.tgz", + "integrity": "sha512-K8CsHbmG2nvV1jrNN9PYxyA0zJNoIWp+qf2udvPhG8rJ+Pyw61qmptrarpQUUkr8ONOtjwtOsnKa9/w+15nExw==", "dependencies": { "tslib": "^2.3.0" }, @@ -646,16 +614,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.1.2", - "@angular/compiler": "17.1.2", - "@angular/core": "17.1.2", - "@angular/platform-browser": "17.1.2" + "@angular/common": "17.2.3", + "@angular/compiler": "17.2.3", + "@angular/core": "17.2.3", + "@angular/platform-browser": "17.2.3" } }, "node_modules/@angular/router": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.1.2.tgz", - "integrity": "sha512-8OexxiiscRdfEiB6jOKlZFyAKZtvIQvh0ugW6U7nAXPV5XsA2UL80sXkc829eH0DnJn2Wj/HS6ZNGgG81PWDHg==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.2.3.tgz", + "integrity": "sha512-8UPjMzI98xZ6cDNm0MzHd9hFq6aOQJGmgxKDUPIG2h74glRwwbiewpo5hPo2EGIF8BLvQmmAm9ytr5zesHu0cg==", "dependencies": { "tslib": "^2.3.0" }, @@ -663,18 +631,12 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.1.2", - "@angular/core": "17.1.2", - "@angular/platform-browser": "17.1.2", + "@angular/common": "17.2.3", + "@angular/core": "17.2.3", + "@angular/platform-browser": "17.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@assemblyscript/loader": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", - "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", - "dev": true - }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -698,9 +660,9 @@ } }, "node_modules/@babel/core": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", - "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -708,11 +670,11 @@ "@babel/generator": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.7", - "@babel/parser": "^7.23.6", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -807,9 +769,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz", - "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.0.tgz", + "integrity": "sha512-QAH+vfvts51BCsNZ2PhY6HAggnlS6omLLFTsIpeqZk/MmJ6cW7tgz5yRv0fMJThcr6FmbMrENh1RgrWPTYA76g==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -970,9 +932,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1090,14 +1052,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", "dev": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1118,9 +1080,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1440,9 +1402,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", - "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -1895,14 +1857,14 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.0.tgz", + "integrity": "sha512-y/yKMm7buHpFFXfxVFS4Vk1ToRJDilIa6fKRioB9Vjichv58TDGXTvqV0dN7plobAmTW5eSEGXDngE+Mm+uO+w==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.23.3" }, @@ -2058,16 +2020,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", - "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz", + "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "semver": "^6.3.1" }, "engines": { @@ -2226,9 +2188,9 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz", - "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", + "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", "dev": true, "dependencies": { "@babel/compat-data": "^7.23.5", @@ -2258,13 +2220,13 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.7", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", "@babel/plugin-transform-async-to-generator": "^7.23.3", "@babel/plugin-transform-block-scoped-functions": "^7.23.3", "@babel/plugin-transform-block-scoping": "^7.23.4", "@babel/plugin-transform-class-properties": "^7.23.3", "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-classes": "^7.23.8", "@babel/plugin-transform-computed-properties": "^7.23.3", "@babel/plugin-transform-destructuring": "^7.23.3", "@babel/plugin-transform-dotall-regex": "^7.23.3", @@ -2280,7 +2242,7 @@ "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-amd": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.23.3", @@ -2306,9 +2268,9 @@ "@babel/plugin-transform-unicode-regex": "^7.23.3", "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -2349,9 +2311,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", - "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2361,23 +2323,23 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", @@ -2386,8 +2348,8 @@ "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2396,9 +2358,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -2752,9 +2714,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", + "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", "cpu": [ "ppc64" ], @@ -2768,9 +2730,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", + "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", "cpu": [ "arm" ], @@ -2784,9 +2746,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", + "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", "cpu": [ "arm64" ], @@ -2800,9 +2762,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", + "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", "cpu": [ "x64" ], @@ -2816,9 +2778,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", + "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==", "cpu": [ "arm64" ], @@ -2832,9 +2794,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", + "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", "cpu": [ "x64" ], @@ -2848,9 +2810,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", + "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", "cpu": [ "arm64" ], @@ -2864,9 +2826,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", + "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", "cpu": [ "x64" ], @@ -2880,9 +2842,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", + "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", "cpu": [ "arm" ], @@ -2896,9 +2858,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", + "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", "cpu": [ "arm64" ], @@ -2912,9 +2874,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", + "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", "cpu": [ "ia32" ], @@ -2928,9 +2890,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", + "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", "cpu": [ "loong64" ], @@ -2944,9 +2906,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", + "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", "cpu": [ "mips64el" ], @@ -2960,9 +2922,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", + "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", "cpu": [ "ppc64" ], @@ -2976,9 +2938,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", + "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", "cpu": [ "riscv64" ], @@ -2992,9 +2954,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", + "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", "cpu": [ "s390x" ], @@ -3008,9 +2970,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", - "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", + "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", "cpu": [ "x64" ], @@ -3024,9 +2986,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", + "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", "cpu": [ "x64" ], @@ -3040,9 +3002,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", + "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", "cpu": [ "x64" ], @@ -3056,9 +3018,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", + "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", "cpu": [ "x64" ], @@ -3072,9 +3034,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", + "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", "cpu": [ "arm64" ], @@ -3088,9 +3050,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", + "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", "cpu": [ "ia32" ], @@ -3104,9 +3066,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", + "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", "cpu": [ "x64" ], @@ -3120,9 +3082,9 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true, "engines": { "node": ">=14" @@ -3275,9 +3237,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz", + "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==", "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", @@ -3289,18 +3251,18 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -3323,9 +3285,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3434,9 +3396,9 @@ "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==" }, "node_modules/@ngtools/webpack": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.1.2.tgz", - "integrity": "sha512-MdNVSIp0x8AK26L+CxMTXH4weq2sNIp4C09RSdk7y6UkfBxMA3O0jTto9tW3ehkBaaGZ4dSiWkXA8L/ydMiQmA==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.2.tgz", + "integrity": "sha512-HgvClGO6WVq4VA5d0ZvlDG5hrj8lQzRH99Gt87URm7G8E5XkatysdOsMqUQsJz+OwFWhP4PvTRWVblpBDiDl/A==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -3485,9 +3447,9 @@ } }, "node_modules/@npmcli/agent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz", - "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.1.tgz", + "integrity": "sha512-H4FrOVtNyWC8MUwL3UfjOsAihHvT1Pe8POj3JvjXhSTJipsZMtgUALCT4mGyYZNxymkUfOw3PUj6dE4QPp6osQ==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -3513,9 +3475,9 @@ } }, "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -3786,9 +3748,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", + "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", "cpu": [ "arm" ], @@ -3799,9 +3761,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", + "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", "cpu": [ "arm64" ], @@ -3812,9 +3774,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", "cpu": [ "arm64" ], @@ -3825,9 +3787,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", + "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", "cpu": [ "x64" ], @@ -3838,9 +3800,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", + "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", "cpu": [ "arm" ], @@ -3851,9 +3813,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", + "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", "cpu": [ "arm64" ], @@ -3864,9 +3826,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", + "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", "cpu": [ "arm64" ], @@ -3877,9 +3839,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", + "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", "cpu": [ "riscv64" ], @@ -3890,9 +3852,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", + "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", "cpu": [ "x64" ], @@ -3903,9 +3865,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", + "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", "cpu": [ "x64" ], @@ -3916,9 +3878,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", + "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", "cpu": [ "arm64" ], @@ -3929,9 +3891,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", + "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", "cpu": [ "ia32" ], @@ -3942,9 +3904,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", + "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", "cpu": [ "x64" ], @@ -3955,14 +3917,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.1.2.tgz", - "integrity": "sha512-1GlH0POaN7hVDF1sAm90E5SvAqnKK+PbD1oKSpug9l+1AUQ3vOamyGhEAaO+IxUqvNdgqZexxd5o9MyySTT2Zw==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.2.2.tgz", + "integrity": "sha512-Q3VAQ/S4gj8D1JPWgWG4enDdDZUu8mUXWVRG1rOi4sHgOF5zgPieQFp3LXqMUgOncmzbXrctkbO6NKc4N2FAag==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.1.2", - "@angular-devkit/schematics": "17.1.2", - "jsonc-parser": "3.2.0" + "@angular-devkit/core": "17.2.2", + "@angular-devkit/schematics": "17.2.2", + "jsonc-parser": "3.2.1" }, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -3992,44 +3954,44 @@ "dev": true }, "node_modules/@sigstore/bundle": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz", - "integrity": "sha512-v3/iS+1nufZdKQ5iAlQKcCsoh0jffQyABvYIxKsZQFWc4ubuGjwZklFHpDgV6O6T7vvV78SW5NHI91HFKEcxKg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.2.0.tgz", + "integrity": "sha512-5VI58qgNs76RDrwXNhpmyN/jKpq9evV/7f1XrcqcAfvxDl5SeVY/I5Rmfe96ULAV7/FK5dge9RBKGBJPhL1WsQ==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.2.1" + "@sigstore/protobuf-specs": "^0.3.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sigstore/core": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-0.2.0.tgz", - "integrity": "sha512-THobAPPZR9pDH2CAvDLpkrYedt7BlZnsyxDe+Isq4ZmGfPy5juOFZq487vCU2EgKD7aHSiTfE/i7sN7aEdzQnA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.0.0.tgz", + "integrity": "sha512-dW2qjbWLRKGu6MIDUTBuJwXCnR8zivcSpf5inUzk7y84zqy/dji0/uahppoIgMoKeR+6pUZucrwHfkQQtiG9Rw==", "dev": true, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz", - "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.0.tgz", + "integrity": "sha512-zxiQ66JFOjVvP9hbhGj/F/qNdsZfkGb/dVXSanNRNuAzMlr4MC95voPUBX8//ZNnmv3uSYzdfR/JSkrgvZTGxA==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@sigstore/sign": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.1.tgz", - "integrity": "sha512-U5sKQEj+faE1MsnLou1f4DQQHeFZay+V9s9768lw48J4pKykPj34rWyI1lsMOGJ3Mae47Ye6q3HAJvgXO21rkQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.3.tgz", + "integrity": "sha512-LqlA+ffyN02yC7RKszCdMTS6bldZnIodiox+IkT8B2f8oRYXCB3LQ9roXeiEL21m64CVH1wyveYAORfD65WoSw==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.1.1", - "@sigstore/core": "^0.2.0", - "@sigstore/protobuf-specs": "^0.2.1", + "@sigstore/bundle": "^2.2.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.0", "make-fetch-happen": "^13.0.0" }, "engines": { @@ -4037,12 +3999,12 @@ } }, "node_modules/@sigstore/tuf": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.0.tgz", - "integrity": "sha512-S98jo9cpJwO1mtQ+2zY7bOdcYyfVYCUaofCG6wWRzk3pxKHVAkSfshkfecto2+LKsx7Ovtqbgb2LS8zTRhxJ9Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.1.tgz", + "integrity": "sha512-9Iv40z652td/QbV0o5n/x25H9w6IYRt2pIGbTX55yFDYlApDQn/6YZomjz6+KBx69rXHLzHcbtTS586mDdFD+Q==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.2.1", + "@sigstore/protobuf-specs": "^0.3.0", "tuf-js": "^2.2.0" }, "engines": { @@ -4050,14 +4012,14 @@ } }, "node_modules/@sigstore/verify": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-0.1.0.tgz", - "integrity": "sha512-2UzMNYAa/uaz11NhvgRnIQf4gpLTJ59bhb8ESXaoSS5sxedfS+eLak8bsdMc+qpNQfITUTFoSKFx5h8umlRRiA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.1.0.tgz", + "integrity": "sha512-1fTqnqyTBWvV7cftUUFtDcHPdSox0N3Ub7C0lRyReYx4zZUlNTZjCV+HPy4Lre+r45dV7Qx5JLKvqqsgxuyYfg==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.1.1", - "@sigstore/core": "^0.2.0", - "@sigstore/protobuf-specs": "^0.2.1" + "@sigstore/bundle": "^2.2.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -4217,9 +4179,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "version": "8.56.5", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", + "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -4255,9 +4217,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.42", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz", - "integrity": "sha512-ckM3jm2bf/MfB3+spLPWYPUH573plBFwpOhqQ2WottxYV85j1HQFlxmnTq57X1yHY9awZPig06hL/cLMgNWHIQ==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dev": true, "dependencies": { "@types/node": "*", @@ -4339,9 +4301,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", - "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -4368,9 +4330,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.11", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", - "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", + "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==", "dev": true }, "node_modules/@types/range-parser": { @@ -4466,9 +4428,9 @@ } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz", - "integrity": "sha512-DKHKVtpI+eA5fvObVgQ3QtTGU70CcCnedalzqmGSR050AzKZMdUzgC8KmlOneHWH8dF2hJ3wkC9+8FDVAaDRCw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", "dev": true, "engines": { "node": ">=14.6.0" @@ -4914,9 +4876,9 @@ "dev": true }, "node_modules/app-builder-lib": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.1.tgz", - "integrity": "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.12.0.tgz", + "integrity": "sha512-t/xinVrMbsEhwljLDoFOtGkiZlaxY1aceZbHERGAS02EkUHJp9lgs/+L8okXLlYCaDSqYdB05Yb8Co+krvguXA==", "dev": true, "dependencies": { "@develar/schema-utils": "~2.6.5", @@ -4925,15 +4887,14 @@ "@electron/universal": "1.4.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", - "7zip-bin": "~5.2.0", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "24.8.1", + "builder-util": "24.9.4", "builder-util-runtime": "9.2.3", "chromium-pickle-js": "^0.2.0", "debug": "^4.3.4", "ejs": "^3.1.8", - "electron-publish": "24.8.1", + "electron-publish": "24.9.4", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", @@ -5021,13 +4982,16 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5040,17 +5004,18 @@ "dev": true }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -5142,9 +5107,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", "dev": true, "funding": [ { @@ -5161,9 +5126,9 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -5179,10 +5144,13 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -5258,29 +5226,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", - "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.4", - "core-js-compat": "^3.33.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", - "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5400,13 +5352,13 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -5414,7 +5366,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -5615,9 +5567,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -5634,8 +5586,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001580", - "electron-to-chromium": "^1.4.648", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -5704,9 +5656,9 @@ "dev": true }, "node_modules/builder-util": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz", - "integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==", + "version": "24.9.4", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.9.4.tgz", + "integrity": "sha512-YNon3rYjPSm4XDDho9wD6jq7vLRJZUy9FR+yFZnHoWvvdVCnZakL4BctTlPABP41MvIH5yk2cTZ2YfkOhGistQ==", "dev": true, "dependencies": { "@types/debug": "^4.1.6", @@ -5997,14 +5949,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6029,9 +5986,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001581", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", - "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", "dev": true, "funding": [ { @@ -6069,14 +6026,14 @@ "dev": true }, "node_modules/chart.js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", - "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", + "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", "dependencies": { "@kurkle/color": "^0.3.0" }, "engines": { - "pnpm": ">=7" + "pnpm": ">=8" } }, "node_modules/chartjs-plugin-zoom": { @@ -6091,16 +6048,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -6113,6 +6064,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -6485,19 +6439,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/config-file-ts/node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -6610,12 +6551,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", - "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", + "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", "dev": true, "dependencies": { - "browserslist": "^4.22.2" + "browserslist": "^4.22.3" }, "funding": { "type": "opencollective", @@ -6629,15 +6570,15 @@ "dev": true }, "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "dependencies": { + "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" @@ -6853,19 +6794,19 @@ } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">= 12.13.0" @@ -6875,7 +6816,16 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/css-select": { @@ -7008,17 +6958,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -7162,13 +7115,13 @@ } }, "node_modules/dmg-builder": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.1.tgz", - "integrity": "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.12.0.tgz", + "integrity": "sha512-nS22OyHUIYcK40UnILOtqC5Qffd1SN1Ljqy/6e+QR2H1wM3iNBrKJoEbDRfEmYYaALKNFRkKPqSbZKRsGUBdPw==", "dev": true, "dependencies": { - "app-builder-lib": "24.9.1", - "builder-util": "24.8.1", + "app-builder-lib": "24.12.0", + "builder-util": "24.9.4", "builder-util-runtime": "9.2.3", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", @@ -7403,14 +7356,14 @@ } }, "node_modules/electron": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.2.1.tgz", - "integrity": "sha512-wlzXf+OvOiVlBf9dcSeMMf7Q+N6DG+wtgFbMK0sA/JpIJcdosRbLMQwLg/LTwNVKIbmayqFLDp4FmmFkEMhbYA==", + "version": "29.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.1.0.tgz", + "integrity": "sha512-giJVIm0sWVp+8V1GXrKqKTb+h7no0P3ooYqEd34AD9wMJzGnAeL+usj+R0155/0pdvvP1mgydnA7lcaFA2M9lw==", "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -7421,16 +7374,16 @@ } }, "node_modules/electron-builder": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.1.tgz", - "integrity": "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.12.0.tgz", + "integrity": "sha512-dH4O9zkxFxFbBVFobIR5FA71yJ1TZSCvjZ2maCskpg7CWjBF+SNRSQAThlDyUfRuB+jBTMwEMzwARywmap0CSw==", "dev": true, "dependencies": { - "app-builder-lib": "24.9.1", - "builder-util": "24.8.1", + "app-builder-lib": "24.12.0", + "builder-util": "24.9.4", "builder-util-runtime": "9.2.3", "chalk": "^4.1.2", - "dmg-builder": "24.9.1", + "dmg-builder": "24.12.0", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", @@ -7589,13 +7542,13 @@ } }, "node_modules/electron-publish": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz", - "integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==", + "version": "24.9.4", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.9.4.tgz", + "integrity": "sha512-FghbeVMfxHneHjsG2xUSC0NMZYWOOWhBxfZKPTbibcJ0CjPH0Ph8yb5CUO62nqywXfA5u1Otq6K8eOdOixxmNg==", "dev": true, "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "24.8.1", + "builder-util": "24.9.4", "builder-util-runtime": "9.2.3", "chalk": "^4.1.2", "fs-extra": "^10.1.0", @@ -7856,20 +7809,11 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.653", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.653.tgz", - "integrity": "sha512-wA2A2LQCqnEwQAvwADQq3KpMpNwgAUBnRmrFgRzHnPhbQUFArTR32Ab46f4p0MovDLcg4uqd4nCsN2hTltslpA==", + "version": "1.4.688", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.688.tgz", + "integrity": "sha512-3/tHg2ChPF00eukURIB8cSVt3/9oeS1oTUIEt3ivngBInUaEcBhG2VdyEDejhwQdR6SLqaiEAEc0dHS0V52pOw==", "dev": true }, - "node_modules/electron/node_modules/@types/node": { - "version": "18.19.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.12.tgz", - "integrity": "sha512-uLcpWEAvatBEubmgCMzWforZbAu1dT9syweWnU3/DNwbeUBq2miP5nG8Y4JL9MDMKWt+7Yv1CSvA8xELdEl54w==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -7935,9 +7879,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz", + "integrity": "sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -7997,50 +7941,52 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.1", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.0", + "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.8", "string.prototype.trimend": "^1.0.7", "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -8049,6 +7995,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", @@ -8056,14 +8023,14 @@ "dev": true }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -8094,11 +8061,12 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", - "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", + "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==", "dev": true, "hasInstallScript": true, + "optional": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8106,35 +8074,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.11", - "@esbuild/android-arm": "0.19.11", - "@esbuild/android-arm64": "0.19.11", - "@esbuild/android-x64": "0.19.11", - "@esbuild/darwin-arm64": "0.19.11", - "@esbuild/darwin-x64": "0.19.11", - "@esbuild/freebsd-arm64": "0.19.11", - "@esbuild/freebsd-x64": "0.19.11", - "@esbuild/linux-arm": "0.19.11", - "@esbuild/linux-arm64": "0.19.11", - "@esbuild/linux-ia32": "0.19.11", - "@esbuild/linux-loong64": "0.19.11", - "@esbuild/linux-mips64el": "0.19.11", - "@esbuild/linux-ppc64": "0.19.11", - "@esbuild/linux-riscv64": "0.19.11", - "@esbuild/linux-s390x": "0.19.11", - "@esbuild/linux-x64": "0.19.11", - "@esbuild/netbsd-x64": "0.19.11", - "@esbuild/openbsd-x64": "0.19.11", - "@esbuild/sunos-x64": "0.19.11", - "@esbuild/win32-arm64": "0.19.11", - "@esbuild/win32-ia32": "0.19.11", - "@esbuild/win32-x64": "0.19.11" + "@esbuild/aix-ppc64": "0.20.0", + "@esbuild/android-arm": "0.20.0", + "@esbuild/android-arm64": "0.20.0", + "@esbuild/android-x64": "0.20.0", + "@esbuild/darwin-arm64": "0.20.0", + "@esbuild/darwin-x64": "0.20.0", + "@esbuild/freebsd-arm64": "0.20.0", + "@esbuild/freebsd-x64": "0.20.0", + "@esbuild/linux-arm": "0.20.0", + "@esbuild/linux-arm64": "0.20.0", + "@esbuild/linux-ia32": "0.20.0", + "@esbuild/linux-loong64": "0.20.0", + "@esbuild/linux-mips64el": "0.20.0", + "@esbuild/linux-ppc64": "0.20.0", + "@esbuild/linux-riscv64": "0.20.0", + "@esbuild/linux-s390x": "0.20.0", + "@esbuild/linux-x64": "0.20.0", + "@esbuild/netbsd-x64": "0.20.0", + "@esbuild/openbsd-x64": "0.20.0", + "@esbuild/sunos-x64": "0.20.0", + "@esbuild/win32-arm64": "0.20.0", + "@esbuild/win32-ia32": "0.20.0", + "@esbuild/win32-x64": "0.20.0" } }, "node_modules/esbuild-wasm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.11.tgz", - "integrity": "sha512-MIhnpc1TxERUHomteO/ZZHp+kUawGEc03D/8vMHGzffLvbFLeDe6mwxqEZwlqBNY7SLWbyp6bBQAcCen8+wpjQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.0.tgz", + "integrity": "sha512-Lc9KeQCg1Zf8kCtfDXgy29rx0x8dOuhDWbkP76Wc64q7ctOOc1Zv1C39AxiE+y4N6ONyXtJk4HKpM7jlU7/jSA==", "dev": true, "bin": { "esbuild": "bin/esbuild" @@ -8144,9 +8112,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -8317,14 +8285,14 @@ "dev": true }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", + "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", @@ -8473,9 +8441,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -8503,28 +8471,15 @@ } }, "node_modules/figures": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" + "escape-string-regexp": "^1.0.5" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8831,16 +8786,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8870,13 +8829,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -9082,21 +9042,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -9118,12 +9078,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -9171,9 +9131,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -9182,23 +9142,6 @@ "node": ">= 0.4" } }, - "node_modules/hdr-histogram-js": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", - "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", - "dev": true, - "dependencies": { - "@assemblyscript/loader": "^0.10.1", - "base64-js": "^1.2.0", - "pako": "^1.0.3" - } - }, - "node_modules/hdr-histogram-percentiles-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", - "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", - "dev": true - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -9240,6 +9183,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/hotkeys-js": { + "version": "3.13.7", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.7.tgz", + "integrity": "sha512-ygFIdTqqwG4fFP7kkiYlvayZppeIQX2aPpirsngkv1xM1lP0piDY5QEh68nQnIKvz64hfocxhBaD/uK3sSK1yQ==", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -9647,18 +9598,18 @@ } }, "node_modules/inquirer": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", - "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", + "version": "9.2.14", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz", + "integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==", "dev": true, "dependencies": { - "@ljharb/through": "^2.3.11", + "@ljharb/through": "^2.3.12", "ansi-escapes": "^4.3.2", "chalk": "^5.3.0", "cli-cursor": "^3.1.0", "cli-width": "^4.1.0", "external-editor": "^3.1.0", - "figures": "^5.0.0", + "figures": "^3.2.0", "lodash": "^4.17.21", "mute-stream": "1.0.0", "ora": "^5.4.1", @@ -9669,7 +9620,7 @@ "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=14.18.0" + "node": ">=18" } }, "node_modules/inquirer/node_modules/chalk": { @@ -9693,12 +9644,12 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -9706,10 +9657,23 @@ "node": ">= 0.4" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "node_modules/ipaddr.js": { @@ -9738,14 +9702,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9940,9 +9906,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -10016,12 +9982,15 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10070,12 +10039,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -10085,12 +10054,12 @@ } }, "node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10133,12 +10102,12 @@ "dev": true }, "node_modules/isbinaryfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz", - "integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", "dev": true, "engines": { - "node": ">= 14.0.0" + "node": ">= 18.0.0" }, "funding": { "url": "https://github.com/sponsors/gjtorikian/" @@ -10369,9 +10338,9 @@ } }, "node_modules/joi": { - "version": "17.12.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz", - "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==", + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "dev": true, "dependencies": { "@hapi/hoek": "^9.3.0", @@ -10400,6 +10369,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -10459,9 +10434,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/jsonfile": { @@ -10796,18 +10771,6 @@ "node": ">=8" } }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-symbols/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10839,9 +10802,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -11095,12 +11058,13 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", - "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz", + "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==", "dev": true, "dependencies": { - "schema-utils": "^4.0.0" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" }, "engines": { "node": ">= 12.13.0" @@ -11649,9 +11613,9 @@ } }, "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.2.tgz", - "integrity": "sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.3.tgz", + "integrity": "sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==", "dev": true, "engines": { "node": ">=16" @@ -12009,13 +11973,13 @@ } }, "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -12200,18 +12164,6 @@ "node": ">=8" } }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ora/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12322,9 +12274,9 @@ } }, "node_modules/pacote": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.5.tgz", - "integrity": "sha512-TAE0m20zSDMnchPja9vtQjri19X3pZIyRpm2TJVeI+yU42leJBBDTRYhOcWFsPhaMxf+3iwQkFiKz16G9AEeeA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.6.tgz", + "integrity": "sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ==", "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", @@ -12342,7 +12294,7 @@ "promise-retry": "^2.0.1", "read-package-json": "^7.0.0", "read-package-json-fast": "^3.0.0", - "sigstore": "^2.0.0", + "sigstore": "^2.2.0", "ssri": "^10.0.0", "tar": "^6.1.11" }, @@ -12582,12 +12534,12 @@ "dev": true }, "node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -12616,14 +12568,10 @@ } }, "node_modules/piscina": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.2.1.tgz", - "integrity": "sha512-LShp0+lrO+WIzB9LXO+ZmO4zGHxtTJNZhEO56H9SSu+JPaUQb6oLcTCzWi5IL2DS8/vIkCE88ElahuSSw4TAkA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.3.1.tgz", + "integrity": "sha512-MBj0QYm3hJQ/C/wIXTN1OCYC8uQ4BBJ4LVele2P4ZwVQAH04vkk8E1SpDbuemLAL1dZorbuOob9rYqJeWCcCRg==", "dev": true, - "dependencies": { - "hdr-histogram-js": "^2.0.1", - "hdr-histogram-percentiles-obj": "^3.0.0" - }, "optionalDependencies": { "nice-napi": "^1.0.2" } @@ -12739,10 +12687,19 @@ "node": ">=10.4.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -12768,25 +12725,34 @@ } }, "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.0.tgz", + "integrity": "sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w==", "dev": true, "dependencies": { - "cosmiconfig": "^8.3.5", + "cosmiconfig": "^9.0.0", "jiti": "^1.20.0", "semver": "^7.5.4" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/postcss-modules-extract-imports": { @@ -12890,9 +12856,9 @@ "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" }, "node_modules/primeng": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.4.0.tgz", - "integrity": "sha512-fqIFXORQjfTZFepg1MBz/vqheMjHy104ugxuTT1BCzEbVsjc2bjVyedEsS3u+N7bYag7TLMvv/FjzbTAq6hzOw==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.9.0.tgz", + "integrity": "sha512-XGeyponzYKpBYj2vD/H+vKPgnl4v8RQSEZmZEbo6Nr8NU4PZiDSvHRhqkOChsmqegmGvS8cLal1R2YedKcYykg==", "dependencies": { "tslib": "^2.3.0" }, @@ -13115,9 +13081,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -13377,9 +13343,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", - "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==", "dev": true }, "node_modules/regenerate": { @@ -13422,14 +13388,15 @@ "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -13666,9 +13633,9 @@ "optional": true }, "node_modules/rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", + "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -13681,19 +13648,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", + "@rollup/rollup-android-arm-eabi": "4.12.0", + "@rollup/rollup-android-arm64": "4.12.0", + "@rollup/rollup-darwin-arm64": "4.12.0", + "@rollup/rollup-darwin-x64": "4.12.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", + "@rollup/rollup-linux-arm64-gnu": "4.12.0", + "@rollup/rollup-linux-arm64-musl": "4.12.0", + "@rollup/rollup-linux-riscv64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-musl": "4.12.0", + "@rollup/rollup-win32-arm64-msvc": "4.12.0", + "@rollup/rollup-win32-ia32-msvc": "4.12.0", + "@rollup/rollup-win32-x64-msvc": "4.12.0", "fsevents": "~2.3.2" } }, @@ -13776,13 +13743,13 @@ ] }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -13808,9 +13775,9 @@ } }, "node_modules/sass": { - "version": "1.69.7", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", - "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", + "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -13825,29 +13792,29 @@ } }, "node_modules/sass-loader": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", - "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.0.tgz", + "integrity": "sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q==", "dev": true, "dependencies": { "neo-async": "^2.6.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "fibers": ">= 3.1.0", + "@rspack/core": "0.x || 1.x", "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", "sass-embedded": "*", "webpack": "^5.0.0" }, "peerDependenciesMeta": { - "fibers": { + "@rspack/core": { "optional": true }, "node-sass": { @@ -13858,6 +13825,9 @@ }, "sass-embedded": { "optional": true + }, + "webpack": { + "optional": true } } }, @@ -13906,9 +13876,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -14134,14 +14104,15 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.1" }, @@ -14150,14 +14121,15 @@ } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -14231,14 +14203,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14251,17 +14227,17 @@ "dev": true }, "node_modules/sigstore": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.0.tgz", - "integrity": "sha512-fcU9clHwEss2/M/11FFM8Jwc4PjBgbhXoNskoK5guoK0qGQBSeUbQZRJ+B2fDFIvhyf0gqCaPrel9mszbhAxug==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.2.tgz", + "integrity": "sha512-2A3WvXkQurhuMgORgT60r6pOWiCOO5LlEqY2ADxGBDGVYLSo5HN0uLtb68YpVpuL/Vi8mLTe7+0Dx2Fq8lLqEg==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.1.1", - "@sigstore/core": "^0.2.0", - "@sigstore/protobuf-specs": "^0.2.1", - "@sigstore/sign": "^2.2.1", - "@sigstore/tuf": "^2.3.0", - "@sigstore/verify": "^0.1.0" + "@sigstore/bundle": "^2.2.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.0", + "@sigstore/sign": "^2.2.3", + "@sigstore/tuf": "^2.3.1", + "@sigstore/verify": "^1.1.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -14373,16 +14349,16 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -14480,9 +14456,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", - "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -14496,9 +14472,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "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==", "dev": true }, "node_modules/spdy": { @@ -14930,9 +14906,9 @@ } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -15072,12 +15048,6 @@ "node": "*" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -15127,15 +15097,12 @@ } }, "node_modules/tmp-promise/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/to-fast-properties": { @@ -15294,29 +15261,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -15326,16 +15294,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -15345,14 +15314,20 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15365,9 +15340,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -15393,9 +15368,9 @@ } }, "node_modules/undici": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.2.1.tgz", - "integrity": "sha512-7Wa9thEM6/LMnnKtxJHlc8SrTlDmxqJecgz1iy8KlsN0/iskQXOQCuPkrZLXbElPaSw5slFFyKIKXyJ3UtbApw==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.6.2.tgz", + "integrity": "sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" @@ -15700,6 +15675,412 @@ } } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -15757,9 +16138,9 @@ } }, "node_modules/webpack": { - "version": "5.90.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.0.tgz", - "integrity": "sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dev": true, "peer": true, "dependencies": { @@ -16078,16 +16459,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -16319,9 +16700,9 @@ } }, "node_modules/zone.js": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.3.tgz", - "integrity": "sha512-jYoNqF046Q+JfcZSItRSt+oXFcpXL88yq7XAZjb/NKTS7w2hHpKjRJ3VlFD1k75wMaRRXNUt5vrZVlygiMyHbA==", + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.4.tgz", + "integrity": "sha512-NtTUvIlNELez7Q1DzKVIFZBzNb646boQMgpATo9z3Ftuu/gWvzxCW7jdjcUDoRGxRikrhVHB/zLXh1hxeJawvw==", "dependencies": { "tslib": "^2.3.0" } diff --git a/desktop/package.json b/desktop/package.json index fa0d424f1..91d2d23f1 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -29,48 +29,49 @@ "lint": "ng lint" }, "dependencies": { - "@angular/animations": "17.1.2", - "@angular/cdk": "17.1.2", - "@angular/common": "17.1.2", - "@angular/compiler": "17.1.2", - "@angular/core": "17.1.2", - "@angular/forms": "17.1.2", - "@angular/language-service": "17.1.2", - "@angular/platform-browser": "17.1.2", - "@angular/platform-browser-dynamic": "17.1.2", - "@angular/router": "17.1.2", + "@angular/animations": "17.2.3", + "@angular/cdk": "17.2.1", + "@angular/common": "17.2.3", + "@angular/compiler": "17.2.3", + "@angular/core": "17.2.3", + "@angular/forms": "17.2.3", + "@angular/language-service": "17.2.3", + "@angular/platform-browser": "17.2.3", + "@angular/platform-browser-dynamic": "17.2.3", + "@angular/router": "17.2.3", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.4.47", - "chart.js": "4.4.1", + "chart.js": "4.4.2", "chartjs-plugin-zoom": "2.0.1", + "hotkeys-js": "3.13.7", "interactjs": "1.10.26", "leaflet": "1.9.4", "moment": "2.30.1", "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "17.4.0", + "primeng": "17.9.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", - "zone.js": "0.14.3" + "zone.js": "0.14.4" }, "devDependencies": { - "@angular-builders/custom-webpack": "17.0.0", - "@angular-devkit/build-angular": "17.1.2", - "@angular/cli": "17.1.2", - "@angular/compiler-cli": "17.1.2", + "@angular-builders/custom-webpack": "17.0.1", + "@angular-devkit/build-angular": "17.2.2", + "@angular/cli": "17.2.2", + "@angular/compiler-cli": "17.2.3", "@types/leaflet": "1.9.8", - "@types/node": "20.11.14", + "@types/node": "20.11.24", "@types/uuid": "9.0.8", - "electron": "28.2.1", - "electron-builder": "24.9.1", + "electron": "29.1.0", + "electron-builder": "24.12.0", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", "ts-node": "10.9.2", - "typescript": "5.2.2", + "typescript": "5.3.3", "wait-on": "7.2.0" }, "overrides": { diff --git a/desktop/sky-atlas.png b/desktop/sky-atlas.png index 0e1dbec70..82c34f101 100644 Binary files a/desktop/sky-atlas.png and b/desktop/sky-atlas.png differ diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index d67825797..08c56d023 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -2,59 +2,142 @@
- + - +
+
+ + + + + -
-
+
- + - -
- {{ darvStatus | enum | lowercase }} - - + {{ status | enum | lowercase }} + + {{ darvDirection }} - - {{ darvRemainingTime | exposureTime }} + + {{ remainingTime | exposureTime }} + + + {{ progress | percent:'1.1-1' }} + + + + + {{ elapsedTime | exposureTime }} + + + {{ tppaRightAscension }} - - {{ darvProgress | percent:'1.1-1' }} + + {{ tppaDeclination }}
- - + + +
+
+ + + + +
+
+ + + + +
+
+ East + +
+
+ +
+
+ +
+
+
+ Azimuth + {{ tppaAzimuthError }} + {{ tppaAzimuthErrorDirection }} +
+
+ Altitude + {{ tppaAltitudeError }} + {{ tppaAltitudeErrorDirection }} +
+
+ Total + {{ tppaTotalError }} +
+
+
+
+ + + + + +
+
+
+ +
- +
- +
@@ -66,14 +149,14 @@
-
+
- - - + + +
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index b46ceddd5..d11200c64 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -2,11 +2,16 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angu 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 { DARVState, Hemisphere } from '../../shared/types/alignment.types' -import { Camera, CameraPreference, CameraStartCapture, cameraPreferenceKey } from '../../shared/types/camera.types' -import { GuideDirection, GuideOutput } from '../../shared/types/guider.types' +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' +import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureTimeUnit } 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_OPTIONS } from '../../shared/types/settings.types' +import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' +import { CameraComponent } from '../camera/camera.component' @Component({ selector: 'app-alignment', @@ -16,38 +21,70 @@ import { AppComponent } from '../app.component' export class AlignmentComponent implements AfterViewInit, OnDestroy { cameras: Camera[] = [] - camera?: Camera - cameraConnected = false + camera = structuredClone(EMPTY_CAMERA) + + mounts: Mount[] = [] + mount = structuredClone(EMPTY_MOUNT) guideOutputs: GuideOutput[] = [] - guideOutput?: GuideOutput - guideOutputConnected = false + guideOutput = structuredClone(EMPTY_GUIDE_OUTPUT) + + tab = 0 + + running = false + alignmentMethod?: AlignmentMethod + status: DARVState | TPPAState = 'IDLE' + elapsedTime = 0 + remainingTime = 0 + progress = 0 + private id = '' + + readonly tppaRequest: TPPAStart = { + capture: structuredClone(EMPTY_CAMERA_START_CAPTURE), + plateSolver: structuredClone(EMPTY_PLATE_SOLVER_OPTIONS), + startFromCurrentPosition: true, + eastDirection: true, + compensateRefraction: true, + stopTrackingWhenDone: true, + stepDistance: 10, + } - darvInitialPause = 5 - darvDrift = 30 - darvInProgress = false + readonly plateSolverTypes = Array.from(DEFAULT_SOLVER_TYPES) + tppaAzimuthError: Angle = `00ยฐ00'00"` + tppaAzimuthErrorDirection = '' + tppaAltitudeError: Angle = `00ยฐ00'00"` + tppaAltitudeErrorDirection = '' + tppaTotalError: Angle = `00ยฐ00'00"` + tppaRightAscension: Angle = '00h00m00s' + tppaDeclination: Angle = `00ยฐ00'00"` + + readonly darvRequest: DARVStart = { + capture: structuredClone(EMPTY_CAMERA_START_CAPTURE), + initialPause: 5, + exposureTime: 30, + direction: 'NORTH', + reversed: false + } + + readonly driftExposureUnit = ExposureTimeUnit.SECOND readonly darvHemispheres: Hemisphere[] = ['NORTHERN', 'SOUTHERN'] darvHemisphere: Hemisphere = 'NORTHERN' darvDirection?: GuideDirection - darvStatus: DARVState | 'IDLE' = 'IDLE' - darvRemainingTime = 0 - darvProgress = 0 constructor( app: AppComponent, private api: ApiService, private browserWindow: BrowserWindowService, - private storage: LocalStorageService, + private preference: PreferenceService, electron: ElectronService, ngZone: NgZone, ) { app.title = 'Alignment' electron.on('CAMERA.UPDATED', event => { - if (event.device.name === this.camera?.name) { + if (event.device.id === this.camera.id) { ngZone.run(() => { - Object.assign(this.camera!, event.device) - this.updateCamera() + Object.assign(this.camera, event.device) }) } }) @@ -55,168 +92,300 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { 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.name === event.device.name) + const index = this.cameras.findIndex(e => e.id === event.device.id) if (index >= 0) { if (this.cameras[index] === this.camera) { - this.camera = undefined - this.cameraConnected = false + Object.assign(this.camera, this.cameras[0] ?? EMPTY_CAMERA) } this.cameras.splice(index, 1) + this.cameras.sort(deviceComparator) } }) }) + electron.on('MOUNT.UPDATED', event => { + if (event.device.id === this.mount.id) { + ngZone.run(() => { + Object.assign(this.mount, event.device) + }) + } + }) + + electron.on('MOUNT.ATTACHED', event => { + ngZone.run(() => { + this.mounts.push(event.device) + this.mounts.sort(deviceComparator) + }) + }) + + electron.on('MOUNT.DETACHED', event => { + ngZone.run(() => { + const index = this.mounts.findIndex(e => e.id === event.device.id) + + if (index >= 0) { + if (this.mounts[index] === this.mount) { + Object.assign(this.mount, this.mounts[0] ?? EMPTY_MOUNT) + } + + this.mounts.splice(index, 1) + this.mounts.sort(deviceComparator) + } + }) + }) + + electron.on('GUIDE_OUTPUT.UPDATED', event => { + if (event.device.id === this.guideOutput.id) { + ngZone.run(() => { + Object.assign(this.guideOutput, event.device) + }) + } + }) + electron.on('GUIDE_OUTPUT.ATTACHED', event => { ngZone.run(() => { this.guideOutputs.push(event.device) + this.guideOutputs.sort(deviceComparator) }) }) electron.on('GUIDE_OUTPUT.DETACHED', event => { ngZone.run(() => { - const index = this.guideOutputs.findIndex(e => e.name === event.device.name) + const index = this.guideOutputs.findIndex(e => e.id === event.device.id) if (index >= 0) { if (this.guideOutputs[index] === this.guideOutput) { - this.guideOutput = undefined - this.guideOutputConnected = false + Object.assign(this.guideOutput, this.guideOutputs[0] ?? EMPTY_GUIDE_OUTPUT) } this.guideOutputs.splice(index, 1) + this.guideOutputs.sort(deviceComparator) } }) }) - electron.on('GUIDE_OUTPUT.UPDATED', event => { - if (event.device.name === this.guideOutput?.name) { + electron.on('TPPA.ELAPSED', event => { + if (event.id === this.id) { ngZone.run(() => { - Object.assign(this.guideOutput!, event.device) - this.updateGuideOutput() + if (this.status !== 'PAUSING' || event.state === 'PAUSED') { + this.status = event.state + } + + this.running = event.state !== 'FINISHED' + this.elapsedTime = event.elapsedTime + + if (event.state === 'COMPUTED') { + this.tppaAzimuthError = event.azimuthError + this.tppaAltitudeError = event.altitudeError + this.tppaAzimuthErrorDirection = event.azimuthErrorDirection + this.tppaAltitudeErrorDirection = event.altitudeErrorDirection + this.tppaTotalError = event.totalError + } else if (event.state === 'SOLVED' || event.state === 'SLEWING') { + this.tppaRightAscension = event.rightAscension + this.tppaDeclination = event.declination + } + + if (!this.running) { + this.alignmentMethod = undefined + } }) } }) - electron.on('DARV_ALIGNMENT.ELAPSED', event => { - if (event.camera.name === this.camera?.name && - event.guideOutput.name === this.guideOutput?.name) { + electron.on('DARV.ELAPSED', event => { + if (event.id === this.id) { ngZone.run(() => { - this.darvStatus = event.state - this.darvRemainingTime = event.remainingTime - this.darvProgress = event.progress - this.darvInProgress = event.remainingTime > 0 + this.status = event.state + this.remainingTime = event.remainingTime + this.progress = event.progress + this.running = event.remainingTime > 0 if (event.state === 'FORWARD' || event.state === 'BACKWARD') { this.darvDirection = event.direction } else { this.darvDirection = undefined } + + if (!this.running) { + this.alignmentMethod = undefined + } }) } }) + + this.loadPreference() } async ngAfterViewInit() { - this.cameras = await this.api.cameras() - this.guideOutputs = await this.api.guideOutputs() + this.cameras = (await this.api.cameras()).sort(deviceComparator) + this.mounts = (await this.api.mounts()).sort(deviceComparator) + this.guideOutputs = (await this.api.guideOutputs()).sort(deviceComparator) } @HostListener('window:unload') ngOnDestroy() { this.darvStop() + this.tppaStop() } async cameraChanged() { - if (this.camera) { + if (this.camera.name) { const camera = await this.api.camera(this.camera.name) Object.assign(this.camera, camera) + this.loadPreference() + } + } - this.updateCamera() + async mountChanged() { + if (this.mount.name) { + const mount = await this.api.mount(this.mount.name) + Object.assign(this.mount, mount) } } async guideOutputChanged() { - if (this.guideOutput) { + if (this.guideOutput.name) { const guideOutput = await this.api.guideOutput(this.guideOutput.name) Object.assign(this.guideOutput, guideOutput) - - this.updateGuideOutput() } } - cameraConnect() { - if (this.cameraConnected) { - this.api.cameraDisconnect(this.camera!) - } else { - this.api.cameraConnect(this.camera!) + mountConnect() { + if (this.mount.name) { + if (this.mount.connected) { + this.api.mountDisconnect(this.mount) + } else { + this.api.mountConnect(this.mount) + } } } guideOutputConnect() { - if (this.guideOutputConnected) { - this.api.guideOutputDisconnect(this.guideOutput!) - } else { - this.api.guideOutputConnect(this.guideOutput!) + if (this.guideOutput.name) { + if (this.guideOutput.connected) { + this.api.guideOutputDisconnect(this.guideOutput) + } else { + this.api.guideOutputConnect(this.guideOutput) + } + } + } + + async showCameraDialog() { + if (this.camera.name) { + if (this.tab === 0) { + if (await CameraComponent.showAsDialog(this.browserWindow, 'TPPA', this.camera, this.tppaRequest.capture)) { + this.savePreference() + } + } else if (this.tab === 1) { + this.darvRequest.capture.exposureTime = this.darvRequest.exposureTime * 1000000 + this.darvRequest.capture.exposureDelay = this.darvRequest.initialPause + + if (await CameraComponent.showAsDialog(this.browserWindow, 'DARV', this.camera, this.darvRequest.capture)) { + this.savePreference() + } + } } } + plateSolverChanged() { + this.tppaRequest.plateSolver = this.preference.plateSolverOptions(this.tppaRequest.plateSolver.type).get() + this.savePreference() + } + + initialPauseChanged() { + this.darvRequest.capture.exposureDelay = this.darvRequest.initialPause + this.savePreference() + } + + driftForChanged() { + this.darvRequest.capture.exposureTime = this.darvRequest.exposureTime * 1000000 + this.savePreference() + } + async darvStart(direction: GuideDirection = 'EAST') { - // TODO: Horizonte leste e oeste tem um impacto no "reversed"? - const reversed = this.darvHemisphere === 'SOUTHERN' + this.alignmentMethod = 'DARV' + this.darvRequest.direction = direction + this.darvRequest.reversed = this.darvHemisphere === 'SOUTHERN' + this.darvRequest.capture.exposureTime = this.darvRequest.exposureTime * 1000000 + this.darvRequest.capture.exposureDelay = this.darvRequest.initialPause await this.openCameraImage() - const capture = this.makeCameraStartCapture(this.camera!) - await this.api.darvStart(this.camera!, this.guideOutput!, this.darvDrift, this.darvInitialPause, direction, reversed, capture) + this.id = await this.api.darvStart(this.camera, this.guideOutput, this.darvRequest) } darvStop() { - this.api.darvStop(this.camera!, this.guideOutput!) + this.api.darvStop(this.id) + } + + async tppaStart() { + this.alignmentMethod = 'TPPA' + await this.openCameraImage() + this.id = await this.api.tppaStart(this.camera, this.mount, this.tppaRequest) + } + + tppaPause() { + this.status = 'PAUSING' + this.api.tppaPause(this.id) + } + + tppaUnpause() { + this.api.tppaUnpause(this.id) + } + + tppaStop() { + this.api.tppaStop(this.id) } openCameraImage() { - return this.browserWindow.openCameraImage(this.camera!) + return this.browserWindow.openCameraImage(this.camera) } - private updateCamera() { - if (!this.camera) { - return + private loadPreference() { + const preference = this.preference.alignmentPreference.get() + + this.tppaRequest.startFromCurrentPosition = preference.tppaStartFromCurrentPosition + this.tppaRequest.eastDirection = preference.tppaEastDirection + this.tppaRequest.compensateRefraction = preference.tppaCompensateRefraction + this.tppaRequest.stopTrackingWhenDone = preference.tppaStopTrackingWhenDone + this.tppaRequest.stepDistance = preference.tppaStepDistance + this.tppaRequest.plateSolver.type = preference.tppaPlateSolverType + this.darvRequest.initialPause = preference.darvInitialPause + this.darvRequest.exposureTime = preference.darvExposureTime + this.darvHemisphere = preference.darvHemisphere + + if (this.camera.name) { + Object.assign(this.tppaRequest.capture, this.preference.cameraStartCaptureForTPPA(this.camera).get(this.tppaRequest.capture)) + Object.assign(this.darvRequest.capture, this.preference.cameraStartCaptureForDARV(this.camera).get(this.darvRequest.capture)) } - this.cameraConnected = this.camera.connected + this.plateSolverChanged() } - private updateGuideOutput() { - if (!this.guideOutput) { - return + savePreference() { + if (this.tab === 0) { + this.preference.cameraStartCaptureForTPPA(this.camera).set(this.tppaRequest.capture) + } else if (this.tab === 1) { + this.preference.cameraStartCaptureForDARV(this.camera).set(this.darvRequest.capture) } - this.guideOutputConnected = this.guideOutput.connected - } - - private makeCameraStartCapture(camera: Camera): CameraStartCapture { - const preference = this.storage.get(cameraPreferenceKey(camera), {}) - - return { - exposureTime: 0, - exposureAmount: 0, - exposureDelay: 0, - frameType: 'LIGHT', - autoSave: false, - autoSubFolderMode: 'OFF', - x: preference.x ?? camera.minX, - y: preference.y ?? camera.minY, - width: preference.width ?? camera.maxWidth, - height: preference.height ?? camera.maxHeight, - binX: preference.binX ?? 1, - binY: preference.binY ?? 1, - gain: preference.gain ?? 0, - offset: preference.offset ?? 0, - frameFormat: preference.frameFormat ?? (camera.frameFormats[0] || ''), + const preference: AlignmentPreference = { + tppaStartFromCurrentPosition: this.tppaRequest.startFromCurrentPosition, + tppaEastDirection: this.tppaRequest.eastDirection, + tppaCompensateRefraction: this.tppaRequest.compensateRefraction, + tppaStopTrackingWhenDone: this.tppaRequest.stopTrackingWhenDone, + tppaStepDistance: this.tppaRequest.stepDistance, + tppaPlateSolverType: this.tppaRequest.plateSolver.type, + darvInitialPause: this.darvRequest.initialPause, + darvExposureTime: this.darvRequest.exposureTime, + darvHemisphere: this.darvHemisphere, } + + this.preference.alignmentPreference.set(preference) } } \ No newline at end of file diff --git a/desktop/src/app/app-routing.module.ts b/desktop/src/app/app-routing.module.ts index 1faea077b..989fc38bb 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 { CalculatorComponent } from './calculator/calculator.component' import { CalibrationComponent } from './calibration/calibration.component' import { CameraComponent } from './camera/camera.component' import { FilterWheelComponent } from './filterwheel/filterwheel.component' @@ -80,6 +81,10 @@ const routes: Routes = [ path: 'calibration', component: CalibrationComponent, }, + { + path: 'calculator', + component: CalculatorComponent, + }, { path: 'settings', component: SettingsComponent, diff --git a/desktop/src/app/app.component.ts b/desktop/src/app/app.component.ts index ab4b79e16..c62ee9d04 100644 --- a/desktop/src/app/app.component.ts +++ b/desktop/src/app/app.component.ts @@ -18,7 +18,7 @@ export class AppComponent implements AfterViewInit { pinned = false maximizable = false - readonly modal = window.modal + readonly modal = window.options.modal ?? false subTitle? = '' backgroundColor = '#212121' topMenu: ExtendedMenuItem[] = [] @@ -39,9 +39,9 @@ export class AppComponent implements AfterViewInit { console.info('APP_CONFIG', APP_CONFIG) if (electron.isElectron) { - console.info('Run in electron') + console.info('Run in electron', window.options) } else { - console.info('Run in browser') + console.info('Run in browser', window.options) } } @@ -49,20 +49,24 @@ export class AppComponent implements AfterViewInit { this.route.queryParams.subscribe(e => { this.maximizable = e.resizable === 'true' }) + + if (window.options.autoResizable !== false) { + this.electron.autoResizeWindow() + } } pin() { this.pinned = !this.pinned - if (this.pinned) this.electron.send('WINDOW.PIN') - else this.electron.send('WINDOW.UNPIN') + if (this.pinned) this.electron.pinWindow() + else this.electron.unpinWindow() } minimize() { - this.electron.send('WINDOW.MINIMIZE') + this.electron.minimizeWindow() } maximize() { - this.electron.send('WINDOW.MAXIMIZE') + this.electron.maximizeWindow() } close(data?: any) { diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 46087773c..fb4b0f37c 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -12,6 +12,7 @@ import { CalendarModule } from 'primeng/calendar' import { CardModule } from 'primeng/card' import { ChartModule } from 'primeng/chart' import { CheckboxModule } from 'primeng/checkbox' +import { ColorPickerModule } from 'primeng/colorpicker' import { ConfirmDialogModule } from 'primeng/confirmdialog' import { ContextMenuModule } from 'primeng/contextmenu' import { DialogModule } from 'primeng/dialog' @@ -23,6 +24,7 @@ import { InputSwitchModule } from 'primeng/inputswitch' import { InputTextModule } from 'primeng/inputtext' import { ListboxModule } from 'primeng/listbox' import { MenuModule } from 'primeng/menu' +import { MessageModule } from 'primeng/message' import { MultiSelectModule } from 'primeng/multiselect' import { OverlayPanelModule } from 'primeng/overlaypanel' import { ScrollPanelModule } from 'primeng/scrollpanel' @@ -57,6 +59,8 @@ import { AlignmentComponent } from './alignment/alignment.component' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './app.component' import { AtlasComponent } from './atlas/atlas.component' +import { CalculatorComponent } from './calculator/calculator.component' +import { FormulaComponent } from './calculator/formula/formula.component' import { CalibrationComponent } from './calibration/calibration.component' import { CameraComponent } from './camera/camera.component' import { FilterWheelComponent } from './filterwheel/filterwheel.component' @@ -79,6 +83,7 @@ import { SettingsComponent } from './settings/settings.component' AnglePipe, AppComponent, AtlasComponent, + CalculatorComponent, CalibrationComponent, CameraComponent, CameraExposureComponent, @@ -90,6 +95,7 @@ import { SettingsComponent } from './settings/settings.component' FilterWheelComponent, FlatWizardComponent, FocuserComponent, + FormulaComponent, FramingComponent, GuiderComponent, HistogramComponent, @@ -119,6 +125,7 @@ import { SettingsComponent } from './settings/settings.component' CardModule, ChartModule, CheckboxModule, + ColorPickerModule, CommonModule, ConfirmDialogModule, ContextMenuModule, @@ -134,6 +141,7 @@ import { SettingsComponent } from './settings/settings.component' InputTextModule, ListboxModule, MenuModule, + MessageModule, MultiSelectModule, OverlayPanelModule, ScrollPanelModule, diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index 06d5368d6..27d9cbf85 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -7,9 +7,11 @@
-
- - SDO/HMI + @@ -18,8 +20,8 @@
-
- +
+
@@ -96,8 +98,10 @@ - - +
+ + +
@@ -136,10 +140,12 @@ - - +
+ + +
@@ -252,12 +258,12 @@
+ label="Sync" size="small" /> + label="Go To" size="small" /> - + label="Slew" size="small" /> +
@@ -267,13 +273,13 @@ -
- +
+ + {{ name ?? '' }} -
@@ -308,7 +314,7 @@
+ styleClass="border-0" [autoDisplayFirst]="false">
{{ item | enum }} @@ -326,7 +332,7 @@
+ styleClass="border-0" [autoDisplayFirst]="false">
{{ item | enum }} @@ -347,7 +353,7 @@
- + @@ -361,8 +367,9 @@
- - + + diff --git a/desktop/src/app/atlas/atlas.component.scss b/desktop/src/app/atlas/atlas.component.scss index 83e56ac40..d304e8880 100644 --- a/desktop/src/app/atlas/atlas.component.scss +++ b/desktop/src/app/atlas/atlas.component.scss @@ -9,16 +9,16 @@ padding-right: 0.21rem; p-table.planet .p-datatable-wrapper { - height: calc(100vh - 301px); + height: 229px; } p-table.minorPlanet .p-datatable-wrapper { - height: calc(100vh - 343px); + height: 189px; } p-table.skyObject .p-datatable-wrapper, p-table.satellite .p-datatable-wrapper { - height: calc(100vh - 389px); + height: 143px; } .p-tabview-nav li { diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 76a4d024e..ed1e9c9dc 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -80,7 +80,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, refreshing = false tab = SkyAtlasTab.SUN - readonly bodyPosition = Object.assign({}, EMPTY_BODY_POSITION) + readonly bodyPosition = structuredClone(EMPTY_BODY_POSITION) moonIlluminated = 1 moonWaning = false @@ -136,7 +136,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, skyObject?: DeepSkyObject skyObjectItems: DeepSkyObject[] = [] skyObjectSearchText = '' - readonly skyObjectFilter = Object.assign({}, EMPTY_SEARCH_FILTER) + readonly skyObjectFilter = structuredClone(EMPTY_SEARCH_FILTER) showSkyObjectFilter = false readonly constellationOptions: (Constellation | 'ALL')[] = ['ALL', ...CONSTELLATIONS] diff --git a/desktop/src/app/calculator/calculator.component.html b/desktop/src/app/calculator/calculator.component.html new file mode 100644 index 000000000..971a39546 --- /dev/null +++ b/desktop/src/app/calculator/calculator.component.html @@ -0,0 +1,24 @@ +
+
+ + +
+ {{ item?.formula?.title }} +
+
+ +
+ {{ item.formula.title }} + {{ item.formula.description }} +
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/desktop/src/app/calculator/calculator.component.scss b/desktop/src/app/calculator/calculator.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/desktop/src/app/calculator/calculator.component.ts b/desktop/src/app/calculator/calculator.component.ts new file mode 100644 index 000000000..d67c42912 --- /dev/null +++ b/desktop/src/app/calculator/calculator.component.ts @@ -0,0 +1,208 @@ +import { Component, Type } from '@angular/core' +import { ElectronService } from '../../shared/services/electron.service' +import { CalculatorFormula } from '../../shared/types/calculator.types' +import { AppComponent } from '../app.component' +import { FormulaComponent } from './formula/formula.component' + +@Component({ + selector: 'app-calculator', + templateUrl: './calculator.component.html', + styleUrls: ['./calculator.component.scss'], +}) +export class CalculatorComponent { + + readonly formulae: { component: Type, formula: CalculatorFormula }[] = [ + { + component: FormulaComponent, + formula: { + title: 'Focal Length', + description: 'Calculate the focal length of your telescope.', + expression: 'Aperture * Focal Ratio', + operands: [ + { + label: 'Aperture', + suffix: 'mm', + }, + { + label: 'Focal Ratio', + prefix: 'f/', + }, + ], + result: { + label: 'Focal Length', + suffix: 'mm', + }, + calculate: (aperture, focalRatio) => { + if (aperture && focalRatio) { + return aperture * focalRatio + } + }, + }, + }, + { + component: FormulaComponent, + formula: { + title: 'Focal Ratio', + description: 'Calculate the focal ratio of your telescope.', + expression: 'Focal Length / Aperture', + operands: [ + { + label: 'Focal Length', + suffix: 'mm', + }, + { + label: 'Aperture', + suffix: 'mm', + }, + ], + result: { + label: 'Focal Length', + prefix: 'f/', + }, + calculate: (focalLength, aperture) => { + if (focalLength && aperture) { + return focalLength / aperture + } + }, + }, + }, + { + component: FormulaComponent, + formula: { + title: 'Dawes Limit', + description: 'Calculate the maximum resolving power of your telescope using the Dawes Limit formula.', + expression: '116 / Aperture', + operands: [ + { + label: 'Aperture', + suffix: 'mm', + }, + ], + result: { + label: 'Max. Resolution', + suffix: 'arcsec', + }, + calculate: (aperture) => { + if (aperture) { + return 116 / aperture + } + }, + }, + }, + { + component: FormulaComponent, + formula: { + title: 'Rayleigh Limit', + description: 'Calculate the maximum resolving power of your telescope using the Rayleigh Limit formula.', + expression: '138 / Aperture', + operands: [ + { + label: 'Aperture', + suffix: 'mm', + }, + ], + result: { + label: 'Max. Resolution', + suffix: 'arcsec', + }, + calculate: (aperture) => { + if (aperture) { + return 138 / aperture + } + }, + }, + }, + { + component: FormulaComponent, + formula: { + title: 'Limiting Magnitude', + description: 'Calculate a telescopes approximate limiting magnitude.', + expression: '2.7 + (5 * Log(Aperture))', // 7.7 + (5 * Log(Telescope Aperture(cm))) + operands: [ + { + label: 'Aperture', + suffix: 'mm', + }, + ], + result: { + label: 'Limiting Magnitude', + }, + calculate: (aperture) => { + if (aperture) { + return 2.7 + 5 * Math.log10(aperture) + } + }, + }, + }, + { + component: FormulaComponent, + formula: { + title: 'Light Grasp Ratio', + description: 'Calculate the light grasp ratio between two telescopes.', + expression: 'Larger Apertureยฒ / Smaller Apertureยฒ', + operands: [ + { + label: 'Larger Aperture', + suffix: 'mm', + }, + { + label: 'Smaller Aperture', + suffix: 'mm', + }, + ], + result: { + label: 'Ratio', + }, + calculate: (larger, smaller) => { + if (larger && smaller) { + return Math.pow(larger, 2) / Math.pow(smaller, 2) + } + }, + tip: 'Compare against the human eye by putting 7 in the smaller telescope aperture box. 7mm is the aproximate maximum aperture of the human eye.' + }, + }, + { + component: FormulaComponent, + formula: { + title: 'CCD Resolution', + description: 'Calculate the resoution in arc seconds per pixel of a CCD with a particular telescope.', + expression: '(Pixel Size / Focal Length) * 206.265', + operands: [ + { + label: 'Pixel Size', + suffix: 'ยตm', + }, + { + label: 'Focal Length', + suffix: 'mm', + }, + ], + result: { + label: 'Resolution', + suffix: `"/pixel`, + }, + calculate: (pixelSize, focalLength) => { + if (pixelSize && focalLength) { + return pixelSize / focalLength * 206.265 + } + }, + }, + }, + ] + + formula = this.formulae[0] + + private autoResizeTimeout: any + + constructor( + app: AppComponent, + private electron: ElectronService, + ) { + app.title = 'Calculator' + } + + formulaChanged() { + clearTimeout(this.autoResizeTimeout) + this.autoResizeTimeout = this.electron.autoResizeWindow() + } +} \ No newline at end of file diff --git a/desktop/src/app/calculator/formula/formula.component.html b/desktop/src/app/calculator/formula/formula.component.html new file mode 100644 index 000000000..e3fea9186 --- /dev/null +++ b/desktop/src/app/calculator/formula/formula.component.html @@ -0,0 +1,38 @@ +

{{ formula.description }}

+
+ +
+
+ @for (item of formula.operands; track $index) { +
+
+ {{ item.prefix }} +
+
+ + + + +
+
+ {{ item.suffix }} +
+
+ } +
+

=

+
+ {{ formula.result.prefix }} + + + + + {{ formula.result.suffix }} +
+
+ +
\ No newline at end of file diff --git a/desktop/src/app/calculator/formula/formula.component.scss b/desktop/src/app/calculator/formula/formula.component.scss new file mode 100644 index 000000000..07eaf92d8 --- /dev/null +++ b/desktop/src/app/calculator/formula/formula.component.scss @@ -0,0 +1,3 @@ +:host { + width: 100%; +} \ No newline at end of file diff --git a/desktop/src/app/calculator/formula/formula.component.ts b/desktop/src/app/calculator/formula/formula.component.ts new file mode 100644 index 000000000..fac567d59 --- /dev/null +++ b/desktop/src/app/calculator/formula/formula.component.ts @@ -0,0 +1,25 @@ +import { AfterViewInit, Component, Input } from '@angular/core' +import { CalculatorFormula } from '../../../shared/types/calculator.types' + +@Component({ + selector: 'app-formula', + templateUrl: './formula.component.html', + styleUrls: ['./formula.component.scss'], +}) +export class FormulaComponent implements AfterViewInit { + + @Input({ required: true }) + readonly formula!: CalculatorFormula + + ngAfterViewInit() { + + } + + calculateFormula() { + const result = this.formula.calculate(...this.formula.operands.map(e => e.value)) + + if (result !== undefined) { + this.formula.result.value = result + } + } +} \ No newline at end of file diff --git a/desktop/src/app/calibration/calibration.component.html b/desktop/src/app/calibration/calibration.component.html index 18b290ced..7b4ebf8ee 100644 --- a/desktop/src/app/calibration/calibration.component.html +++ b/desktop/src/app/calibration/calibration.component.html @@ -36,8 +36,9 @@

Frames

- +
+ pTooltip="View image" tooltipPosition="bottom" size="small" /> + pTooltip="Replace" tooltipPosition="bottom" size="small" /> --> + pTooltip="Delete" tooltipPosition="bottom" size="small" />
diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 2fcb30834..5edb6afc9 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -12,13 +12,13 @@
+ pTooltip="View image" tooltipPosition="bottom" size="small" /> + pTooltip="Calibration" tooltipPosition="bottom" size="small" /> + icon="mdi mdi-dots-vertical" (click)="cameraMenu.show()" size="small" />
-
+
+ class="flex flex-1 align-items-center gap-2 bg-gray-800 border-round px-3 text-overflow-scroll"> Exposure Time - +
@@ -106,9 +106,9 @@
- +
@@ -154,7 +154,7 @@
+ severity="info" size="small" pTooltip="Full size" tooltipPosition="bottom" />
@@ -176,8 +176,9 @@
- +
@@ -191,9 +192,9 @@
- +
@@ -201,11 +202,11 @@
+ severity="success" size="small" /> + severity="danger" size="small" /> + severity="info" size="small" />
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 18af649d2..8ffcdb140 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -5,8 +5,8 @@ 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 { LocalStorageService } from '../../shared/services/local-storage.service' -import { Camera, CameraDialogInput, CameraDialogMode, CameraPreference, CameraStartCapture, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureMode, ExposureTimeUnit, FrameType, cameraPreferenceKey } from '../../shared/types/camera.types' +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 { FilterWheel } from '../../shared/types/wheel.types' import { AppComponent } from '../app.component' @@ -17,7 +17,7 @@ import { AppComponent } from '../app.component' }) export class CameraComponent implements AfterContentInit, OnDestroy { - readonly camera = Object.assign({}, EMPTY_CAMERA) + readonly camera = structuredClone(EMPTY_CAMERA) savePath = '' capturesPath = '' @@ -40,11 +40,19 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } get canExposureTime() { - return this.mode !== 'FLAT_WIZARD' + return this.mode !== 'FLAT_WIZARD' && this.mode !== 'DARV' + } + + get canExposureTimeUnit() { + return this.mode !== 'DARV' + } + + get canExposureAmount() { + return this.mode === 'CAPTURE' } get canFrameType() { - return this.mode !== 'FLAT_WIZARD' + return this.mode !== 'FLAT_WIZARD' && this.mode !== 'DARV' } get canStartOrAbort() { @@ -77,7 +85,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { exposureMode: ExposureMode = 'SINGLE' subFrame = false - readonly request = Object.assign({}, EMPTY_CAMERA_START_CAPTURE) + readonly request = structuredClone(EMPTY_CAMERA_START_CAPTURE) running = false readonly exposureModeOptions: ExposureMode[] = ['SINGLE', 'FIXED', 'LOOP'] @@ -122,14 +130,14 @@ export class CameraComponent implements AfterContentInit, OnDestroy { private api: ApiService, private browserWindow: BrowserWindowService, private electron: ElectronService, - private storage: LocalStorageService, + private preference: PreferenceService, private route: ActivatedRoute, ngZone: NgZone, ) { if (app) app.title = 'Camera' electron.on('CAMERA.UPDATED', event => { - if (event.device.name === this.camera.name) { + if (event.device.id === this.camera.id) { ngZone.run(() => { Object.assign(this.camera, event.device) this.update() @@ -138,15 +146,15 @@ export class CameraComponent implements AfterContentInit, OnDestroy { }) electron.on('CAMERA.DETACHED', event => { - if (event.device.name === this.camera.name) { + if (event.device.id === this.camera.id) { ngZone.run(() => { - Object.assign(this.camera, event.device) + Object.assign(this.camera, EMPTY_CAMERA) }) } }) electron.on('CAMERA.CAPTURE_ELAPSED', event => { - if (event.camera.name === this.camera.name) { + if (event.camera.id === this.camera.id) { ngZone.run(() => { this.running = this.cameraExposure.handleCameraCaptureEvent(event) }) @@ -177,9 +185,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy { if (data) { this.mode = data.mode Object.assign(this.request, data.request) - await this.cameraChanged(this.request.camera) - this.normalizeExposureTimeAndUnit(this.request.exposureTime) + await this.cameraChanged(data.camera) this.loadDefaultsForMode(data.mode) + this.normalizeExposureTimeAndUnit(this.request.exposureTime) } } @@ -189,6 +197,11 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } else if (this.mode === 'FLAT_WIZARD') { this.exposureMode = 'SINGLE' this.request.frameType = 'FLAT' + } else if (mode === 'TPPA') { + this.exposureMode = 'FIXED' + this.request.exposureAmount = 1 + } else if (mode === 'DARV') { + this.exposureTimeUnit = ExposureTimeUnit.SECOND } } @@ -204,6 +217,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy { if (this.app) { this.app.subTitle = camera?.name ?? '' } + if (this.mode !== 'CAPTURE') { + this.app.subTitle += ` ยท ${this.mode}` + } } connect() { @@ -281,7 +297,6 @@ export class CameraComponent implements AfterContentInit, OnDestroy { return { ...this.request, - camera: this.camera, x, y, width, height, exposureTime, exposureAmount, savePath, @@ -297,7 +312,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { this.api.cameraAbortCapture(this.camera) } - private static exposureUnitFactor(unit: ExposureTimeUnit) { + static exposureUnitFactor(unit: ExposureTimeUnit) { switch (unit) { case ExposureTimeUnit.MINUTE: return 1 case ExposureTimeUnit.SECOND: return 60 @@ -306,13 +321,15 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } } - private updateExposureUnit(unit: ExposureTimeUnit) { - if (this.camera.exposureMax) { - const a = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) + private updateExposureUnit(unit: ExposureTimeUnit, from: ExposureTimeUnit = this.exposureTimeUnit) { + const exposureMax = this.camera.exposureMax || 60000000 + + if (exposureMax) { + const a = CameraComponent.exposureUnitFactor(from) const b = CameraComponent.exposureUnitFactor(unit) const exposureTime = Math.trunc(this.request.exposureTime * b / a) const exposureTimeMin = Math.trunc(this.camera.exposureMin * b / 60000000) - const exposureTimeMax = Math.trunc(this.camera.exposureMax * b / 60000000) + const exposureTimeMax = Math.trunc(exposureMax * b / 60000000) this.exposureTimeMax = Math.max(1, exposureTimeMax) this.exposureTimeMin = Math.max(1, exposureTimeMin) this.request.exposureTime = Math.max(this.exposureTimeMin, Math.min(exposureTime, this.exposureTimeMax)) @@ -321,34 +338,33 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } private normalizeExposureTimeAndUnit(exposureTime: number) { - const factors = [ - { unit: ExposureTimeUnit.MINUTE, time: 60000000 }, - { unit: ExposureTimeUnit.SECOND, time: 1000000 }, - { unit: ExposureTimeUnit.MILLISECOND, time: 1000 }, - ] - - for (const { unit, time } of factors) { - if (exposureTime >= time) { - const k = exposureTime / time - - // exposureTime is multiple of time. - if (k === Math.floor(k)) { - this.updateExposureUnit(unit) - return + if (this.canExposureTimeUnit) { + const factors = [ + { unit: ExposureTimeUnit.MINUTE, time: 60000000 }, + { unit: ExposureTimeUnit.SECOND, time: 1000000 }, + { unit: ExposureTimeUnit.MILLISECOND, time: 1000 }, + ] + + for (const { unit, time } of factors) { + if (exposureTime >= time) { + const k = exposureTime / time + + // exposureTime is multiple of time. + if (k === Math.floor(k)) { + this.updateExposureUnit(unit, ExposureTimeUnit.MICROSECOND) + return + } } } + } else { + this.updateExposureUnit(this.exposureTimeUnit, ExposureTimeUnit.MICROSECOND) } } private update() { if (this.camera.name) { if (this.camera.connected) { - this.request.x = Math.max(this.camera.minX, Math.min(this.request.x, this.camera.maxX)) - this.request.y = Math.max(this.camera.minY, Math.min(this.request.y, this.camera.maxY)) - this.request.width = Math.max(this.camera.minWidth, Math.min(this.request.width < 8 ? this.camera.maxWidth : this.request.width, this.camera.maxWidth)) - this.request.height = Math.max(this.camera.minHeight, Math.min(this.request.height < 8 ? this.camera.maxHeight : this.request.width, this.camera.maxHeight)) - if (this.camera.frameFormats.length && (!this.request.frameFormat || !this.camera.frameFormats.includes(this.request.frameFormat))) this.request.frameFormat = this.camera.frameFormats[0] - + updateCameraStartCaptureFromCamera(this.request, this.camera) this.updateExposureUnit(this.exposureTimeUnit) } @@ -367,7 +383,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { private loadPreference() { if (this.mode === 'CAPTURE' && this.camera.name) { - const preference = this.storage.get(cameraPreferenceKey(this.camera), {}) + const preference = this.preference.cameraPreference(this.camera).get() this.request.autoSave = preference.autoSave ?? false this.savePath = preference.savePath ?? '' @@ -400,35 +416,19 @@ export class CameraComponent implements AfterContentInit, OnDestroy { savePreference() { if (this.mode === 'CAPTURE' && this.camera.connected) { const preference: CameraPreference = { - autoSave: this.request.autoSave, - savePath: this.savePath, - autoSubFolderMode: this.request.autoSubFolderMode, + ...this.request, setpointTemperature: this.setpointTemperature, - exposureTime: this.request.exposureTime, exposureTimeUnit: this.exposureTimeUnit, exposureMode: this.exposureMode, - exposureDelay: this.request.exposureDelay, - exposureAmount: this.request.exposureAmount, - x: this.request.x, - y: this.request.y, - width: this.request.width, - height: this.request.height, subFrame: this.subFrame, - binX: this.request.binX, - binY: this.request.binY, - frameType: this.request.frameType, - gain: this.request.gain, - offset: this.request.offset, - frameFormat: this.request.frameFormat, - dither: this.request.dither, } - this.storage.set(cameraPreferenceKey(this.camera), preference) + this.preference.cameraPreference(this.camera).set(preference) } } - static async showAsDialog(window: BrowserWindowService, mode: CameraDialogMode, request: CameraStartCapture) { - const result = await window.openCameraDialog({ data: { mode, request } }) + static async showAsDialog(window: BrowserWindowService, mode: CameraDialogMode, camera: Camera, request: CameraStartCapture) { + const result = await window.openCameraDialog({ data: { mode, camera, request } }) if (result) { Object.assign(request, result) diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index 6ceefce1f..0c0ab0aee 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -1,6 +1,6 @@
-
+
@@ -47,7 +47,7 @@
+ (onClick)="filter && moveTo(filter)" size="small" />
@@ -73,7 +73,7 @@
+ severity="info" size="small" />
\ No newline at end of file diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 145597328..d0ea85152 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -5,9 +5,9 @@ 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 { LocalStorageService } from '../../shared/services/local-storage.service' +import { PreferenceService } from '../../shared/services/preference.service' import { CameraStartCapture, EMPTY_CAMERA_START_CAPTURE } from '../../shared/types/camera.types' -import { EMPTY_WHEEL, FilterSlot, FilterWheel, WheelDialogInput, WheelDialogMode, WheelPreference, wheelPreferenceKey } from '../../shared/types/wheel.types' +import { EMPTY_WHEEL, FilterSlot, FilterWheel, WheelDialogInput, WheelDialogMode, WheelPreference } from '../../shared/types/wheel.types' import { AppComponent } from '../app.component' @Component({ @@ -17,8 +17,8 @@ import { AppComponent } from '../app.component' }) export class FilterWheelComponent implements AfterContentInit, OnDestroy { - readonly wheel = Object.assign({}, EMPTY_WHEEL) - readonly request = Object.assign({}, EMPTY_CAMERA_START_CAPTURE) + readonly wheel = structuredClone(EMPTY_WHEEL) + readonly request = structuredClone(EMPTY_CAMERA_START_CAPTURE) moving = false position = 0 @@ -54,26 +54,30 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { private app: AppComponent, private api: ApiService, private electron: ElectronService, - private storage: LocalStorageService, + private preference: PreferenceService, private route: ActivatedRoute, ngZone: NgZone, ) { if (app) app.title = 'Filter Wheel' electron.on('WHEEL.UPDATED', event => { - if (event.device.name === this.wheel.name) { + if (event.device.id === this.wheel.id) { ngZone.run(() => { const wasConnected = this.wheel.connected Object.assign(this.wheel, event.device) this.update() + + if (wasConnected !== event.device.connected) { + setTimeout(() => electron.autoResizeWindow(), 250) + } }) } }) electron.on('WHEEL.DETACHED', event => { - if (event.device.name === this.wheel.name) { + if (event.device.id === this.wheel.id) { ngZone.run(() => { - Object.assign(this.wheel, event.device) + Object.assign(this.wheel, EMPTY_WHEEL) }) } }) @@ -94,7 +98,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { const request = decodedData as WheelDialogInput Object.assign(this.request, request.request) this.mode = request.mode - this.wheelChanged(this.request.wheel) + this.wheelChanged(request.wheel) } else { this.wheelChanged(decodedData) } @@ -135,17 +139,17 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { shutterToggled(filter: FilterSlot, event: CheckboxChangeEvent) { this.filters.forEach(e => e.dark = event.checked && e === filter) - this.filterChangedPublisher.next(Object.assign({}, filter)) + this.filterChangedPublisher.next(structuredClone(filter)) } filterNameChanged(filter: FilterSlot) { if (filter.name) { - this.filterChangedPublisher.next(Object.assign({}, filter)) + this.filterChangedPublisher.next(structuredClone(filter)) } } focusOffsetChanged(filter: FilterSlot) { - this.filterChangedPublisher.next(Object.assign({}, filter)) + this.filterChangedPublisher.next(structuredClone(filter)) } private update() { @@ -171,7 +175,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { filters = this.filters } - const preference = this.storage.get(wheelPreferenceKey(this.wheel), {}) + const preference = this.preference.wheelPreference(this.wheel).get() for (let position = 1; position <= filters.length; position++) { const name = preference.names?.[position - 1] ?? `Filter #${position}` @@ -187,7 +191,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { private loadPreference() { if (this.mode === 'CAPTURE' && this.wheel.name) { - const preference = this.storage.get(wheelPreferenceKey(this.wheel), {}) + const preference = this.preference.wheelPreference(this.wheel).get() const shutterPosition = preference.shutterPosition ?? 0 this.filters.forEach(e => e.dark = e.position === shutterPosition) } @@ -202,7 +206,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { names: this.filters.map(e => e.name) } - this.storage.set(wheelPreferenceKey(this.wheel), preference) + this.preference.wheelPreference(this.wheel).set(preference) this.api.wheelSync(this.wheel, preference.names!) } } @@ -210,7 +214,6 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { private makeCameraStartCapture(): CameraStartCapture { return { ...this.request, - wheel: this.wheel, filterPosition: this.filter?.position ?? 0, } } @@ -219,8 +222,8 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { this.app.close(this.makeCameraStartCapture()) } - static async showAsDialog(window: BrowserWindowService, mode: WheelDialogMode, request: CameraStartCapture) { - const result = await window.openWheelDialog({ data: { mode, request } }) + static async showAsDialog(window: BrowserWindowService, mode: WheelDialogMode, wheel: FilterWheel, request: CameraStartCapture) { + const result = await window.openWheelDialog({ data: { mode, wheel, request } }) if (result) { Object.assign(request, result) diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.html b/desktop/src/app/flat-wizard/flat-wizard.component.html index 027618aa6..0b82d55d5 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.html +++ b/desktop/src/app/flat-wizard/flat-wizard.component.html @@ -2,21 +2,21 @@
- - +
- - -
@@ -26,7 +26,7 @@
- @@ -34,7 +34,7 @@
- @@ -42,7 +42,7 @@
- @@ -50,7 +50,7 @@
- @@ -58,20 +58,20 @@
- + [maxSelectedLabels]="wheel.count" scrollHeight="105px" />
- - + +
\ No newline at end of file diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index 5b2ddec4a..71bc72924 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -1,13 +1,14 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' -import { MessageService } from 'primeng/api' 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 { LocalStorageService } from '../../shared/services/local-storage.service' -import { Camera, EMPTY_CAMERA_START_CAPTURE } from '../../shared/types/camera.types' +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' import { FlatWizardRequest } from '../../shared/types/flat-wizard.types' -import { FilterSlot, FilterWheel, WheelPreference, wheelPreferenceKey } from '../../shared/types/wheel.types' +import { EMPTY_WHEEL, FilterSlot, FilterWheel } from '../../shared/types/wheel.types' +import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' import { CameraComponent } from '../camera/camera.component' @@ -19,10 +20,10 @@ import { CameraComponent } from '../camera/camera.component' export class FlatWizardComponent implements AfterViewInit, OnDestroy { cameras: Camera[] = [] - camera?: Camera + camera = structuredClone(EMPTY_CAMERA) wheels: FilterWheel[] = [] - wheel?: FilterWheel + wheel = structuredClone(EMPTY_WHEEL) running = false @@ -35,7 +36,7 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { private readonly selectedFiltersMap = new Map() readonly request: FlatWizardRequest = { - captureRequest: Object.assign({}, EMPTY_CAMERA_START_CAPTURE), + captureRequest: structuredClone(EMPTY_CAMERA_START_CAPTURE), exposureMin: 1, exposureMax: 2000, meanTarget: 32768, @@ -55,51 +56,111 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { private api: ApiService, electron: ElectronService, private browserWindow: BrowserWindowService, - private storage: LocalStorageService, - private message: MessageService, + private prime: PrimeService, + private preference: PreferenceService, ngZone: NgZone, ) { app.title = 'Flat Wizard' electron.on('FLAT_WIZARD.ELAPSED', event => { - if (event.capture) { + if (event.state === 'EXPOSURING' && event.capture && event.capture.camera?.id === this.camera?.id) { ngZone.run(() => { - this.cameraExposure.handleCameraCaptureEvent(event.capture!) + this.running = this.cameraExposure.handleCameraCaptureEvent(event.capture!, true) + }) + } else if (event.state === 'CAPTURED') { + ngZone.run(() => { + this.running = false + this.prime.message(`Flat frame saved at ${event.savedPath}`) + }) + } else if (event.state === 'FAILED') { + ngZone.run(() => { + this.running = false + this.prime.message(`Failed to find an optimal exposure time from given parameters`, 'error') + }) + } + + if (!this.running) { + ngZone.run(() => { + this.cameraExposure.reset() + }) + } + }) - if (event.capture!.state === 'CAPTURE_STARTED') { - this.running = true - } else if (event.capture!.state === 'CAPTURE_FINISHED' && event.capture!.aborted) { - this.running = false + electron.on('CAMERA.UPDATED', event => { + if (event.device.id === this.camera.id) { + ngZone.run(() => { + Object.assign(this.camera, event.device) + this.cameraChanged() + }) + } + }) + + 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) + this.cameras.sort(deviceComparator) + } + }) + }) + + electron.on('WHEEL.UPDATED', event => { + if (event.device.id === this.wheel.id) { + ngZone.run(() => { + Object.assign(this.wheel, event.device) + this.wheelChanged() }) } }) - electron.on('FLAT_WIZARD.FRAME_CAPTURED', event => { + electron.on('WHEEL.ATTACHED', event => { ngZone.run(() => { - this.running = false - this.message.add({ severity: 'success', detail: `Flat frame saved at ${event.savedPath}` }) + this.wheels.push(event.device) + this.wheels.sort(deviceComparator) }) }) - electron.on('FLAT_WIZARD.FAILED', event => { + electron.on('WHEEL.DETACHED', event => { ngZone.run(() => { - this.running = false - this.message.add({ severity: 'error', detail: `Failed to find an optimal exposure time from given parameters` }) + const index = this.wheels.findIndex(e => e.id === event.device.id) + + if (index >= 0) { + if (this.wheels[index] === this.wheel) { + Object.assign(this.wheel, this.wheels[0] ?? EMPTY_WHEEL) + } + + this.wheels.splice(index, 1) + this.wheels.sort(deviceComparator) + } }) }) } async ngAfterViewInit() { - this.cameras = await this.api.cameras() - this.wheels = await this.api.wheels() + this.cameras = (await this.api.cameras()).sort(deviceComparator) + this.wheels = (await this.api.wheels()).sort(deviceComparator) } @HostListener('window:unload') ngOnDestroy() { } async showCameraDialog() { - CameraComponent.showAsDialog(this.browserWindow, 'FLAT_WIZARD', this.request.captureRequest) + if (this.camera.name && await CameraComponent.showAsDialog(this.browserWindow, 'FLAT_WIZARD', this.camera, this.request.captureRequest)) { + this.preference.cameraStartCaptureForFlatWizard(this.camera).set(this.request.captureRequest) + } } cameraChanged() { @@ -107,32 +168,22 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } wheelConnect() { - if (this.wheel?.connected) { + if (this.wheel.connected) { this.api.wheelDisconnect(this.wheel) } else { - this.api.wheelConnect(this.wheel!) + this.api.wheelConnect(this.wheel) } } private updateEntryFromCamera(camera?: Camera) { if (camera) { - const request = this.request.captureRequest - - request.camera = camera + const request = this.preference.cameraStartCaptureForFlatWizard(camera).get(this.request.captureRequest) if (camera.connected) { - if (camera.maxX > 1) request.x = Math.max(camera.minX, Math.min(request.x, camera.maxX)) - if (camera.maxY > 1) request.y = Math.max(camera.minY, Math.min(request.y, camera.maxY)) - - if (camera.maxWidth > 1 && (request.width <= 0 || request.width > camera.maxWidth)) request.width = camera.maxWidth - if (camera.maxHeight > 1 && (request.height <= 0 || request.height > camera.maxHeight)) request.height = camera.maxHeight - - if (camera.maxBinX > 1) request.binX = Math.max(1, Math.min(request.binX, camera.maxBinX)) - if (camera.maxBinY > 1) request.binY = Math.max(1, Math.min(request.binY, camera.maxBinY)) - if (camera.gainMax) request.gain = Math.max(camera.gainMin, Math.min(request.gain, camera.gainMax)) - if (camera.offsetMax) request.offset = Math.max(camera.offsetMin, Math.min(request.offset, camera.offsetMax)) - if (!request.frameFormat || !camera.frameFormats.includes(request.frameFormat)) request.frameFormat = camera.frameFormats[0] + updateCameraStartCaptureFromCamera(request, camera) } + + this.request.captureRequest = request } } @@ -150,7 +201,7 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { filters = this.filters } - const preference = this.storage.get(wheelPreferenceKey(this.wheel), {}) + const preference = this.preference.wheelPreference(this.wheel).get() for (let position = 1; position <= filters.length; position++) { const name = preference.names?.[position - 1] ?? `Filter #${position}` @@ -166,13 +217,13 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy { } async start() { - await this.browserWindow.openCameraImage(this.camera!, 'FLAT_WIZARD') + await this.browserWindow.openCameraImage(this.camera, 'FLAT_WIZARD') // TODO: Iniciar para cada filtro selecionado. Usar os eventos para percorrer (se houver filtro). // Se Falhar, interrompe todo o fluxo. - this.api.flatWizardStart(this.camera!, this.request) + this.api.flatWizardStart(this.camera, this.request) } stop() { - this.api.flatWizardStop(this.camera!) + this.api.flatWizardStop(this.camera) } } diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html index 367b9011a..cb1a400f1 100644 --- a/desktop/src/app/focuser/focuser.component.html +++ b/desktop/src/app/focuser/focuser.component.html @@ -1,6 +1,6 @@
-
+
@@ -16,10 +16,10 @@
-
+
- +
@@ -29,9 +29,11 @@
-
+
+
-
\ No newline at end of file diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index 3f519b6f3..9e7f62522 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -1,5 +1,6 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' 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 { LocalStorageService } from '../../shared/services/local-storage.service' @@ -13,7 +14,7 @@ import { AppComponent } from '../app.component' }) export class FocuserComponent implements AfterViewInit, OnDestroy { - readonly focuser = Object.assign({}, EMPTY_FOCUSER) + readonly focuser = structuredClone(EMPTY_FOCUSER) moving = false position = 0 @@ -23,7 +24,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { canRelativeMove = false canAbort = false canReverse = false - reverse = false + reversed = false canSync = false hasBacklash = false maxPosition = 0 @@ -42,7 +43,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { app.title = 'Focuser' electron.on('FOCUSER.UPDATED', event => { - if (event.device.name === this.focuser.name) { + if (event.device.id === this.focuser.id) { ngZone.run(() => { Object.assign(this.focuser, event.device) this.update() @@ -51,12 +52,27 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { }) electron.on('FOCUSER.DETACHED', event => { - if (event.device.name === this.focuser.name) { + if (event.device.id === this.focuser.id) { ngZone.run(() => { - Object.assign(this.focuser, event.device) + Object.assign(this.focuser, EMPTY_FOCUSER) }) } }) + + 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.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.maxPosition, this.stepsAbsolute + 1) }) } async ngAfterViewInit() { @@ -93,27 +109,35 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { } } - moveIn() { - this.moving = true - this.api.focuserMoveIn(this.focuser, this.stepsRelative) - this.savePreference() + moveIn(stepSize: number = 1) { + if (!this.moving) { + this.moving = true + this.api.focuserMoveIn(this.focuser, Math.trunc(this.stepsRelative * stepSize)) + this.savePreference() + } } - moveOut() { - this.moving = true - this.api.focuserMoveOut(this.focuser, this.stepsRelative) - this.savePreference() + moveOut(stepSize: number = 1) { + if (!this.moving) { + this.moving = true + this.api.focuserMoveOut(this.focuser, Math.trunc(this.stepsRelative * stepSize)) + this.savePreference() + } } moveTo() { - this.moving = true - this.api.focuserMoveTo(this.focuser, this.stepsAbsolute) - this.savePreference() + if (!this.moving && this.stepsAbsolute !== this.position) { + this.moving = true + this.api.focuserMoveTo(this.focuser, this.stepsAbsolute) + this.savePreference() + } } sync() { - this.api.focuserSync(this.focuser, this.stepsAbsolute) - this.savePreference() + if (!this.moving) { + this.api.focuserSync(this.focuser, this.stepsAbsolute) + this.savePreference() + } } abort() { @@ -121,7 +145,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { } private update() { - if (!this.focuser) { + if (!this.focuser.name) { return } @@ -133,7 +157,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { this.canRelativeMove = this.focuser.canRelativeMove this.canAbort = this.focuser.canAbort this.canReverse = this.focuser.canReverse - this.reverse = this.focuser.reverse + this.reversed = this.focuser.reversed this.canSync = this.focuser.canSync this.hasBacklash = this.focuser.hasBacklash this.maxPosition = this.focuser.maxPosition diff --git a/desktop/src/app/framing/framing.component.html b/desktop/src/app/framing/framing.component.html index 1274870e9..e0811d2ac 100644 --- a/desktop/src/app/framing/framing.component.html +++ b/desktop/src/app/framing/framing.component.html @@ -41,16 +41,17 @@
- +
- {{ item.regime }} - {{ item.id }} + {{ item?.regime }} ({{ item?.skyFraction | percent:'1.1-1' }}) + {{ item?.id }}
- {{ item.regime }} + {{ item.regime }} ({{ item?.skyFraction | percent:'1.1-1' }}) {{ item.id }}
@@ -59,7 +60,7 @@
- +
Made use of { ngZone.run(() => this.frameFromData(event)) }) - } - ngAfterViewInit() { this.loadPreference() + } + + async ngAfterViewInit() { + this.hipsSurveys = await this.api.hipsSurveys() + + if (this.hipsSurvey) { + this.hipsSurvey = this.hipsSurveys.find(e => e.id === this.hipsSurvey!.id) + } + + if (!this.hipsSurvey) { + this.hipsSurvey = this.hipsSurveys[0] + } + + this.electron.autoResizeWindow() this.route.queryParams.subscribe(e => { const data = JSON.parse(decodeURIComponent(e.data)) as FramingData @@ -130,7 +141,11 @@ export class FramingComponent implements AfterViewInit, OnDestroy { this.height = preference.height ?? 720 this.fov = preference.fov ?? 1 this.rotation = preference.rotation ?? 0 - this.hipsSurvey ??= preference.hipsSurvey ?? this.hipsSurvey + + if (preference.hipsSurvey) { + this.hipsSurveys = [preference.hipsSurvey] + this.hipsSurvey = this.hipsSurveys[0] + } } private savePreference() { diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 5dc034e14..34a0758e6 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -128,11 +128,11 @@
+ icon="mdi mdi-play" severity="success" size="small" /> --> + severity="info" size="small" /> + severity="danger" size="small" />
@@ -141,7 +141,7 @@
+ [autoDisplayFirst]="false" styleClass="p-inputtext-sm border-0" emptyMessage="No guide output found" /> diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index a8036664f..70ff30f5e 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -221,7 +221,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { title.setTitle('Guider') electron.on('GUIDE_OUTPUT.UPDATED', event => { - if (event.device.name === this.guideOutput?.name) { + if (event.device.id === this.guideOutput?.id) { ngZone.run(() => { Object.assign(this.guideOutput!, event.device) this.update() @@ -237,7 +237,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { electron.on('GUIDE_OUTPUT.DETACHED', event => { ngZone.run(() => { - const index = this.guideOutputs.findIndex(e => e.name === event.device.name) + const index = this.guideOutputs.findIndex(e => e.id === event.device.id) if (index >= 0) this.guideOutputs.splice(index, 1) }) }) diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 91994d46e..05c0a890b 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -1,42 +1,41 @@
-
+
- +
- {{ item.host }} + {{ item?.name }}
-
-
- {{ item.host }}:{{ item.port }} - +
+
+ {{ item.name }} + {{ item.host }}:{{ item.port }} +
+ + {{ (item.connectedAt | date:'yyyy-MM-dd HH:mm:ss') ?? 'never' }} +
-
- - {{ item.connectedAt | date:'yyyy-MM-dd HH:mm:ss' }} +
+ +
- - -
-
- - - + - - + + +
-
+
@@ -136,6 +135,12 @@
INDI
+
+ + +
Calculator
+
+
@@ -151,5 +156,37 @@
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+ + + +
+ \ No newline at end of file diff --git a/desktop/src/app/home/home.component.scss b/desktop/src/app/home/home.component.scss index ab73ace58..ef0bd1a18 100644 --- a/desktop/src/app/home/home.component.scss +++ b/desktop/src/app/home/home.component.scss @@ -1,19 +1,28 @@ -p-button ::ng-deep { - display: contents; +:host { + p-splitbutton ::ng-deep { + .p-splitbutton-menubutton { + width: 0rem; + padding: 4px; + } + } - &.open { - min-height: 56px; - max-height: 56px; - display: flex; + p-button ::ng-deep { + display: contents; - img { - height: 32px; + &.open { + min-height: 56px; + max-height: 56px; + display: flex; + + img { + height: 32px; + } } - } - &.p-disabled { - img { - filter: grayscale(1); + &.p-disabled { + img { + filter: grayscale(1); + } } } } diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index a17693914..baaa2922f 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,20 +1,19 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import path from 'path' import { MenuItem, MessageService } from 'primeng/api' -import { AutoCompleteCompleteEvent } from 'primeng/autocomplete' import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { DialogMenuComponent } from '../../shared/components/dialog-menu/dialog-menu.component' 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 { PreferenceService } from '../../shared/services/preference.service' import { Camera } from '../../shared/types/camera.types' import { Device } from '../../shared/types/device.types' import { Focuser } from '../../shared/types/focuser.types' -import { ConnectionDetails, EMPTY_CONNECTION_DETAILS, HomeWindowType } from '../../shared/types/home.types' +import { CONNECTION_TYPES, ConnectionDetails, EMPTY_CONNECTION_DETAILS, HomeWindowType } from '../../shared/types/home.types' import { Mount } from '../../shared/types/mount.types' import { FilterWheel } from '../../shared/types/wheel.types' -import { compareDevice } from '../../shared/utils/comparators' +import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' type MappedDevice = { @@ -24,9 +23,6 @@ type MappedDevice = { 'WHEEL': FilterWheel } -export const IMAGE_DIR_KEY = 'home.image.directory' -export const LAST_CONNECTED_HOSTS_KEY = 'home.lastConnectedHosts' - @Component({ selector: 'app-home', templateUrl: './home.component.html', @@ -40,9 +36,11 @@ export class HomeComponent implements AfterContentInit, OnDestroy { @ViewChild('imageMenu') private readonly imageMenu!: DeviceListMenuComponent - connected = false - lastConnectedHosts: ConnectionDetails[] = [] - connection: ConnectionDetails + readonly connectionTypes = Array.from(CONNECTION_TYPES) + showConnectionDialog = false + readonly connections: ConnectionDetails[] = [] + connection?: ConnectionDetails + newConnection?: [ConnectionDetails, ConnectionDetails | undefined] cameras: Camera[] = [] mounts: Mount[] = [] @@ -52,6 +50,10 @@ export class HomeComponent implements AfterContentInit, OnDestroy { rotators: Camera[] = [] switches: Camera[] = [] + get connected() { + return !!this.connection && this.connection.connected + } + get hasCamera() { return this.cameras.length > 0 } @@ -137,7 +139,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { private browserWindow: BrowserWindowService, private api: ApiService, private message: MessageService, - private storage: LocalStorageService, + private preference: PreferenceService, private ngZone: NgZone, ) { app.title = 'Nebulosa' @@ -147,7 +149,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { return this.cameras.push(device) }, (device) => { - this.cameras.splice(this.cameras.findIndex(e => e.name === device.name), 1) + this.cameras.splice(this.cameras.findIndex(e => e.id === device.id), 1) return this.cameras.length }, ) @@ -157,7 +159,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { return this.mounts.push(device) }, (device) => { - this.mounts.splice(this.mounts.findIndex(e => e.name === device.name), 1) + this.mounts.splice(this.mounts.findIndex(e => e.id === device.id), 1) return this.mounts.length }, ) @@ -167,7 +169,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { return this.focusers.push(device) }, (device) => { - this.focusers.splice(this.focusers.findIndex(e => e.name === device.name), 1) + this.focusers.splice(this.focusers.findIndex(e => e.id === device.id), 1) return this.focusers.length }, ) @@ -177,13 +179,22 @@ export class HomeComponent implements AfterContentInit, OnDestroy { return this.wheels.push(device) }, (device) => { - this.wheels.splice(this.wheels.findIndex(e => e.name === device.name), 1) + this.wheels.splice(this.wheels.findIndex(e => e.id === device.id), 1) return this.wheels.length }, ) - this.lastConnectedHosts = storage.get(LAST_CONNECTED_HOSTS_KEY, []) - this.connection = Object.assign({}, this.lastConnectedHosts[0] ?? EMPTY_CONNECTION_DETAILS) + electron.on('CONNECTION.CLOSED', event => { + if (this.connection?.id === event.id) { + ngZone.run(() => { + this.updateConnection() + }) + } + }) + + this.connections = preference.connections.get() + this.connections.forEach(e => e.connected = false) + this.connection = this.connections[0] } async ngAfterContentInit() { @@ -198,48 +209,57 @@ export class HomeComponent implements AfterContentInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { } - hostChanged(event: string | ConnectionDetails) { - if (typeof event === 'string') { - this.connection.host = event - } else { - Object.assign(this.connection, event) - } + addConnection() { + this.newConnection = [structuredClone(EMPTY_CONNECTION_DETAILS), undefined] + this.showConnectionDialog = true } - removeConnection(connection: ConnectionDetails, event: MouseEvent) { - const { host, port } = connection - const index = this.lastConnectedHosts.findIndex(e => e.host === host && e.port === port) - - if (index >= 0) { - this.lastConnectedHosts.splice(index, 1) - this.storage.set(LAST_CONNECTED_HOSTS_KEY, this.lastConnectedHosts) - } - + editConnection(connection: ConnectionDetails, event: MouseEvent) { + this.newConnection = [structuredClone(connection), connection] + this.showConnectionDialog = true event.stopImmediatePropagation() } - async connect() { - try { - if (this.connected) { - await this.api.disconnect() - } else { - let { host, port } = this.connection + deleteConnection(connection: ConnectionDetails, event: MouseEvent) { + const index = this.connections.findIndex(e => e === connection) - host ||= 'localhost' - port ||= 7624 + if (index >= 0 && !connection.connected) { + this.connections.splice(index, 1) - await this.api.connect(host, port) + if (connection === this.connection) { + this.connection = undefined + } - const index = this.lastConnectedHosts.findIndex(e => e.host === host && e.port === port) + this.preference.connections.set(this.connections) + } - if (index >= 0) { - this.lastConnectedHosts.splice(index, 1) - } + event.stopImmediatePropagation() + } - this.lastConnectedHosts.splice(0, 0, Object.assign({}, this.connection)) - this.lastConnectedHosts[0].connectedAt = Date.now() + saveConnection() { + if (this.newConnection) { + // Edit. + if (this.newConnection[1]) { + Object.assign(this.newConnection[1], this.newConnection[0]) + } + // New. + else { + this.connections.push(this.newConnection[0]) + } + } - this.storage.set(LAST_CONNECTED_HOSTS_KEY, this.lastConnectedHosts) + this.preference.connections.set(this.connections) + + this.newConnection = undefined + this.showConnectionDialog = false + } + + async connect() { + try { + if (this.connection && this.connection.connected) { + await this.api.disconnect(this.connection.id!) + } else if (this.connection) { + this.connection.id = await this.api.connect(this.connection.host, this.connection.port, this.connection.type) } } catch (e) { console.error(e) @@ -250,10 +270,6 @@ export class HomeComponent implements AfterContentInit, OnDestroy { } } - filterLastConnected(event: AutoCompleteCompleteEvent) { - - } - private openDevice(type: K) { this.deviceModel.length = 0 @@ -266,7 +282,7 @@ 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(compareDevice)) { + for (const device of [...devices].sort(deviceComparator)) { this.deviceModel.push({ icon: 'mdi mdi-connection', label: device.name, @@ -298,11 +314,11 @@ export class HomeComponent implements AfterContentInit, OnDestroy { private async openImage(force: boolean = false) { if (force || this.cameras.length === 0) { - const defaultPath = this.storage.get(IMAGE_DIR_KEY, '') + const defaultPath = this.preference.homeImageDefaultDirectory.get() const filePath = await this.electron.openFits({ defaultPath }) if (filePath) { - this.storage.set(IMAGE_DIR_KEY, path.dirname(filePath)) + this.preference.homeImageDefaultDirectory.set(path.dirname(filePath)) this.browserWindow.openImage({ path: filePath, source: 'PATH' }) } } else { @@ -349,6 +365,9 @@ export class HomeComponent implements AfterContentInit, OnDestroy { case 'SETTINGS': this.browserWindow.openSettings() break + case 'CALCULATOR': + this.browserWindow.openCalculator() + break case 'ABOUT': this.browserWindow.openAbout() break @@ -356,20 +375,28 @@ export class HomeComponent implements AfterContentInit, OnDestroy { } private async updateConnection() { - try { - this.connected = await this.api.connectionStatus() - } catch { - this.connected = false - } - - if (!this.connected) { - this.cameras = [] - this.mounts = [] - this.focusers = [] - this.wheels = [] - this.domes = [] - this.rotators = [] - this.switches = [] + if (this.connection && this.connection.id) { + try { + const status = await this.api.connectionStatus(this.connection.id!) + + if (status && !this.connection.connected) { + this.connection.connectedAt = Date.now() + this.preference.connections.set(this.connections) + this.connection.connected = true + } else if (!status) { + this.connection.connected = false + } + } catch { + this.connection.connected = false + + this.cameras = [] + this.mounts = [] + this.focusers = [] + this.wheels = [] + this.domes = [] + this.rotators = [] + this.switches = [] + } } } } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index 95e090be3..e2a0b3830 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -30,6 +30,15 @@ + + @for (item of fovs; track $index) { + @if (item.enabled && item.computed) { + + } + } + +
X: {{ roiX }} Y: {{ roiY }} W: {{ roiWidth }} H: {{ roiHeight }}
@@ -68,7 +77,7 @@
- + @@ -129,10 +138,10 @@
- - - - + + + +
@@ -142,7 +151,7 @@
- + @@ -175,56 +184,56 @@
- +
- +
- +
- +
+ [value]="(imageSolved.width.toFixed(2)) + ' x ' + (imageSolved.height.toFixed(2))" />
- +
-
+
- - - - + + + +
- + @@ -261,9 +270,9 @@
- - - + + + @@ -276,7 +285,7 @@
+ [options]="scnrProtectionMethodOptions" styleClass="border-0" [autoDisplayFirst]="false">
{{ item | enum }} @@ -301,7 +310,7 @@
- + @@ -386,7 +395,8 @@
+ [autoDisplayFirst]="false" styleClass="p-inputtext-sm border-0" appendTo="body" + (ngModelChange)="statisticsBitLengthChanged()" />
@@ -396,6 +406,119 @@
+ +
+
+ Telescope +
+
+ +
+
+ + + + +
+
+ + + + +
+
+ Camera Resolution (px) +
+
+ Camera Pixel Size (ยตm) +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +
+ + + +
+
+ @for (item of fovs; track $index) { +
+
+ +
+
+ + + + + + + + @if (item.computed) { + + + + } +
+
+ + +
+
+ } +
+
+
+
X: {{ mouseCoordinate.x }}
diff --git a/desktop/src/app/image/image.component.scss b/desktop/src/app/image/image.component.scss index 29381c256..088176a61 100644 --- a/desktop/src/app/image/image.component.scss +++ b/desktop/src/app/image/image.component.scss @@ -2,6 +2,11 @@ width: 100vw; height: 100vh; display: block; + + svg.fov { + fill: transparent; + stroke-width: 0.25rem; + } } .roi { diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 167affeb8..13236c505 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -1,6 +1,7 @@ import { AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { Interactable } from '@interactjs/types/index' +import hotkeys from 'hotkeys-js' import interact from 'interactjs' import createPanZoom, { PanZoom } from 'panzoom' import * as path from 'path' @@ -12,32 +13,15 @@ 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 { LocalStorageService } from '../../shared/services/local-storage.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 { DetectedStar, EMPTY_IMAGE_SOLVED, FITSHeaderItem, ImageAnnotation, ImageChannel, ImageInfo, ImageSource, ImageStatisticsBitOption, SCNRProtectionMethod, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types' +import { DEFAULT_FOV, DetectedStar, EMPTY_IMAGE_SOLVED, FITSHeaderItem, FOV, ImageAnnotation, ImageChannel, ImageData, ImageInfo, ImagePreference, ImageSolved, ImageStatisticsBitOption, SCNRProtectionMethod, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' -import { EMPTY_PLATE_SOLVER_OPTIONS, PlateSolverType } from '../../shared/types/settings.types' +import { DEFAULT_SOLVER_TYPES } from '../../shared/types/settings.types' import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' -import { SETTINGS_PLATE_SOLVER_KEY } from '../settings/settings.component' - -export function imagePreferenceKey(camera?: Camera) { - return camera ? `image.${camera.name}` : 'image' -} - -export interface ImagePreference { - solverRadius?: number -} - -export interface ImageData { - camera?: Camera - path?: string - source?: ImageSource - title?: string -} @Component({ selector: 'app-image', @@ -89,12 +73,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { solving = false solved = false solverBlind = true - solverCenterRA = '' - solverCenterDEC = '' + solverCenterRA: Angle = '' + solverCenterDEC: Angle = '' solverRadius = 4 - readonly solvedData = Object.assign({}, EMPTY_IMAGE_SOLVED) - readonly solverTypes: PlateSolverType[] = ['ASTAP', 'ASTROMETRY_NET_ONLINE'] - solverType: PlateSolverType + readonly imageSolved = structuredClone(EMPTY_IMAGE_SOLVED) + readonly solverTypes = Array.from(DEFAULT_SOLVER_TYPES) + solverType = this.solverTypes[0] crossHair = false annotations: ImageAnnotation[] = [] @@ -124,6 +108,11 @@ export class ImageComponent implements AfterViewInit, OnDestroy { statisticsBitLength = this.statisticsBitOptions[0] imageInfo?: ImageInfo + showFOVDialog = false + readonly fov = structuredClone(DEFAULT_FOV) + fovs: FOV[] = [] + editedFOV?: FOV + private panZoom?: PanZoom private imageURL!: string private imageMouseX = 0 @@ -250,6 +239,19 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } + private readonly frameAtThisCoordinateMenuItem: MenuItem = { + label: 'Frame at this coordinate', + icon: 'mdi mdi-image', + disabled: true, + command: () => { + const coordinate = this.mouseCoordinateInterpolation?.interpolateAsText(this.imageMouseX, this.imageMouseY, false, false, false) + + if (coordinate) { + this.browserWindow.openFraming({ data: { rightAscension: coordinate.alpha, declination: coordinate.delta } }) + } + }, + } + private readonly crosshairMenuItem: CheckableMenuItem = { label: 'Crosshair', icon: 'mdi mdi-bullseye', @@ -332,6 +334,18 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, } + private readonly fovMenuItem: MenuItem = { + label: 'Field of View', + icon: 'mdi mdi-camera-metering-spot', + command: () => { + this.showFOVDialog = !this.showFOVDialog + + if (this.showFOVDialog) { + this.fovs.forEach(e => this.computeFOV(e)) + } + }, + } + private readonly overlayMenuItem: MenuItem = { label: 'Overlay', icon: 'mdi mdi-layers', @@ -340,6 +354,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.annotationMenuItem, this.detectStarsMenuItem, this.roiMenuItem, + this.fovMenuItem, ] } @@ -361,6 +376,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.fitsHeaderMenuItem, SEPARATOR_MENU_ITEM, this.pointMountHereMenuItem, + this.frameAtThisCoordinateMenuItem, ] mouseCoordinate?: InterpolatedCoordinate & Partial<{ x: number, y: number }> @@ -376,14 +392,14 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private api: ApiService, private electron: ElectronService, private browserWindow: BrowserWindowService, - private storage: LocalStorageService, + private preference: PreferenceService, private prime: PrimeService, private ngZone: NgZone, ) { app.title = 'Image' electron.on('CAMERA.CAPTURE_ELAPSED', async (event) => { - if (event.state === 'EXPOSURE_FINISHED' && event.camera.name === this.imageData.camera?.name) { + if (event.state === 'EXPOSURE_FINISHED' && event.camera.id === this.imageData.camera?.id) { await this.closeImage(event.savePath !== this.imageData.path) ngZone.run(() => { @@ -402,28 +418,17 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }) }) - window.addEventListener('keydown', event => { - if (event.ctrlKey && !event.shiftKey && !event.altKey) { - switch (event.key) { - case 'a': this.toggleStretch(); break - case 'i': this.invertImage(); break - case 'x': this.toggleCrosshair(); break - case '-': this.zoomOut(); break - case '=': this.zoomIn(); break - case '0': this.resetZoom(); break - default: return - } - - event.preventDefault() - } - }, true) + 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() }) - this.solverType = this.storage.get(SETTINGS_PLATE_SOLVER_KEY, EMPTY_PLATE_SOLVER_OPTIONS).type + this.loadPreference() } ngAfterViewInit() { - this.loadPreference() - this.route.queryParams.subscribe(e => { const data = JSON.parse(decodeURIComponent(e.data)) as ImageData this.loadImageFromData(data) @@ -498,6 +503,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { if (data.source === 'FRAMING') { this.disableAutoStretch() + this.resetStretch(false) } else if (data.source === 'FLAT_WIZARD') { this.disableCalibrate(false) } @@ -520,7 +526,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.detectedStarsIsVisible = false this.detectStarsMenuItem.toggleable = false - Object.assign(this.solvedData, EMPTY_IMAGE_SOLVED) + Object.assign(this.imageSolved, EMPTY_IMAGE_SOLVED) this.histogram?.update([]) } @@ -539,8 +545,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { await this.loadImageFromPath(this.imageData.path) } - this.loadPreference(this.imageData.camera) - if (this.imageData.title) { this.app.subTitle = this.imageData.title } else if (this.imageData.camera) { @@ -571,8 +575,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.stretchMidtone = Math.trunc(info.stretchMidtone * 65536) } - this.annotationMenuItem.disabled = !info.solved - this.pointMountHereMenuItem.disabled = !info.solved + this.updateImageSolved(info.solved) + this.fitsHeaders = info.headers if (this.imageURL) window.URL.revokeObjectURL(this.imageURL) @@ -643,11 +647,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.loadImage() } - resetStretch() { + resetStretch(load: boolean = true) { this.stretchShadowHighlight = [0, 65536] this.stretchMidtone = 32768 - this.stretchImage() + if (load) { + this.stretchImage() + } } toggleStretch() { @@ -663,7 +669,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { stretchImage() { this.disableAutoStretch() - this.loadImage() + return this.loadImage() } invertImage() { @@ -717,29 +723,32 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.solving = true try { - const options = this.storage.get(SETTINGS_PLATE_SOLVER_KEY, EMPTY_PLATE_SOLVER_OPTIONS) - options.type = this.solverType - - Object.assign(this.solvedData, - await this.api.solveImage(options, this.imageData.path!, this.solverBlind, - this.solverCenterRA, this.solverCenterDEC, this.solverRadius)) + const options = this.preference.plateSolverOptions(this.solverType).get() + const solved = await this.api.solveImage(options, this.imageData.path!, this.solverBlind, + this.solverCenterRA, this.solverCenterDEC, this.solverRadius) this.savePreference() - - this.solved = true - this.annotationMenuItem.disabled = false - this.pointMountHereMenuItem.disabled = false + this.updateImageSolved(solved) } catch { - this.solved = false - Object.assign(this.solvedData, EMPTY_IMAGE_SOLVED) - this.annotationMenuItem.disabled = true - this.pointMountHereMenuItem.disabled = true + this.updateImageSolved(this.imageInfo?.solved) } finally { this.solving = false this.retrieveCoordinateInterpolation() } } + private updateImageSolved(solved?: ImageSolved) { + this.solved = !!solved + Object.assign(this.imageSolved, solved ?? EMPTY_IMAGE_SOLVED) + this.annotationMenuItem.disabled = !this.solved + this.fovMenuItem.disabled = !this.solved + this.pointMountHereMenuItem.disabled = !this.solved + this.frameAtThisCoordinateMenuItem.disabled = !this.solved + + if (solved) this.fovs.forEach(e => this.computeFOV(e)) + else this.fovs.forEach(e => e.computed = undefined) + } + mountSync(coordinate: EquatorialCoordinateJ2000) { this.executeMount((mount) => { this.api.mountSync(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) @@ -759,7 +768,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } frame(coordinate: EquatorialCoordinateJ2000) { - this.browserWindow.openFraming({ data: { rightAscension: coordinate.rightAscensionJ2000, declination: coordinate.declinationJ2000, fov: this.solvedData!.width / 60, rotation: this.solvedData!.orientation } }) + this.browserWindow.openFraming({ data: { rightAscension: coordinate.rightAscensionJ2000, declination: coordinate.declinationJ2000, fov: this.imageSolved!.width / 60, rotation: this.imageSolved!.orientation } }) } imageLoaded() { @@ -782,17 +791,98 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - private loadPreference(camera?: Camera) { - const preference = this.storage.get(imagePreferenceKey(camera), {}) + addFOV() { + if (this.computeFOV(this.fov)) { + this.fovs.push(structuredClone(this.fov)) + this.preference.imageFOVs.set(this.fovs) + } + } + + editFOV(fov: FOV) { + Object.assign(this.fov, structuredClone(fov)) + this.editedFOV = fov + } + + cancelEditFOV() { + this.editedFOV = undefined + } + + saveFOV() { + if (this.editedFOV && this.computeFOV(this.fov)) { + Object.assign(this.editedFOV, structuredClone(this.fov)) + this.preference.imageFOVs.set(this.fovs) + this.editedFOV = undefined + } + } + + private computeFOV(fov: FOV) { + if (this.imageInfo && this.imageSolved.scale > 0) { + const focalLength = fov.focalLength * (fov.barlowReducer || 1) + + const resolution = { + width: fov.pixelSize.width / focalLength * 206.265, // arcsec/pixel + height: fov.pixelSize.height / focalLength * 206.265, // arcsec/pixel + } + + const svg = { + x: this.imageInfo.width / 2, + y: this.imageInfo.height / 2, + width: fov.cameraSize.width * (resolution.width / this.imageSolved.scale), + height: fov.cameraSize.height * (resolution.height / this.imageSolved.scale), + } + + svg.x += (this.imageInfo.width - svg.width) / 2 + svg.y += (this.imageInfo.height - svg.height) / 2 + + fov.computed = { + cameraResolution: { + width: resolution.width * fov.bin, + height: resolution.height * fov.bin, + }, + focalRatio: focalLength / fov.aperture, + fieldSize: { + width: resolution.width * fov.cameraSize.width / 3600, // deg + height: resolution.height * fov.cameraSize.height / 3600, // deg + }, + svg, + } + + console.info(fov.computed) + + return true + } else { + return false + } + } + + deleteFOV(fov: FOV) { + const index = this.fovs.indexOf(fov) + + if (index >= 0) { + if (this.fovs[index] === this.editedFOV) { + this.editedFOV = undefined + } + + this.fovs.splice(index, 1) + this.preference.imageFOVs.set(this.fovs) + } + } + + private loadPreference() { + const preference = this.preference.imagePreference.get() this.solverRadius = preference.solverRadius ?? this.solverRadius + this.solverType = preference.solverType ?? this.solverTypes[0] + this.fovs = this.preference.imageFOVs.get() + this.fovs.forEach(e => { e.enabled = false; e.computed = undefined }) } private savePreference() { const preference: ImagePreference = { - solverRadius: this.solverRadius + solverRadius: this.solverRadius, + solverType: this.solverType } - this.storage.set(imagePreferenceKey(this.imageData.camera), preference) + this.preference.imagePreference.set(preference) } private async executeMount(action: (mount: Mount) => void) { diff --git a/desktop/src/app/indi/indi.component.html b/desktop/src/app/indi/indi.component.html index 9a0c7baab..ceeea7a75 100644 --- a/desktop/src/app/indi/indi.component.html +++ b/desktop/src/app/indi/indi.component.html @@ -3,14 +3,14 @@
+ styleClass="border-0" emptyMessage="No device found" [autoDisplayFirst]="false" />
- +
@@ -19,7 +19,7 @@
- +
diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 0087b872b..13d2591cb 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -1,10 +1,11 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { MenuItem } from 'primeng/api' +import { Listbox } from 'primeng/listbox' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' import { Device, INDIProperty, INDIPropertyItem, INDISendProperty } from '../../shared/types/device.types' -import { compareDevice, compareText } from '../../shared/utils/comparators' +import { deviceComparator, textComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' @Component({ @@ -23,6 +24,9 @@ export class INDIComponent implements AfterViewInit, OnDestroy { showLog = false messages: string[] = [] + @ViewChild('listbox') + readonly messageListbox!: Listbox + constructor( app: AppComponent, private route: ActivatedRoute, @@ -33,27 +37,32 @@ export class INDIComponent implements AfterViewInit, OnDestroy { app.title = 'INDI' electron.on('DEVICE.PROPERTY_CHANGED', event => { - ngZone.run(() => { - this.addOrUpdateProperty(event.property!) - this.updateGroups() - }) - }) - - electron.on('DEVICE.PROPERTY_DELETED', event => { - const index = this.properties.findIndex((e) => e.name === event.property!.name) - - if (index >= 0) { + if (this.device?.id === event.device.id) { ngZone.run(() => { - this.properties.splice(index, 1) + this.addOrUpdateProperty(event.property!) this.updateGroups() }) } }) + electron.on('DEVICE.PROPERTY_DELETED', event => { + if (this.device?.id === event.device.id) { + const index = this.properties.findIndex((e) => e.name === event.property!.name) + + if (index >= 0) { + ngZone.run(() => { + this.properties.splice(index, 1) + this.updateGroups() + }) + } + } + }) + electron.on('DEVICE.MESSAGE_RECEIVED', event => { - if (this.device && event.device?.name === this.device.name) { + if (this.device && event.device?.id === this.device.id) { ngZone.run(() => { this.messages.splice(0, 0, event.message!) + this.messageListbox.cd.markForCheck() }) } }) @@ -73,9 +82,13 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ...await this.api.mounts(), ...await this.api.focusers(), ...await this.api.wheels(), - ].sort(compareDevice) + ].sort(deviceComparator) this.device = this.devices[0] + + if (this.device) { + this.deviceChanged(this.device) + } } @HostListener('window:unload') @@ -87,13 +100,13 @@ export class INDIComponent implements AfterViewInit, OnDestroy { async deviceChanged(device: Device) { if (this.device) { - this.api.indiStopListening(this.device) + await this.api.indiStopListening(this.device) } this.device = device this.updateProperties() - this.api.indiStartListening(device) + await this.api.indiStartListening(device) this.messages = await this.api.indiLog(device) } @@ -116,10 +129,14 @@ export class INDIComponent implements AfterViewInit, OnDestroy { let groupsChanged = false if (this.groups.length === groups.size) { - let index = 0 - - for (const item of groups) { - if (this.groups[index++].label !== item) { + for (const group of groups) { + if (!this.groups.find(e => e.label === group)) { + groupsChanged = true + break + } + } + for (const group of this.groups) { + if (!groups.has(group.label!)) { groupsChanged = true break } @@ -130,7 +147,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { if (this.groups.length === 0 || groupsChanged) { this.groups = Array.from(groups) - .sort(compareText) + .sort(textComparator) .map(e => { icon: 'mdi mdi-sitemap', label: e, diff --git a/desktop/src/app/indi/property/indi-property.component.html b/desktop/src/app/indi/property/indi-property.component.html index 6016f8293..458bb5a0e 100644 --- a/desktop/src/app/indi/property/indi-property.component.html +++ b/desktop/src/app/indi/property/indi-property.component.html @@ -6,17 +6,17 @@
+ (onClick)="sendSwitch(item)" icon="pi" [severity]="item.value ? 'success' : 'danger'" size="small">
+ (onClick)="sendSwitch(item)" icon="mdi mdi-check" size="small" />
+ (onClick)="sendSwitch(item)" icon="pi" [severity]="item.value ? 'success' : 'danger'" size="small">
@@ -39,7 +39,8 @@
- +
@@ -62,7 +63,7 @@
- +
diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index ab1f75f82..d0f9e7cd3 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -81,7 +81,7 @@
- +
@@ -158,7 +158,7 @@
+ (onClick)="abort()" size="small" />
- - - + + +
+ - -
-
+
@@ -239,13 +239,76 @@
+ severity="success" size="small" /> + severity="danger" size="small" />
-
\ No newline at end of file +
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+ + \ No newline at end of file diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index 3b3aa011b..22b1ef9a8 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -1,16 +1,18 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop' import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, QueryList, ViewChildren } from '@angular/core' -import { MessageService } from 'primeng/api' +import { MenuItem, MessageService } from 'primeng/api' import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' +import { DialogMenuComponent } from '../../shared/components/dialog-menu/dialog-menu.component' 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 { JsonFile } from '../../shared/types/app.types' -import { Camera, CameraCaptureEvent, CameraStartCapture } from '../../shared/types/camera.types' +import { Camera, CameraCaptureElapsed, CameraStartCapture, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { Focuser } from '../../shared/types/focuser.types' -import { EMPTY_SEQUENCE_PLAN, SequenceCaptureMode, SequencePlan, SequencerEvent } from '../../shared/types/sequencer.types' +import { EMPTY_SEQUENCE_PLAN, SEQUENCE_ENTRY_PROPERTIES, SequenceCaptureMode, SequenceEntryProperty, SequencePlan, SequencerElapsed } from '../../shared/types/sequencer.types' import { FilterWheel } from '../../shared/types/wheel.types' +import { deviceComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' import { CameraComponent } from '../camera/camera.component' import { FilterWheelComponent } from '../filterwheel/filterwheel.component' @@ -34,18 +36,65 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { focuser?: Focuser readonly captureModes: SequenceCaptureMode[] = ['FULLY', 'INTERLEAVED'] - readonly plan = Object.assign({}, EMPTY_SEQUENCE_PLAN) + readonly plan = structuredClone(EMPTY_SEQUENCE_PLAN) + + private entryToApply?: CameraStartCapture + private entryToApplyCount: [number, number] = [0, 0] + readonly availableEntryPropertiesToApply = new Map() + showEntryPropertiesToApplyDialog = false + readonly entryMenuModel: MenuItem[] = [ + { + icon: 'mdi mdi-content-copy', + label: 'Apply to all', + command: () => { + this.entryToApplyCount = [-1000, 1000] + this.showEntryPropertiesToApplyDialog = true + } + }, + { + icon: 'mdi mdi-content-copy', + label: 'Apply to all above', + command: () => { + this.entryToApplyCount = [-1000, 0] + this.showEntryPropertiesToApplyDialog = true + } + }, + { + icon: 'mdi mdi-content-copy', + label: 'Apply to above', + command: () => { + this.entryToApplyCount = [-1, 0] + this.showEntryPropertiesToApplyDialog = true + } + }, + { + icon: 'mdi mdi-content-copy', + label: 'Apply to below', + command: () => { + this.entryToApplyCount = [1, 0] + this.showEntryPropertiesToApplyDialog = true + } + }, + { + icon: 'mdi mdi-content-copy', + label: 'Apply to all below', + command: () => { + this.entryToApplyCount = [1000, 0] + this.showEntryPropertiesToApplyDialog = true + } + }, + ] - readonly sequenceEvents: CameraCaptureEvent[] = [] + readonly sequenceEvents: CameraCaptureElapsed[] = [] - event?: SequencerEvent + event?: SequencerElapsed running = false @ViewChildren('cameraExposure') private readonly cameraExposures!: QueryList get canStart() { - return !this.plan.entries.find(e => e.enabled && !e.camera?.connected) + return !!this.camera && this.camera.connected && !!this.plan.entries.find(e => e.enabled) } get savedPath() { @@ -83,7 +132,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { this.savedPathWasModified = false this.storage.delete(SEQUENCER_SAVED_PATH_KEY) - Object.assign(this.plan, EMPTY_SEQUENCE_PLAN) + Object.assign(this.plan, structuredClone(EMPTY_SEQUENCE_PLAN)) this.add() }, }) @@ -123,33 +172,30 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { electron.on('CAMERA.UPDATED', event => { ngZone.run(() => { - const camera = this.cameras.find(e => e.name === event.device.name) + const camera = this.cameras.find(e => e.id === event.device.id) if (camera) { Object.assign(camera, event.device) - this.updateEntriesFromCamera(camera) } }) }) electron.on('WHEEL.UPDATED', event => { ngZone.run(() => { - const wheel = this.wheels.find(e => e.name === event.device.name) + const wheel = this.wheels.find(e => e.id === event.device.id) if (wheel) { Object.assign(wheel, event.device) - this.updateEntriesFromWheel(wheel) } }) }) electron.on('FOCUSER.UPDATED', event => { ngZone.run(() => { - const focuser = this.focusers.find(e => e.name === event.device.name) + const focuser = this.focusers.find(e => e.id === event.device.id) if (focuser) { Object.assign(focuser, event.device) - this.updateEntriesFromFocuser(focuser) } }) }) @@ -173,12 +219,16 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { } }) }) + + for (const p of SEQUENCE_ENTRY_PROPERTIES) { + this.availableEntryPropertiesToApply.set(p, true) + } } async ngAfterContentInit() { - this.cameras = await this.api.cameras() - this.wheels = await this.api.wheels() - this.focusers = await this.api.focusers() + this.cameras = (await this.api.cameras()).sort(deviceComparator) + this.wheels = (await this.api.wheels()).sort(deviceComparator) + this.focusers = (await this.api.focusers()).sort(deviceComparator) this.loadSavedJsonFileFromPathOrAddDefault() @@ -194,12 +244,11 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { add() { const camera = this.camera ?? this.cameras[0] - const wheel = this.wheel ?? this.wheels[0] - const focuser = this.focuser ?? this.focusers[0] + // const wheel = this.wheel ?? this.wheels[0] + // const focuser = this.focuser ?? this.focusers[0] this.plan.entries.push({ enabled: true, - camera, exposureTime: 1000000, exposureAmount: 1, exposureDelay: 0, @@ -215,8 +264,6 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { frameFormat: camera?.frameFormats[0], autoSave: true, autoSubFolderMode: 'OFF', - wheel, - focuser, }) this.savePlan() @@ -263,77 +310,22 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { } updateEntryFromCamera(entry: CameraStartCapture, camera?: Camera) { - entry.camera = camera - if (camera) { if (camera.connected) { - if (camera.maxX > 1) entry.x = Math.max(camera.minX, Math.min(entry.x, camera.maxX)) - if (camera.maxY > 1) entry.y = Math.max(camera.minY, Math.min(entry.y, camera.maxY)) - - if (camera.maxWidth > 1 && (entry.width <= 0 || entry.width > camera.maxWidth)) entry.width = camera.maxWidth - if (camera.maxHeight > 1 && (entry.height <= 0 || entry.height > camera.maxHeight)) entry.height = camera.maxHeight - - if (camera.maxBinX > 1) entry.binX = Math.max(1, Math.min(entry.binX, camera.maxBinX)) - if (camera.maxBinY > 1) entry.binY = Math.max(1, Math.min(entry.binY, camera.maxBinY)) - if (camera.gainMax) entry.gain = Math.max(camera.gainMin, Math.min(entry.gain, camera.gainMax)) - if (camera.offsetMax) entry.offset = Math.max(camera.offsetMin, Math.min(entry.offset, camera.offsetMax)) - if (!entry.frameFormat || !camera.frameFormats.includes(entry.frameFormat)) entry.frameFormat = camera.frameFormats[0] - - this.savePlan() - } - } - } - - updateEntriesFromCamera(camera?: Camera) { - for (const entry of this.plan.entries) { - this.updateEntryFromCamera(entry, camera) - } - } - - updateEntryFromWheel(entry: CameraStartCapture, wheel?: FilterWheel) { - entry.wheel = wheel - - if (wheel) { - if (wheel.connected) { - this.savePlan() - } - } - } - - updateEntriesFromWheel(wheel?: FilterWheel) { - for (const entry of this.plan.entries) { - this.updateEntryFromWheel(entry, wheel) - } - } - - updateEntryFromFocuser(entry: CameraStartCapture, focuser?: Focuser) { - entry.focuser = focuser - - if (focuser) { - if (focuser.connected) { + updateCameraStartCaptureFromCamera(entry, camera) this.savePlan() } } } - updateEntriesFromFocuser(focuser?: Focuser) { - for (const entry of this.plan.entries) { - this.updateEntryFromFocuser(entry, focuser) - } - } - private loadPlan(plan?: SequencePlan) { plan ??= this.storage.get(SEQUENCER_PLAN_KEY, this.plan) - Object.assign(this.plan, plan) - - this.camera = this.cameras.find(e => e.name === this.plan.entries[0]?.camera?.name) ?? this.cameras[0] - this.focuser = this.focusers.find(e => e.name === this.plan.entries[0]?.focuser?.name) ?? this.focusers[0] - this.wheel = this.wheels.find(e => e.name === this.plan.entries[0]?.wheel?.name) ?? this.wheels[0] + Object.assign(this.plan, structuredClone(plan)) - this.updateEntriesFromCamera(this.camera) - this.updateEntriesFromWheel(this.wheel) - this.updateEntriesFromFocuser(this.focuser) + this.camera = this.cameras.find(e => e.name === this.plan.camera?.name) ?? this.cameras[0] + this.focuser = this.focusers.find(e => e.name === this.plan.focuser?.name) ?? this.focusers[0] + this.wheel = this.wheels.find(e => e.name === this.plan.wheel?.name) ?? this.wheels[0] return plan.entries.length } @@ -364,32 +356,109 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { } async showCameraDialog(entry: CameraStartCapture) { - if (await CameraComponent.showAsDialog(this.browserWindow, 'SEQUENCER', entry)) { + if (await CameraComponent.showAsDialog(this.browserWindow, 'SEQUENCER', this.camera!, entry)) { this.savePlan() } } async showWheelDialog(entry: CameraStartCapture) { - if (await FilterWheelComponent.showAsDialog(this.browserWindow, 'SEQUENCER', entry)) { + if (await FilterWheelComponent.showAsDialog(this.browserWindow, 'SEQUENCER', this.wheel!, entry)) { this.savePlan() } } savePlan() { + this.plan.camera = this.camera + this.plan.wheel = this.wheel + this.plan.focuser = this.focuser this.storage.set(SEQUENCER_PLAN_KEY, this.plan) this.savedPathWasModified = !!this.savedPath } + showEntryMenu(entry: CameraStartCapture, dialogMenu: DialogMenuComponent) { + this.entryToApply = entry + const index = this.plan.entries.indexOf(entry) + + this.entryMenuModel.forEach(e => e.visible = true) + + if (index === 0 || this.plan.entries.length === 1) { + // Hides all above and above. + this.entryMenuModel[1].visible = false + this.entryMenuModel[2].visible = false + } else if (index === 1) { + // Hides all above. + this.entryMenuModel[1].visible = false + } + + if (index === this.plan.entries.length - 1 || this.plan.entries.length === 1) { + // Hides below and all below. + this.entryMenuModel[3].visible = false + this.entryMenuModel[4].visible = false + } else if (index === this.plan.entries.length - 2) { + // Hides all below. + this.entryMenuModel[4].visible = false + } + + dialogMenu.show() + } + + updateAllAvailableEntryPropertiesToApply(selected: boolean) { + for (const p of SEQUENCE_ENTRY_PROPERTIES) { + this.availableEntryPropertiesToApply.set(p, selected) + } + } + + applyCameraStartCaptureToEntries() { + const source = this.entryToApply! + const index = this.plan.entries.indexOf(source) + + for (let count of this.entryToApplyCount) { + if (index < 0 || count === 0) continue + + const below = Math.sign(count) + + count = Math.abs(count) + + for (let i = 1; i <= count; i++) { + const pos = index + (i * below) + + if (pos >= 0 && pos < this.plan.entries.length) { + const dest = this.plan.entries[pos] + + if (!dest.enabled) continue + + if (this.availableEntryPropertiesToApply.get('EXPOSURE_TIME')) dest.exposureTime = source.exposureTime + if (this.availableEntryPropertiesToApply.get('EXPOSURE_AMOUNT')) dest.exposureAmount = source.exposureAmount + if (this.availableEntryPropertiesToApply.get('EXPOSURE_DELAY')) dest.exposureDelay = source.exposureDelay + if (this.availableEntryPropertiesToApply.get('FRAME_TYPE')) dest.frameType = source.frameType + if (this.availableEntryPropertiesToApply.get('X')) dest.x = source.x + if (this.availableEntryPropertiesToApply.get('Y')) dest.y = source.y + if (this.availableEntryPropertiesToApply.get('WIDTH')) dest.width = source.width + if (this.availableEntryPropertiesToApply.get('HEIGHT')) dest.height = source.height + if (this.availableEntryPropertiesToApply.get('BIN')) dest.binX = source.binX + if (this.availableEntryPropertiesToApply.get('BIN')) dest.binY = source.binY + if (this.availableEntryPropertiesToApply.get('FRAME_FORMAT')) dest.frameFormat = source.frameFormat + if (this.availableEntryPropertiesToApply.get('GAIN')) dest.gain = source.gain + if (this.availableEntryPropertiesToApply.get('OFFSET')) dest.offset = source.offset + } else { + break + } + } + } + + this.showEntryPropertiesToApplyDialog = false + } + deleteEntry(entry: CameraStartCapture, index: number) { this.plan.entries.splice(index, 1) } duplicateEntry(entry: CameraStartCapture, index: number) { - this.plan.entries.splice(index + 1, 0, Object.assign({}, entry)) + this.plan.entries.splice(index + 1, 0, structuredClone(entry)) } async start() { - for (let i = 0; i < this.plan.entries.length; i++) { + for (let i = 0; i < this.cameraExposures.length; i++) { this.cameraExposures.get(i)?.reset() } @@ -397,10 +466,10 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { await this.browserWindow.openCameraImage(this.camera!) - this.api.sequencerStart(this.plan) + this.api.sequencerStart(this.camera!, this.plan) } stop() { - this.api.sequencerStop() + this.api.sequencerStop(this.camera!) } } diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index d9f7f1737..507732abe 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -15,7 +15,8 @@
+ optionLabel="name" dataKey="id" styleClass="p-inputtext-sm border-0" emptyMessage="No location found" + [autoDisplayFirst]="false" />
@@ -29,16 +30,16 @@
- +
-
+
- + @@ -46,31 +47,34 @@
-
+
- +
-
+
- +
-
+
- +
- +
diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index d26ce46b0..39a13d451 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -4,14 +4,12 @@ import { MenuItem } from 'primeng/api' import { LocationDialog } from '../../shared/dialogs/location/location.dialog' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' -import { LocalStorageService } from '../../shared/services/local-storage.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 { EMPTY_PLATE_SOLVER_OPTIONS, PlateSolverOptions, PlateSolverType } from '../../shared/types/settings.types' +import { DEFAULT_SOLVER_TYPES, PlateSolverOptions, PlateSolverType } from '../../shared/types/settings.types' import { AppComponent } from '../app.component' -export const SETTINGS_PLATE_SOLVER_KEY = 'settings.plateSolver' - @Component({ selector: 'app-settings', templateUrl: './settings.component.html', @@ -22,10 +20,11 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { activeTab = 0 locations: Location[] = [] - location = Object.assign({}, EMPTY_LOCATION) + location = structuredClone(EMPTY_LOCATION) - readonly plateSolverTypes: PlateSolverType[] = ['ASTAP', /*'ASTROMETRY_NET',*/ 'ASTROMETRY_NET_ONLINE'] - readonly plateSolver: PlateSolverOptions + readonly plateSolverTypes = Array.from(DEFAULT_SOLVER_TYPES) + plateSolverType = this.plateSolverTypes[0] + readonly plateSolvers = new Map() readonly items: MenuItem[] = [ { @@ -43,13 +42,15 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { constructor( app: AppComponent, private api: ApiService, - private storage: LocalStorageService, + private preference: PreferenceService, private electron: ElectronService, private prime: PrimeService, ) { app.title = 'Settings' - this.plateSolver = storage.get(SETTINGS_PLATE_SOLVER_KEY, EMPTY_PLATE_SOLVER_OPTIONS) + for (const type of this.plateSolverTypes) { + this.plateSolvers.set(type, preference.plateSolverOptions(type).get()) + } } async ngAfterViewInit() { @@ -60,7 +61,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { ngOnDestroy() { } addLocation() { - this.showLocation(Object.assign({}, EMPTY_LOCATION)) + this.showLocation(structuredClone(EMPTY_LOCATION)) } editLocation() { @@ -101,15 +102,18 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { } async chooseExecutablePath() { - const executablePath = await this.electron.openFile({ defaultPath: path.dirname(this.plateSolver.executablePath) }) + const options = this.plateSolvers.get(this.plateSolverType)! + const executablePath = await this.electron.openFile({ defaultPath: path.dirname(options.executablePath) }) if (executablePath) { - this.plateSolver.executablePath = executablePath + options.executablePath = executablePath this.save() } } async save() { - this.storage.set(SETTINGS_PLATE_SOLVER_KEY, this.plateSolver) + for (const type of this.plateSolverTypes) { + this.preference.plateSolverOptions(type).set(this.plateSolvers.get(type)!) + } } } \ No newline at end of file diff --git a/desktop/src/assets/data/hipsSurveys.json b/desktop/src/assets/data/hipsSurveys.json deleted file mode 100644 index 7b1f5f902..000000000 --- a/desktop/src/assets/data/hipsSurveys.json +++ /dev/null @@ -1,522 +0,0 @@ -[ - { - "type": "CDS_P_DSS2_NIR", - "id": "CDS/P/DSS2/NIR", - "category": "Image/Optical/DSS", - "frame": "equatorial", - "regime": "Optical", - "bitPix": 16, - "pixelScale": 2.236E-4, - "skyFraction": 0.9955 - }, - { - "type": "CDS_P_DSS2_BLUE", - "id": "CDS/P/DSS2/blue", - "category": "Image/Optical/DSS", - "frame": "equatorial", - "regime": "Optical", - "bitPix": 16, - "pixelScale": 2.236E-4, - "skyFraction": 0.9972 - }, - { - "type": "CDS_P_DSS2_COLOR", - "id": "CDS/P/DSS2/color", - "category": "Image/Optical/DSS", - "frame": "equatorial", - "regime": "Optical", - "bitPix": 0, - "pixelScale": 2.236E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_DSS2_RED", - "id": "CDS/P/DSS2/red", - "category": "Image/Optical/DSS", - "frame": "equatorial", - "regime": "Optical", - "bitPix": 16, - "pixelScale": 2.236E-4, - "skyFraction": 1.0 - }, - { - "type": "FZU_CZ_P_CTA_FRAM_SURVEY_B", - "id": "fzu.cz/P/CTA-FRAM/survey/B", - "category": "Image/Optical/CTA-FRAM", - "frame": "equatorial", - "regime": "Optical", - "bitPix": -64, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "FZU_CZ_P_CTA_FRAM_SURVEY_R", - "id": "fzu.cz/P/CTA-FRAM/survey/R", - "category": "Image/Optical/CTA-FRAM", - "frame": "equatorial", - "regime": "Optical", - "bitPix": -64, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "FZU_CZ_P_CTA_FRAM_SURVEY_V", - "id": "fzu.cz/P/CTA-FRAM/survey/V", - "category": "Image/Optical/CTA-FRAM", - "frame": "equatorial", - "regime": "Optical", - "bitPix": -64, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "FZU_CZ_P_CTA_FRAM_SURVEY_COLOR", - "id": "fzu.cz/P/CTA-FRAM/survey/color", - "category": "Image/Optical/CTA-FRAM", - "frame": "equatorial", - "regime": "Optical", - "bitPix": 0, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_2MASS_H", - "id": "CDS/P/2MASS/H", - "category": "Image/Infrared/2MASS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 2.236E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_2MASS_J", - "id": "CDS/P/2MASS/J", - "category": "Image/Infrared/2MASS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 2.236E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_2MASS_K", - "id": "CDS/P/2MASS/K", - "category": "Image/Infrared/2MASS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 2.236E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_2MASS_COLOR", - "id": "CDS/P/2MASS/color", - "category": "Image/Infrared/2MASS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": 0, - "pixelScale": 2.236E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_AKARI_FIS_COLOR", - "id": "CDS/P/AKARI/FIS/Color", - "category": "Image/Infrared/AKARI-FIS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": 0, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_AKARI_FIS_N160", - "id": "CDS/P/AKARI/FIS/N160", - "category": "Image/Infrared/AKARI-FIS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_AKARI_FIS_N60", - "id": "CDS/P/AKARI/FIS/N60", - "category": "Image/Infrared/AKARI-FIS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_AKARI_FIS_WIDEL", - "id": "CDS/P/AKARI/FIS/WideL", - "category": "Image/Infrared/AKARI-FIS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_AKARI_FIS_WIDES", - "id": "CDS/P/AKARI/FIS/WideS", - "category": "Image/Infrared/AKARI-FIS", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 0.003579, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_NEOWISER_COLOR", - "id": "CDS/P/NEOWISER/Color", - "category": "Image/Infrared/WISE/NEOWISER", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": 0, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_NEOWISER_W1", - "id": "CDS/P/NEOWISER/W1", - "category": "Image/Infrared/WISE/NEOWISER", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_NEOWISER_W2", - "id": "CDS/P/NEOWISER/W2", - "category": "Image/Infrared/WISE/NEOWISER", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_WISE_WSSA_12UM", - "id": "CDS/P/WISE/WSSA/12um", - "category": "Image/Infrared/WISE/WSSA", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 8.946E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_ALLWISE_W1", - "id": "CDS/P/allWISE/W1", - "category": "Image/Infrared/WISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_ALLWISE_W2", - "id": "CDS/P/allWISE/W2", - "category": "Image/Infrared/WISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_ALLWISE_W3", - "id": "CDS/P/allWISE/W3", - "category": "Image/Infrared/WISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 4.473E-4, - "skyFraction": 0.9999 - }, - { - "type": "CDS_P_ALLWISE_W4", - "id": "CDS/P/allWISE/W4", - "category": "Image/Infrared/WISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_ALLWISE_COLOR", - "id": "CDS/P/allWISE/color", - "category": "Image/Infrared/WISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": 0, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_UNWISE_W1", - "id": "CDS/P/unWISE/W1", - "category": "Image/Infrared/WISE/unWISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 0.229, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_UNWISE_W2", - "id": "CDS/P/unWISE/W2", - "category": "Image/Infrared/WISE/unWISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 0.229, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_UNWISE_COLOR_W2_W1W2_W1", - "id": "CDS/P/unWISE/color-W2-W1W2-W1", - "category": "Image/Infrared/WISE/unWISE", - "frame": "equatorial", - "regime": "Infrared", - "bitPix": -32, - "pixelScale": 4.473E-4, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_RASS", - "id": "CDS/P/RASS", - "category": "Image/X/ROSAT", - "frame": "equatorial", - "regime": "X-ray", - "bitPix": 16, - "pixelScale": 0.007157, - "skyFraction": 1.0 - }, - { - "type": "JAXA_P_ASCA_GIS", - "id": "JAXA/P/ASCA_GIS", - "category": "Image/X/ASCA", - "frame": "equatorial", - "regime": "X-ray", - "bitPix": 0, - "pixelScale": 0.001789, - "skyFraction": 1.0 - }, - { - "type": "JAXA_P_ASCA_SIS", - "id": "JAXA/P/ASCA_SIS", - "category": "Image/X/ASCA", - "frame": "equatorial", - "regime": "X-ray", - "bitPix": 0, - "pixelScale": 0.001789, - "skyFraction": 1.0 - }, - { - "type": "JAXA_P_MAXI_GSC", - "id": "JAXA/P/MAXI-GSC", - "category": "Image/X/MAXI", - "frame": "equatorial", - "regime": "X-ray", - "bitPix": 0, - "pixelScale": 0.001789, - "skyFraction": 1.0 - }, - { - "type": "JAXA_P_MAXI_SSC", - "id": "JAXA/P/MAXI-SSC", - "category": "Image/X/MAXI", - "frame": "equatorial", - "regime": "X-ray", - "bitPix": 0, - "pixelScale": 0.1145, - "skyFraction": 1.0 - }, - { - "type": "JAXA_P_SUZAKU", - "id": "JAXA/P/SUZAKU", - "category": "Image/X", - "frame": "equatorial", - "regime": "X-ray", - "bitPix": 0, - "pixelScale": 0.001789, - "skyFraction": 1.0 - }, - { - "type": "JAXA_P_SWIFT_BAT_FLUX", - "id": "JAXA/P/SWIFT_BAT_FLUX", - "category": "Image/X", - "frame": "equatorial", - "regime": "X-ray", - "bitPix": 0, - "pixelScale": 0.001789, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_100_150", - "id": "CDS/P/EGRET/Dif/100-150", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_1000_2000", - "id": "CDS/P/EGRET/Dif/1000-2000", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_150_300", - "id": "CDS/P/EGRET/Dif/150-300", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_2000_4000", - "id": "CDS/P/EGRET/Dif/2000-4000", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_30_50", - "id": "CDS/P/EGRET/Dif/30-50", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_300_500", - "id": "CDS/P/EGRET/Dif/300-500", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_4000_10000", - "id": "CDS/P/EGRET/Dif/4000-10000", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_50_70", - "id": "CDS/P/EGRET/Dif/50-70", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_500_1000", - "id": "CDS/P/EGRET/Dif/500-1000", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_DIF_70_100", - "id": "CDS/P/EGRET/Dif/70-100", - "category": "Image/Gamma-ray/EGRET/Diffuse", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_INF100", - "id": "CDS/P/EGRET/inf100", - "category": "Image/Gamma-ray/EGRET", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_EGRET_SUP100", - "id": "CDS/P/EGRET/sup100", - "category": "Image/Gamma-ray/EGRET", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_FERMI_3", - "id": "CDS/P/Fermi/3", - "category": "Image/Gamma-ray", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_FERMI_4", - "id": "CDS/P/Fermi/4", - "category": "Image/Gamma-ray", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_FERMI_5", - "id": "CDS/P/Fermi/5", - "category": "Image/Gamma-ray", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": -32, - "pixelScale": 0.01431, - "skyFraction": 1.0 - }, - { - "type": "CDS_P_FERMI_COLOR", - "id": "CDS/P/Fermi/color", - "category": "Image/Gamma-ray", - "frame": "equatorial", - "regime": "Gamma-ray", - "bitPix": 0, - "pixelScale": 0.01431, - "skyFraction": 1.0 - } -] \ No newline at end of file diff --git a/desktop/src/assets/icons/CREDITS.md b/desktop/src/assets/icons/CREDITS.md index 75b7d1cf3..ff387b1b3 100644 --- a/desktop/src/assets/icons/CREDITS.md +++ b/desktop/src/assets/icons/CREDITS.md @@ -28,4 +28,5 @@ * 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/calculator.png b/desktop/src/assets/icons/calculator.png new file mode 100644 index 000000000..42eaa964d Binary files /dev/null and b/desktop/src/assets/icons/calculator.png differ diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.html b/desktop/src/shared/components/camera-exposure/camera-exposure.component.html index 1deaae18b..0b4fc1559 100644 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.html +++ b/desktop/src/shared/components/camera-exposure/camera-exposure.component.html @@ -1,37 +1,41 @@ - - +
+ + {{ (state ?? 'IDLE') | enum | lowercase }} + + + - {{ exposure.count }} of {{ capture.amount }} + {{ exposure.count }} / {{ capture.amount }} + + + {{ capture.progress * 100 | number:'1.1-1' }} - + @if (capture.looping) { + {{ capture.elapsedTime | exposureTime }} - @if (!capture.looping) { - + } @else if(showRemainingTime) { + {{ capture.remainingTime | exposureTime }} - - {{ capture.progress * 100 | number:'1.1-1' }} + } @else { + + {{ capture.elapsedTime | exposureTime }} } - - - - - {{ (state ?? 'IDLE') | enum | lowercase }} - - - + + @if (state === 'EXPOSURING') { + {{ exposure.remainingTime | exposureTime }} {{ exposure.progress * 100 | number:'1.1-1' }} - - + } + + @if (state === 'WAITING') { {{ wait.remainingTime | exposureTime }} @@ -39,5 +43,6 @@ {{ wait.progress * 100 | number:'1.1-1' }} + } - \ No newline at end of file +
\ No newline at end of file diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.scss b/desktop/src/shared/components/camera-exposure/camera-exposure.component.scss index 35a709bcc..595ee8c38 100644 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.scss +++ b/desktop/src/shared/components/camera-exposure/camera-exposure.component.scss @@ -1,12 +1,10 @@ :host { min-height: 29px; - display: flex; - flex-direction: row; - gap: 4px; - align-items: end; + width: 100%; .state { padding: 1px 6px; + height: 13px; &.percentage { min-width: 50px; @@ -25,18 +23,6 @@ } } - .state-group { - .state:first-child { - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - } - - .state:last-child { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - } - } - .mdi-information::before { font-size: 0.9rem !important; } diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts b/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts index f151d5545..0115b0389 100644 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts +++ b/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core' -import { CameraCaptureEvent, CameraCaptureState, EMPTY_CAMERA_CAPTURE_INFO, EMPTY_CAMERA_EXPOSURE_INFO, EMPTY_CAMERA_WAIT_INFO } from '../../types/camera.types' +import { CameraCaptureElapsed, CameraCaptureState, EMPTY_CAMERA_CAPTURE_INFO, EMPTY_CAMERA_EXPOSURE_INFO, EMPTY_CAMERA_WAIT_INFO } from '../../types/camera.types' @Component({ selector: 'neb-camera-exposure', @@ -12,15 +12,18 @@ export class CameraExposureComponent { state?: CameraCaptureState = 'IDLE' @Input() - readonly exposure = Object.assign({}, EMPTY_CAMERA_EXPOSURE_INFO) + showRemainingTime: boolean = true @Input() - readonly capture = Object.assign({}, EMPTY_CAMERA_CAPTURE_INFO) + readonly exposure = structuredClone(EMPTY_CAMERA_EXPOSURE_INFO) @Input() - readonly wait = Object.assign({}, EMPTY_CAMERA_WAIT_INFO) + readonly capture = structuredClone(EMPTY_CAMERA_CAPTURE_INFO) - handleCameraCaptureEvent(event: CameraCaptureEvent) { + @Input() + readonly wait = structuredClone(EMPTY_CAMERA_WAIT_INFO) + + handleCameraCaptureEvent(event: CameraCaptureElapsed, looping: boolean = false) { this.capture.elapsedTime = event.captureElapsedTime this.capture.remainingTime = event.captureRemainingTime this.capture.progress = event.captureProgress @@ -37,16 +40,17 @@ export class CameraExposureComponent { } else if (event.state === 'SETTLING') { this.state = event.state } else if (event.state === 'CAPTURE_STARTED') { - this.capture.looping = event.exposureAmount <= 0 + this.capture.looping = looping || event.exposureAmount <= 0 this.capture.amount = event.exposureAmount this.state = 'EXPOSURING' } else if (event.state === 'EXPOSURE_STARTED') { this.state = 'EXPOSURING' - } else if (event.state === 'CAPTURE_FINISHED' || (!this.capture.looping && !this.capture.remainingTime)) { + } else if ((!looping && event.state === 'CAPTURE_FINISHED') || (!this.capture.looping && !this.capture.remainingTime)) { this.state = 'IDLE' } - return this.state !== undefined && this.state !== 'CAPTURE_FINISHED' && this.state !== 'IDLE' + return this.state !== undefined && this.state !== 'CAPTURE_FINISHED' + && this.state !== 'IDLE' && !event.aborted } reset() { @@ -56,4 +60,8 @@ export class CameraExposureComponent { Object.assign(this.capture, EMPTY_CAMERA_CAPTURE_INFO) Object.assign(this.wait, EMPTY_CAMERA_WAIT_INFO) } + + toggleRemainingTime() { + this.showRemainingTime = !this.showRemainingTime + } } \ 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 a88fc98ba..c9a973ec0 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 @@ -2,8 +2,8 @@ import { Component, Input, ViewChild } from '@angular/core' import { MenuItem, MessageService } from 'primeng/api' import { SEPARATOR_MENU_ITEM } from '../../constants' import { Device } from '../../types/device.types' +import { deviceComparator } from '../../utils/comparators' import { DialogMenuComponent } from '../dialog-menu/dialog-menu.component' -import { compareDevice } from '../../utils/comparators' @Component({ selector: 'neb-device-list-menu', @@ -48,7 +48,7 @@ export class DeviceListMenuComponent { model.push(SEPARATOR_MENU_ITEM) } - for (const device of devices.sort(compareDevice)) { + for (const device of devices.sort(deviceComparator)) { model.push({ icon: 'mdi mdi-connection', label: device.name, diff --git a/desktop/src/shared/dialogs/location/location.dialog.html b/desktop/src/shared/dialogs/location/location.dialog.html index 2ed864f7a..ca24459b9 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.html +++ b/desktop/src/shared/dialogs/location/location.dialog.html @@ -38,5 +38,5 @@
\ No newline at end of file diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index 8343f8a30..48fb830c2 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -1,9 +1,17 @@ import { Pipe, PipeTransform } from '@angular/core' +import { DARVState, TPPAState } from '../types/alignment.types' +import { Constellation, SatelliteGroupType, SkyObjectType } from '../types/atlas.types' +import { CameraCaptureState } from '../types/camera.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' @Pipe({ name: 'enum' }) export class EnumPipe implements PipeTransform { - readonly enums: Record = { + readonly enums: Record = { // General. 'ALL': 'All', // SCNRProtectiveMethod. @@ -319,13 +327,24 @@ export class EnumPipe implements PipeTransform { 'FORWARD': 'Forward', 'BACKWARD': 'Backward', 'IDLE': 'Idle', + 'SLEWING': 'Slewing', + 'SOLVING': 'Solving', + 'SOLVED': 'Solved', + 'COMPUTED': 'Computed', + 'FAILED': 'Failed', + 'FINISHED': 'Finished', + 'PAUSING': 'Pausing', // Camera Exposure. 'SETTLING': 'Settling', 'WAITING': 'Waiting', 'EXPOSURING': 'Exposuring', + 'CAPTURE_STARTED': undefined, + 'EXPOSURE_STARTED': undefined, + 'EXPOSURE_FINISHED': undefined, + 'CAPTURE_FINISHED': undefined } - transform(value: string) { + transform(value: EnumPipeKey) { return this.enums[value] ?? value } } diff --git a/desktop/src/shared/pipes/exposureTime.pipe.ts b/desktop/src/shared/pipes/exposureTime.pipe.ts index 365391ac9..0a48d1b86 100644 --- a/desktop/src/shared/pipes/exposureTime.pipe.ts +++ b/desktop/src/shared/pipes/exposureTime.pipe.ts @@ -52,6 +52,7 @@ function minutes(value: number) { function seconds(value: number) { return `${TWO_DIGITS_FORMATTER.format(value / 1000000)}s` + // return format(value, [1000000, 1000], [secondFormatter, millisecondFormatter]) } function milliseconds(value: number) { diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 52ca0f9f4..60a5a9798 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core' import moment from 'moment' -import { DARVStart } from '../types/alignment.types' +import { DARVStart, TPPAStart } from '../types/alignment.types' import { Angle, BodyPosition, ComputedLocation, Constellation, DeepSkyObject, Location, MinorPlanet, Satellite, SatelliteGroupType, SkyObjectType, Twilight } from '../types/atlas.types' import { CalibrationFrame, CalibrationFrameGroup } from '../types/calibration.types' import { Camera, CameraStartCapture } from '../types/camera.types' @@ -9,6 +9,7 @@ import { FlatWizardRequest } from '../types/flat-wizard.types' import { Focuser } from '../types/focuser.types' import { HipsSurvey } from '../types/framing.types' import { GuideDirection, GuideOutput, Guider, GuiderHistoryStep, SettleInfo } from '../types/guider.types' +import { ConnectionStatus, ConnectionType } from '../types/home.types' import { CoordinateInterpolation, DetectedStar, ImageAnnotation, ImageChannel, ImageInfo, ImageSolved, SCNRProtectionMethod } from '../types/image.types' import { CelestialLocationType, Mount, SlewRate, TrackMode } from '../types/mount.types' import { SequencePlan } from '../types/sequencer.types' @@ -27,17 +28,21 @@ export class ApiService { // CONNECTION - connect(host: string, port: number) { - const query = this.http.query({ host, port }) - return this.http.put(`connection?${query}`) + connect(host: string, port: number, type: ConnectionType) { + const query = this.http.query({ host, port, type }) + return this.http.put(`connection?${query}`) + } + + disconnect(id: string) { + return this.http.delete(`connection/${id}`) } - disconnect() { - return this.http.delete(`connection`) + connectionStatuses() { + return this.http.get(`connection`) } - connectionStatus() { - return this.http.get(`connection`) + connectionStatus(id: string) { + return this.http.get(`connection/${id}`) } // CAMERA @@ -521,34 +526,55 @@ export class ApiService { // FRAMING + hipsSurveys() { + return this.http.get('framing/hips-surveys') + } + frame(rightAscension: Angle, declination: Angle, width: number, height: number, fov: number, rotation: number, hipsSurvey: HipsSurvey, ) { - const query = this.http.query({ rightAscension, declination, width, height, fov, rotation, hipsSurvey: hipsSurvey.type }) + const query = this.http.query({ rightAscension, declination, width, height, fov, rotation, hipsSurvey: hipsSurvey.id }) return this.http.put(`framing?${query}`) } // DARV - darvStart(camera: Camera, guideOutput: GuideOutput, - exposureTime: number, initialPause: number, direction: GuideDirection, reversed: boolean = false, capture?: CameraStartCapture) { - const data: DARVStart = { capture, exposureTime, initialPause, direction, reversed } - return this.http.put(`polar-alignment/darv/${camera.name}/${guideOutput.name}/start`, data) + darvStart(camera: Camera, guideOutput: GuideOutput, data: DARVStart) { + return this.http.put(`polar-alignment/darv/${camera.name}/${guideOutput.name}/start`, data) + } + + darvStop(id: string) { + return this.http.put(`polar-alignment/darv/${id}/stop`) + } + + // TPPA + + tppaStart(camera: Camera, mount: Mount, data: TPPAStart) { + return this.http.put(`polar-alignment/tppa/${camera.name}/${mount.name}/start`, data) + } + + tppaStop(id: string) { + return this.http.put(`polar-alignment/tppa/${id}/stop`) + } + + tppaPause(id: string) { + return this.http.put(`polar-alignment/tppa/${id}/pause`) } - darvStop(camera: Camera, guideOutput: GuideOutput) { - return this.http.put(`polar-alignment/darv/${camera.name}/${guideOutput.name}/stop`) + tppaUnpause(id: string) { + return this.http.put(`polar-alignment/tppa/${id}/unpause`) } // SEQUENCER - sequencerStart(plan: SequencePlan) { - return this.http.put(`sequencer/start`, plan) + sequencerStart(camera: Camera, plan: SequencePlan) { + const body: SequencePlan = { ...plan, camera: undefined, wheel: undefined, focuser: undefined } + return this.http.put(`sequencer/${camera.name}/start`, body) } - sequencerStop() { - return this.http.put(`sequencer/stop`) + sequencerStop(camera: Camera) { + return this.http.put(`sequencer/${camera.name}/stop`) } // FLAT WIZARD diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 6767e9e98..dc231be4b 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -2,12 +2,11 @@ import { Injectable } from '@angular/core' import { v4 as uuidv4 } from 'uuid' import { SkyAtlasData } from '../../app/atlas/atlas.component' import { FramingData } from '../../app/framing/framing.component' -import { ImageData } from '../../app/image/image.component' import { OpenWindow, OpenWindowOptions, OpenWindowOptionsWithData } from '../types/app.types' import { Camera, CameraDialogInput, CameraStartCapture } from '../types/camera.types' import { Device } from '../types/device.types' import { Focuser } from '../types/focuser.types' -import { ImageSource } from '../types/image.types' +import { ImageData, ImageSource } from '../types/image.types' import { Mount } from '../types/mount.types' import { FilterWheel, WheelDialogInput } from '../types/wheel.types' import { ElectronService } from './electron.service' @@ -26,37 +25,37 @@ export class BrowserWindowService { } openMount(options: OpenWindowOptionsWithData) { - Object.assign(options, { icon: 'telescope', width: 400, height: 469 }) + Object.assign(options, { icon: 'telescope', width: 400, height: 477 }) this.openWindow({ ...options, id: `mount.${options.data.name}`, path: 'mount' }) } openCamera(options: OpenWindowOptionsWithData) { - Object.assign(options, { icon: 'camera', width: 400, height: 476 }) + Object.assign(options, { icon: 'camera', width: 400, height: 470 }) return this.openWindow({ ...options, id: `camera.${options.data.name}`, path: 'camera' }) } openCameraDialog(options: OpenWindowOptionsWithData) { Object.assign(options, { icon: 'camera', width: 400, height: 424 }) - return this.openModal({ ...options, id: `camera.${options.data.request.camera!.name}.modal`, path: 'camera' }) + return this.openModal({ ...options, id: `camera.${options.data.camera.name}.modal`, path: 'camera' }) } openFocuser(options: OpenWindowOptionsWithData) { - Object.assign(options, { icon: 'focus', width: 360, height: 203 }) + Object.assign(options, { icon: 'focus', width: 348, height: 202 }) this.openWindow({ ...options, id: `focuser.${options.data.name}`, path: 'focuser' }) } openWheel(options: OpenWindowOptionsWithData) { - Object.assign(options, { icon: 'filter-wheel', width: 300, height: 283 }) + Object.assign(options, { icon: 'filter-wheel', width: 285, height: 195 }) this.openWindow({ ...options, id: `wheel.${options.data.name}`, path: 'wheel' }) } openWheelDialog(options: OpenWindowOptionsWithData) { Object.assign(options, { icon: 'filter-wheel', width: 300, height: 217 }) - return this.openModal({ ...options, id: `wheel.${options.data.request.camera!.name}.modal`, path: 'wheel' }) + return this.openModal({ ...options, id: `wheel.${options.data.wheel.name}.modal`, path: 'wheel' }) } openGuider(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'guider', width: 425, height: 450 }) + Object.assign(options, { icon: 'guider', width: 425, height: 438 }) this.openWindow({ ...options, id: 'guider', path: 'guider', data: undefined }) } @@ -80,17 +79,17 @@ export class BrowserWindowService { } openSkyAtlas(options: OpenWindowOptionsWithData) { - Object.assign(options, { icon: 'atlas', width: 450, height: 523 }) + Object.assign(options, { icon: 'atlas', width: 450, height: 530, autoResizable: false }) this.openWindow({ ...options, id: 'atlas', path: 'atlas' }) } openFraming(options: OpenWindowOptionsWithData) { - Object.assign(options, { icon: 'framing', width: 280, height: 310 }) + Object.assign(options, { icon: 'framing', width: 280, height: 303 }) this.openWindow({ ...options, id: 'framing', path: 'framing' }) } openAlignment(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'star', width: 400, height: 280 }) + Object.assign(options, { icon: 'star', width: 450, height: 343 }) this.openWindow({ ...options, id: 'alignment', path: 'alignment', data: undefined }) } @@ -100,21 +99,26 @@ export class BrowserWindowService { } openFlatWizard(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'star', width: 410, height: 330 }) + Object.assign(options, { icon: 'star', width: 410, height: 326 }) this.openWindow({ ...options, id: 'flat-wizard', path: 'flat-wizard', data: undefined }) } openSettings(options: OpenWindowOptions = {}) { - Object.assign(options, { icon: 'settings', width: 580, height: 445 }) + Object.assign(options, { icon: 'settings', width: 580, height: 451 }) this.openWindow({ ...options, id: 'settings', path: 'settings', data: undefined }) } + openCalculator(options: OpenWindowOptions = {}) { + Object.assign(options, { icon: 'calculator', width: 345, height: 340 }) + this.openWindow({ ...options, id: 'calculator', path: 'calculator', data: undefined }) + } + openCalibration(options: OpenWindowOptionsWithData) { Object.assign(options, { icon: 'stack', width: 510, height: 508 }) this.openWindow({ ...options, id: 'calibration', path: 'calibration' }) } openAbout() { - this.openWindow({ id: 'about', path: 'about', icon: 'about', width: 480, height: 252, bringToFront: true, data: undefined }) + this.openWindow({ id: 'about', path: 'about', icon: 'about', width: 430, height: 246, bringToFront: true, data: undefined }) } } diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 5bf1758f5..4de8889f7 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -7,17 +7,18 @@ import { Injectable } from '@angular/core' import * as childProcess from 'child_process' import { ipcRenderer, webFrame } from 'electron' import * as fs from 'fs' -import { DARVEvent } from '../types/alignment.types' +import { DARVElapsed, TPPAElapsed } from '../types/alignment.types' import { ApiEventType, DeviceMessageEvent } from '../types/api.types' import { CloseWindow, InternalEventType, JsonFile, OpenDirectory, OpenFile, SaveJson } from '../types/app.types' import { Location } from '../types/atlas.types' -import { Camera, CameraCaptureEvent } from '../types/camera.types' +import { Camera, CameraCaptureElapsed } from '../types/camera.types' import { INDIMessageEvent } from '../types/device.types' -import { FlatWizardEvent } from '../types/flat-wizard.types' +import { FlatWizardElapsed } 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 { Mount } from '../types/mount.types' -import { SequencerEvent } from '../types/sequencer.types' +import { SequencerElapsed } from '../types/sequencer.types' import { FilterWheel } from '../types/wheel.types' import { ApiService } from './api.service' @@ -28,7 +29,7 @@ type EventMappedType = { 'CAMERA.UPDATED': DeviceMessageEvent 'CAMERA.ATTACHED': DeviceMessageEvent 'CAMERA.DETACHED': DeviceMessageEvent - 'CAMERA.CAPTURE_ELAPSED': CameraCaptureEvent + 'CAMERA.CAPTURE_ELAPSED': CameraCaptureElapsed 'MOUNT.UPDATED': DeviceMessageEvent 'MOUNT.ATTACHED': DeviceMessageEvent 'MOUNT.DETACHED': DeviceMessageEvent @@ -46,13 +47,13 @@ type EventMappedType = { 'GUIDER.UPDATED': GuiderMessageEvent 'GUIDER.STEPPED': GuiderMessageEvent 'GUIDER.MESSAGE_RECEIVED': GuiderMessageEvent - 'DARV_ALIGNMENT.ELAPSED': DARVEvent + 'DARV.ELAPSED': DARVElapsed + 'TPPA.ELAPSED': TPPAElapsed 'DATA.CHANGED': any 'LOCATION.CHANGED': Location - 'SEQUENCER.ELAPSED': SequencerEvent - 'FLAT_WIZARD.ELAPSED': FlatWizardEvent - 'FLAT_WIZARD.FRAME_CAPTURED': FlatWizardEvent - 'FLAT_WIZARD.FAILED': FlatWizardEvent + 'SEQUENCER.ELAPSED': SequencerElapsed + 'FLAT_WIZARD.ELAPSED': FlatWizardElapsed + 'CONNECTION.CLOSED': ConnectionClosed } @Injectable({ providedIn: 'root' }) @@ -156,6 +157,38 @@ export class ElectronService { return this.send('JSON.READ', path) } + resizeWindow(size: number) { + this.send('WINDOW.RESIZE', Math.floor(size)) + } + + autoResizeWindow(timeout: number = 500): any { + if (timeout <= 0) { + const size = document.getElementsByTagName('app-root')[0]?.getBoundingClientRect()?.height + + if (size > 0) { + this.resizeWindow(size) + } + } else { + return setTimeout(() => this.autoResizeWindow(), timeout) + } + } + + pinWindow() { + this.send('WINDOW.PIN') + } + + unpinWindow() { + this.send('WINDOW.UNPIN') + } + + minimizeWindow() { + this.send('WINDOW.MINIMIZE') + } + + maximizeWindow() { + this.send('WINDOW.MAXIMIZE') + } + closeWindow(data: CloseWindow) { return this.send('WINDOW.CLOSE', data) } diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index c6451a051..94785897e 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -1,30 +1,65 @@ import { Injectable } from '@angular/core' -import { ApiService } from './api.service' -import { StorageService } from './storage.service' +import { AlignmentPreference, EMPTY_ALIGNMENT_PREFERENCE } from '../types/alignment.types' +import { Camera, CameraPreference, CameraStartCapture, EMPTY_CAMERA_PREFERENCE } from '../types/camera.types' +import { ConnectionDetails } from '../types/home.types' +import { EMPTY_IMAGE_PREFERENCE, FOV, ImagePreference } from '../types/image.types' +import { EMPTY_PLATE_SOLVER_OPTIONS, PlateSolverOptions, PlateSolverType } from '../types/settings.types' +import { FilterWheel, WheelPreference } from '../types/wheel.types' +import { LocalStorageService } from './local-storage.service' + +export class PreferenceData { + + constructor(private storage: LocalStorageService, private key: string, private defaultValue: T | (() => T)) { } + + has() { + return this.storage.has(this.key) + } + + get(defaultValue?: T | (() => T)): T { + return this.storage.get(this.key, defaultValue ?? this.defaultValue) + } + + set(value: T | undefined) { + this.storage.set(this.key, value) + } + + remove() { + this.storage.delete(this.key) + } +} @Injectable({ providedIn: 'root' }) -export class RemoteStorageService implements StorageService { +export class PreferenceService { - constructor(private api: ApiService) { } + constructor(private storage: LocalStorageService) { } - clear() { - return this.api.clearPreferences() + wheelPreference(wheel: FilterWheel) { + return new PreferenceData(this.storage, `wheel.${wheel.name}`, {}) } - delete(key: string) { - return this.api.deletePreference(key) + cameraPreference(camera: Camera) { + return new PreferenceData(this.storage, `camera.${camera.name}`, () => structuredClone(EMPTY_CAMERA_PREFERENCE)) } - async get(key: string, defaultValue: T) { - return await this.api.getPreference(key) ?? defaultValue + cameraStartCaptureForFlatWizard(camera: Camera) { + return new PreferenceData(this.storage, `camera.${camera.name}.flatWizard`, () => this.cameraPreference(camera).get()) } - has(key: string) { - return this.api.hasPreference(key) + cameraStartCaptureForDARV(camera: Camera) { + return new PreferenceData(this.storage, `camera.${camera.name}.darv`, () => this.cameraPreference(camera).get()) } - set(key: string, value: any) { - if (value === null || value === undefined) return this.delete(key) - else return this.api.setPreference(key, value) + cameraStartCaptureForTPPA(camera: Camera) { + return new PreferenceData(this.storage, `camera.${camera.name}.tppa`, () => this.cameraPreference(camera).get()) } + + plateSolverOptions(type: PlateSolverType) { + return new PreferenceData(this.storage, `settings.plateSolver.${type}`, () => { ...EMPTY_PLATE_SOLVER_OPTIONS, type }) + } + + readonly imagePreference = new PreferenceData(this.storage, 'image', () => structuredClone(EMPTY_IMAGE_PREFERENCE)) + readonly alignmentPreference = new PreferenceData(this.storage, `alignment`, () => structuredClone(EMPTY_ALIGNMENT_PREFERENCE)) + readonly connections = new PreferenceData(this.storage, 'home.connections', () => []) + readonly homeImageDefaultDirectory = new PreferenceData(this.storage, 'home.image.directory', '') + readonly imageFOVs = new PreferenceData(this.storage, 'image.fovs', () => []) } \ No newline at end of file diff --git a/desktop/src/shared/services/prime.service.ts b/desktop/src/shared/services/prime.service.ts index 63003b436..1899c0902 100644 --- a/desktop/src/shared/services/prime.service.ts +++ b/desktop/src/shared/services/prime.service.ts @@ -1,5 +1,5 @@ import { Injectable, Type } from '@angular/core' -import { ConfirmEventType, ConfirmationService } from 'primeng/api' +import { ConfirmEventType, ConfirmationService, MessageService } from 'primeng/api' import { DialogService, DynamicDialogConfig } from 'primeng/dynamicdialog' @Injectable({ providedIn: 'root' }) @@ -8,6 +8,7 @@ export class PrimeService { constructor( private dialog: DialogService, private confirmation: ConfirmationService, + private messager: MessageService, ) { } open(componentType: Type, config: DynamicDialogConfig) { @@ -51,4 +52,8 @@ export class PrimeService { }) }) } + + message(text: string, severity: 'info' | 'warn' | 'error' | 'success' = 'success') { + this.messager.add({ severity, detail: text, life: 8500 }) + } } \ No newline at end of file diff --git a/desktop/src/shared/services/remote-storage.service.ts b/desktop/src/shared/services/remote-storage.service.ts new file mode 100644 index 000000000..c6451a051 --- /dev/null +++ b/desktop/src/shared/services/remote-storage.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core' +import { ApiService } from './api.service' +import { StorageService } from './storage.service' + +@Injectable({ providedIn: 'root' }) +export class RemoteStorageService implements StorageService { + + constructor(private api: ApiService) { } + + clear() { + return this.api.clearPreferences() + } + + delete(key: string) { + return this.api.deletePreference(key) + } + + async get(key: string, defaultValue: T) { + return await this.api.getPreference(key) ?? defaultValue + } + + has(key: string) { + return this.api.hasPreference(key) + } + + set(key: string, value: any) { + if (value === null || value === undefined) return this.delete(key) + else return this.api.setPreference(key, value) + } +} \ No newline at end of file diff --git a/desktop/src/shared/types/alignment.types.ts b/desktop/src/shared/types/alignment.types.ts index 1412fdc87..90c9333c9 100644 --- a/desktop/src/shared/types/alignment.types.ts +++ b/desktop/src/shared/types/alignment.types.ts @@ -1,23 +1,76 @@ -import { Camera, CameraStartCapture } from './camera.types' -import { GuideDirection, GuideOutput } from './guider.types' +import { Angle } from './atlas.types' +import { CameraStartCapture } from './camera.types' +import { GuideDirection } from './guider.types' +import { PlateSolverOptions, PlateSolverType } from './settings.types' export type Hemisphere = 'NORTHERN' | 'SOUTHERN' export type DARVState = 'IDLE' | 'INITIAL_PAUSE' | 'FORWARD' | 'BACKWARD' +export type TPPAState = 'IDLE' | 'SLEWING' | 'SOLVING' | 'SOLVED' | 'PAUSING' | 'PAUSED' | 'COMPUTED' | 'FAILED' | 'FINISHED' + +export type AlignmentMethod = 'DARV' | 'TPPA' + +export interface AlignmentPreference { + darvInitialPause: number + darvExposureTime: number + darvHemisphere: Hemisphere + tppaStartFromCurrentPosition: boolean + tppaEastDirection: boolean + tppaCompensateRefraction: boolean + tppaStopTrackingWhenDone: boolean + tppaStepDistance: number + tppaPlateSolverType: PlateSolverType +} + +export const EMPTY_ALIGNMENT_PREFERENCE: AlignmentPreference = { + darvInitialPause: 5, + darvExposureTime: 30, + darvHemisphere: 'NORTHERN', + tppaStartFromCurrentPosition: true, + tppaEastDirection: true, + tppaCompensateRefraction: true, + tppaStopTrackingWhenDone: true, + tppaStepDistance: 10, + tppaPlateSolverType: 'ASTAP', +} + export interface DARVStart { - capture?: CameraStartCapture + capture: CameraStartCapture exposureTime: number initialPause: number direction: GuideDirection reversed: boolean } -export interface DARVEvent extends MessageEvent { - camera: Camera - guideOutput: GuideOutput +export interface DARVElapsed extends MessageEvent { + id: string remainingTime: number progress: number state: DARVState direction?: GuideDirection } + +export interface TPPAStart { + capture: CameraStartCapture + plateSolver: PlateSolverOptions + startFromCurrentPosition: boolean + eastDirection: boolean + compensateRefraction: boolean + stopTrackingWhenDone: boolean + stepDistance: number +} + +export interface TPPAElapsed extends MessageEvent { + id: string + elapsedTime: number + stepCount: number + state: TPPAState + rightAscension: Angle + declination: Angle + azimuthError: Angle + altitudeError: Angle + totalError: Angle + azimuthErrorDirection: string + altitudeErrorDirection: string +} diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts index 1caabe7e4..59dff8d69 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -22,7 +22,7 @@ export interface NotificationEvent extends MessageEvent { export const INTERNAL_EVENT_TYPES = [ 'DIRECTORY.OPEN', 'FILE.OPEN', 'FILE.SAVE', 'WINDOW.OPEN', 'WINDOW.CLOSE', - 'WINDOW.PIN', 'WINDOW.UNPIN', 'WINDOW.MINIMIZE', 'WINDOW.MAXIMIZE', + 'WINDOW.PIN', 'WINDOW.UNPIN', 'WINDOW.MINIMIZE', 'WINDOW.MAXIMIZE', 'WINDOW.RESIZE', 'WHEEL.RENAMED', 'LOCATION.CHANGED', 'JSON.WRITE', 'JSON.READ' ] as const @@ -45,6 +45,7 @@ export interface OpenWindow extends OpenWindowOptionsWithData { id: string path: string modal?: boolean + autoResizable?: boolean } export interface CloseWindow { diff --git a/desktop/src/shared/types/calculator.types.ts b/desktop/src/shared/types/calculator.types.ts new file mode 100644 index 000000000..b82d5da25 --- /dev/null +++ b/desktop/src/shared/types/calculator.types.ts @@ -0,0 +1,20 @@ + +export interface CalculatorOperand { + label: string + prefix?: string + suffix?: string + value?: number + minFractionDigits?: number + maxFractionDigits?: number +} + +export interface CalculatorFormula { + title: string + description?: string + expression: string + operands: CalculatorOperand[] + result: CalculatorOperand + tip?: string + calculate: (...operands: (number | undefined)[]) => number | undefined +} + diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 0d1a926d0..75e188f5d 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -1,11 +1,9 @@ import { MessageEvent } from './api.types' import { Thermometer } from './auxiliary.types' import { PropertyState } from './device.types' -import { Focuser } from './focuser.types' import { GuideOutput } from './guider.types' -import { FilterWheel } from './wheel.types' -export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' +export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' export type FrameType = 'LIGHT' | 'DARK' | 'FLAT' | 'BIAS' @@ -71,6 +69,8 @@ export interface Camera extends GuideOutput, Thermometer { } export const EMPTY_CAMERA: Camera = { + sender: '', + id: '', exposuring: false, hasCoolerControl: false, coolerPower: 0, @@ -133,7 +133,6 @@ export interface Dither { export interface CameraStartCapture { enabled?: boolean - camera?: Camera exposureTime: number exposureAmount: number exposureDelay: number @@ -151,10 +150,8 @@ export interface CameraStartCapture { savePath?: string autoSubFolderMode: AutoSubFolderMode dither?: Dither - wheel?: FilterWheel filterPosition?: number shutterPosition?: number - focuser?: Focuser focusOffset?: number } @@ -181,7 +178,23 @@ export const EMPTY_CAMERA_START_CAPTURE: CameraStartCapture = { } } -export interface CameraCaptureEvent extends MessageEvent { +export function updateCameraStartCaptureFromCamera(request: CameraStartCapture, camera: Camera) { + if (camera.maxX > 1) request.x = Math.max(camera.minX, Math.min(request.x, camera.maxX)) + if (camera.maxY > 1) request.y = Math.max(camera.minY, Math.min(request.y, camera.maxY)) + + if (camera.maxWidth > 1 && (request.width <= 1 || request.width > camera.maxWidth)) request.width = camera.maxWidth + if (camera.maxHeight > 1 && (request.height <= 1 || request.height > camera.maxHeight)) request.height = camera.maxHeight + if (camera.minWidth > 1 && request.width < camera.minWidth) request.width = camera.minWidth + if (camera.minHeight > 1 && request.height < camera.minHeight) request.height = camera.minHeight + + if (camera.maxBinX > 1) request.binX = Math.max(1, Math.min(request.binX, camera.maxBinX)) + if (camera.maxBinY > 1) request.binY = Math.max(1, Math.min(request.binY, camera.maxBinY)) + if (camera.gainMax) request.gain = Math.max(camera.gainMin, Math.min(request.gain, camera.gainMax)) + if (camera.offsetMax) request.offset = Math.max(camera.offsetMin, Math.min(request.offset, camera.offsetMax)) + if (!request.frameFormat || !camera.frameFormats.includes(request.frameFormat)) request.frameFormat = camera.frameFormats[0] +} + +export interface CameraCaptureElapsed extends MessageEvent { camera: Camera exposureAmount: number exposureCount: number @@ -201,18 +214,23 @@ export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' export interface CameraDialogInput { mode: CameraDialogMode + camera: Camera request: CameraStartCapture } -export function cameraPreferenceKey(camera: Camera) { - return `camera.${camera.name}` +export interface CameraPreference extends CameraStartCapture { + setpointTemperature: number + exposureTimeUnit: ExposureTimeUnit + exposureMode: ExposureMode + subFrame: boolean } -export interface CameraPreference extends Partial { - setpointTemperature?: number - exposureTimeUnit?: ExposureTimeUnit - exposureMode?: ExposureMode - subFrame?: boolean +export const EMPTY_CAMERA_PREFERENCE: CameraPreference = { + ...EMPTY_CAMERA_START_CAPTURE, + setpointTemperature: 0, + exposureTimeUnit: ExposureTimeUnit.MICROSECOND, + exposureMode: 'SINGLE', + subFrame: false, } export interface CameraExposureInfo { diff --git a/desktop/src/shared/types/device.types.ts b/desktop/src/shared/types/device.types.ts index 5b7494536..53b60d1a7 100644 --- a/desktop/src/shared/types/device.types.ts +++ b/desktop/src/shared/types/device.types.ts @@ -9,6 +9,8 @@ export type INDIPropertyType = 'NUMBER' | 'SWITCH' | 'TEXT' export type SwitchRule = 'ONE_OF_MANY' | 'AT_MOST_ONE' | 'ANY_OF_MANY' export interface Device { + readonly sender: string + readonly id: string readonly name: string connected: boolean } diff --git a/desktop/src/shared/types/flat-wizard.types.ts b/desktop/src/shared/types/flat-wizard.types.ts index 0a8e9e10a..f3919befc 100644 --- a/desktop/src/shared/types/flat-wizard.types.ts +++ b/desktop/src/shared/types/flat-wizard.types.ts @@ -1,4 +1,4 @@ -import { CameraCaptureEvent, CameraStartCapture } from './camera.types' +import { CameraCaptureElapsed, CameraStartCapture } from './camera.types' export interface FlatWizardRequest { captureRequest: CameraStartCapture @@ -8,8 +8,12 @@ export interface FlatWizardRequest { meanTolerance: number } -export interface FlatWizardEvent { +export type FlatWizardState = 'EXPOSURING' | 'CAPTURED' | 'FAILED' + +export interface FlatWizardElapsed { + state: FlatWizardState exposureTime: number - capture?: CameraCaptureEvent + capture?: CameraCaptureElapsed savedPath?: string + message?: string } diff --git a/desktop/src/shared/types/focuser.types.ts b/desktop/src/shared/types/focuser.types.ts index 43b0206f1..eb1e2f8ca 100644 --- a/desktop/src/shared/types/focuser.types.ts +++ b/desktop/src/shared/types/focuser.types.ts @@ -8,20 +8,22 @@ export interface Focuser extends Device, Thermometer { canRelativeMove: boolean canAbort: boolean canReverse: boolean - reverse: boolean + reversed: boolean canSync: boolean hasBacklash: boolean maxPosition: number } export const EMPTY_FOCUSER: Focuser = { + sender: '', + id: '', moving: false, position: 0, canAbsoluteMove: false, canRelativeMove: false, canAbort: false, canReverse: false, - reverse: false, + reversed: false, canSync: false, hasBacklash: false, maxPosition: 0, diff --git a/desktop/src/shared/types/framing.types.ts b/desktop/src/shared/types/framing.types.ts index c4f335584..48472023a 100644 --- a/desktop/src/shared/types/framing.types.ts +++ b/desktop/src/shared/types/framing.types.ts @@ -1,36 +1,4 @@ -export const HIPS_SURVEY_TYPES = [ - 'CDS_P_DSS2_NIR', - 'CDS_P_DSS2_BLUE', 'CDS_P_DSS2_COLOR', - 'CDS_P_DSS2_RED', 'FZU_CZ_P_CTA_FRAM_SURVEY_B', - 'FZU_CZ_P_CTA_FRAM_SURVEY_R', 'FZU_CZ_P_CTA_FRAM_SURVEY_V', - 'FZU_CZ_P_CTA_FRAM_SURVEY_COLOR', 'CDS_P_2MASS_H', - 'CDS_P_2MASS_J', 'CDS_P_2MASS_K', - 'CDS_P_2MASS_COLOR', 'CDS_P_AKARI_FIS_COLOR', - 'CDS_P_AKARI_FIS_N160', 'CDS_P_AKARI_FIS_N60', - 'CDS_P_AKARI_FIS_WIDEL', 'CDS_P_AKARI_FIS_WIDES', - 'CDS_P_NEOWISER_COLOR', 'CDS_P_NEOWISER_W1', - 'CDS_P_NEOWISER_W2', 'CDS_P_WISE_WSSA_12UM', - 'CDS_P_ALLWISE_W1', 'CDS_P_ALLWISE_W2', - 'CDS_P_ALLWISE_W3', 'CDS_P_ALLWISE_W4', - 'CDS_P_ALLWISE_COLOR', 'CDS_P_UNWISE_W1', - 'CDS_P_UNWISE_W2', 'CDS_P_UNWISE_COLOR_W2_W1W2_W1', - 'CDS_P_RASS', 'JAXA_P_ASCA_GIS', - 'JAXA_P_ASCA_SIS', 'JAXA_P_MAXI_GSC', - 'JAXA_P_MAXI_SSC', 'JAXA_P_SUZAKU', - 'JAXA_P_SWIFT_BAT_FLUX', 'CDS_P_EGRET_DIF_100_150', - 'CDS_P_EGRET_DIF_1000_2000', 'CDS_P_EGRET_DIF_150_300', - 'CDS_P_EGRET_DIF_2000_4000', 'CDS_P_EGRET_DIF_30_50', - 'CDS_P_EGRET_DIF_300_500', 'CDS_P_EGRET_DIF_4000_10000', - 'CDS_P_EGRET_DIF_50_70', 'CDS_P_EGRET_DIF_500_1000', - 'CDS_P_EGRET_DIF_70_100', 'CDS_P_EGRET_INF100', - 'CDS_P_EGRET_SUP100', 'CDS_P_FERMI_3', - 'CDS_P_FERMI_4', 'CDS_P_FERMI_5', 'CDS_P_FERMI_COLOR' -] as const - -export type HipsSurveyType = (typeof HIPS_SURVEY_TYPES)[number] - export interface HipsSurvey { - type: HipsSurveyType | string id: string category: string frame: string diff --git a/desktop/src/shared/types/guider.types.ts b/desktop/src/shared/types/guider.types.ts index c0708b291..07c74d824 100644 --- a/desktop/src/shared/types/guider.types.ts +++ b/desktop/src/shared/types/guider.types.ts @@ -61,6 +61,8 @@ export interface GuideOutput extends Device { } export const EMPTY_GUIDE_OUTPUT: GuideOutput = { + sender: '', + id: '', canPulseGuide: false, pulseGuiding: false, name: '', diff --git a/desktop/src/shared/types/home.types.ts b/desktop/src/shared/types/home.types.ts index 4c71f3c11..a85f81787 100644 --- a/desktop/src/shared/types/home.types.ts +++ b/desktop/src/shared/types/home.types.ts @@ -1,13 +1,30 @@ export type HomeWindowType = 'CAMERA' | 'MOUNT' | 'GUIDER' | 'WHEEL' | 'FOCUSER' | 'DOME' | 'ROTATOR' | 'SWITCH' | - 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'ABOUT' | 'FLAT_WIZARD' + 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' + +export const CONNECTION_TYPES = ['INDI', 'ALPACA'] as const + +export type ConnectionType = (typeof CONNECTION_TYPES)[number] export interface ConnectionDetails { + name: string host: string port: number + type: ConnectionType + connected: boolean connectedAt?: number + id?: string } +export type ConnectionStatus = Omit, 'connected' | 'name' | 'connectedAt'> + export const EMPTY_CONNECTION_DETAILS: ConnectionDetails = { + name: '', host: 'localhost', - port: 7624 + port: 7624, + type: 'INDI', + connected: false +} + +export interface ConnectionClosed { + id: string } diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 2001d029d..4d30bf254 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -1,5 +1,7 @@ -import { AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from './atlas.types' +import { Point, Size } from 'electron' +import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from './atlas.types' import { Camera } from './camera.types' +import { PlateSolverType } from './settings.types' export type ImageChannel = 'RED' | 'GREEN' | 'BLUE' | 'GRAY' | 'NONE' @@ -22,9 +24,9 @@ export interface ImageInfo { stretchShadow: number stretchHighlight: number stretchMidtone: number - rightAscension?: string - declination?: string - solved: boolean + rightAscension?: Angle + declination?: Angle + solved?: ImageSolved headers: FITSHeaderItem[] statistics: ImageStatistics } @@ -92,3 +94,56 @@ export interface ImageStatistics { minimum: number maximum: number } + +export interface ImagePreference { + solverRadius?: number + solverType?: PlateSolverType +} + +export const EMPTY_IMAGE_PREFERENCE: ImagePreference = { + solverRadius: 4, + solverType: 'ASTROMETRY_NET_ONLINE' +} + +export interface ImageData { + camera?: Camera + path?: string + source?: ImageSource + title?: string +} + +export interface FOV { + enabled: boolean + focalLength: number + aperture: number + cameraSize: Size + pixelSize: Size + barlowReducer: number + bin: number + rotation: number + color: string + computed?: { + cameraResolution: Size + focalRatio: number + fieldSize: Size + svg: Size & Point + } +} + +export const DEFAULT_FOV: FOV = { + enabled: true, + focalLength: 600, + aperture: 80, + cameraSize: { + width: 1392, + height: 1040, + }, + pixelSize: { + width: 6.45, + height: 6.45, + }, + barlowReducer: 1, + bin: 1, + rotation: 0, + color: '#FFFF00', +} diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts index 2370d20b4..ddb5dbc65 100644 --- a/desktop/src/shared/types/mount.types.ts +++ b/desktop/src/shared/types/mount.types.ts @@ -38,6 +38,8 @@ export interface Mount extends EquatorialCoordinate, GPS, GuideOutput, Parkable } export const EMPTY_MOUNT: Mount = { + sender: '', + id: '', slewing: false, tracking: false, canAbort: false, diff --git a/desktop/src/shared/types/sequencer.types.ts b/desktop/src/shared/types/sequencer.types.ts index eba4eeb9a..a40396f57 100644 --- a/desktop/src/shared/types/sequencer.types.ts +++ b/desktop/src/shared/types/sequencer.types.ts @@ -1,7 +1,17 @@ -import { AutoSubFolderMode, CameraCaptureEvent, CameraStartCapture, Dither } from './camera.types' +import { AutoSubFolderMode, Camera, CameraCaptureElapsed, CameraStartCapture, Dither } from './camera.types' +import { Focuser } from './focuser.types' +import { FilterWheel } from './wheel.types' export type SequenceCaptureMode = 'FULLY' | 'INTERLEAVED' +export const SEQUENCE_ENTRY_PROPERTIES = [ + 'EXPOSURE_TIME', 'EXPOSURE_AMOUNT', 'EXPOSURE_DELAY', + 'FRAME_TYPE', 'X', 'Y', 'WIDTH', 'HEIGHT', + 'BIN', 'FRAME_FORMAT', 'GAIN', 'OFFSET' +] as const + +export type SequenceEntryProperty = (typeof SEQUENCE_ENTRY_PROPERTIES)[number] + export interface AutoFocusAfterConditions { enabled: boolean onStart: boolean @@ -24,6 +34,9 @@ export interface SequencePlan { entries: CameraStartCapture[] dither: Dither autoFocus: AutoFocusAfterConditions + camera?: Camera + wheel?: FilterWheel + focuser?: Focuser } export const EMPTY_SEQUENCE_PLAN: SequencePlan = { @@ -52,10 +65,10 @@ export const EMPTY_SEQUENCE_PLAN: SequencePlan = { }, } -export interface SequencerEvent extends MessageEvent { +export interface SequencerElapsed extends MessageEvent { id: number elapsedTime: number remainingTime: number progress: number - capture?: CameraCaptureEvent + capture?: CameraCaptureElapsed } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index 6d95db4be..df1391d7b 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -1,5 +1,7 @@ export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP' +export const DEFAULT_SOLVER_TYPES: PlateSolverType[] = ['ASTROMETRY_NET_ONLINE', 'ASTAP'] + export interface PlateSolverOptions { type: PlateSolverType executablePath: string @@ -15,5 +17,5 @@ export const EMPTY_PLATE_SOLVER_OPTIONS: PlateSolverOptions = { downsampleFactor: 0, apiUrl: 'https://nova.astrometry.net/', apiKey: '', - timeout: 0, + timeout: 600, } diff --git a/desktop/src/shared/types/wheel.types.ts b/desktop/src/shared/types/wheel.types.ts index 40aa5c1dd..141bdf170 100644 --- a/desktop/src/shared/types/wheel.types.ts +++ b/desktop/src/shared/types/wheel.types.ts @@ -7,25 +7,26 @@ export interface FilterWheel extends Device { count: number position: number moving: boolean + names: string[] } export const EMPTY_WHEEL: FilterWheel = { + sender: '', + id: '', count: 0, position: 0, moving: false, name: '', - connected: false + connected: false, + names: [], } export interface WheelDialogInput { mode: WheelDialogMode + wheel: FilterWheel request: CameraStartCapture } -export function wheelPreferenceKey(wheel: FilterWheel) { - return `wheel.${wheel.name}` -} - export interface WheelPreference { shutterPosition?: number names?: string[] diff --git a/desktop/src/shared/utils/comparators.ts b/desktop/src/shared/utils/comparators.ts index ffef54d28..7c29fc757 100644 --- a/desktop/src/shared/utils/comparators.ts +++ b/desktop/src/shared/utils/comparators.ts @@ -1,9 +1,6 @@ import { Device } from '../types/device.types' -export function compareText(a: string, b: string) { - return a.localeCompare(b) -} +export type Comparator = (a: T, b: T) => number -export function compareDevice(a: Device, b: Device) { - return compareText(a.name, b.name) -} \ No newline at end of file +export const textComparator: Comparator = (a: string, b: string) => a.localeCompare(b) +export const deviceComparator: Comparator = (a: Device, b: Device) => textComparator(a.name, b.name) diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index 860032632..1a9358367 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -151,7 +151,8 @@ p-calendar.border-0 .p-calendar-w-btn { } .p-tag { - padding: 0.13rem 0.4rem; + padding: 0.1rem 0.4rem; + border-radius: 2px; .p-tag-value { line-height: 1.2 !important; @@ -250,6 +251,10 @@ p-dropdownitem *, font-family: monospace !important; } +.gap-1px { + gap: 1px; +} + ::-webkit-scrollbar { width: 6px; } diff --git a/desktop/src/typings.d.ts b/desktop/src/typings.d.ts index 271e0b75c..5ac88f709 100644 --- a/desktop/src/typings.d.ts +++ b/desktop/src/typings.d.ts @@ -8,5 +8,17 @@ interface Window { process: any require: any apiPort: number - modal: boolean + options: { + icon?: string + resizable?: boolean + width?: number | string + height?: number | string + bringToFront?: boolean + requestFocus?: boolean + id: string + path: string + modal?: boolean + autoResizable?: boolean + data: any + } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c4..d64cd4917 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6aba2515..2ea3535dc 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.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a5..1aa94a426 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/nebulosa-alignment/build.gradle.kts b/nebulosa-alignment/build.gradle.kts index cdcb818a3..432cfa8f9 100644 --- a/nebulosa-alignment/build.gradle.kts +++ b/nebulosa-alignment/build.gradle.kts @@ -4,6 +4,8 @@ plugins { } dependencies { + api(project(":nebulosa-erfa")) + api(project(":nebulosa-time")) api(project(":nebulosa-plate-solving")) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/StarAlignment.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/StarAlignment.kt deleted file mode 100644 index 095664c82..000000000 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/StarAlignment.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.alignment - -interface StarAlignment diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/algorithms/ThreePointPolarAlignment.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/algorithms/ThreePointPolarAlignment.kt deleted file mode 100644 index 48f433b55..000000000 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/algorithms/ThreePointPolarAlignment.kt +++ /dev/null @@ -1,5 +0,0 @@ -package nebulosa.alignment.algorithms - -import nebulosa.alignment.StarAlignment - -class ThreePointPolarAlignment : StarAlignment diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/PolarErrorDetermination.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/PolarErrorDetermination.kt new file mode 100644 index 000000000..5e1aef6be --- /dev/null +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/PolarErrorDetermination.kt @@ -0,0 +1,50 @@ +package nebulosa.alignment.polar.point.three + +import nebulosa.constants.PI +import nebulosa.constants.TAU +import nebulosa.math.Angle +import nebulosa.math.Vector3D +import kotlin.math.abs + +internal data class PolarErrorDetermination( + @JvmField val firstPosition: Position, + @JvmField val secondPosition: Position, + @JvmField val thirdPosition: Position, + @JvmField val longitude: Angle, + @JvmField val latitude: Angle, +) { + + private inline val isNorthern + get() = latitude > 0.0 + + @JvmField val plane = with(Vector3D.plane(firstPosition.vector, secondPosition.vector, thirdPosition.vector)) { + // Flip vector if pointing to the wrong direction. + if (isNorthern && x < 0 || !isNorthern && x > 0) -normalized else normalized + } + + @JvmField val errorPosition = Position(plane, longitude, latitude) + + fun compute(): DoubleArray { + val altitudeError: Double + var azimuthError: Double + + val pole = abs(latitude) + + if (isNorthern) { + altitudeError = errorPosition.topocentric.altitude - pole + azimuthError = errorPosition.topocentric.azimuth + } else { + altitudeError = pole - errorPosition.topocentric.altitude + azimuthError = errorPosition.topocentric.azimuth + PI + } + + if (azimuthError > PI) { + azimuthError -= TAU + } + if (azimuthError < -PI) { + azimuthError += TAU + } + + return doubleArrayOf(azimuthError, altitudeError) + } +} diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/Position.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/Position.kt new file mode 100644 index 000000000..fb28668e4 --- /dev/null +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/Position.kt @@ -0,0 +1,56 @@ +package nebulosa.alignment.polar.point.three + +import nebulosa.constants.PIOVERTWO +import nebulosa.erfa.eraAtco13 +import nebulosa.math.Angle +import nebulosa.math.ONE_ATM +import nebulosa.math.Vector3D +import nebulosa.time.CurrentTime +import nebulosa.time.IERS +import nebulosa.time.InstantOfTime +import kotlin.math.cos +import kotlin.math.sin + +internal data class Position( + @JvmField val topocentric: Topocentric, + @JvmField val vector: Vector3D, +) { + + companion object { + + operator fun invoke( + rightAscension: Angle, declination: Angle, + longitude: Angle, latitude: Angle, + time: InstantOfTime = CurrentTime, + compensateRefraction: Boolean = false, + ): Position { + // SOFA.CelestialToTopocentric. + val dut1 = IERS.delta(time) + val (xp, yp) = IERS.pmAngles(time) + val pressure = if (compensateRefraction) ONE_ATM else 0.0 + // @formatter:off + val (b) = eraAtco13(rightAscension, declination, 0.0, 0.0, 0.0, 0.0, time.utc.whole, time.utc.fraction, dut1, longitude, latitude, 0.0, xp, yp, pressure, 15.0, 0.5, 0.55) + // @formatter:on + val topocentric = Topocentric(b[0], PIOVERTWO - b[1], longitude, latitude) + // val vector = CartesianCoordinate.of(-b[0], b[1], 1.0) + val theta = -b[0] + val phi = b[1] + val x = cos(theta) * sin(phi) + val y = sin(theta) * sin(phi) + val z = cos(phi) + return Position(topocentric, Vector3D(x, y, z)) + } + + operator fun invoke( + vector: Vector3D, longitude: Angle, latitude: Angle, + ): Position { + val topocentric = if (vector.x == 0.0 && vector.y == 0.0) { + Topocentric(0.0, PIOVERTWO, longitude, latitude) + } else { + Topocentric(-vector.longitude, PIOVERTWO - vector.latitude, longitude, latitude) + } + + return Position(topocentric, vector) + } + } +} 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 new file mode 100644 index 000000000..5b7772fc8 --- /dev/null +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt @@ -0,0 +1,80 @@ +package nebulosa.alignment.polar.point.three + +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.constants.DEG2RAD +import nebulosa.fits.declination +import nebulosa.fits.observationDate +import nebulosa.fits.rightAscension +import nebulosa.imaging.Image +import nebulosa.math.Angle +import nebulosa.plate.solving.PlateSolution +import nebulosa.plate.solving.PlateSolver +import nebulosa.plate.solving.PlateSolvingException +import nebulosa.time.TimeYMDHMS +import nebulosa.time.UTC +import java.nio.file.Path +import kotlin.math.min + +/** + * Three Point Polar Alignment almost anywhere in the sky. + * + * Based on Stefan Berg's algorithm. + * + * @see
BitBucket + */ +data class ThreePointPolarAlignment( + private val solver: PlateSolver, + private val longitude: Angle, + private val latitude: Angle, +) { + + private val positions = arrayOfNulls(3) + + @Volatile var state = 0 + private set + + fun align( + path: Path, image: Image, + rightAscension: Angle = image.header.rightAscension, + declination: Angle = image.header.declination, + radius: Angle = DEFAULT_RADIUS, + compensateRefraction: Boolean = false, + cancellationToken: CancellationToken = CancellationToken.NONE, + ): ThreePointPolarAlignmentResult { + val solution = try { + solver.solve(path, image, rightAscension, declination, radius, cancellationToken = cancellationToken) + } catch (e: PlateSolvingException) { + return ThreePointPolarAlignmentResult.NoPlateSolution(e) + } + + if (!solution.solved || cancellationToken.isCancelled) { + return ThreePointPolarAlignmentResult.NoPlateSolution(null) + } else { + val time = image.header.observationDate?.let { UTC(TimeYMDHMS(it)) } ?: UTC.now() + + positions[min(state, 2)] = solution.position(time, compensateRefraction) + + if (state++ >= 2) { + val polarErrorDetermination = PolarErrorDetermination(positions[0]!!, positions[1]!!, positions[2]!!, longitude, latitude) + val (azimuth, altitude) = polarErrorDetermination.compute() + return ThreePointPolarAlignmentResult.Measured(solution.rightAscension, solution.declination, azimuth, altitude) + } + + return ThreePointPolarAlignmentResult.NeedMoreMeasurement(solution.rightAscension, solution.declination) + } + } + + fun reset() { + state = 0 + positions.fill(null) + } + + private fun PlateSolution.position(time: UTC, compensateRefraction: Boolean): Position { + return Position(rightAscension, declination, longitude, latitude, time, compensateRefraction) + } + + companion object { + + const val DEFAULT_RADIUS: Angle = 4 * DEG2RAD + } +} diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignmentResult.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignmentResult.kt new file mode 100644 index 000000000..6496728d5 --- /dev/null +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignmentResult.kt @@ -0,0 +1,16 @@ +package nebulosa.alignment.polar.point.three + +import nebulosa.math.Angle +import nebulosa.plate.solving.PlateSolvingException + +sealed interface ThreePointPolarAlignmentResult { + + data class NeedMoreMeasurement(@JvmField val rightAscension: Angle, @JvmField val declination: Angle) : ThreePointPolarAlignmentResult + + data class Measured( + @JvmField val rightAscension: Angle, @JvmField val declination: Angle, + @JvmField val azimuth: Angle, @JvmField val altitude: Angle, + ) : ThreePointPolarAlignmentResult + + data class NoPlateSolution(@JvmField val exception: PlateSolvingException?) : ThreePointPolarAlignmentResult +} diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/Topocentric.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/Topocentric.kt new file mode 100644 index 000000000..ec1ca0366 --- /dev/null +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/Topocentric.kt @@ -0,0 +1,8 @@ +package nebulosa.alignment.polar.point.three + +import nebulosa.math.Angle + +data class Topocentric( + @JvmField val azimuth: Angle, @JvmField val altitude: Angle, + @JvmField val longitude: Angle, @JvmField val latitude: Angle, +) diff --git a/nebulosa-alignment/src/test/kotlin/ThreePointPolarAlignmentTest.kt b/nebulosa-alignment/src/test/kotlin/ThreePointPolarAlignmentTest.kt new file mode 100644 index 000000000..5d4f8eb01 --- /dev/null +++ b/nebulosa-alignment/src/test/kotlin/ThreePointPolarAlignmentTest.kt @@ -0,0 +1,103 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.doubles.plusOrMinus +import io.kotest.matchers.shouldBe +import nebulosa.alignment.polar.point.three.PolarErrorDetermination +import nebulosa.alignment.polar.point.three.Position +import nebulosa.math.deg +import nebulosa.math.hours +import nebulosa.math.toArcsec +import nebulosa.time.IERS +import nebulosa.time.IERSA +import nebulosa.time.TimeYMDHMS +import nebulosa.time.UTC +import java.nio.file.Path +import kotlin.io.path.inputStream + +class ThreePointPolarAlignmentTest : StringSpec() { + + init { + val iersa = IERSA() + iersa.load(Path.of("../data/finals2000A.all").inputStream()) + IERS.attach(iersa) + + // Based on logs generated by N.I.N.A. using Telescope Simulator for .NET and Sky Simulator (ASCOM). + // https://sourceforge.net/projects/sky-simulator/ + + "perfectly aligned" { + val position1 = Position("05:35:18".hours, "-05 23 26".deg, LNG, SLAT, UTC(TimeYMDHMS(2024, 2, 10, 22, 58, 42.4979))) + position1.vector[0] shouldBe (0.301851589038 plusOrMinus 1e-4) + position1.vector[1] shouldBe (-0.0681426041296783 plusOrMinus 1e-4) + position1.vector[2] shouldBe (0.950916507216938 plusOrMinus 1e-4) + + val position2 = Position("04:54:45".hours, "-05 24 50".deg, LNG, SLAT, UTC(TimeYMDHMS(2024, 2, 10, 22, 58, 58.1655))) + position2.vector[0] shouldBe (0.300426130373811 plusOrMinus 1e-4) + position2.vector[1] shouldBe (0.108903442814494 plusOrMinus 1e-4) + position2.vector[2] shouldBe (0.947567507005051 plusOrMinus 1e-4) + + val position3 = Position("04:14:08".hours, "-05 26 10".deg, LNG, SLAT, UTC(TimeYMDHMS(2024, 2, 10, 22, 59, 13.3739))) + position3.vector[0] shouldBe (0.286747300379159 plusOrMinus 1e-4) + position3.vector[1] shouldBe (0.282671401982864 plusOrMinus 1e-4) + position3.vector[2] shouldBe (0.915353955705828 plusOrMinus 1e-4) + + val pe = PolarErrorDetermination(position1, position2, position3, LNG, SLAT) + val (az, alt) = pe.compute() + + // Calculated Error: Az: -00ยฐ 00' 04", Alt: -00ยฐ 00' 07", Tot: 00ยฐ 00' 08" + az.toArcsec shouldBe (-4.0 plusOrMinus 2.5) + alt.toArcsec shouldBe (-7.0 plusOrMinus 2.5) + } + "bad southern polar aligned" { + val position1 = Position("05:35:29".hours, "-05 23 44".deg, LNG, SLAT, UTC(TimeYMDHMS(2024, 2, 11, 1, 2, 28.0693))) + position1.vector[0] shouldBe (0.260120895582042 plusOrMinus 1e-4) + position1.vector[1] shouldBe (0.452793316696993 plusOrMinus 1e-4) + position1.vector[2] shouldBe (0.852827844313337 plusOrMinus 1e-4) + + val position2 = Position("04:54:48".hours, "-05 23 16".deg, LNG, SLAT, UTC(TimeYMDHMS(2024, 2, 11, 1, 2, 43.0120))) + position2.vector[0] shouldBe (0.223679240068826 plusOrMinus 1e-4) + position2.vector[1] shouldBe (0.603179137475884 plusOrMinus 1e-4) + position2.vector[2] shouldBe (0.765599455117414 plusOrMinus 1e-4) + + val position3 = Position("04:14:05".hours, "-05 22 47".deg, LNG, SLAT, UTC(TimeYMDHMS(2024, 2, 11, 1, 2, 57.8800))) + position3.vector[0] shouldBe (0.177343985686423 plusOrMinus 1e-4) + position3.vector[1] shouldBe (0.734426214459154 plusOrMinus 1e-4) + position3.vector[2] shouldBe (0.65510857592925 plusOrMinus 1e-4) + + val pe = PolarErrorDetermination(position1, position2, position3, LNG, SLAT) + val (az, alt) = pe.compute() + + // Calculated Error: Az: 00ยฐ 10' 10", Alt: 00ยฐ 04' 41", Tot: 00ยฐ 11' 11" + az.toArcsec shouldBe (610.0 plusOrMinus 7.0) + alt.toArcsec shouldBe (281.0 plusOrMinus 7.0) + } + "bad northern polar aligned" { + val position1 = Position("05:35:35".hours, "-05 32 31".deg, LNG, NLAT, UTC(TimeYMDHMS(2024, 2, 11, 1, 19, 31.1390))) + position1.vector[0] shouldBe (-0.420977957462894 plusOrMinus 1e-4) + position1.vector[1] shouldBe (0.517127315719859 plusOrMinus 1e-4) + position1.vector[2] shouldBe (0.745222717492391 plusOrMinus 1e-4) + + val position2 = Position("04:54:49".hours, "-05 34 43".deg, LNG, NLAT, UTC(TimeYMDHMS(2024, 2, 11, 1, 19, 46.2383))) + position2.vector[0] shouldBe (-0.379893065189774 plusOrMinus 1e-4) + position2.vector[1] shouldBe (0.660293278184844 plusOrMinus 1e-4) + position2.vector[2] shouldBe (0.647837978050554 plusOrMinus 1e-4) + + val position3 = Position("04:13:55".hours, "-05 36 32".deg, LNG, NLAT, UTC(TimeYMDHMS(2024, 2, 11, 1, 20, 1.6394))) + position3.vector[0] shouldBe (-0.329258727296886 plusOrMinus 1e-4) + position3.vector[1] shouldBe (0.782693400663722 plusOrMinus 1e-4) + position3.vector[2] shouldBe (0.528185318857211 plusOrMinus 1e-4) + + val pe = PolarErrorDetermination(position1, position2, position3, LNG, NLAT) + val (az, alt) = pe.compute() + + // Calculated Error: Az: -00ยฐ 09' 58", Alt: 00ยฐ 04' 51", Tot: 00ยฐ 11' 05" + az.toArcsec shouldBe (-598.0 plusOrMinus 7.0) + alt.toArcsec shouldBe (291.0 plusOrMinus 7.0) + } + } + + companion object { + + @JvmStatic private val SLAT = "-023".deg + @JvmStatic private val NLAT = "+023".deg + @JvmStatic private val LNG = "-045".deg + } +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlignmentMode.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlignmentMode.kt new file mode 100644 index 000000000..11aad11ee --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlignmentMode.kt @@ -0,0 +1,10 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonFormat + +@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) +enum class AlignmentMode { + ALT_AZ, + EQUATORIAL, + GERMAN_EQUATORIAL, +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlignmentModeResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlignmentModeResponse.kt new file mode 100644 index 000000000..208965baa --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlignmentModeResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class AlignmentModeResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: AlignmentMode = AlignmentMode.ALT_AZ, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaCameraService.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaCameraService.kt new file mode 100644 index 000000000..188520472 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaCameraService.kt @@ -0,0 +1,249 @@ +package nebulosa.alpaca.api + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.* + +interface AlpacaCameraService : AlpacaGuideOutputService { + + @GET("api/v1/camera/{id}/connected") + override fun isConnected(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/connected") + override fun connect(@Path("id") id: Int, @Field("Connected") connected: Boolean): Call + + @GET("api/v1/camera/{id}/bayeroffsetx") + fun bayerOffsetX(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/bayeroffsety") + fun bayerOffsetY(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/binx") + fun binX(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/binx") + fun binX(@Path("id") id: Int, @Field("BinX") value: Int): Call + + @GET("api/v1/camera/{id}/biny") + fun binY(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/biny") + fun binY(@Path("id") id: Int, @Field("BinY") value: Int): Call + + @GET("api/v1/camera/{id}/camerastate") + fun cameraState(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/cameraxsize") + fun x(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/cameraysize") + fun y(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/canabortexposure") + fun canAbortExposure(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/canasymmetricbin") + fun canAsymmetricBin(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/canfastreadout") + fun canFastReadout(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/cangetcoolerpower") + fun canCoolerPower(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/canpulseguide") + override fun canPulseGuide(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/cansetccdtemperature") + fun canSetCCDTemperature(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/canstopexposure") + fun canStopExposure(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/ccdtemperature") + fun ccdTemperature(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/cooleron") + fun isCoolerOn(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/cooleron") + fun cooler(@Path("id") id: Int, @Field("CoolerOn") value: Boolean): Call + + @GET("api/v1/camera/{id}/coolerpower") + fun coolerPower(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/electronsperadu") + fun electronsPerADU(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/exposuremax") + fun exposureMax(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/exposuremin") + fun exposureMin(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/exposureresolution") + fun exposureResolution(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/fastreadout") + fun isFastReadout(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/fastreadout") + fun fastReadout(@Path("id") id: Int, @Field("FastReadout") value: Boolean): Call + + @GET("api/v1/camera/{id}/fullwellcapacity") + fun fullWellCapacity(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/gain") + fun gain(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/gain") + fun gain(@Path("id") id: Int, @Field("Gain") value: Int): Call + + @GET("api/v1/camera/{id}/gainmax") + fun gainMax(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/gainmin") + fun gainMin(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/gains") + fun gains(@Path("id") id: Int): Call> + + @GET("api/v1/camera/{id}/hasshutter") + fun hasShutter(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/heatsinktemperature") + fun heatSinkTemperature(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/imageready") + fun isImageReady(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/ispulseguiding") + override fun isPulseGuiding(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/lastexposureduration") + fun lastExposureDuration(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/lastexposurestarttime") + fun lastExposureStartTime(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/maxadu") + fun maxADU(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/maxbinx") + fun maxBinX(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/maxbiny") + fun maxBinY(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/numx") + fun numX(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/numx") + fun numX(@Path("id") id: Int, @Field("NumX") value: Int): Call + + @GET("api/v1/camera/{id}/numy") + fun numY(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/numy") + fun numY(@Path("id") id: Int, @Field("NumY") value: Int): Call + + @GET("api/v1/camera/{id}/offset") + fun offset(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/offset") + fun offset(@Path("id") id: Int, @Field("Offset") value: Int): Call + + @GET("api/v1/camera/{id}/offsetmax") + fun offsetMax(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/offsetmin") + fun offsetMin(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/offsets") + fun offsets(@Path("id") id: Int): Call> + + @GET("api/v1/camera/{id}/percentcompleted") + fun percentCompleted(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/pixelsizex") + fun pixelSizeX(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/pixelsizey") + fun pixelSizeY(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/readoutmode") + fun readoutMode(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/readoutmode") + fun readoutMode(@Path("id") id: Int, @Field("ReadoutMode") value: Int): Call + + @GET("api/v1/camera/{id}/readoutmodes") + fun readoutModes(@Path("id") id: Int): Call> + + @GET("api/v1/camera/{id}/sensorname") + fun sensorName(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/sensortype") + fun sensorType(@Path("id") id: Int): Call + + @GET("api/v1/camera/{id}/setccdtemperature") + fun setpointCCDTemperature(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/setccdtemperature") + fun setpointCCDTemperature(@Path("id") id: Int, @Field("SetCCDTemperature") value: Double): Call + + @GET("api/v1/camera/{id}/startx") + fun startX(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/startx") + fun startX(@Path("id") id: Int, @Field("StartX") value: Int): Call + + @GET("api/v1/camera/{id}/starty") + fun startY(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/starty") + fun startY(@Path("id") id: Int, @Field("StartY") value: Int): Call + + @GET("api/v1/camera/{id}/subexposureduration") + fun subExposureDuration(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/subexposureduration") + fun subExposureDuration(@Path("id") id: Int, @Field("SubExposureDuration") value: Double): Call + + @PUT("api/v1/camera/{id}/abortexposure") + fun abortExposure(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/pulseguide") + override fun pulseGuide( + @Path("id") id: Int, + @Field("Direction") direction: PulseGuideDirection, + @Field("Duration") durationInMilliseconds: Long + ): Call + + @FormUrlEncoded + @PUT("api/v1/camera/{id}/startexposure") + fun startExposure(@Path("id") id: Int, @Field("Duration") durationInSeconds: Double, @Field("Light") light: Boolean): Call + + @PUT("api/v1/camera/{id}/stopexposure") + fun stopExposure(@Path("id") id: Int): Call + + // https://github.com/ASCOMInitiative/ASCOMRemote/blob/main/Documentation/AlpacaImageBytes.pdf + @Headers("Accept: application/imagebytes") + @GET("api/v1/camera/{id}/imagearray") + fun imageArray(@Path("id") id: Int): Call +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Management.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaDeviceManagementService.kt similarity index 53% rename from nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Management.kt rename to nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaDeviceManagementService.kt index 1f6e64ab1..db09d948d 100644 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Management.kt +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaDeviceManagementService.kt @@ -3,8 +3,8 @@ package nebulosa.alpaca.api import retrofit2.Call import retrofit2.http.GET -interface Management { +interface AlpacaDeviceManagementService { @GET("management/v1/configureddevices") - fun configuredDevices(): Call>> + fun configuredDevices(): Call> } diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaDeviceService.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaDeviceService.kt new file mode 100644 index 000000000..aab1de24b --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaDeviceService.kt @@ -0,0 +1,10 @@ +package nebulosa.alpaca.api + +import retrofit2.Call + +sealed interface AlpacaDeviceService { + + fun isConnected(id: Int): Call + + fun connect(id: Int, connected: Boolean): Call +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaException.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaException.kt index 6bed44490..e4d543563 100644 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaException.kt +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaException.kt @@ -3,4 +3,4 @@ package nebulosa.alpaca.api import retrofit2.HttpException import retrofit2.Response -class AlpacaException(response: Response>) : HttpException(response) +open class AlpacaException(response: Response>) : HttpException(response) diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaFilterWheelService.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaFilterWheelService.kt new file mode 100644 index 000000000..8a3e72f44 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaFilterWheelService.kt @@ -0,0 +1,26 @@ +package nebulosa.alpaca.api + +import retrofit2.Call +import retrofit2.http.* + +interface AlpacaFilterWheelService : AlpacaDeviceService { + + @GET("api/v1/filterwheel/{id}/connected") + override fun isConnected(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/filterwheel/{id}/connected") + override fun connect(@Path("id") id: Int, @Field("Connected") connected: Boolean): Call + + @GET("api/v1/filterwheel/{id}/focusoffsets") + fun focusOffsets(@Path("id") id: Int): Call + + @GET("api/v1/filterwheel/{id}/names") + fun names(@Path("id") id: Int): Call> + + @GET("api/v1/filterwheel/{id}/position") + fun position(@Path("id") id: Int): Call + + @GET("api/v1/filterwheel/{id}/alignmentmode") + fun position(@Path("id") id: Int, @Field("Position") position: Int): Call +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaFocuserService.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaFocuserService.kt new file mode 100644 index 000000000..40556cbf1 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaFocuserService.kt @@ -0,0 +1,52 @@ +package nebulosa.alpaca.api + +import retrofit2.Call +import retrofit2.http.* + +interface AlpacaFocuserService : AlpacaDeviceService { + + @GET("api/v1/focuser/{id}/connected") + override fun isConnected(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/focuser/{id}/connected") + override fun connect(@Path("id") id: Int, @Field("Connected") connected: Boolean): Call + + @GET("api/v1/focuser/{id}/absolute") + fun canAbsolute(@Path("id") id: Int): Call + + @GET("api/v1/focuser/{id}/ismoving") + fun isMoving(@Path("id") id: Int): Call + + @GET("api/v1/focuser/{id}/maxincrement") + fun maxIncrement(@Path("id") id: Int): Call + + @GET("api/v1/focuser/{id}/maxstep") + fun maxStep(@Path("id") id: Int): Call + + @GET("api/v1/focuser/{id}/position") + fun position(@Path("id") id: Int): Call + + @GET("api/v1/focuser/{id}/stepsize") + fun stepSize(@Path("id") id: Int): Call + + @GET("api/v1/focuser/{id}/tempcomp") + fun temperatureCompensation(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/focuser/{id}/tempcomp") + fun temperatureCompensation(@Path("id") id: Int, @Field("TempComp") enabled: Boolean): Call + + @GET("api/v1/focuser/{id}/tempcompavailable") + fun hasTemperatureCompensation(@Path("id") id: Int): Call + + @GET("api/v1/focuser/{id}/temperature") + fun temperature(@Path("id") id: Int): Call + + @PUT("api/v1/focuser/{id}/halt") + fun halt(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/focuser/{id}/move") + fun move(@Path("id") id: Int, @Field("Position") position: Int): Call +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaGuideOutputService.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaGuideOutputService.kt new file mode 100644 index 000000000..17531e06c --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaGuideOutputService.kt @@ -0,0 +1,12 @@ +package nebulosa.alpaca.api + +import retrofit2.Call + +interface AlpacaGuideOutputService : AlpacaDeviceService { + + fun canPulseGuide(id: Int): Call + + fun isPulseGuiding(id: Int): Call + + fun pulseGuide(id: Int, direction: PulseGuideDirection, durationInMilliseconds: Long): Call +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaResponse.kt index 2f96af9b0..f1d09f5ea 100644 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaResponse.kt +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaResponse.kt @@ -1,11 +1,14 @@ package nebulosa.alpaca.api -import com.fasterxml.jackson.annotation.JsonProperty - -data class AlpacaResponse( - @field:JsonProperty("ClientTransactionID") val clientTransactionID: Int = 0, - @field:JsonProperty("ServerTransactionID") val serverTransactionID: Int = 0, - @field:JsonProperty("ErrorNumber") val errorNumber: Int = 0, - @field:JsonProperty("ErrorMessage") val errorMessage: String = "", - @field:JsonProperty("Value") val value: T? = null, -) +sealed interface AlpacaResponse { + + val clientTransactionID: Int + + val serverTransactionID: Int + + val errorNumber: Int + + val errorMessage: String + + val value: T +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaService.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaService.kt index 2fc71fcea..94c180c0a 100644 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaService.kt +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaService.kt @@ -15,9 +15,13 @@ class AlpacaService( httpClient: OkHttpClient? = null, ) : RetrofitService(url, httpClient) { - val management by lazy { retrofit.create() } + val management by lazy { retrofit.create() } - val camera by lazy { retrofit.create() } + val camera by lazy { retrofit.create() } - val telescope by lazy { retrofit.create() } + val telescope by lazy { retrofit.create() } + + val filterWheel by lazy { retrofit.create() } + + val focuser by lazy { retrofit.create() } } diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaTelescopeService.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaTelescopeService.kt new file mode 100644 index 000000000..74e4ef4a4 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AlpacaTelescopeService.kt @@ -0,0 +1,302 @@ +package nebulosa.alpaca.api + +import retrofit2.Call +import retrofit2.http.* +import java.time.Instant + +interface AlpacaTelescopeService : AlpacaGuideOutputService { + + @GET("api/v1/telescope/{id}/connected") + override fun isConnected(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/connected") + override fun connect(@Path("id") id: Int, @Field("Connected") connected: Boolean): Call + + @GET("api/v1/telescope/{id}/alignmentmode") + fun alignmentMode(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/altitude") + fun altitude(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/aperturearea") + fun apertureArea(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/aperturediameter") + fun apertureDiameter(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/athome") + fun isAtHome(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/atpark") + fun isAtPark(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/azimuth") + fun azimuth(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canfindhome") + fun canFindHome(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canpark") + fun canPark(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canpulseguide") + override fun canPulseGuide(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansetdeclinationrate") + fun canSetDeclinationRate(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansetguiderates") + fun canSetGuideRates(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansetpark") + fun canSetPark(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansetpierside") + fun canSetPierSide(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansetrightascensionrate") + fun canSetRightAscensionRate(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansettracking") + fun canSetTracking(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canslew") + fun canSlew(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canslewaltaz") + fun canSlewAltAz(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canslewaltazasync") + fun canSlewAltAzAsync(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canslewasync") + fun canSlewAsync(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansync") + fun canSync(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/cansyncaltaz") + fun canSyncAltAz(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/canunpark") + fun canUnpark(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/declination") + fun declination(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/declinationrate") + fun declinationRate(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/declinationrate") + fun declinationRate(@Path("id") id: Int, @Field("DeclinationRate") rate: Double): Call + + @GET("api/v1/telescope/{id}/doesrefraction") + fun doesRefraction(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/doesrefraction") + fun doesRefraction(@Path("id") id: Int, @Field("DoesRefraction") doesRefraction: Boolean): Call + + @GET("api/v1/telescope/{id}/equatorialsystem") + fun equatorialSystem(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/focallength") + fun focalLength(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/guideratedeclination") + fun guideRateDeclination(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/guideratedeclination") + fun guideRateDeclination(@Path("id") id: Int, @Field("GuideRateDeclination") rate: Double): Call + + @GET("api/v1/telescope/{id}/guideraterightascension") + fun guideRateRightAscension(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/guideraterightascension") + fun guideRateRightAscension(@Path("id") id: Int, @Field("GuideRateRightAscension") rate: Double): Call + + @GET("api/v1/telescope/{id}/ispulseguiding") + override fun isPulseGuiding(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/rightascension") + fun rightAscension(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/rightascensionrate") + fun rightAscensionRate(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/rightascensionrate") + fun rightAscensionRate(@Path("id") id: Int, @Field("RightAscensionRate") rate: Double): Call + + @GET("api/v1/telescope/{id}/sideofpier") + fun sideOfPier(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/sideofpier") + fun sideofPier(@Path("id") id: Int, @Field("SideOfPier") side: PierSide): Call + + @GET("api/v1/telescope/{id}/siderealtime") + fun siderealTime(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/siteelevation") + fun siteElevation(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/siteelevation") + fun siteElevation(@Path("id") id: Int, @Field("SiteElevation") elevation: Double): Call + + @GET("api/v1/telescope/{id}/sitelatitude") + fun siteLatitude(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/sitelatitude") + fun siteLatitude(@Path("id") id: Int, @Field("SiteLatitude") latitude: Double): Call + + @GET("api/v1/telescope/{id}/sitelongitude") + fun siteLongitude(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/sitelongitude") + fun siteLongitude(@Path("id") id: Int, @Field("SiteLongitude") longitude: Double): Call + + @GET("api/v1/telescope/{id}/slewing") + fun isSlewing(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/slewsettletime") + fun slewSettleTime(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/slewsettletime") + fun slewSettleTime(@Path("id") id: Int, @Field("SlewSettleTime") settleTime: Int): Call + + @GET("api/v1/telescope/{id}/targetdeclination") + fun targetDeclination(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/targetdeclination") + fun targetDeclination(@Path("id") id: Int, @Field("TargetDeclination") declination: Double): Call + + @GET("api/v1/telescope/{id}/targetrightascension") + fun targetRightAscension(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/targetrightascension") + fun targetRightAscension(@Path("id") id: Int, @Field("TargetRightAscension") rightAscension: Double): Call + + @GET("api/v1/telescope/{id}/tracking") + fun isTracking(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/tracking") + fun tracking(@Path("id") id: Int, @Field("Tracking") tracking: Boolean): Call + + @GET("api/v1/telescope/{id}/trackingrate") + fun trackingRate(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/trackingrate") + fun trackingRate(@Path("id") id: Int, @Field("TrackingRate") rate: DriveRate): Call + + @GET("api/v1/telescope/{id}/trackingrates") + fun trackingRates(@Path("id") id: Int): Call> + + @GET("api/v1/telescope/{id}/utcdate") + fun utcDate(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/utcDate") + fun utcDate(@Path("id") id: Int, @Field("UTCDate") date: Instant): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/abortslew") + fun abortSlew(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/axisrates") + fun axisRates(@Path("id") id: Int): Call> + + @GET("api/v1/telescope/{id}/canmoveaxis") + fun canMoveAxis(@Path("id") id: Int): Call + + @GET("api/v1/telescope/{id}/destinationsideofpier") + fun destinationSideOfPier(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/findhome") + fun findHome(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/moveaxis") + fun moveAxis(@Path("id") id: Int, @Field("Axis") axis: AxisType, @Field("Rate") rate: Double): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/park") + fun park(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/pulseguide") + override fun pulseGuide( + @Path("id") id: Int, + @Field("Direction") direction: PulseGuideDirection, + @Field("Duration") durationInMilliseconds: Long + ): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/setpark") + fun setPark(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/slewtoaltaz") + fun slewToAltAz(@Path("id") id: Int, @Field("Azimuth") azimuth: Double, @Field("Altitude") altitude: Double): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/slewtoaltazasync") + fun slewtoAltAzAsync(@Path("id") id: Int, @Field("Azimuth") azimuth: Double, @Field("Altitude") altitude: Double): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/slewtocoordinates") + fun slewToCoordinates( + @Path("id") id: Int, + @Field("RightAscension") rightAscension: Double, + @Field("Declination") declination: Double + ): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/slewtocoordinatesasync") + fun slewToCoordinatesAsync( + @Path("id") id: Int, + @Field("RightAscension") rightAscension: Double, + @Field("Declination") declination: Double + ): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/slewtotarget") + fun slewToTarget(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/slewtotargetasync") + fun slewToTargetAsync(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/synctoaltaz") + fun syncToAltAz(@Path("id") id: Int, @Field("Azimuth") azimuth: Double, @Field("Altitude") altitude: Double): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/synctocoordinates") + fun syncToCoordinates( + @Path("id") id: Int, + @Field("RightAscension") rightAscension: Double, + @Field("Declination") declination: Double + ): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/synctotarget") + fun syncToTarget(@Path("id") id: Int): Call + + @FormUrlEncoded + @PUT("api/v1/telescope/{id}/unpark") + fun unpark(@Path("id") id: Int): Call +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ArrayResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ArrayResponse.kt new file mode 100644 index 000000000..90f3b2827 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ArrayResponse.kt @@ -0,0 +1,18 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +@Suppress("ArrayInDataClass", "UNCHECKED_CAST") +data class ArrayResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: Array = EMPTY_ARRAY as Array, +) : AlpacaResponse> { + + companion object { + + @JvmStatic internal val EMPTY_ARRAY = arrayOfNulls(0) + } +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AxisRate.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AxisRate.kt new file mode 100644 index 000000000..2fafac8d6 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AxisRate.kt @@ -0,0 +1,8 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class AxisRate( + @field:JsonProperty("Maximum") val maximum: Double, + @field:JsonProperty("Minimum") val minimum: Double, +) diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AxisType.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AxisType.kt new file mode 100644 index 000000000..8c1819b93 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/AxisType.kt @@ -0,0 +1,10 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonFormat + +@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) +enum class AxisType { + PRIMARY, + SECONDARY, + TERTIARY, +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/BoolResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/BoolResponse.kt new file mode 100644 index 000000000..798eb1cf6 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/BoolResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class BoolResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: Boolean = false, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Camera.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Camera.kt deleted file mode 100644 index 9b0503b29..000000000 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Camera.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nebulosa.alpaca.api - -import retrofit2.Call -import retrofit2.http.* - -interface Camera : Device { - - @GET("camera/{deviceNumber}/connected") - override fun isConnected(@Path("deviceNumber") deviceNumber: Int): Call> - - @FormUrlEncoded - @PUT("camera/{deviceNumber}/connected") - override fun connect(@Path("deviceNumber") deviceNumber: Int, @Field("Connected") connected: Boolean): Call> -} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/CameraState.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/CameraState.kt new file mode 100644 index 000000000..2a54a4272 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/CameraState.kt @@ -0,0 +1,13 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonFormat + +@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) +enum class CameraState { + IDLE, + WAITING, + EXPOSURING, + READING, + DOWNLOAD, + ERROR, +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/CameraStateResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/CameraStateResponse.kt new file mode 100644 index 000000000..c1584c1ba --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/CameraStateResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class CameraStateResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: CameraState = CameraState.IDLE, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ConfiguredDevice.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ConfiguredDevice.kt index e02c06f46..d1c85fa30 100644 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ConfiguredDevice.kt +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ConfiguredDevice.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty data class ConfiguredDevice( @JsonProperty("DeviceName") val name: String = "", - @JsonProperty("DeviceType") val type: String = "", + @JsonProperty("DeviceType") val type: DeviceType = DeviceType.CAMERA, @JsonProperty("DeviceNumber") val number: Int = 0, @JsonProperty("UniqueID") val uid: String = "", ) diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DateTimeResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DateTimeResponse.kt new file mode 100644 index 000000000..a43a09382 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DateTimeResponse.kt @@ -0,0 +1,12 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty +import java.time.Instant + +data class DateTimeResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: Instant = Instant.now(), +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Device.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Device.kt deleted file mode 100644 index 5e8156c1c..000000000 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Device.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.alpaca.api - -import retrofit2.Call - -sealed interface Device { - - fun isConnected(deviceNumber: Int): Call> - - fun connect(deviceNumber: Int, connected: Boolean): Call> -} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DeviceType.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DeviceType.kt new file mode 100644 index 000000000..3c5afee5a --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DeviceType.kt @@ -0,0 +1,17 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonValue + +enum class DeviceType(@JsonValue val type: String) { + CAMERA("Camera"), + TELESCOPE("Telescope"), + FOCUSER("Focuser"), + FILTER_WHEEL("FilterWheel"), + ROTATOR("Rotator"), + DOME("Dome"), + SWITCH("Switch"), + COVER_CALIBRATOR("CoverCalibrator"), + OBSERVING_CONDITIONS("ObservingConditions"), + SAFETY_MONITOR("SafetyMonitor"), + VIDEO("Video"), +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DoubleResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DoubleResponse.kt new file mode 100644 index 000000000..69e07864f --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DoubleResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class DoubleResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: Double = 0.0, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DriveRate.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DriveRate.kt new file mode 100644 index 000000000..99aec1c84 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DriveRate.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonFormat + +@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) +enum class DriveRate { + SIDEREAL, + LUNAR, + SOLAR, + KING, +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DriveRateResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DriveRateResponse.kt new file mode 100644 index 000000000..9cdbc1520 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DriveRateResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class DriveRateResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: DriveRate = DriveRate.SIDEREAL, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/EquatorialCoordinateType.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/EquatorialCoordinateType.kt new file mode 100644 index 000000000..b0bca8f98 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/EquatorialCoordinateType.kt @@ -0,0 +1,12 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonFormat + +@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) +enum class EquatorialCoordinateType { + OTHER, + TOPOCENTRIC, + J2000, + J2050, + B1950, +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/EquatorialCoordinateTypeResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/EquatorialCoordinateTypeResponse.kt new file mode 100644 index 000000000..b8e3a4774 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/EquatorialCoordinateTypeResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class EquatorialCoordinateTypeResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: EquatorialCoordinateType = EquatorialCoordinateType.J2000, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/IntArrayResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/IntArrayResponse.kt new file mode 100644 index 000000000..020e9d72d --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/IntArrayResponse.kt @@ -0,0 +1,18 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +@Suppress("ArrayInDataClass") +data class IntArrayResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: IntArray = EMPTY_ARRAY, +) : AlpacaResponse { + + companion object { + + @JvmStatic private val EMPTY_ARRAY = IntArray(0) + } +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/IntResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/IntResponse.kt new file mode 100644 index 000000000..3c5b0c294 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/IntResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class IntResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: Int = 0, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ListResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ListResponse.kt new file mode 100644 index 000000000..194401b36 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/ListResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class ListResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: List = emptyList(), +) : AlpacaResponse> diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/NoneResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/NoneResponse.kt new file mode 100644 index 000000000..f8e3a9a5a --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/NoneResponse.kt @@ -0,0 +1,13 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class NoneResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", +) : AlpacaResponse { + + @JsonProperty("Value") override val value = Unit +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PierSide.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PierSide.kt new file mode 100644 index 000000000..e70817fbf --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PierSide.kt @@ -0,0 +1,9 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonValue + +enum class PierSide(@field:JsonValue val code: Int) { + UNKNOWN(-1), + EAST(0), + WEST(1), +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PierSideResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PierSideResponse.kt new file mode 100644 index 000000000..40f621ce2 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PierSideResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class PierSideResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: PierSide = PierSide.UNKNOWN, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PulseGuideDirection.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PulseGuideDirection.kt new file mode 100644 index 000000000..a41bb08be --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/PulseGuideDirection.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonFormat + +@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) +enum class PulseGuideDirection { + NORTH, + SOUTH, + EAST, + WEST, +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/SensorType.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/SensorType.kt new file mode 100644 index 000000000..669484310 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/SensorType.kt @@ -0,0 +1,13 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonFormat + +@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) +enum class SensorType { + MONOCHROME, + NO_COLOR, + RGGB, + CMYB, + CMYG2, + LRGB_TRUESENSE, +} diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/SensorTypeResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/SensorTypeResponse.kt new file mode 100644 index 000000000..378a76206 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/SensorTypeResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class SensorTypeResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: SensorType = SensorType.MONOCHROME, +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/StringResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/StringResponse.kt new file mode 100644 index 000000000..e449e4045 --- /dev/null +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/StringResponse.kt @@ -0,0 +1,11 @@ +package nebulosa.alpaca.api + +import com.fasterxml.jackson.annotation.JsonProperty + +data class StringResponse( + @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, + @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, + @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, + @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", + @field:JsonProperty("Value") override val value: String = "", +) : AlpacaResponse diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Telescope.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Telescope.kt deleted file mode 100644 index c066cc173..000000000 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/Telescope.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nebulosa.alpaca.api - -import retrofit2.Call -import retrofit2.http.* - -interface Telescope : Device { - - @GET("telescope/{deviceNumber}/connected") - override fun isConnected(@Path("deviceNumber") deviceNumber: Int): Call> - - @FormUrlEncoded - @PUT("telescope/{deviceNumber}/connected") - override fun connect(@Path("deviceNumber") deviceNumber: Int, @Field("Connected") connected: Boolean): Call> -} diff --git a/nebulosa-alpaca-api/src/test/kotlin/AlpacaServiceTest.kt b/nebulosa-alpaca-api/src/test/kotlin/AlpacaServiceTest.kt index 3c05650e1..462085682 100644 --- a/nebulosa-alpaca-api/src/test/kotlin/AlpacaServiceTest.kt +++ b/nebulosa-alpaca-api/src/test/kotlin/AlpacaServiceTest.kt @@ -1,20 +1,21 @@ +import io.kotest.core.annotation.EnabledIf import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.booleans.shouldBeTrue -import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull import nebulosa.alpaca.api.AlpacaService +import nebulosa.test.NonGitHubOnlyCondition +@EnabledIf(NonGitHubOnlyCondition::class) class AlpacaServiceTest : StringSpec() { init { - val client = AlpacaService("https://virtserver.swaggerhub.com/ASCOMInitiative/api/v1/") + val client = AlpacaService("http://localhost:11111/") - "camera" { - client.camera.isConnected(0).execute().body()!!.value!!.shouldBeTrue() - client.camera.connect(0, true).execute().body()!!.value.shouldBeNull() - } - "telescope" { - client.telescope.isConnected(0).execute().body()!!.value!!.shouldBeTrue() - client.telescope.connect(0, true).execute().body()!!.value.shouldBeNull() + "management" { + val body = client.management.configuredDevices().execute().body().shouldNotBeNull() + + for (device in body.value) { + println(device) + } } } } diff --git a/nebulosa-alpaca-discovery-protocol/src/test/kotlin/AlpacaDiscoveryProtocolTest.kt b/nebulosa-alpaca-discovery-protocol/src/test/kotlin/AlpacaDiscoveryProtocolTest.kt new file mode 100644 index 000000000..515dc40f5 --- /dev/null +++ b/nebulosa-alpaca-discovery-protocol/src/test/kotlin/AlpacaDiscoveryProtocolTest.kt @@ -0,0 +1,24 @@ +import io.kotest.core.annotation.EnabledIf +import io.kotest.core.spec.style.StringSpec +import nebulosa.alpaca.discovery.AlpacaDiscoveryProtocol +import nebulosa.alpaca.discovery.DiscoveryListener +import nebulosa.test.NonGitHubOnlyCondition +import java.net.InetAddress +import kotlin.concurrent.thread + +@EnabledIf(NonGitHubOnlyCondition::class) +class AlpacaDiscoveryProtocolTest : StringSpec(), DiscoveryListener { + + init { + "discovery" { + val discoverer = AlpacaDiscoveryProtocol() + discoverer.registerDiscoveryListener(this@AlpacaDiscoveryProtocolTest) + thread { Thread.sleep(10000); discoverer.close() } + discoverer.run() + } + } + + override fun onServerFound(address: InetAddress, port: Int) { + println("$address:$port") + } +} diff --git a/nebulosa-alpaca-indi/build.gradle.kts b/nebulosa-alpaca-indi/build.gradle.kts new file mode 100644 index 000000000..c2649cd18 --- /dev/null +++ b/nebulosa-alpaca-indi/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + api(project(":nebulosa-alpaca-api")) + api(project(":nebulosa-indi-device")) + implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-test")) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/client/AlpacaClient.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/client/AlpacaClient.kt new file mode 100644 index 000000000..ebe79320c --- /dev/null +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/client/AlpacaClient.kt @@ -0,0 +1,250 @@ +package nebulosa.alpaca.indi.client + +import nebulosa.alpaca.api.AlpacaService +import nebulosa.alpaca.api.DeviceType +import nebulosa.alpaca.indi.device.ASCOMDevice +import nebulosa.alpaca.indi.device.cameras.ASCOMCamera +import nebulosa.alpaca.indi.device.focusers.ASCOMFocuser +import nebulosa.alpaca.indi.device.mounts.ASCOMMount +import nebulosa.alpaca.indi.device.wheels.ASCOMFilterWheel +import nebulosa.indi.device.DeviceEvent +import nebulosa.indi.device.DeviceEventHandler +import nebulosa.indi.device.INDIDeviceProvider +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.camera.CameraAttached +import nebulosa.indi.device.camera.CameraDetached +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.filterwheel.FilterWheelAttached +import nebulosa.indi.device.filterwheel.FilterWheelDetached +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserAttached +import nebulosa.indi.device.focuser.FocuserDetached +import nebulosa.indi.device.gps.GPS +import nebulosa.indi.device.guide.GuideOutput +import nebulosa.indi.device.guide.GuideOutputAttached +import nebulosa.indi.device.guide.GuideOutputDetached +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.mount.MountAttached +import nebulosa.indi.device.mount.MountDetached +import nebulosa.indi.device.thermometer.Thermometer +import nebulosa.indi.protocol.INDIProtocol +import nebulosa.log.loggerFor +import okhttp3.OkHttpClient +import java.util.* + +data class AlpacaClient( + val host: String, val port: Int, + private val httpClient: OkHttpClient? = null, +) : INDIDeviceProvider { + + private val service = AlpacaService("http://$host:$port/", httpClient) + private val handlers = LinkedHashSet() + private val cameras = HashMap() + private val mounts = HashMap() + private val wheels = HashMap() + private val focusers = HashMap() + private val guideOutputs = HashMap() + + override val id = UUID.randomUUID().toString() + + override fun registerDeviceEventHandler(handler: DeviceEventHandler) { + handlers.add(handler) + } + + override fun unregisterDeviceEventHandler(handler: DeviceEventHandler) { + handlers.remove(handler) + } + + override fun sendMessageToServer(message: INDIProtocol) {} + + internal fun fireOnEventReceived(event: DeviceEvent<*>) { + handlers.forEach { it.onEventReceived(event) } + } + + internal fun fireOnConnectionClosed() { + handlers.forEach { it.onConnectionClosed() } + } + + override fun cameras(): List { + return synchronized(cameras) { cameras.values.toList() } + } + + override fun camera(name: String): Camera? { + return synchronized(cameras) { cameras[name] ?: cameras.values.find { it.name == name } } + } + + override fun mounts(): List { + return emptyList() + } + + override fun mount(name: String): Mount? { + return null + } + + override fun focusers(): List { + return emptyList() + } + + override fun focuser(name: String): Focuser? { + return null + } + + override fun wheels(): List { + return emptyList() + } + + override fun wheel(name: String): FilterWheel? { + return null + } + + override fun gps(): List { + return emptyList() + } + + override fun gps(name: String): GPS? { + return null + } + + override fun guideOutputs(): List { + return emptyList() + } + + override fun guideOutput(name: String): GuideOutput? { + return null + } + + override fun thermometers(): List { + return emptyList() + } + + override fun thermometer(name: String): Thermometer? { + return null + } + + fun discovery() { + val response = service.management.configuredDevices().execute() + + if (response.isSuccessful) { + val body = response.body() ?: return + + for (device in body.value) { + when (device.type) { + DeviceType.CAMERA -> { + if (device.uid in cameras) continue + + synchronized(cameras) { + with(ASCOMCamera(device, service.camera, this)) { + cameras[device.uid] = this + LOG.info("camera attached: {}", device.name) + fireOnEventReceived(CameraAttached(this)) + } + } + } + DeviceType.TELESCOPE -> { + if (device.uid in mounts) continue + + synchronized(mounts) { + with(ASCOMMount(device, service.telescope, this)) { + mounts[device.uid] = this + LOG.info("mount attached: {}", device.name) + fireOnEventReceived(MountAttached(this)) + } + } + } + DeviceType.FILTER_WHEEL -> { + if (device.uid in wheels) continue + + synchronized(wheels) { + with(ASCOMFilterWheel(device, service.filterWheel, this)) { + wheels[device.uid] = this + LOG.info("filter wheel attached: {}", device.name) + fireOnEventReceived(FilterWheelAttached(this)) + } + } + } + DeviceType.FOCUSER -> { + if (device.uid in focusers) continue + + synchronized(focusers) { + with(ASCOMFocuser(device, service.focuser, this)) { + focusers[device.uid] = this + LOG.info("focuser attached: {}", device.name) + fireOnEventReceived(FocuserAttached(this)) + } + } + } + DeviceType.ROTATOR -> Unit + DeviceType.DOME -> Unit + DeviceType.SWITCH -> Unit + DeviceType.COVER_CALIBRATOR -> Unit + DeviceType.OBSERVING_CONDITIONS -> Unit + DeviceType.SAFETY_MONITOR -> Unit + DeviceType.VIDEO -> Unit + } + } + } else { + val body = response.errorBody() + LOG.warn("unsuccessful response. code={}, body={}", response.code(), body?.string()) + body?.close() + } + } + + internal fun registerGuideOutput(device: GuideOutput) { + if (device is ASCOMDevice) { + guideOutputs[device.id] = device + fireOnEventReceived(GuideOutputAttached(device)) + } + } + + internal fun unregisterGuideOutput(device: GuideOutput) { + if (device.name in guideOutputs) { + guideOutputs.remove(device.name) + fireOnEventReceived(GuideOutputDetached(device)) + } + } + + override fun close() { + for ((_, device) in cameras) { + device.close() + LOG.info("camera detached: {}", device.name) + fireOnEventReceived(CameraDetached(device)) + } + + for ((_, device) in mounts) { + device.close() + LOG.info("mount detached: {}", device.name) + fireOnEventReceived(MountDetached(device)) + } + + for ((_, device) in wheels) { + device.close() + LOG.info("filter wheel detached: {}", device.name) + fireOnEventReceived(FilterWheelDetached(device)) + } + + for ((_, device) in focusers) { + device.close() + LOG.info("focuser detached: {}", device.name) + fireOnEventReceived(FocuserDetached(device)) + } + + // for ((_, device) in gps) { + // device.close() + // LOG.info("gps detached: {}", device.name) + // fireOnEventReceived(GPSDetached(device)) + // } + + cameras.clear() + mounts.clear() + wheels.clear() + focusers.clear() + // gps.clear() + + handlers.clear() + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} 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 new file mode 100644 index 000000000..e3af0574c --- /dev/null +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt @@ -0,0 +1,156 @@ +package nebulosa.alpaca.indi.device + +import nebulosa.alpaca.api.AlpacaDeviceService +import nebulosa.alpaca.api.AlpacaResponse +import nebulosa.alpaca.api.ConfiguredDevice +import nebulosa.alpaca.indi.client.AlpacaClient +import nebulosa.common.time.Stopwatch +import nebulosa.indi.device.* +import nebulosa.log.loggerFor +import retrofit2.Call +import retrofit2.HttpException +import java.time.LocalDateTime +import java.util.* + +abstract class ASCOMDevice : Device { + + protected abstract val device: ConfiguredDevice + protected abstract val service: AlpacaDeviceService + abstract override val sender: AlpacaClient + + @Suppress("PropertyName") + @JvmField protected val LOG = loggerFor(javaClass) + + override val name + get() = device.name + + override val id + get() = device.uid + + @Volatile final override var connected = false + private set + + override val properties = emptyMap>() + override val messages = LinkedList() + + @Volatile private var refresher: Refresher? = null + + override fun connect() { + service.connect(device.number, true).doRequest() + } + + override fun disconnect() { + service.connect(device.number, false).doRequest() + } + + open fun refresh(elapsedTimeInSeconds: Long) { + service.isConnected(device.number).doRequest { processConnected(it.value) } + } + + open fun reset() { + connected = false + } + + override fun close() { + refresher?.interrupt() + refresher = null + } + + protected abstract fun onConnected() + + protected abstract fun onDisconnected() + + private fun addMessageAndFireEvent(text: String) { + synchronized(messages) { + messages.addFirst(text) + + sender.fireOnEventReceived(DeviceMessageReceived(this, text)) + + if (messages.size > 100) { + messages.removeLast() + } + } + } + + protected fun > Call.doRequest(): T? { + try { + val response = execute().body() + + return if (response == null) { + LOG.warn("response has no body. device={}", name) + null + } else if (response.errorNumber != 0) { + val message = response.errorMessage + + if (message.isNotEmpty()) { + addMessageAndFireEvent("[%s]: %s".format(LocalDateTime.now(), message)) + } + + // LOG.warn("unsuccessful response. device={}, code={}, message={}", name, response.errorNumber, response.errorMessage) + + null + } else { + response + } + } catch (e: HttpException) { + LOG.error("unexpected response. device=$name", e) + } catch (e: Throwable) { + sender.fireOnConnectionClosed() + LOG.error("unexpected error. device=$name", e) + } + + return null + } + + protected inline fun > Call.doRequest(action: (T) -> Unit): Boolean { + return doRequest()?.also(action) != null + } + + protected fun processConnected(value: Boolean) { + if (connected != value) { + connected = value + + if (value) { + sender.fireOnEventReceived(DeviceConnected(this)) + + onConnected() + + if (refresher == null) { + refresher = Refresher() + refresher!!.start() + } + } else { + sender.fireOnEventReceived(DeviceDisconnected(this)) + + onDisconnected() + + refresher?.interrupt() + refresher = null + } + } + } + + private inner class Refresher : Thread("$name ASCOM Refresher") { + + private val stopwatch = Stopwatch() + + init { + isDaemon = true + } + + override fun run() { + stopwatch.start() + + while (true) { + val startTime = System.currentTimeMillis() + refresh(stopwatch.elapsedSeconds) + val endTime = System.currentTimeMillis() + val delayTime = 2000L - (endTime - startTime) + + if (delayTime > 1L) { + sleep(delayTime) + } + } + } + } +} 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 new file mode 100644 index 000000000..f6929f99d --- /dev/null +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt @@ -0,0 +1,778 @@ +package nebulosa.alpaca.indi.device.cameras + +import nebulosa.alpaca.api.AlpacaCameraService +import nebulosa.alpaca.api.CameraState +import nebulosa.alpaca.api.ConfiguredDevice +import nebulosa.alpaca.api.PulseGuideDirection +import nebulosa.alpaca.indi.client.AlpacaClient +import nebulosa.alpaca.indi.device.ASCOMDevice +import nebulosa.common.concurrency.latch.CountUpDownLatch +import nebulosa.fits.* +import nebulosa.imaging.algorithms.transformation.CfaPattern +import nebulosa.indi.device.Device +import nebulosa.indi.device.camera.* +import nebulosa.indi.device.camera.Camera.Companion.NANO_SECONDS +import nebulosa.indi.device.guide.GuideOutputPulsingChanged +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.protocol.INDIProtocol +import nebulosa.indi.protocol.PropertyState +import nebulosa.io.readDoubleLe +import nebulosa.io.readFloatLe +import nebulosa.math.formatHMS +import nebulosa.math.formatSignedDMS +import nebulosa.math.normalized +import nebulosa.math.toDegrees +import nebulosa.nova.position.Geoid +import nebulosa.nova.position.ICRF +import nebulosa.time.CurrentTime +import okio.buffer +import okio.source +import java.nio.ByteBuffer +import java.time.Duration +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import kotlin.math.max +import kotlin.math.min + +data class ASCOMCamera( + override val device: ConfiguredDevice, + override val service: AlpacaCameraService, + override val sender: AlpacaClient, +) : ASCOMDevice(), Camera { + + @Volatile override var exposuring = false + private set + @Volatile override var hasCoolerControl = false + private set + @Volatile override var coolerPower = 0.0 + private set + @Volatile override var cooler = false + private set + @Volatile override var hasDewHeater = false + private set + @Volatile override var dewHeater = false + private set + @Volatile override var frameFormats = emptyList() + private set + @Volatile override var canAbort = false + private set + @Volatile override var cfaOffsetX = 0 + private set + @Volatile override var cfaOffsetY = 0 + private set + @Volatile override var cfaType = CfaPattern.RGGB + private set + @Volatile override var exposureMin: Duration = Duration.ZERO + private set + @Volatile override var exposureMax: Duration = Duration.ZERO + private set + @Volatile override var exposureState = PropertyState.IDLE + private set + @Volatile override var exposureTime: Duration = Duration.ZERO + private set + @Volatile override var hasCooler = false + private set + @Volatile override var canSetTemperature = false + private set + @Volatile override var canSubFrame = true + private set + @Volatile override var x = 0 + private set + @Volatile override var minX = 0 + private set + @Volatile override var maxX = 0 + private set + @Volatile override var y = 0 + private set + @Volatile override var minY = 0 + private set + @Volatile override var maxY = 0 + private set + @Volatile override var width = 0 + private set + @Volatile override var minWidth = 0 + private set + @Volatile override var maxWidth = 0 + private set + @Volatile override var height = 0 + private set + @Volatile override var minHeight = 0 + private set + @Volatile override var maxHeight = 0 + private set + @Volatile override var canBin = true + private set + @Volatile override var maxBinX = 1 + private set + @Volatile override var maxBinY = 1 + private set + @Volatile override var binX = 1 + private set + @Volatile override var binY = 1 + private set + @Volatile override var gain = 0 + private set + @Volatile override var gainMin = 0 + private set + @Volatile override var gainMax = 0 + private set + @Volatile override var offset = 0 + private set + @Volatile override var offsetMin = 0 + private set + @Volatile override var offsetMax = 0 + private set + @Volatile override var hasGuiderHead = false // TODO: ASCOM has guider head? + private set + @Volatile override var pixelSizeX = 0.0 + private set + @Volatile override var pixelSizeY = 0.0 + private set + + @Volatile override var hasThermometer = false + private set + @Volatile override var temperature = 0.0 + private set + + @Volatile override var canPulseGuide = false + private set + @Volatile override var pulseGuiding = false + private set + + @Volatile private var cameraState = CameraState.IDLE + @Volatile private var frameType = FrameType.LIGHT + @Volatile private var mount: Mount? = null + + private val imageReadyWaiter = ImageReadyWaiter() + + init { + refresh(0L) + imageReadyWaiter.start() + } + + override fun cooler(enabled: Boolean) { + service.cooler(device.number, enabled).doRequest() + } + + override fun dewHeater(enabled: Boolean) { + // TODO + } + + override fun temperature(value: Double) { + service.setpointCCDTemperature(device.number, value).doRequest() + } + + override fun frameFormat(format: String?) { + val index = frameFormats.indexOf(format) + + if (index >= 0) { + service.readoutMode(device.number, index).doRequest() + } + } + + override fun frameType(type: FrameType) { + frameType = type + } + + override fun frame(x: Int, y: Int, width: Int, height: Int) { + service.startX(device.number, x).doRequest() ?: return + service.startY(device.number, y).doRequest() ?: return + service.numX(device.number, width).doRequest() ?: return + service.numY(device.number, height).doRequest() + } + + override fun bin(x: Int, y: Int) { + service.binX(device.number, x).doRequest() ?: return + service.binY(device.number, y).doRequest() + } + + override fun gain(value: Int) { + service.gain(device.number, value).doRequest() + } + + override fun offset(value: Int) { + service.offset(device.number, value).doRequest() + } + + override fun startCapture(exposureTime: Duration) { + this.exposureTime = exposureTime + + service.startExposure(device.number, exposureTime.toNanos() / NANO_SECONDS, frameType == FrameType.DARK).doRequest { + imageReadyWaiter.captureStarted() + } + } + + override fun abortCapture() { + service.abortExposure(device.number).doRequest() + imageReadyWaiter.captureAborted() + } + + private fun pulseGuide(direction: PulseGuideDirection, duration: Duration) { + if (canPulseGuide) { + val durationInMilliseconds = duration.toMillis() + + service.pulseGuide(device.number, direction, durationInMilliseconds).doRequest() ?: return + + if (durationInMilliseconds > 0) { + pulseGuiding = true + sender.fireOnEventReceived(GuideOutputPulsingChanged(this)) + } + } + } + + override fun guideNorth(duration: Duration) { + pulseGuide(PulseGuideDirection.NORTH, duration) + } + + override fun guideSouth(duration: Duration) { + pulseGuide(PulseGuideDirection.SOUTH, duration) + } + + override fun guideEast(duration: Duration) { + pulseGuide(PulseGuideDirection.EAST, duration) + } + + override fun guideWest(duration: Duration) { + pulseGuide(PulseGuideDirection.WEST, duration) + } + + override fun snoop(devices: Iterable) { + for (device in devices) { + if (device is Mount) mount = device + } + } + + override fun handleMessage(message: INDIProtocol) { + } + + override fun onConnected() { + processExposureMinMax() + processFrameMinMax() + processGainMinMax() + processOffsetMinMax() + processCapabilities() + processPixelSize() + processCfaOffset() + processReadoutModes() + } + + override fun onDisconnected() { + } + + override fun reset() { + super.reset() + + exposuring = false + hasCoolerControl = false + coolerPower = 0.0 + cooler = false + hasDewHeater = false + dewHeater = false + frameFormats = emptyList() + canAbort = false + cfaOffsetX = 0 + cfaOffsetY = 0 + cfaType = CfaPattern.RGGB + exposureMin = Duration.ZERO + exposureMax = Duration.ZERO + exposureState = PropertyState.IDLE + exposureTime = Duration.ZERO + hasCooler = false + canSetTemperature = false + canSubFrame = true + x = 0 + minX = 0 + maxX = 0 + y = 0 + minY = 0 + maxY = 0 + width = 0 + minWidth = 0 + maxWidth = 0 + height = 0 + minHeight = 0 + maxHeight = 0 + canBin = true + maxBinX = 1 + maxBinY = 1 + binX = 1 + binY = 1 + gain = 0 + gainMin = 0 + gainMax = 0 + offset = 0 + offsetMin = 0 + offsetMax = 0 + hasGuiderHead = false + pixelSizeX = 0.0 + pixelSizeY = 0.0 + hasThermometer = false + temperature = 0.0 + canPulseGuide = false + pulseGuiding = false + cameraState = CameraState.IDLE + } + + override fun close() { + super.close() + reset() + imageReadyWaiter.interrupt() + } + + @Synchronized + override fun refresh(elapsedTimeInSeconds: Long) { + super.refresh(elapsedTimeInSeconds) + + if (connected) { + service.cameraState(device.number).doRequest { processCameraState(it.value) } + + processBin() + processGain() + processOffset() + processCooler() + } + } + + private fun processCameraState(value: CameraState) { + if (cameraState != value) { + cameraState = value + + val prevExposuring = exposuring + val prevExposureState = exposureState + + when (value) { + CameraState.IDLE -> { + if (exposuring) { + exposuring = false + exposureState = PropertyState.IDLE + } + } + CameraState.WAITING -> { + if (exposuring) { + exposuring = false + exposureState = PropertyState.BUSY + } + } + CameraState.EXPOSURING -> { + if (!exposuring) { + exposuring = true + exposureState = PropertyState.BUSY + } + } + CameraState.READING -> { + if (exposuring) { + exposuring = false + exposureState = PropertyState.OK + } + } + CameraState.DOWNLOAD -> { + if (exposuring) { + exposuring = false + exposureState = PropertyState.OK + } + } + CameraState.ERROR -> { + if (exposuring) { + exposuring = false + exposureState = PropertyState.ALERT + } + } + } + + if (prevExposuring != exposuring) sender.fireOnEventReceived(CameraExposuringChanged(this)) + if (prevExposureState != exposureState) sender.fireOnEventReceived(CameraExposureStateChanged(this, prevExposureState)) + + if (exposuring) { + service.percentCompleted(device.number).doRequest { + + } + } + + if (exposureState == PropertyState.IDLE && (prevExposureState == PropertyState.BUSY || exposuring)) { + sender.fireOnEventReceived(CameraExposureAborted(this)) + } else if (exposureState == PropertyState.OK && prevExposureState == PropertyState.BUSY) { + sender.fireOnEventReceived(CameraExposureFinished(this)) + } else if (exposureState == PropertyState.ALERT && prevExposureState != PropertyState.ALERT) { + sender.fireOnEventReceived(CameraExposureFailed(this)) + } + } + } + + private fun processBin() { + service.binX(device.number).doRequest { x -> + service.binY(device.number).doRequest { y -> + if (x.value != binX || y.value != binY) { + binX = x.value + binY = y.value + + sender.fireOnEventReceived(CameraBinChanged(this)) + } + } + } + } + + private fun processGainMinMax() { + service.gainMin(device.number).doRequest { min -> + service.gainMax(device.number).doRequest { max -> + gainMin = min.value + gainMax = max.value + gain = max(gainMin, min(gain, gainMax)) + + sender.fireOnEventReceived(CameraGainMinMaxChanged(this)) + } + } + } + + private fun processGain() { + service.gain(device.number).doRequest { + if (it.value != gain) { + gain = it.value + + sender.fireOnEventReceived(CameraGainChanged(this)) + } + } + } + + private fun processOffsetMinMax() { + service.offsetMin(device.number).doRequest { min -> + service.offsetMax(device.number).doRequest { max -> + offsetMin = min.value + offsetMax = max.value + offset = max(offsetMin, min(offset, offsetMax)) + + sender.fireOnEventReceived(CameraOffsetMinMaxChanged(this)) + } + } + } + + private fun processOffset() { + service.offset(device.number).doRequest { + if (it.value != offset) { + offset = it.value + + sender.fireOnEventReceived(CameraOffsetChanged(this)) + } + } + } + + private fun processFrameMinMax() { + service.x(device.number).doRequest { w -> + service.y(device.number).doRequest { h -> + width = w.value + height = h.value + minWidth = 0 + maxWidth = width + minHeight = 0 + maxHeight = height + x = 0 + minX = 0 + maxX = width - 1 + y = 0 + minY = 0 + maxY = height - 1 + + if (!processFrame()) { + sender.fireOnEventReceived(CameraFrameChanged(this)) + } + } + } + } + + private fun processFrame(): Boolean { + service.numX(device.number).doRequest { w -> + service.numY(device.number).doRequest { h -> + service.startX(device.number).doRequest { x -> + service.startY(device.number).doRequest { y -> + if (w.value != width || h.value != height || x.value != this.x || y.value != this.y) { + width = w.value + height = h.value + this.x = x.value + this.y = y.value + + sender.fireOnEventReceived(CameraFrameChanged(this)) + + return true + } + } + } + } + } + + return false + } + + private fun processCooler() { + if (hasCoolerControl) { + service.coolerPower(device.number).doRequest { + if (coolerPower != it.value) { + coolerPower = it.value + + sender.fireOnEventReceived(CameraCoolerPowerChanged(this)) + } + } + } + + if (hasCooler) { + service.isCoolerOn(device.number).doRequest { + if (cooler != it.value) { + cooler = it.value + + sender.fireOnEventReceived(CameraCoolerChanged(this)) + } + } + } + } + + private fun processPixelSize() { + service.pixelSizeX(device.number).doRequest { x -> + service.pixelSizeY(device.number).doRequest { y -> + if (pixelSizeX != x.value || pixelSizeY != y.value) { + pixelSizeX = x.value + pixelSizeY = y.value + + sender.fireOnEventReceived(CameraPixelSizeChanged(this)) + } + } + } + } + + private fun processCfaOffset() { + service.bayerOffsetX(device.number).doRequest { x -> + service.bayerOffsetY(device.number).doRequest { y -> + if (cfaOffsetX != x.value || cfaOffsetY != y.value) { + cfaOffsetX = x.value + cfaOffsetY = y.value + + sender.fireOnEventReceived(CameraCfaChanged(this)) + } + } + } + } + + private fun processReadoutModes() { + service.readoutModes(device.number).doRequest { + frameFormats = it.value.toList() + + sender.fireOnEventReceived(CameraFrameFormatsChanged(this)) + } + } + + private fun processExposureMinMax() { + service.exposureMin(device.number).doRequest { min -> + service.exposureMax(device.number).doRequest { max -> + exposureMin = Duration.ofNanos((min.value * NANO_SECONDS).toLong()) + exposureMax = Duration.ofNanos((max.value * NANO_SECONDS).toLong()) + + sender.fireOnEventReceived(CameraExposureMinMaxChanged(this)) + } + } + } + + private fun processCapabilities() { + service.canAbortExposure(device.number).doRequest { + if (it.value) { + canAbort = true + sender.fireOnEventReceived(CameraCanAbortChanged(this)) + } + } + + service.canCoolerPower(device.number).doRequest { + if (it.value) { + hasCoolerControl = true + sender.fireOnEventReceived(CameraCoolerControlChanged(this)) + } + } + + service.canPulseGuide(device.number).doRequest { + if (it.value) { + canPulseGuide = true + sender.registerGuideOutput(this) + LOG.info("guide output attached: {}", name) + } + } + + service.canSetCCDTemperature(device.number).doRequest { + if (it.value) { + canSetTemperature = true + hasCooler = true + + sender.fireOnEventReceived(CameraHasCoolerChanged(this)) + sender.fireOnEventReceived(CameraCanSetTemperatureChanged(this)) + } + } + } + + private fun readImage() { + service.imageArray(device.number).execute().body()?.use { body -> + val stream = body.byteStream() + val metadata = ImageMetadata.from(stream.readNBytes(44)) + + if (metadata.errorNumber != 0) { + LOG.error("failed to read image. device={}, error={}", name, metadata.errorNumber) + return + } + + val width = metadata.dimension1 + val height = metadata.dimension2 + val planes = max(1, metadata.dimension3) + val source = stream.source().buffer() + val data = Array(planes) { FloatImageData(width, height) } + + for (x in 0 until width) { + for (y in 0 until height) { + val idx = y * width + x + + for (p in 0 until planes) { + val pixel = when (metadata.imageElementType.bitpix) { + Bitpix.BYTE -> (source.readByte().toInt() and 0xFF) / 255f + Bitpix.SHORT -> (source.readShortLe().toInt() + 32768) / 65535f + Bitpix.INTEGER -> ((source.readIntLe().toLong() + 2147483648) / 4294967295.0).toFloat() + Bitpix.FLOAT -> source.readFloatLe() + Bitpix.DOUBLE -> source.readDoubleLe().toFloat() + Bitpix.LONG -> return + } + + data[p].data[idx] = pixel + } + } + } + + source.close() + + val header = Header() + header.add(Standard.SIMPLE, true) + header.add(Standard.BITPIX, -32) + header.add(Standard.NAXIS, if (planes == 3) 3 else 2) + header.add(Standard.NAXIS1, width) + header.add(Standard.NAXIS2, height) + if (planes == 3) header.add(Standard.NAXIS3, planes) + header.add(Standard.EXTEND, true) + header.add(Standard.INSTRUME, name) + header.add(Standard.EXPTIME, 0.0) // TODO + header.add(SBFitsExt.CCD_TEMP, temperature) + header.add(NOAOExt.PIXSIZEn.n(1), pixelSizeX) + header.add(NOAOExt.PIXSIZEn.n(2), pixelSizeY) + header.add(SBFitsExt.XBINNING, binX) + header.add(SBFitsExt.YBINNING, binY) + header.add(SBFitsExt.XPIXSZ, pixelSizeX * binX) + header.add(SBFitsExt.YPIXSZ, pixelSizeY * binY) + header.add("FRAME", frameType.description, "Frame Type") + header.add(SBFitsExt.IMAGETYP, "${frameType.description} Frame") + + mount?.also { + header.add(Standard.TELESCOP, it.name) + header.add(SBFitsExt.SITELAT, it.latitude.toDegrees) + header.add(SBFitsExt.SITELONG, it.longitude.toDegrees) + val center = Geoid.IERS2010.lonLat(it.longitude, it.latitude, it.elevation) + val icrf = ICRF.equatorial(it.rightAscension, it.declination, epoch = CurrentTime, center = center) + val raDec = icrf.equatorial() + header.add(SBFitsExt.OBJCTRA, raDec.longitude.normalized.formatHMS()) + header.add(SBFitsExt.OBJCTDEC, raDec.longitude.formatSignedDMS()) + header.add(Standard.RA, raDec.longitude.normalized.toDegrees) + header.add(Standard.DEC, raDec.longitude.toDegrees) + header.add(MaxImDLExt.PIERSIDE, it.pierSide.name) + header.add(Standard.EQUINOX, 2000) + header.add(Standard.DATE_OBS, LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + header.add(Standard.COMMENT, "Generated by Nebulosa via ASCOM") + header.add(NOAOExt.GAIN, gain) + header.add("OFFSET", offset, "Offset") + } + + val hdu = ImageHdu(header, data) + + val fits = Fits() + fits.add(hdu) + + sender.fireOnEventReceived(CameraFrameCaptured(this, null, fits, false)) + } ?: LOG.error("image body is null. device={}", name) + } + + 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)" + + data class ImageMetadata( + @JvmField val metadataVersion: Int, // Bytes 0..3 - Metadata version = 1 + @JvmField val errorNumber: Int, // Bytes 4..7 - Alpaca error number or zero for success + @JvmField val clientTransactionID: Int, // Bytes 8..11 - Client's transaction ID + @JvmField val serverTransactionID: Int, // Bytes 12..15 - Device's transaction ID + @JvmField val dataStart: Int, // Bytes 16..19 - Offset of the start of the data bytes + @JvmField val imageElementType: ImageArrayElementType, // Bytes 20..23 - Element type of the source image array + @JvmField val transmissionElementType: Int, // Bytes 24..27 - Element type as sent over the network + @JvmField val rank: Int, // Bytes 28..31 - Image array rank (2 or 3) + @JvmField val dimension1: Int, // Bytes 32..35 - Length of image array first dimension + @JvmField val dimension2: Int, // Bytes 36..39 - Length of image array second dimension + @JvmField val dimension3: Int, // Bytes 40..43 - Length of image array third dimension (0 for 2D array) + ) { + + companion object { + + @JvmStatic + fun from(data: ByteBuffer) = ImageMetadata( + data.getInt(), data.getInt(), data.getInt(), data.getInt(), data.getInt(), + ImageArrayElementType.entries[data.getInt()], data.getInt(), data.getInt(), + data.getInt(), data.getInt(), data.getInt() + ) + + @JvmStatic + fun from(data: ByteArray) = from(ByteBuffer.wrap(data, 0, 44)) + } + } + + private inner class ImageReadyWaiter : Thread("$name ASCOM Image Ready Waiter") { + + private val latch = CountUpDownLatch(1) + + init { + isDaemon = true + } + + fun captureStarted() { + latch.reset() + } + + fun captureAborted() { + latch.countUp() + } + + override fun run() { + while (true) { + latch.await() + + while (latch.get()) { + val startTime = System.currentTimeMillis() + + service.isImageReady(device.number).doRequest { + if (it.value) { + latch.countUp() + readImage() + } + } + + if (!latch.get()) { + val endTime = System.currentTimeMillis() + val delayTime = 1000L - (endTime - startTime) + + if (delayTime > 1L) { + sleep(delayTime) + } + } + } + } + } + } +} diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ImageArrayElementType.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ImageArrayElementType.kt new file mode 100644 index 000000000..1cce8be78 --- /dev/null +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ImageArrayElementType.kt @@ -0,0 +1,16 @@ +package nebulosa.alpaca.indi.device.cameras + +import nebulosa.fits.Bitpix + +enum class ImageArrayElementType(@JvmField val bitpix: Bitpix) { + UNKNOWN(Bitpix.BYTE), // 0 to 3 are values already used in the Alpaca standard + INT16(Bitpix.SHORT), + INT32(Bitpix.INTEGER), + DOUBLE(Bitpix.DOUBLE), + SINGLE(Bitpix.FLOAT), // 4 to 9 are an extension to include other numeric types + UINT64(Bitpix.LONG), + BYTE(Bitpix.BYTE), + INT64(Bitpix.LONG), + UINT16(Bitpix.SHORT), + UINT32(Bitpix.INTEGER), +} 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 new file mode 100644 index 000000000..70247550c --- /dev/null +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt @@ -0,0 +1,172 @@ +package nebulosa.alpaca.indi.device.focusers + +import nebulosa.alpaca.api.AlpacaFocuserService +import nebulosa.alpaca.api.ConfiguredDevice +import nebulosa.alpaca.indi.client.AlpacaClient +import nebulosa.alpaca.indi.device.ASCOMDevice +import nebulosa.indi.device.Device +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserCanAbsoluteMoveChanged +import nebulosa.indi.device.focuser.FocuserMovingChanged +import nebulosa.indi.device.focuser.FocuserPositionChanged +import nebulosa.indi.device.thermometer.ThermometerAttached +import nebulosa.indi.device.thermometer.ThermometerDetached +import nebulosa.indi.device.thermometer.ThermometerTemperatureChanged +import nebulosa.indi.protocol.INDIProtocol + +data class ASCOMFocuser( + override val device: ConfiguredDevice, + override val service: AlpacaFocuserService, + override val sender: AlpacaClient, +) : ASCOMDevice(), Focuser { + + @Volatile override var moving = false + private set + @Volatile override var position = 0 + private set + @Volatile override var canAbsoluteMove = false + private set + @Volatile override var canRelativeMove = false + private set + @Volatile override var canAbort = false + private set + @Volatile override var canReverse = false + private set + @Volatile override var reversed = false + private set + @Volatile override var canSync = false + private set + @Volatile override var hasBacklash = false + private set + @Volatile override var maxPosition = 0 + private set + + @Volatile override var hasThermometer = false + private set + @Volatile override var temperature = 0.0 + private set + + override fun moveFocusIn(steps: Int) { + if (canAbsoluteMove) { + service.move(device.number, position + steps).doRequest() + } else { + service.move(device.number, steps).doRequest() + } + } + + override fun moveFocusOut(steps: Int) { + if (canAbsoluteMove) { + service.move(device.number, position - steps).doRequest() + } else { + service.move(device.number, -steps).doRequest() + } + } + + override fun moveFocusTo(steps: Int) { + } + + override fun abortFocus() { + service.halt(device.number).doRequest() + } + + override fun reverseFocus(enable: Boolean) { + } + + override fun syncFocusTo(steps: Int) { + } + + override fun snoop(devices: Iterable) { + } + + override fun handleMessage(message: INDIProtocol) { + } + + override fun onConnected() { + processCapabilities() + processPosition() + processTemperature() + } + + override fun onDisconnected() { + } + + override fun reset() { + super.reset() + + moving = false + position = 0 + canAbsoluteMove = false + canRelativeMove = false + canAbort = false + canReverse = false + reversed = false + canSync = false + hasBacklash = false + maxPosition = 0 + hasThermometer = false + temperature = 0.0 + } + + override fun close() { + super.close() + + if (hasThermometer) { + hasThermometer = false + sender.fireOnEventReceived(ThermometerDetached(this)) + } + } + + override fun refresh(elapsedTimeInSeconds: Long) { + super.refresh(elapsedTimeInSeconds) + + processMoving() + processPosition() + processTemperature() + } + + private fun processCapabilities() { + service.canAbsolute(device.number).doRequest { + if (it.value) { + canAbsoluteMove = true + sender.fireOnEventReceived(FocuserCanAbsoluteMoveChanged(this)) + } + } + + service.temperature(device.number).doRequest { + hasThermometer = true + sender.fireOnEventReceived(ThermometerAttached(this)) + } + } + + private fun processMoving() { + service.isMoving(device.number).doRequest { + if (it.value != moving) { + moving = it.value + + sender.fireOnEventReceived(FocuserMovingChanged(this)) + } + } + } + + private fun processPosition() { + service.position(device.number).doRequest { + if (it.value != position) { + position = it.value + + sender.fireOnEventReceived(FocuserPositionChanged(this)) + } + } + } + + private fun processTemperature() { + if (hasThermometer) { + service.temperature(device.number).doRequest { + if (it.value != temperature) { + temperature = it.value + + sender.fireOnEventReceived(ThermometerTemperatureChanged(this)) + } + } + } + } +} 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 new file mode 100644 index 000000000..d87ded2f8 --- /dev/null +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt @@ -0,0 +1,478 @@ +package nebulosa.alpaca.indi.device.mounts + +import nebulosa.alpaca.api.* +import nebulosa.alpaca.indi.client.AlpacaClient +import nebulosa.alpaca.indi.device.ASCOMDevice +import nebulosa.indi.device.Device +import nebulosa.indi.device.guide.GuideOutputPulsingChanged +import nebulosa.indi.device.mount.* +import nebulosa.indi.device.mount.PierSide +import nebulosa.indi.protocol.INDIProtocol +import nebulosa.math.* +import nebulosa.nova.position.ICRF +import nebulosa.time.CurrentTime +import java.time.Duration +import java.time.OffsetDateTime +import java.time.ZoneOffset + +data class ASCOMMount( + override val device: ConfiguredDevice, + override val service: AlpacaTelescopeService, + override val sender: AlpacaClient, +) : ASCOMDevice(), Mount { + + @Volatile override var slewing = false + private set + @Volatile override var tracking = false + private set + @Volatile override var parking = false + private set + @Volatile override var parked = false + private set + @Volatile override var canAbort = true + private set + @Volatile override var canSync = false + private set + @Volatile override var canGoTo = false + private set + @Volatile override var canPark = false + private set + @Volatile override var canHome = false + private set + @Volatile override var slewRates = emptyList() + private set + @Volatile override var slewRate: SlewRate? = null + private set + @Volatile override var mountType = MountType.EQ_GEM + private set + @Volatile override var trackModes = emptyList() + private set + @Volatile override var trackMode = TrackMode.SIDEREAL + private set + @Volatile override var pierSide = PierSide.NEITHER + private set + @Volatile override var guideRateWE = 0.0 + private set + @Volatile override var guideRateNS = 0.0 + private set + @Volatile override var rightAscension = 0.0 + private set + @Volatile override var declination = 0.0 + private set + + @Volatile override var canPulseGuide = false + private set + @Volatile override var pulseGuiding = false + private set + + @Volatile override var hasGPS = false + private set + @Volatile override var longitude = 0.0 + private set + @Volatile override var latitude = 0.0 + private set + @Volatile override var elevation = 0.0 + private set + @Volatile override var dateTime = OffsetDateTime.now()!! + private set + + private val axisRates = HashMap(4) + @Volatile private var axisRate: AxisRate? = null + @Volatile private var equatorialSystem = EquatorialCoordinateType.J2000 + + override fun park() { + if (canPark) { + service.park(device.number).doRequest() + } + } + + override fun unpark() { + if (canPark) { + service.unpark(device.number).doRequest() + } + } + + private fun pulseGuide(direction: PulseGuideDirection, duration: Duration) { + if (canPulseGuide) { + val durationInMilliseconds = duration.toMillis() + + service.pulseGuide(device.number, direction, durationInMilliseconds).doRequest() ?: return + + if (durationInMilliseconds > 0) { + pulseGuiding = true + sender.fireOnEventReceived(GuideOutputPulsingChanged(this)) + } + } + } + + override fun guideNorth(duration: Duration) { + pulseGuide(PulseGuideDirection.NORTH, duration) + } + + override fun guideSouth(duration: Duration) { + pulseGuide(PulseGuideDirection.SOUTH, duration) + } + + override fun guideEast(duration: Duration) { + pulseGuide(PulseGuideDirection.EAST, duration) + } + + override fun guideWest(duration: Duration) { + pulseGuide(PulseGuideDirection.WEST, duration) + } + + override fun tracking(enable: Boolean) { + service.tracking(device.number, enable).doRequest() + } + + override fun sync(ra: Angle, dec: Angle) { + if (canSync) { + if (equatorialSystem != EquatorialCoordinateType.J2000) { + service.syncToCoordinates(device.number, ra.toHours, dec.toDegrees).doRequest() + } else { + // J2000 -> JNOW. + with(ICRF.equatorial(ra, dec).equatorialAtDate()) { + service.syncToCoordinates(device.number, longitude.normalized.toHours, latitude.toDegrees).doRequest() + } + } + } + } + + override fun syncJ2000(ra: Angle, dec: Angle) { + if (canSync) { + if (equatorialSystem == EquatorialCoordinateType.J2000) { + service.syncToCoordinates(device.number, ra.toHours, dec.toDegrees).doRequest() + } else { + // JNOW -> J2000. + with(ICRF.equatorial(ra, dec, epoch = CurrentTime).equatorial()) { + service.syncToCoordinates(device.number, longitude.normalized.toHours, latitude.toDegrees).doRequest() + } + } + } + } + + override fun slewTo(ra: Angle, dec: Angle) { + service.tracking(device.number, true) + + if (equatorialSystem != EquatorialCoordinateType.J2000) { + service.slewToCoordinates(device.number, ra.toHours, dec.toDegrees).doRequest() + } else { + // J2000 -> JNOW. + with(ICRF.equatorial(ra, dec).equatorialAtDate()) { + service.slewToCoordinates(device.number, longitude.normalized.toHours, latitude.toDegrees).doRequest() + } + } + } + + override fun slewToJ2000(ra: Angle, dec: Angle) { + service.tracking(device.number, true) + + if (equatorialSystem == EquatorialCoordinateType.J2000) { + service.slewToCoordinates(device.number, ra.toHours, dec.toDegrees).doRequest() + } else { + // JNOW -> J2000. + with(ICRF.equatorial(ra, dec, epoch = CurrentTime).equatorial()) { + service.slewToCoordinates(device.number, longitude.normalized.toHours, latitude.toDegrees).doRequest() + } + } + } + + override fun goTo(ra: Angle, dec: Angle) { + slewTo(ra, dec) + } + + override fun goToJ2000(ra: Angle, dec: Angle) { + slewToJ2000(ra, dec) + } + + override fun home() { + if (canHome) { + service.findHome(device.number).doRequest() + } + } + + override fun abortMotion() { + if (canAbort) { + service.abortSlew(device.number).doRequest() + } + } + + override fun trackMode(mode: TrackMode) { + if (mode != TrackMode.CUSTOM) { + service.trackingRate(device.number, DriveRate.entries[mode.ordinal]) + } + } + + override fun slewRate(rate: SlewRate) { + axisRate = axisRates[rate.name]?.takeIf { it !== axisRate } ?: return + sender.fireOnEventReceived(MountSlewRateChanged(this)) + } + + private fun moveAxis(axisType: AxisType, negative: Boolean, enabled: Boolean) { + val rate = axisRate?.maximum ?: return + + service.moveAxis(device.number, axisType, 0.0).doRequest() + + if (enabled) { + service.moveAxis(device.number, axisType, if (negative) -rate else rate).doRequest() + } + } + + override fun moveNorth(enabled: Boolean) { + moveAxis(AxisType.SECONDARY, false, enabled) + } + + override fun moveSouth(enabled: Boolean) { + moveAxis(AxisType.SECONDARY, true, enabled) + } + + override fun moveWest(enabled: Boolean) { + moveAxis(AxisType.PRIMARY, true, enabled) + } + + override fun moveEast(enabled: Boolean) { + moveAxis(AxisType.PRIMARY, false, enabled) + } + + 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 {} + } + + override fun dateTime(dateTime: OffsetDateTime) { + service.utcDate(device.number, dateTime.toInstant()) + } + + override fun snoop(devices: Iterable) {} + + override fun handleMessage(message: INDIProtocol) {} + + override fun onConnected() { + processCapabilities() + processGuideRates() + processSiteCoordinates() + processDateTime() + + equatorialSystem = service.equatorialSystem(device.number).doRequest()?.value ?: equatorialSystem + + LOG.info("The mount {} uses {} equatorial system", name, equatorialSystem) + } + + override fun onDisconnected() {} + + override fun reset() { + super.reset() + + slewing = false + tracking = false + parking = false + parked = false + canAbort = false + canSync = false + canGoTo = false + canPark = false + canHome = false + slewRates = emptyList() + slewRate = null + mountType = MountType.EQ_GEM + trackModes = emptyList() + trackMode = TrackMode.SIDEREAL + pierSide = PierSide.NEITHER + guideRateWE = 0.0 + guideRateNS = 0.0 + rightAscension = 0.0 + declination = 0.0 + + canPulseGuide = false + pulseGuiding = false + + hasGPS = false + longitude = 0.0 + latitude = 0.0 + elevation = 0.0 + dateTime = OffsetDateTime.now()!! + + axisRates.clear() + axisRate = null + } + + override fun close() { + super.close() + + if (canPulseGuide) { + canPulseGuide = false + sender.unregisterGuideOutput(this) + } + } + + override fun refresh(elapsedTimeInSeconds: Long) { + super.refresh(elapsedTimeInSeconds) + + processTracking() + processSlewing() + processParked() + processEquatorialCoordinates() + processSiteCoordinates() + processTrackMode() + } + + private fun processCapabilities() { + service.canFindHome(device.number).doRequest { + if (it.value) { + canHome = true + sender.fireOnEventReceived(MountCanHomeChanged(this)) + } + } + + service.canPark(device.number).doRequest { + if (it.value) { + canPark = true + sender.fireOnEventReceived(MountCanParkChanged(this)) + } + } + + service.canPulseGuide(device.number).doRequest { + if (it.value) { + canPulseGuide = true + sender.registerGuideOutput(this) + LOG.info("guide output attached: {}", name) + } + } + + service.canSync(device.number).doRequest { + if (it.value) { + canSync = true + sender.fireOnEventReceived(MountCanSyncChanged(this)) + } + } + + service.trackingRates(device.number).doRequest { + trackModes = it.value.map { m -> TrackMode.valueOf(m.name) } + sender.fireOnEventReceived(MountTrackModesChanged(this)) + processTrackMode() + } + + service.axisRates(device.number).doRequest { + val rates = ArrayList(it.value.size) + + axisRates.clear() + + for (i in it.value.indices) { + val rate = it.value[i] + val name = "RATE_$i" + axisRates[name] = rate + rates.add(SlewRate(name, "%f.1f deg/s".format(rate.maximum))) + } + + axisRate = it.value.firstOrNull() + + if (axisRate != null) { + sender.fireOnEventReceived(MountSlewRateChanged(this)) + } + } + } + + private fun processTracking() { + service.isTracking(device.number).doRequest { + if (it.value != tracking) { + tracking = it.value + sender.fireOnEventReceived(MountTrackingChanged(this)) + } + } + } + + private fun processSlewing() { + service.isSlewing(device.number).doRequest { + if (it.value != slewing) { + slewing = it.value + sender.fireOnEventReceived(MountSlewingChanged(this)) + } + } + } + + private fun processParked() { + if (canPark) { + service.isAtPark(device.number).doRequest { + if (it.value != parked) { + parked = it.value + sender.fireOnEventReceived(MountParkChanged(this)) + } + } + } + } + + private fun processSiteCoordinates() { + service.siteLongitude(device.number).doRequest { a -> + val lng = a.value.deg + + service.siteLatitude(device.number).doRequest { b -> + val lat = b.value.deg + + service.siteElevation(device.number).doRequest { c -> + val elev = c.value.m + + if (lng != longitude || lat != latitude || elev != elevation) { + longitude = lng + latitude = lat + elevation = elev + + sender.fireOnEventReceived(MountGeographicCoordinateChanged(this)) + } + } + } + } + } + + private fun processDateTime() { + service.utcDate(device.number).doRequest { + dateTime = it.value.atOffset(ZoneOffset.systemDefault().rules.getOffset(it.value)) + sender.fireOnEventReceived(MountTimeChanged(this)) + } + } + + private fun processTrackMode() { + service.trackingRate(device.number).doRequest { + if (it.value.name != trackMode.name) { + trackMode = TrackMode.valueOf(it.value.name) + sender.fireOnEventReceived(MountTrackModeChanged(this)) + } + } + } + + private fun processGuideRates() { + service.guideRateRightAscension(device.number).doRequest { ra -> + // TODO: deg/s is the same for INDI? + guideRateWE = ra.value + + service.guideRateDeclination(device.number).doRequest { de -> + guideRateNS = de.value + } + } + } + + private fun processEquatorialCoordinates() { + service.rightAscension(device.number).doRequest { a -> + var ra = a.value.hours + + service.declination(device.number).doRequest { b -> + var dec = b.value.deg + + // J2000 -> JNOW. + if (equatorialSystem == EquatorialCoordinateType.J2000) { + with(ICRF.equatorial(ra, dec).equatorialAtDate()) { + ra = longitude.normalized + dec = latitude + } + } + + if (ra != rightAscension || dec != declination) { + rightAscension = ra + declination = dec + + sender.fireOnEventReceived(MountEquatorialCoordinatesChanged(this)) + } + } + } + } +} 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 new file mode 100644 index 000000000..846b76726 --- /dev/null +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/wheels/ASCOMFilterWheel.kt @@ -0,0 +1,80 @@ +package nebulosa.alpaca.indi.device.wheels + +import nebulosa.alpaca.api.AlpacaFilterWheelService +import nebulosa.alpaca.api.ConfiguredDevice +import nebulosa.alpaca.indi.client.AlpacaClient +import nebulosa.alpaca.indi.device.ASCOMDevice +import nebulosa.indi.device.Device +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.filterwheel.FilterWheelNamesChanged +import nebulosa.indi.device.filterwheel.FilterWheelPositionChanged +import nebulosa.indi.protocol.INDIProtocol + +data class ASCOMFilterWheel( + override val device: ConfiguredDevice, + override val service: AlpacaFilterWheelService, + override val sender: AlpacaClient, +) : ASCOMDevice(), FilterWheel { + + @Volatile override var count = 0 + private set + @Volatile override var position = 0 + private set + @Volatile override var moving = false + private set + @Volatile override var names: List = emptyList() + private set + + override fun onConnected() { + processPosition() + processNames() + } + + override fun onDisconnected() {} + + override fun moveTo(position: Int) { + if (position != this.position) { + service.position(device.number, position).doRequest() + } + } + + override fun refresh(elapsedTimeInSeconds: Long) { + super.refresh(elapsedTimeInSeconds) + + if (connected) { + processPosition() + processMoving() + } + } + + override fun names(names: Iterable) { + this.names = names.toList() + } + + override fun snoop(devices: Iterable) {} + + override fun handleMessage(message: INDIProtocol) {} + + private fun processMoving() {} + + private fun processPosition() { + service.position(device.number).doRequest { + if (it.value != position) { + val prevPosition = position + position = it.value + + sender.fireOnEventReceived(FilterWheelPositionChanged(this, prevPosition)) + } + } + } + + private fun processNames() { + service.names(device.number).doRequest { + if (it.value != names) { + names = it.value + + sender.fireOnEventReceived(FilterWheelNamesChanged(this)) + } + } + } +} diff --git a/nebulosa-astap/src/main/kotlin/nebulosa/astap/plate/solving/AstapPlateSolver.kt b/nebulosa-astap/src/main/kotlin/nebulosa/astap/plate/solving/AstapPlateSolver.kt index 23afc4292..cd10570a5 100644 --- a/nebulosa-astap/src/main/kotlin/nebulosa/astap/plate/solving/AstapPlateSolver.kt +++ b/nebulosa-astap/src/main/kotlin/nebulosa/astap/plate/solving/AstapPlateSolver.kt @@ -1,5 +1,6 @@ package nebulosa.astap.plate.solving +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.common.process.ProcessExecutor import nebulosa.fits.Header import nebulosa.fits.NOAOExt @@ -33,6 +34,7 @@ class AstapPlateSolver(path: Path) : PlateSolver { path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, downsampleFactor: Int, timeout: Duration?, + cancellationToken: CancellationToken, ): PlateSolution { requireNotNull(path) { "path is required" } @@ -46,7 +48,7 @@ class AstapPlateSolver(path: Path) : PlateSolver { arguments["-z"] = downsampleFactor arguments["-fov"] = 0 // auto - if (radius.toDegrees >= 0.1) { + if (radius.toDegrees >= 0.1 && centerRA.isFinite() && centerDEC.isFinite()) { arguments["-ra"] = centerRA.toHours arguments["-spd"] = centerDEC.toDegrees + 90.0 arguments["-r"] = ceil(radius.toDegrees) @@ -56,13 +58,17 @@ class AstapPlateSolver(path: Path) : PlateSolver { arguments["-f"] = path - LOG.info("local solving. command={}", arguments) + LOG.info("ASTAP solving. command={}", arguments) try { - val process = executor.execute(arguments, timeout?.takeIf { it.toSeconds() > 0 } ?: Duration.ofMinutes(5), path.parent) + val timeoutOrDefault = timeout?.takeIf { it.toSeconds() > 0 } ?: Duration.ofMinutes(5) + val process = executor.execute(arguments, timeoutOrDefault, path.parent, cancellationToken) + if (process.isAlive) process.destroyForcibly() LOG.info("astap exited. code={}", process.exitValue()) + if (cancellationToken.isCancelled) return PlateSolution.NO_SOLUTION + val ini = Properties() Paths.get("$basePath", "$baseName.ini").inputStream().use(ini::load) @@ -104,7 +110,7 @@ class AstapPlateSolver(path: Path) : PlateSolver { header.add(NOAOExt.CD2_1, cd21) header.add(NOAOExt.CD2_2, cd22) - val solution = PlateSolution(true, crota2.deg, cdelt2.deg, crval1.deg, crval2.deg, width.deg, height.deg) + val solution = PlateSolution(true, crota2.deg, cdelt2.deg, crval1.deg, crval2.deg, width.deg, height.deg, header = header) LOG.info("astap solved. calibration={}", solution) diff --git a/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LibAstrometryNetPlateSolver.kt b/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LibAstrometryNetPlateSolver.kt index 64b054987..8f69b51ab 100644 --- a/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LibAstrometryNetPlateSolver.kt +++ b/nebulosa-astrometrynet-jna/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LibAstrometryNetPlateSolver.kt @@ -1,5 +1,6 @@ package nebulosa.astrometrynet.plate.solving +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.imaging.Image import nebulosa.math.Angle import nebulosa.plate.solving.PlateSolution @@ -13,6 +14,7 @@ data class LibAstrometryNetPlateSolver(private val solver: LibAstrometryNet) : P path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, downsampleFactor: Int, timeout: Duration?, + cancellationToken: CancellationToken, ): PlateSolution { return PlateSolution.NO_SOLUTION } diff --git a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LocalAstrometryNetPlateSolver.kt b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LocalAstrometryNetPlateSolver.kt index 51edea9ce..455f1ca60 100644 --- a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LocalAstrometryNetPlateSolver.kt +++ b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/LocalAstrometryNetPlateSolver.kt @@ -1,5 +1,6 @@ package nebulosa.astrometrynet.plate.solving +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.common.process.ProcessExecutor import nebulosa.imaging.Image import nebulosa.log.loggerFor @@ -24,6 +25,7 @@ class LocalAstrometryNetPlateSolver(path: Path) : PlateSolver { path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, downsampleFactor: Int, timeout: Duration?, + cancellationToken: CancellationToken, ): PlateSolution { requireNotNull(path) { "path is required" } @@ -44,7 +46,7 @@ class LocalAstrometryNetPlateSolver(path: Path) : PlateSolver { arguments["--no-plots"] = null // args["--resort"] = null - if (radius.toDegrees >= 0.1) { + if (radius.toDegrees >= 0.1 && centerRA.isFinite() && centerDEC.isFinite()) { arguments["--ra"] = centerRA.toDegrees arguments["--dec"] = centerDEC.toDegrees arguments["--radius"] = radius.toDegrees @@ -52,7 +54,7 @@ class LocalAstrometryNetPlateSolver(path: Path) : PlateSolver { arguments["$path"] = null - val process = executor.execute(arguments, Duration.ZERO, path.parent) + val process = executor.execute(arguments, Duration.ZERO, path.parent, cancellationToken) val buffer = process.inputReader() diff --git a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/NovaAstrometryNetPlateSolver.kt b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/NovaAstrometryNetPlateSolver.kt index 1e602c7c3..a0f6a6bbe 100644 --- a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/NovaAstrometryNetPlateSolver.kt +++ b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/plate/solving/NovaAstrometryNetPlateSolver.kt @@ -3,6 +3,7 @@ package nebulosa.astrometrynet.plate.solving import nebulosa.astrometrynet.nova.NovaAstrometryNetService import nebulosa.astrometrynet.nova.Session import nebulosa.astrometrynet.nova.Upload +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.fits.Header import nebulosa.imaging.Image import nebulosa.log.loggerFor @@ -43,10 +44,11 @@ data class NovaAstrometryNetPlateSolver( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, downsampleFactor: Int, timeout: Duration?, + cancellationToken: CancellationToken, ): PlateSolution { renewSession() - val blind = radius.toDegrees < 0.1 + val blind = radius.toDegrees < 0.1 || !centerRA.isFinite() || !centerDEC.isFinite() val upload = Upload( session = session!!.session, @@ -69,13 +71,13 @@ data class NovaAstrometryNetPlateSolver( var timeLeft = timeout?.takeIf { it.toSeconds() > 0 }?.toMillis() ?: 300000L - while (timeLeft >= 0L) { + while (timeLeft >= 0L && !cancellationToken.isCancelled) { val startTime = System.currentTimeMillis() val status = service.submissionStatus(submission.subId).execute().body() ?: throw PlateSolvingException("failed to retrieve submission status") - if (status.solved) { + if (status.solved && !cancellationToken.isCancelled) { LOG.info("retrieving WCS from job. id={}", status.jobs[0]) val body = service.wcs(status.jobs[0]).execute().body() @@ -89,9 +91,11 @@ data class NovaAstrometryNetPlateSolver( return calibration ?: PlateSolution.NO_SOLUTION } - timeLeft -= System.currentTimeMillis() - startTime + 5000L + if (!cancellationToken.isCancelled) { + timeLeft -= System.currentTimeMillis() - startTime + 5000L - Thread.sleep(5000L) + Thread.sleep(5000L) + } } throw PlateSolvingException("the plate solving took a long time and finished") diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt index dbc9d05e2..b489d0ca6 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -1,7 +1,8 @@ package nebulosa.batch.processing -import nebulosa.common.concurrency.CancellationListener -import nebulosa.common.concurrency.CancellationSource +import nebulosa.common.concurrency.cancel.CancellationListener +import nebulosa.common.concurrency.cancel.CancellationSource +import nebulosa.common.concurrency.latch.Pauseable import nebulosa.log.debug import nebulosa.log.loggerFor import java.io.Closeable @@ -153,6 +154,36 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI } } + override fun pause(jobExecution: JobExecution) { + if (!jobExecution.isDone && !jobExecution.isPaused) { + val job = jobExecution.job + + if (job is Pauseable) { + jobExecution.status = JobStatus.PAUSED + job.pause() + } + + if (!jobExecution.cancellationToken.isDone) { + jobExecution.cancellationToken.pause() + } + } + } + + override fun unpause(jobExecution: JobExecution) { + if (!jobExecution.isDone && jobExecution.isPaused) { + val job = jobExecution.job + + if (job is Pauseable) { + job.unpause() + jobExecution.status = JobStatus.STARTED + } + + if (!jobExecution.cancellationToken.isDone) { + jobExecution.cancellationToken.unpause() + } + } + } + override fun intercept(chain: StepChain): StepResult { stepListeners.forEach { it.beforeStep(chain.stepExecution) } val result = chain.step.execute(chain.stepExecution) diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt index 60c35e8a4..79a3243bd 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt @@ -2,6 +2,8 @@ package nebulosa.batch.processing interface Job : JobExecutionListener, Stoppable { + val id: String + fun hasNext(jobExecution: JobExecution): Boolean fun next(jobExecution: JobExecution): Step @@ -9,4 +11,6 @@ interface Job : JobExecutionListener, Stoppable { override fun beforeJob(jobExecution: JobExecution) = Unit override fun afterJob(jobExecution: JobExecution) = Unit + + operator fun contains(data: Any): Boolean } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt index b703352be..5556b12c5 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -1,6 +1,6 @@ package nebulosa.batch.processing -import nebulosa.common.concurrency.CancellationToken +import nebulosa.common.concurrency.cancel.CancellationToken import java.time.LocalDateTime import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutionException @@ -24,7 +24,7 @@ class JobExecution( @JvmField val cancellationToken = CancellationToken() inline val canContinue - get() = status == JobStatus.STARTED + get() = status == JobStatus.STARTED || status == JobStatus.PAUSED inline val isStopping get() = status == JobStatus.STOPPING @@ -32,6 +32,9 @@ class JobExecution( inline val isStopped get() = status == JobStatus.STOPPED + inline val isPaused + get() = status == JobStatus.PAUSED + inline val isCompleted get() = status == JobStatus.COMPLETED diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutor.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutor.kt new file mode 100644 index 000000000..e9f7567a3 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutor.kt @@ -0,0 +1,54 @@ +package nebulosa.batch.processing + +import java.util.* + +abstract class JobExecutor { + + protected abstract val jobLauncher: JobLauncher + + @PublishedApi internal val jobExecutions = LinkedList() + + protected fun register(jobExecution: JobExecution) { + jobExecutions.add(jobExecution) + } + + protected inline fun findJobExecutionWith(test: Job.() -> Boolean): JobExecution? { + for (i in jobExecutions.indices.reversed()) { + val jobExecution = jobExecutions[i] + val job = jobExecution.job + + if (!jobExecution.isDone && job.test()) { + return jobExecution + } + } + + return null + } + + protected fun findJobExecutionWithAny(vararg data: Any): JobExecution? { + return findJobExecutionWith { data.any { it in this } } + } + + fun findJobExecution(id: String): JobExecution? { + return jobExecutions.find { it.job.id == id } + } + + fun stop(id: String) { + val jobExecution = findJobExecution(id) ?: return + jobLauncher.stop(jobExecution) + } + + fun pause(id: String) { + val jobExecution = findJobExecution(id) ?: return + jobLauncher.pause(jobExecution) + } + + fun unpause(id: String) { + val jobExecution = findJobExecution(id) ?: return + jobLauncher.unpause(jobExecution) + } + + fun isRunning(id: String): Boolean { + return findJobExecution(id) != null + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt index fa00440a7..71683c80a 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt @@ -19,4 +19,8 @@ interface JobLauncher : Collection, Stoppable { fun launch(job: Job, executionContext: ExecutionContext? = null): JobExecution fun stop(jobExecution: JobExecution, mayInterruptIfRunning: Boolean = true) + + fun pause(jobExecution: JobExecution) + + fun unpause(jobExecution: JobExecution) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt index 005aa08e2..e680702fc 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt @@ -5,6 +5,7 @@ enum class JobStatus { STARTED, STOPPING, STOPPED, + PAUSED, FAILED, COMPLETED, ABANDONED, diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt index 9b062638a..adf372f2d 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt @@ -1,36 +1,93 @@ package nebulosa.batch.processing -abstract class SimpleJob : Job, ArrayList { +import nebulosa.common.concurrency.latch.Pauseable +import java.util.* - constructor(initialCapacity: Int = 4) : super(initialCapacity) +abstract class SimpleJob : Job, Pauseable, Iterable { - constructor(steps: Collection) : super(steps) + private val steps = ArrayList() - constructor(vararg steps: Step) : this(steps.toList()) + constructor(steps: Collection) { + this.steps.addAll(steps) + } + + constructor(vararg steps: Step) { + steps.forEach(this.steps::add) + } + + override val id = UUID.randomUUID().toString() @Volatile private var position = 0 - @Volatile private var stopped = false + @Volatile private var isEnded = false - override fun hasNext(jobExecution: JobExecution): Boolean { - return !stopped && position < size + protected fun register(step: Step): Boolean { + return steps.add(step) } - override fun next(jobExecution: JobExecution): Step { - return this[position++] + protected fun unregister(step: Step): Boolean { + return steps.remove(step) } - override fun stop(mayInterruptIfRunning: Boolean) { - if (stopped) return + protected fun clear() { + return steps.clear() + } - stopped = true + final override fun hasNext(jobExecution: JobExecution): Boolean { + return !isEnded && position < steps.size + } - if (position in 1..size) { - this[position - 1].stop(mayInterruptIfRunning) + final override fun next(jobExecution: JobExecution): Step { + check(!isEnded) { "this job is ended" } + return steps[position++] + } + + final override fun stop(mayInterruptIfRunning: Boolean) { + if (isEnded) return + + isEnded = true + + if (position in 1..steps.size) { + steps[position - 1].stop(mayInterruptIfRunning) + } + } + + final override val isPaused + get() = steps.any { it !== this && it is Pauseable && it.isPaused } + + final override fun pause() { + if (isEnded) return + + if (position in 1..steps.size) { + val step = steps[position - 1] + + if (step is Pauseable) { + step.pause() + } + } + } + + final override fun unpause() { + if (isEnded) return + + if (position in 1..steps.size) { + val step = steps[position - 1] + + if (step is Pauseable) { + step.unpause() + } } } fun reset() { - stopped = false + isEnded = false position = 0 } + + final override fun iterator(): Iterator { + return steps.iterator() + } + + override fun contains(data: Any): Boolean { + return data is Step && data in steps + } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt index fe2d7d914..ab62c8cb3 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt @@ -4,5 +4,15 @@ interface Step : Stoppable, JobExecutionListener { fun execute(stepExecution: StepExecution): StepResult + fun executeSingle(stepExecution: StepExecution): StepResult { + beforeJob(stepExecution.jobExecution) + + try { + return execute(stepExecution) + } finally { + afterJob(stepExecution.jobExecution) + } + } + override fun stop(mayInterruptIfRunning: Boolean) = Unit } diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationListener.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationListener.kt deleted file mode 100644 index c75194c88..000000000 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package nebulosa.common.concurrency - -import java.util.function.Consumer - -fun interface CancellationListener : Consumer diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/Incrementer.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/atomic/Incrementer.kt similarity index 92% rename from nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/Incrementer.kt rename to nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/atomic/Incrementer.kt index 2ea22efb9..46d9ede72 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/Incrementer.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/atomic/Incrementer.kt @@ -1,4 +1,4 @@ -package nebulosa.common.concurrency +package nebulosa.common.concurrency.atomic import java.util.concurrent.atomic.AtomicLong diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationSource.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationSource.kt similarity index 74% rename from nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationSource.kt rename to nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationSource.kt index 4f7adc76e..5dec99c93 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationSource.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationSource.kt @@ -1,6 +1,6 @@ -package nebulosa.common.concurrency +package nebulosa.common.concurrency.cancel -sealed interface CancellationSource { +interface CancellationSource { data object None : CancellationSource diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationToken.kt similarity index 52% rename from nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt rename to nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationToken.kt index b8ba730ee..82311f2cf 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/cancel/CancellationToken.kt @@ -1,11 +1,16 @@ -package nebulosa.common.concurrency +package nebulosa.common.concurrency.cancel +import nebulosa.common.concurrency.latch.Pauser import java.io.Closeable import java.util.concurrent.CompletableFuture import java.util.concurrent.Future import java.util.concurrent.TimeUnit +import java.util.function.Consumer -class CancellationToken private constructor(private val completable: CompletableFuture?) : Closeable, Future { +typealias CancellationListener = Consumer + +class CancellationToken private constructor(private val completable: CompletableFuture?) : Pauser(), Closeable, + Future { constructor() : this(CompletableFuture()) @@ -13,41 +18,51 @@ class CancellationToken private constructor(private val completable: Completable init { completable?.whenComplete { source, _ -> - if (source != null) { - listeners.forEach { it.accept(source) } - } + synchronized(this) { + unpause() + + if (source != null) { + listeners.forEach { it.accept(source) } + } - listeners.clear() + listeners.clear() + } } } - fun listen(listener: CancellationListener): Boolean { - return if (completable == null) { - false - } else if (isDone) { - listener.accept(CancellationSource.Listen) - false - } else { - listeners.add(listener) + @Synchronized + fun listen(listener: CancellationListener) { + if (completable != null) { + if (isDone) { + listener.accept(CancellationSource.Listen) + } else { + listeners.add(listener) + } } } - fun unlisten(listener: CancellationListener): Boolean { - return listeners.remove(listener) + @Synchronized + fun unlisten(listener: CancellationListener) { + listeners.remove(listener) } fun cancel() { cancel(true) } - @Synchronized override fun cancel(mayInterruptIfRunning: Boolean): Boolean { - completable?.complete(CancellationSource.Cancel(mayInterruptIfRunning)) ?: return false + return cancel(CancellationSource.Cancel(mayInterruptIfRunning)) + } + + @Synchronized + fun cancel(source: CancellationSource): Boolean { + unpause() + completable?.complete(source) ?: return false return true } override fun isCancelled(): Boolean { - return isDone + return completable != null && isDone } override fun isDone(): Boolean { @@ -63,6 +78,8 @@ class CancellationToken private constructor(private val completable: Completable } override fun close() { + super.close() + if (!isDone) { completable?.complete(CancellationSource.Close) } diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/CountUpDownLatch.kt similarity index 78% rename from nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt rename to nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/CountUpDownLatch.kt index 1c3ea045a..7499f9448 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/CountUpDownLatch.kt @@ -1,14 +1,18 @@ -package nebulosa.common.concurrency +package nebulosa.common.concurrency.latch +import nebulosa.common.concurrency.cancel.CancellationListener +import nebulosa.common.concurrency.cancel.CancellationSource import java.time.Duration import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.AbstractQueuedSynchronizer +import java.util.function.Supplier import kotlin.math.max -class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0), CancellationListener { +class CountUpDownLatch(initialCount: Int = 0) : Supplier, CancellationListener { - private val sync = Sync(this) + private val latch = AtomicBoolean(initialCount == 0) + private val sync = Sync(latch) init { require(initialCount >= 0) { "initialCount < 0: $initialCount" } @@ -18,11 +22,15 @@ class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0) val count get() = sync.count + override fun get(): Boolean { + return latch.get() + } + @Synchronized fun countUp(n: Int = 1): Int { if (n >= 1) { sync.count += n - set(false) + latch.set(false) } return count @@ -77,7 +85,7 @@ class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0) val next = max(0, this - releases) if (compareAndSetState(this, next)) { - latch.set(next <= state) + latch.set(next <= this) return latch.get() } } diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/Pauseable.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/Pauseable.kt new file mode 100644 index 000000000..d0f0b7089 --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/Pauseable.kt @@ -0,0 +1,10 @@ +package nebulosa.common.concurrency.latch + +interface Pauseable { + + val isPaused: Boolean + + fun pause() + + fun unpause() +} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/Pauser.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/Pauser.kt new file mode 100644 index 000000000..0e3f2a106 --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/latch/Pauser.kt @@ -0,0 +1,41 @@ +package nebulosa.common.concurrency.latch + +import java.io.Closeable +import java.time.Duration +import java.util.concurrent.TimeUnit + +open class Pauser : Pauseable, Closeable { + + private val latch = CountUpDownLatch() + + final override val isPaused + get() = !latch.get() + + final override fun pause() { + if (latch.get()) { + latch.countUp(1) + } + } + + final override fun unpause() { + if (!latch.get()) { + latch.reset() + } + } + + override fun close() { + unpause() + } + + fun waitIfPaused() { + latch.await() + } + + fun waitIfPaused(timeout: Long, unit: TimeUnit): Boolean { + return latch.await(timeout, unit) + } + + fun waitIfPaused(timeout: Duration): Boolean { + return latch.await(timeout) + } +} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/process/ProcessExecutor.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/process/ProcessExecutor.kt index ba2f86f5b..ec0e8d71c 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/process/ProcessExecutor.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/process/ProcessExecutor.kt @@ -1,5 +1,6 @@ package nebulosa.common.process +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.log.loggerFor import java.nio.file.Path import java.time.Duration @@ -11,6 +12,7 @@ open class ProcessExecutor(private val path: Path) { arguments: Map, timeout: Duration? = null, workingDir: Path? = null, + cancellationToken: CancellationToken = CancellationToken.NONE, ): Process { val args = ArrayList(arguments.size * 2) @@ -28,7 +30,8 @@ open class ProcessExecutor(private val path: Path) { LOG.info("executing process. pid={}, args={}", process.pid(), args) // TODO: READ OUTPUT STREAM LINE TO CALLBACK - // TODO: CANCELLATION + + cancellationToken.listen { process.destroyForcibly() } if (timeout == null || timeout.isNegative) process.waitFor() else process.waitFor(timeout.seconds, TimeUnit.SECONDS) diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/time/Stopwatch.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/time/Stopwatch.kt new file mode 100644 index 000000000..4c0ac9035 --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/time/Stopwatch.kt @@ -0,0 +1,106 @@ +package nebulosa.common.time + +import java.time.Duration + +/** + * A stopwatch which measures time while it's running. + * + * A stopwatch is either running or stopped. + * It measures the elapsed time that passes while the stopwatch is running. + * + * When a stopwatch is initially created, it is stopped and has measured no elapsed time. + * + * The elapsed time can be accessed in various formats using [elapsed], + * [elapsedMilliseconds], [elapsedMicroseconds] or [elapsedTicks]. + * + * The stopwatch is started by calling [start]. + */ +class Stopwatch { + + @Volatile private var start = 0L + @Volatile private var stop = 0L + + /** + * The elapsed number of clock ticks since calling [start] while the [Stopwatch] is running. + */ + val elapsedTicks + get() = if (isStopped) stop - start else System.nanoTime() - start + + /** + * The [elapsedTicks] counter converted to [Duration]. + */ + inline val elapsed: Duration + get() = Duration.ofNanos(elapsedTicks) + + /** + * The [elapsedTicks] counter converted to microseconds. + */ + inline val elapsedMicroseconds + get() = elapsedTicks / 1000L + + /** + * The [elapsedTicks] counter converted to milliseconds. + */ + inline val elapsedMilliseconds + get() = elapsedMicroseconds / 1000L + + /** + * The [elapsedTicks] counter converted to seconds. + */ + inline val elapsedSeconds + get() = elapsedMilliseconds / 1000L + + /** + * Determines whether the [Stopwatch] is currently stopped. + */ + val isStopped + get() = stop >= 0L + + /** + * Determines whether the [Stopwatch] is currently running. + */ + val isRunning + get() = stop == -1L + + /** + * Starts the [Stopwatch]. + * + * The [elapsed] count increases monotonically. If the [Stopwatch] has + * been stopped, then calling start again restarts it without resetting the + * [elapsed] count. + * + * If the [Stopwatch] is currently running, then calling start does nothing. + */ + @Synchronized + fun start() { + // Don't count the time while the stopwatch has been stopped. + if (isStopped) { + start += System.nanoTime() - stop + stop = -1L + } + } + + /** + * Stops the [Stopwatch]. + * + * The [elapsedTicks] count stops increasing after this call. If the + * [Stopwatch] is currently not running, then calling this method has no + * effect. + */ + @Synchronized + fun stop() { + if (isRunning) { + stop = System.nanoTime() + } + } + + /** + * Resets the [elapsed] count to zero. + * + * This method does not stop or start the [Stopwatch]. + */ + @Synchronized + fun reset() { + start = if (isStopped) stop else System.nanoTime() + } +} diff --git a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt index 428f5d8de..16f6425a0 100644 --- a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt +++ b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt @@ -2,8 +2,8 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe -import nebulosa.common.concurrency.CancellationSource -import nebulosa.common.concurrency.CancellationToken +import nebulosa.common.concurrency.cancel.CancellationSource +import nebulosa.common.concurrency.cancel.CancellationToken class CancellationTokenTest : StringSpec() { diff --git a/nebulosa-common/src/test/kotlin/CountUpDownLatchTest.kt b/nebulosa-common/src/test/kotlin/CountUpDownLatchTest.kt index caa2580d6..5c96bf082 100644 --- a/nebulosa-common/src/test/kotlin/CountUpDownLatchTest.kt +++ b/nebulosa-common/src/test/kotlin/CountUpDownLatchTest.kt @@ -6,7 +6,7 @@ import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.common.concurrency.latch.CountUpDownLatch import java.util.concurrent.TimeUnit import kotlin.system.measureTimeMillis diff --git a/nebulosa-common/src/test/kotlin/PauserTest.kt b/nebulosa-common/src/test/kotlin/PauserTest.kt new file mode 100644 index 000000000..102d1ef07 --- /dev/null +++ b/nebulosa-common/src/test/kotlin/PauserTest.kt @@ -0,0 +1,30 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import nebulosa.common.concurrency.latch.Pauser +import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread + +class PauserTest : StringSpec() { + + init { + "pause and wait for unpause" { + val pauser = Pauser() + pauser.isPaused.shouldBeFalse() + pauser.pause() + pauser.isPaused.shouldBeTrue() + thread { Thread.sleep(1000); pauser.unpause() } + pauser.waitIfPaused() + pauser.isPaused.shouldBeFalse() + } + "pause and not wait for unpause" { + val pauser = Pauser() + pauser.isPaused.shouldBeFalse() + pauser.pause() + pauser.isPaused.shouldBeTrue() + thread { Thread.sleep(1000); pauser.unpause() } + pauser.waitIfPaused(500, TimeUnit.MILLISECONDS).shouldBeFalse() + pauser.isPaused.shouldBeTrue() + } + } +} diff --git a/nebulosa-constants/src/main/kotlin/nebulosa/constants/Distance.kt b/nebulosa-constants/src/main/kotlin/nebulosa/constants/Distance.kt index a1c50471b..5470ef994 100644 --- a/nebulosa-constants/src/main/kotlin/nebulosa/constants/Distance.kt +++ b/nebulosa-constants/src/main/kotlin/nebulosa/constants/Distance.kt @@ -10,7 +10,7 @@ const val AU_M = 149597870700.0 /** * Astronomical unit (km, IAU 2012). */ -const val AU_KM = 149597870.700 +const val AU_KM = AU_M / 1000.0 /** * Speed of light (m/s). @@ -18,9 +18,9 @@ const val AU_KM = 149597870.700 const val SPEED_OF_LIGHT = 299792458.0 /** - * Light time (au per s). + * Light time for 1 AU in s. */ -const val LIGHT_TIME_AU_S = AU_M / SPEED_OF_LIGHT +const val LIGHT_TIME_AU = AU_M / SPEED_OF_LIGHT /** * Schwarzschild radius of the Sun (au). @@ -28,6 +28,6 @@ const val LIGHT_TIME_AU_S = AU_M / SPEED_OF_LIGHT const val SCHWARZSCHILD_RADIUS_OF_THE_SUN = 1.97412574336e-8 /** - * Speed of light (au per s). + * Speed of light (au per day). */ const val SPEED_OF_LIGHT_AU_DAY = SPEED_OF_LIGHT * DAYSEC / AU_M diff --git a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/AstrometryParameters.kt b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/AstrometryParameters.kt index 3d4b8934e..5f0550fc6 100644 --- a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/AstrometryParameters.kt +++ b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/AstrometryParameters.kt @@ -3,13 +3,14 @@ package nebulosa.erfa import nebulosa.math.Angle import nebulosa.math.Distance import nebulosa.math.Matrix3D +import nebulosa.math.Vector3D data class AstrometryParameters( @JvmField val pmt: Double = 0.0, // PM time interval (SSB, Julian years). - @JvmField val eb: CartesianCoordinate = CartesianCoordinate.ZERO, // SSB to observer (vector, au). - @JvmField val ehx: Double = 0.0, val ehy: Double = 0.0, val ehz: Double = 0.0, // Sun to observer (unit vector). + @JvmField val eb: Vector3D = Vector3D.EMPTY, // SSB to observer (vector, au). + @JvmField val eh: Vector3D = Vector3D.EMPTY, // Sun to observer (unit vector). @JvmField val em: Distance = 0.0, // Distance from Sun to observer. - @JvmField val vx: Double = 0.0, val vy: Double = 0.0, val vz: Double = 0.0, // Barycentric observer velocity (c) + @JvmField val v: Vector3D = Vector3D.EMPTY, // Barycentric observer velocity (c) @JvmField val bm1: Double = 0.0, // sqrt(1-|v|^2): reciprocal of Lorenz factor. @JvmField val bpn: Matrix3D = Matrix3D.IDENTITY, // Bias-precession-nutation matrix. @JvmField val along: Angle = 0.0, // Longitude + s' + dERA(DUT). diff --git a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/CartesianCoordinate.kt b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/CartesianCoordinate.kt index 68041f2b2..b255b309c 100644 --- a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/CartesianCoordinate.kt +++ b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/CartesianCoordinate.kt @@ -1,9 +1,6 @@ package nebulosa.erfa import nebulosa.math.* -import kotlin.math.abs -import kotlin.math.acos -import kotlin.math.hypot class CartesianCoordinate : Vector3D { @@ -13,37 +10,6 @@ class CartesianCoordinate : Vector3D { val spherical by lazy { SphericalCoordinate.of(x, y, z) } - fun angularDistance(coordinate: CartesianCoordinate): Angle { - val dot = x * coordinate.x + y * coordinate.y + z * coordinate.z - val norm0 = hypot(x, y) - val norm1 = hypot(coordinate.x, coordinate.y) - val v = dot / (norm0 * norm1) - return if (abs(v) > 1.0) if (v < 0.0) SEMICIRCLE else 0.0 - else acos(v).rad - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is CartesianCoordinate) return false - - if (x != other.x) return false - if (y != other.y) return false - if (z != other.z) return false - - return true - } - - override fun hashCode(): Int { - var result = x.hashCode() - result = 31 * result + y.hashCode() - result = 31 * result + z.hashCode() - return result - } - - override fun toString(): String { - return "CartesianCoordinate(x=$x, y=$y, z=$z)" - } - companion object { @JvmStatic val ZERO = CartesianCoordinate() @@ -54,11 +20,7 @@ class CartesianCoordinate : Vector3D { * to [CartesianCoordinate]. */ @JvmStatic - fun of( - theta: Angle, - phi: Angle, - r: Distance, - ): CartesianCoordinate { + fun of(theta: Angle, phi: Angle, r: Distance): CartesianCoordinate { val cp = phi.cos val x = r * (theta.cos * cp) val y = r * (theta.sin * cp) diff --git a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt index c5f76b18d..4c5692312 100644 --- a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt +++ b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/Erfa.kt @@ -11,6 +11,10 @@ import okio.BufferedSource import kotlin.math.* import kotlin.math.PI +inline fun eraPdp(a: DoubleArray, b: DoubleArray): Double { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + /** * P-vector to spherical polar coordinates. * @@ -34,6 +38,16 @@ fun eraC2s(x: Distance, y: Distance, z: Distance): DoubleArray { return doubleArrayOf(theta.rad, phi.rad) } +/** + * Convert spherical coordinates to Cartesian. + * + * @return direction cosines. + */ +fun eraS2c(theta: Angle, phi: Angle): Vector3D { + val cp = cos(phi) + return Vector3D(cos(theta) * cp, sin(theta) * cp, sin(phi)) +} + /** * Apply aberration to transform natural direction into proper direction. * @@ -49,12 +63,20 @@ fun eraAb(pnat: Vector3D, v: Vector3D, s: Distance, bm1: Double): Vector3D { val w1 = 1.0 + pdv / (1.0 + bm1) val w2 = SCHWARZSCHILD_RADIUS_OF_THE_SUN / s val p = DoubleArray(3) + var r2 = 0.0 - for (i in 0..2) { - p[i] = pnat[i] * bm1 + w1 * v[i] + w2 * (v[i] - pdv * pnat[i]) + repeat(3) { + p[it] = pnat[it] * bm1 + w1 * v[it] + w2 * (v[it] - pdv * pnat[it]) + r2 += p[it] * p[it] } - return Vector3D(p).normalized + val r = sqrt(r2) + + repeat(3) { + p[it] /= r + } + + return Vector3D(p) } /** @@ -244,8 +266,8 @@ fun eraAnpm(angle: Angle): Angle { * i.e. around 1/298. */ fun eraGc2Gde( - radius: Distance, flattening: Double, - x: Distance, y: Distance, z: Distance, + radius: Double, flattening: Double, + x: Double, y: Double, z: Double, ): SphericalCoordinate { val aeps2 = radius * radius * 1e-32 val e2 = (2.0 - flattening) * flattening @@ -302,7 +324,7 @@ fun eraGc2Gde( height = absz - b } - return SphericalCoordinate(elong.rad, phi.rad, height.au) + return SphericalCoordinate(elong.rad, phi.rad, height) } /** @@ -313,9 +335,9 @@ fun eraGc2Gde( * i.e. around 1/298. */ fun eraGd2Gce( - radius: Distance, flattening: Double, - elong: Angle, phi: Angle, height: Distance, -): CartesianCoordinate { + radius: Double, flattening: Double, + elong: Angle, phi: Angle, height: Double, +): Vector3D { val sp = phi.sin val cp = phi.cos val w = (1.0 - flattening).let { it * it } @@ -331,7 +353,7 @@ fun eraGd2Gce( val y = r * elong.sin val z = (aS + height) * sp - return CartesianCoordinate(x.au, y.au, z.au) + return Vector3D(x, y, z) } /** @@ -384,17 +406,17 @@ inline fun eraPom00(xp: Angle, yp: Angle, sp: Angle): Matrix3D { * @return Position/velocity vector (m, m/s, CIRS) */ fun eraPvtob( - elong: Angle, phi: Angle, hm: Distance, + elong: Angle, phi: Angle, hm: Double, xp: Angle, yp: Angle, sp: Angle, theta: Angle, ): PositionAndVelocity { // Geodetic to geocentric transformation (WGS84). - val xyzm = eraGd2Gce(6378137.0.m, 1.0 / 298.257223563, elong, phi, hm) + val xyzm = eraGd2Gce(6378137.0, 1.0 / 298.257223563, elong, phi, hm) // Polar motion and TIO position. val rpm = eraPom00(xp, yp, sp) - val (x, y, z) = rpm.transposed * Vector3D(xyzm.x.toMeters, xyzm.y.toMeters, xyzm.z.toMeters) + val (x, y, z) = rpm.transposed * xyzm val s = theta.sin val c = theta.cos @@ -408,9 +430,6 @@ fun eraPvtob( return PositionAndVelocity(Vector3D(px, py, z), Vector3D(vx, vy, 0.0)) } -private const val AUDMS = AU_M / DAYSEC -private const val CR = LIGHT_TIME_AU_S / DAYSEC - /** * For an observer whose geocentric position and velocity are known, * prepare star-independent astrometry parameters for transformations @@ -419,79 +438,50 @@ private const val CR = LIGHT_TIME_AU_S / DAYSEC * * @param tdb1 TDB date * @param tdb2 TDB fraction date - * @param px Observer's geocentric position (m) - * @param py Observer's geocentric position (m) - * @param pz Observer's geocentric position (m) - * @param vx Observer's geocentric velocity (m/s) - * @param vy Observer's geocentric velocity (m/s) - * @param vz Observer's geocentric velocity (m/s) - * @param ebpx Earth barycentric position (au) - * @param ebpy Earth barycentric position (au) - * @param ebpz Earth barycentric position (au) - * @param ebvx Earth barycentric velocity (au/day) - * @param ebvy Earth barycentric velocity (au/day) - * @param ebvz Earth barycentric velocity (au/day) - * @param ehpx Earth heliocentric position (au) - * @param ehpy Earth heliocentric position (au) - * @param ehpz Earth heliocentric position (au) - */ -@Suppress("UnnecessaryVariable") + * @param p Observer's geocentric position (m) + * @param v Observer's geocentric velocity (m/s) + * @param ebp Earth barycentric position (au) + * @param ebv Earth barycentric velocity (au/day) + * @param ehp Earth heliocentric position (au) + */ fun eraApcs( tdb1: Double, tdb2: Double, - px: Distance, py: Distance, pz: Distance, - vx: Double, vy: Double, vz: Double, - ebpx: Distance, ebpy: Distance, ebpz: Distance, - ebvx: Velocity, ebvy: Velocity, ebvz: Velocity, - ehpx: Distance, ehpy: Distance, ehpz: Distance, + p: Vector3D, v: Vector3D, + ebp: Vector3D, ebv: Vector3D, ehp: Vector3D, ): AstrometryParameters { // Time since reference epoch, years (for proper motion calculation). val pmt = (tdb1 - J2000 + tdb2) / DAYSPERJY // Adjust Earth ephemeris to observer. - val dpx = px - val dvx = vx / AUDMS - val phx = ehpx + dpx - val vbx = ebvx + dvx - - val dpy = py - val dvy = vy / AUDMS - val vby = ebvy + dvy - val phy = ehpy + dpy - - val dpz = pz - val dvz = vz / AUDMS - val vbz = ebvz + dvz - val phz = ehpz + dpz + val ph = ehp + p / AU_M + val vb = ebv + v / (AU_M / DAYSEC) // Barycentric position of observer (au). - val pbx = ebpx + dpx - val pby = ebpy + dpy - val pbz = ebpz + dpz + val pb = ebp + p / AU_M // Heliocentric direction and distance (unit vector and au). - val ph = Vector3D(phx, phy, phz) val em = ph.length - val (ehx, ehy, ehz) = ph.normalized + val eh = ph.normalized // Barycentric vel. in units of c, and reciprocal of Lorenz factor. var v2 = 0.0 - val wx = vbx * CR + val wx = vb[0] * (LIGHT_TIME_AU / DAYSEC) v2 += wx * wx - val wy = vby * CR + val wy = vb[1] * (LIGHT_TIME_AU / DAYSEC) v2 += wy * wy - val wz = vbz * CR + val wz = vb[2] * (LIGHT_TIME_AU / DAYSEC) v2 += wz * wz val bm1 = sqrt(1.0 - v2) return AstrometryParameters( pmt = pmt, - eb = CartesianCoordinate(pbx, pby, pbz), + eb = pb, em = em.au, - ehx = ehx, ehy = ehy, ehz = ehz, - vx = wx, vy = wy, vz = wz, + eh = eh, + v = Vector3D(wx, wy, wz), bm1 = bm1, ) } @@ -504,16 +494,10 @@ fun eraApcs( * site coordinates. * * @param tdb1 TDB as a 2-part... - * @param tdb2 ...Julian Date (Note 1) - * @param ebpx Earth barycentric position (au) - * @param ebpy Earth barycentric position (au) - * @param ebpz Earth barycentric position (au) - * @param ebvx Earth barycentric velocity (au/day) - * @param ebvy Earth barycentric velocity (au/day) - * @param ebvz Earth barycentric velocity (au/day) - * @param ehpx Earth heliocentric position (au) - * @param ehpy Earth heliocentric position (au) - * @param ehpz Earth heliocentric position (au) + * @param tdb2 ...Julian Date + * @param ebp Earth barycentric position (au) + * @param ebv Earth barycentric velocity (au/day) + * @param ehp Earth heliocentric position (au) * @param x CIP X (components of unit vector) * @param y CIP Y (components of unit vector) * @param s The CIO locator s (radians) @@ -529,13 +513,10 @@ fun eraApcs( */ fun eraApco( tdb1: Double, tdb2: Double, - ebpx: Distance, ebpy: Distance, ebpz: Distance, - ebvx: Velocity, ebvy: Velocity, ebvz: Velocity, - ehpx: Distance, ehpy: Distance, ehpz: Distance, - x: Double, y: Double, - s: Angle, + ebp: Vector3D, ebv: Vector3D, ehp: Vector3D, + x: Double, y: Double, s: Angle, theta: Angle, elong: Angle, phi: Angle, - hm: Distance, + hm: Double, xp: Angle, yp: Angle, sp: Angle, refa: Angle, refb: Angle, @@ -544,16 +525,11 @@ fun eraApco( var r = Matrix3D.rotZ(theta + sp).rotateY(-xp).rotateX(-yp).rotateZ(elong) // Solve for local Earth rotation angle. - val a = r[0, 0] - val b = r[0, 1] - val eral = if (a != 0.0 || b != 0.0) atan2(b, a).rad else 0.0 + val eral = atan2(r[0, 1], r[0, 0]) // Solve for polar motion [X,Y] with respect to local meridian. - val c = r[0, 2] - val xpl = atan2(c, hypot(a, b)).rad - val d = r[1, 2] - val e = r[2, 2] - val ypl = if (d != 0.0 || e != 0.0) (-atan2(d, e)).rad else 0.0 + val xpl = atan2(r[0, 2], hypot(r[0, 0], r[0, 1])).rad + val ypl = -atan2(r[1, 2], r[2, 2]) // Adjusted longitude. val along = eraAnpm(eral - theta) @@ -576,11 +552,8 @@ fun eraApco( // ICRS <-> GCRS parameters. return eraApcs( tdb1, tdb2, - p.x.m, p.y.m, p.z.m, - v.x, v.y, v.z, - ebpx, ebpy, ebpz, - ebvx, ebvy, ebvz, - ehpx, ehpy, ehpz, + p, v, + ebp, ebv, ehp, ).copy( eral = eral, xpl = xpl, ypl = ypl, @@ -629,7 +602,7 @@ fun eraPfw06(tt1: Double, tt2: Double): FukushimaWilliamsFourAngles { /** * Fundamental argument, IERS Conventions (2003): mean anomaly of the Moon. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFal03(t: Double): Angle { return (485868.249036 + t * (1717915923.2178 + t * (31.8792 + t * (0.051635 + t * (-0.00024470))))).mod(TURNAS).arcsec @@ -638,7 +611,7 @@ fun eraFal03(t: Double): Angle { /** * Fundamental argument, IERS Conventions (2003): mean anomaly of the Sun. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFalp03(t: Double): Angle { return (1287104.793048 + t * (129596581.0481 + t * (-0.5532 + t * (0.000136 + t * (-0.00001149))))).mod(TURNAS).arcsec @@ -647,7 +620,7 @@ fun eraFalp03(t: Double): Angle { /** * Fundamental argument, IERS Conventions (2003): mean anomaly of the Sun. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFad03(t: Double): Angle { return (1072260.703692 + t * (1602961601.2090 + t * (-6.3706 + t * (0.006593 + t * (-0.00003169))))).mod(TURNAS).arcsec @@ -657,7 +630,7 @@ fun eraFad03(t: Double): Angle { * Fundamental argument, IERS Conventions (2003): mean longitude of the Moon * minus mean longitude of the ascending node. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFaf03(t: Double): Angle { return (335779.526232 + t * (1739527262.8478 + t * (-12.7512 + t * (-0.001037 + t * (0.00000417))))).mod(TURNAS).arcsec @@ -666,7 +639,7 @@ fun eraFaf03(t: Double): Angle { /** * Fundamental argument, IERS Conventions (2003): mean longitude of the Moon's ascending node. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFaom03(t: Double): Angle { return (450160.398036 + t * (-6962890.5431 + t * (7.4722 + t * (0.007702 + t * (-0.00005939))))).mod(TURNAS).arcsec @@ -675,56 +648,56 @@ fun eraFaom03(t: Double): Angle { /** * Fundamental argument, IERS Conventions (2003): general accumulated precession in longitude. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFapa03(t: Double) = ((0.024381750 + 0.00000538691 * t) * t).rad /** * Fundamental argument, IERS Conventions (2003): mean longitude of Mercury. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFame03(t: Double) = (4.402608842 + 2608.7903141574 * t).mod(TAU).rad /** * Fundamental argument, IERS Conventions (2003): mean longitude of Venus. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFave03(t: Double) = (3.176146697 + 1021.3285546211 * t).mod(TAU).rad /** * Fundamental argument, IERS Conventions (2003): mean longitude of Earth. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFae03(t: Double) = (1.753470314 + 628.3075849991 * t).mod(TAU).rad /** * Fundamental argument, IERS Conventions (2003): mean longitude of Mars. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFama03(t: Double) = (6.203480913 + 334.0612426700 * t).mod(TAU).rad /** * Fundamental argument, IERS Conventions (2003): mean longitude of Jupiter. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFaju03(t: Double) = (0.599546497 + 52.9690962641 * t).mod(TAU).rad /** * Fundamental argument, IERS Conventions (2003): mean longitude of Saturn. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFasa03(t: Double) = (0.874016757 + 21.3299104960 * t).mod(TAU).rad /** * Fundamental argument, IERS Conventions (2003): mean longitude of Uranus. * - * @param t TDB, Julian centuries since J2000.0 (Note 1) + * @param t TDB, Julian centuries since J2000.0 */ fun eraFaur03(t: Double) = (5.481293872 + 7.4781598567 * t).mod(TAU).rad @@ -846,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) @@ -905,112 +878,115 @@ fun eraPnm06a(tt1: Double, tt2: Double): Matrix3D { } @Suppress("ArrayInDataClass") -private data class Term( +internal data class Term( @JvmField val nfa: IntArray, @JvmField val s: Double, @JvmField val c: Double, ) -// Polynomial coefficients -private val SP = doubleArrayOf(94.00e-6, 3808.65e-6, -122.68e-6, -72574.11e-6, 27.98e-6, 15.62e-6) - -// Terms of order t^0 -private val S0 = arrayOf( - // 1-10 - Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), -2640.73e-6, 0.39e-6), - Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -63.53e-6, 0.02e-6), - Term(intArrayOf(0, 0, 2, -2, 3, 0, 0, 0), -11.75e-6, -0.01e-6), - Term(intArrayOf(0, 0, 2, -2, 1, 0, 0, 0), -11.21e-6, -0.01e-6), - Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), 4.57e-6, 0.00e-6), - Term(intArrayOf(0, 0, 2, 0, 3, 0, 0, 0), -2.02e-6, 0.00e-6), - Term(intArrayOf(0, 0, 2, 0, 1, 0, 0, 0), -1.98e-6, 0.00e-6), - Term(intArrayOf(0, 0, 0, 0, 3, 0, 0, 0), 1.72e-6, 0.00e-6), - Term(intArrayOf(0, 1, 0, 0, 1, 0, 0, 0), 1.41e-6, 0.01e-6), - Term(intArrayOf(0, 1, 0, 0, -1, 0, 0, 0), 1.26e-6, 0.01e-6), - // 11-20 - Term(intArrayOf(1, 0, 0, 0, -1, 0, 0, 0), 0.63e-6, 0.00e-6), - Term(intArrayOf(1, 0, 0, 0, 1, 0, 0, 0), 0.63e-6, 0.00e-6), - Term(intArrayOf(0, 1, 2, -2, 3, 0, 0, 0), -0.46e-6, 0.00e-6), - Term(intArrayOf(0, 1, 2, -2, 1, 0, 0, 0), -0.45e-6, 0.00e-6), - Term(intArrayOf(0, 0, 4, -4, 4, 0, 0, 0), -0.36e-6, 0.00e-6), - Term(intArrayOf(0, 0, 1, -1, 1, -8, 12, 0), 0.24e-6, 0.12e-6), - Term(intArrayOf(0, 0, 2, 0, 0, 0, 0, 0), -0.32e-6, 0.00e-6), - Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), -0.28e-6, 0.00e-6), - Term(intArrayOf(1, 0, 2, 0, 3, 0, 0, 0), -0.27e-6, 0.00e-6), - Term(intArrayOf(1, 0, 2, 0, 1, 0, 0, 0), -0.26e-6, 0.00e-6), - // 21-30 - Term(intArrayOf(0, 0, 2, -2, 0, 0, 0, 0), 0.21e-6, 0.00e-6), - Term(intArrayOf(0, 1, -2, 2, -3, 0, 0, 0), -0.19e-6, 0.00e-6), - Term(intArrayOf(0, 1, -2, 2, -1, 0, 0, 0), -0.18e-6, 0.00e-6), - Term(intArrayOf(0, 0, 0, 0, 0, 8, -13, -1), 0.10e-6, -0.05e-6), - Term(intArrayOf(0, 0, 0, 2, 0, 0, 0, 0), -0.15e-6, 0.00e-6), - Term(intArrayOf(2, 0, -2, 0, -1, 0, 0, 0), 0.14e-6, 0.00e-6), - Term(intArrayOf(0, 1, 2, -2, 2, 0, 0, 0), 0.14e-6, 0.00e-6), - Term(intArrayOf(1, 0, 0, -2, 1, 0, 0, 0), -0.14e-6, 0.00e-6), - Term(intArrayOf(1, 0, 0, -2, -1, 0, 0, 0), -0.14e-6, 0.00e-6), - Term(intArrayOf(0, 0, 4, -2, 4, 0, 0, 0), -0.13e-6, 0.00e-6), - // 31-33 - Term(intArrayOf(0, 0, 2, -2, 4, 0, 0, 0), 0.11e-6, 0.00e-6), - Term(intArrayOf(1, 0, -2, 0, -3, 0, 0, 0), -0.11e-6, 0.00e-6), - Term(intArrayOf(1, 0, -2, 0, -1, 0, 0, 0), -0.11e-6, 0.00e-6), -) +internal object IAU2006 { + + // Polynomial coefficients + @JvmField internal val SP = doubleArrayOf(94.00e-6, 3808.65e-6, -122.68e-6, -72574.11e-6, 27.98e-6, 15.62e-6) + + // Terms of order t^0 + @JvmField internal val S0 = arrayOf( + // 1-10 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), -2640.73e-6, 0.39e-6), + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -63.53e-6, 0.02e-6), + Term(intArrayOf(0, 0, 2, -2, 3, 0, 0, 0), -11.75e-6, -0.01e-6), + Term(intArrayOf(0, 0, 2, -2, 1, 0, 0, 0), -11.21e-6, -0.01e-6), + Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), 4.57e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 3, 0, 0, 0), -2.02e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 1, 0, 0, 0), -1.98e-6, 0.00e-6), + Term(intArrayOf(0, 0, 0, 0, 3, 0, 0, 0), 1.72e-6, 0.00e-6), + Term(intArrayOf(0, 1, 0, 0, 1, 0, 0, 0), 1.41e-6, 0.01e-6), + Term(intArrayOf(0, 1, 0, 0, -1, 0, 0, 0), 1.26e-6, 0.01e-6), + // 11-20 + Term(intArrayOf(1, 0, 0, 0, -1, 0, 0, 0), 0.63e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, 0, 1, 0, 0, 0), 0.63e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 3, 0, 0, 0), -0.46e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 1, 0, 0, 0), -0.45e-6, 0.00e-6), + Term(intArrayOf(0, 0, 4, -4, 4, 0, 0, 0), -0.36e-6, 0.00e-6), + Term(intArrayOf(0, 0, 1, -1, 1, -8, 12, 0), 0.24e-6, 0.12e-6), + Term(intArrayOf(0, 0, 2, 0, 0, 0, 0, 0), -0.32e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), -0.28e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 3, 0, 0, 0), -0.27e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 1, 0, 0, 0), -0.26e-6, 0.00e-6), + // 21-30 + Term(intArrayOf(0, 0, 2, -2, 0, 0, 0, 0), 0.21e-6, 0.00e-6), + Term(intArrayOf(0, 1, -2, 2, -3, 0, 0, 0), -0.19e-6, 0.00e-6), + Term(intArrayOf(0, 1, -2, 2, -1, 0, 0, 0), -0.18e-6, 0.00e-6), + Term(intArrayOf(0, 0, 0, 0, 0, 8, -13, -1), 0.10e-6, -0.05e-6), + Term(intArrayOf(0, 0, 0, 2, 0, 0, 0, 0), -0.15e-6, 0.00e-6), + Term(intArrayOf(2, 0, -2, 0, -1, 0, 0, 0), 0.14e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 2, 0, 0, 0), 0.14e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, -2, 1, 0, 0, 0), -0.14e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, -2, -1, 0, 0, 0), -0.14e-6, 0.00e-6), + Term(intArrayOf(0, 0, 4, -2, 4, 0, 0, 0), -0.13e-6, 0.00e-6), + // 31-33 + Term(intArrayOf(0, 0, 2, -2, 4, 0, 0, 0), 0.11e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, 0, -3, 0, 0, 0), -0.11e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, 0, -1, 0, 0, 0), -0.11e-6, 0.00e-6), + ) -// Terms of order t^1 -private val S1 = arrayOf( - // 1 - 3 - Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -0.07e-6, 3.57e-6), - Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 1.73e-6, -0.03e-6), - Term(intArrayOf(0, 0, 2, -2, 3, 0, 0, 0), 0.00e-6, 0.48e-6), -) + // Terms of order t^1 + @JvmField internal val S1 = arrayOf( + // 1 - 3 + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -0.07e-6, 3.57e-6), + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 1.73e-6, -0.03e-6), + Term(intArrayOf(0, 0, 2, -2, 3, 0, 0, 0), 0.00e-6, 0.48e-6), + ) -// Terms of order t^2 -private val S2 = arrayOf( - // 1-10 - Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 743.52e-6, -0.17e-6), - Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), 56.91e-6, 0.06e-6), - Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), 9.84e-6, -0.01e-6), - Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -8.85e-6, 0.01e-6), - Term(intArrayOf(0, 1, 0, 0, 0, 0, 0, 0), -6.38e-6, -0.05e-6), - Term(intArrayOf(1, 0, 0, 0, 0, 0, 0, 0), -3.07e-6, 0.00e-6), - Term(intArrayOf(0, 1, 2, -2, 2, 0, 0, 0), 2.23e-6, 0.00e-6), - Term(intArrayOf(0, 0, 2, 0, 1, 0, 0, 0), 1.67e-6, 0.00e-6), - Term(intArrayOf(1, 0, 2, 0, 2, 0, 0, 0), 1.30e-6, 0.00e-6), - Term(intArrayOf(0, 1, -2, 2, -2, 0, 0, 0), 0.93e-6, 0.00e-6), - // 11-20 - Term(intArrayOf(1, 0, 0, -2, 0, 0, 0, 0), 0.68e-6, 0.00e-6), - Term(intArrayOf(0, 0, 2, -2, 1, 0, 0, 0), -0.55e-6, 0.00e-6), - Term(intArrayOf(1, 0, -2, 0, -2, 0, 0, 0), 0.53e-6, 0.00e-6), - Term(intArrayOf(0, 0, 0, 2, 0, 0, 0, 0), -0.27e-6, 0.00e-6), - Term(intArrayOf(1, 0, 0, 0, 1, 0, 0, 0), -0.27e-6, 0.00e-6), - Term(intArrayOf(1, 0, -2, -2, -2, 0, 0, 0), -0.26e-6, 0.00e-6), - Term(intArrayOf(1, 0, 0, 0, -1, 0, 0, 0), -0.25e-6, 0.00e-6), - Term(intArrayOf(1, 0, 2, 0, 1, 0, 0, 0), 0.22e-6, 0.00e-6), - Term(intArrayOf(2, 0, 0, -2, 0, 0, 0, 0), -0.21e-6, 0.00e-6), - Term(intArrayOf(2, 0, -2, 0, -1, 0, 0, 0), 0.20e-6, 0.00e-6), - // 21-25 - Term(intArrayOf(0, 0, 2, 2, 2, 0, 0, 0), 0.17e-6, 0.00e-6), - Term(intArrayOf(2, 0, 2, 0, 2, 0, 0, 0), 0.13e-6, 0.00e-6), - Term(intArrayOf(2, 0, 0, 0, 0, 0, 0, 0), -0.13e-6, 0.00e-6), - Term(intArrayOf(1, 0, 2, -2, 2, 0, 0, 0), -0.12e-6, 0.00e-6), - Term(intArrayOf(0, 0, 2, 0, 0, 0, 0, 0), -0.11e-6, 0.00e-6), -) + // Terms of order t^2 + @JvmField internal val S2 = arrayOf( + // 1-10 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 743.52e-6, -0.17e-6), + Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), 56.91e-6, 0.06e-6), + Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), 9.84e-6, -0.01e-6), + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -8.85e-6, 0.01e-6), + Term(intArrayOf(0, 1, 0, 0, 0, 0, 0, 0), -6.38e-6, -0.05e-6), + Term(intArrayOf(1, 0, 0, 0, 0, 0, 0, 0), -3.07e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 2, 0, 0, 0), 2.23e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 1, 0, 0, 0), 1.67e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 2, 0, 0, 0), 1.30e-6, 0.00e-6), + Term(intArrayOf(0, 1, -2, 2, -2, 0, 0, 0), 0.93e-6, 0.00e-6), + // 11-20 + Term(intArrayOf(1, 0, 0, -2, 0, 0, 0, 0), 0.68e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, -2, 1, 0, 0, 0), -0.55e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, 0, -2, 0, 0, 0), 0.53e-6, 0.00e-6), + Term(intArrayOf(0, 0, 0, 2, 0, 0, 0, 0), -0.27e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, 0, 1, 0, 0, 0), -0.27e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, -2, -2, 0, 0, 0), -0.26e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, 0, -1, 0, 0, 0), -0.25e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 1, 0, 0, 0), 0.22e-6, 0.00e-6), + Term(intArrayOf(2, 0, 0, -2, 0, 0, 0, 0), -0.21e-6, 0.00e-6), + Term(intArrayOf(2, 0, -2, 0, -1, 0, 0, 0), 0.20e-6, 0.00e-6), + // 21-25 + Term(intArrayOf(0, 0, 2, 2, 2, 0, 0, 0), 0.17e-6, 0.00e-6), + Term(intArrayOf(2, 0, 2, 0, 2, 0, 0, 0), 0.13e-6, 0.00e-6), + Term(intArrayOf(2, 0, 0, 0, 0, 0, 0, 0), -0.13e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, -2, 2, 0, 0, 0), -0.12e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 0, 0, 0, 0), -0.11e-6, 0.00e-6), + ) -// Terms of order t^3 -private val S3 = arrayOf( - // 1-4 - Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 0.30e-6, -23.42e-6), - Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), -0.03e-6, -1.46e-6), - Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), -0.01e-6, -0.25e-6), - Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), 0.00e-6, 0.23e-6), -) + // Terms of order t^3 + @JvmField internal val S3 = arrayOf( + // 1-4 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 0.30e-6, -23.42e-6), + Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), -0.03e-6, -1.46e-6), + Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), -0.01e-6, -0.25e-6), + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), 0.00e-6, 0.23e-6), + ) -// Terms of order t^4 -private val S4 = arrayOf( - // 1-1 - Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), -0.26e-6, -0.01e-6), -) + // Terms of order t^4 + @JvmField internal val S4 = arrayOf( + // 1-1 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), -0.26e-6, -0.01e-6), + ) -private val S = arrayOf(S0, S1, S2, S3, S4) + @JvmField internal val S = arrayOf(S0, S1, S2, S3, S4) +} /** * The CIO locator s, positioning the Celestial Intermediate Origin on @@ -1042,17 +1018,17 @@ fun eraS06(tt1: Double, tt2: Double, x: Double, y: Double): Angle { fa[7] = eraFapa03(t) // Evalutate s. - val w = DoubleArray(6) { SP[it] } + val w = DoubleArray(6) { IAU2006.SP[it] } - for (k in S.indices) { - for (i in S[k].indices.reversed()) { + for (k in IAU2006.S.indices) { + for (i in IAU2006.S[k].indices.reversed()) { var a = 0.0 - for (j in 0..7) { - a += S[k][i].nfa[j] * fa[j] + repeat(8) { + a += IAU2006.S[k][i].nfa[it] * fa[it] } - w[k] += S[k][i].s * sin(a) + S[k][i].c * cos(a) + w[k] += IAU2006.S[k][i].s * sin(a) + IAU2006.S[k][i].c * cos(a) } } @@ -1080,8 +1056,7 @@ fun eraS06a(tt1: Double, tt2: Double): Angle { // Bias-precession-nutation-matrix, IAU 20006/2000A. val rnpb = eraPnm06a(tt1, tt2) // Extract the CIP coordinates. - val x = rnpb[2, 0] - val y = rnpb[2, 1] + val (x, y) = eraBpn2xy(rnpb) // Compute the CIO locator s, given the CIP coordinates. return eraS06(tt1, tt2, x, y) } @@ -1109,7 +1084,9 @@ fun era00(ut11: Double, ut12: Double): Angle { * * @return tan Z coefficient (radians) and tan^3 Z coefficient (radians) */ -fun eraRefco(phpa: Double, tc: Double, rh: Double, wl: Double): DoubleArray { +fun eraRefco(phpa: Pressure, tc: Temperature, rh: Double, wl: Double): DoubleArray { + if (phpa <= 0.0) return doubleArrayOf(0.0, 0.0) + // Decide whether optical/IR or radio case: switch at 100 microns. val optic = wl <= 100.0 @@ -1119,12 +1096,8 @@ fun eraRefco(phpa: Double, tc: Double, rh: Double, wl: Double): DoubleArray { val w = max(0.1, min(wl, 1e+6)) // Water vapour pressure at the observer. - val pw = if (p > 0.0) { - val ps = 10.0.pow((0.7859 + 0.03477 * t) / (1.0 + 0.00412 * t)) * (1.0 + p * (4.5e-6 + 6e-10 * t * t)) - r * ps / (1.0 - (1.0 - r) * ps / p) - } else { - 0.0 - } + val ps = 10.0.pow((0.7859 + 0.03477 * t) / (1.0 + 0.00412 * t)) * (1.0 + p * (4.5e-6 + 6e-10 * t * t)) + val pw = r * ps / (1.0 - (1.0 - r) * ps / p) // Refractive index minus 1 at the observer. @@ -1171,30 +1144,15 @@ fun eraRefco(phpa: Double, tc: Double, rh: Double, wl: Double): DoubleArray { * * @param tdb1 TDB date * @param tdb2 TDB fraction date - * @param ebpx Earth barycentric position (au) - * @param ebpy Earth barycentric position (au) - * @param ebpz Earth barycentric position (au) - * @param ebvx Earth barycentric velocity (au/day) - * @param ebvy Earth barycentric velocity (au/day) - * @param ebvz Earth barycentric velocity (au/day) - * @param ehpx Earth heliocentric position (au) - * @param ehpy Earth heliocentric position (au) - * @param ehpz Earth heliocentric position (au) - */ -fun eraApcg( + * @param ebp Earth barycentric position (au) + * @param ebv Earth barycentric velocity (au/day) + * @param ehp Earth heliocentric position (au) + */ +inline fun eraApcg( tdb1: Double, tdb2: Double, - ebpx: Distance, ebpy: Distance, ebpz: Distance, - ebvx: Velocity, ebvy: Velocity, ebvz: Velocity, - ehpx: Distance, ehpy: Distance, ehpz: Distance, + ebp: Vector3D, ebv: Vector3D, ehp: Vector3D, ): AstrometryParameters { - return eraApcs( - tdb1, tdb2, - 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, - ebpx, ebpy, ebpz, - ebvx, ebvy, ebvz, - ehpx, ehpy, ehpz, - ) + return eraApcs(tdb1, tdb2, Vector3D.EMPTY, Vector3D.EMPTY, ebp, ebv, ehp) } private const val AM12 = 0.000000211284 @@ -1255,25 +1213,25 @@ fun eraEpv00(tdb1: Double, tdb2: Double): Pair { } /** - * Precession-nutation, IAU 2000 model: a multi-purpose function, + * Precession-nutation, IAU 2000 model: a multi-purpose function, * supporting classical (equinox-based) use directly and CIO-based * use indirectly. */ -fun eraPn00(tt1: Double, tt2: Double, dpsi: Angle, deps: Angle): PrecessionNutationMatrices { +fun eraPn00(tt1: Double, tt2: Double, dpsi: Angle, deps: Angle): PrecessionNutationAnglesAndMatrices { // IAU 2000 precession-rate adjustments. val (_, depspr) = eraPr00(tt1, tt2) @@ -2122,7 +2081,7 @@ fun eraPn00(tt1: Double, tt2: Double, dpsi: Angle, deps: Angle): PrecessionNutat // Bias-precession-nutation matrix (classical). val rbpn = rn * rbp - return PrecessionNutationMatrices(epsa, rb, rp, rbp, rn, rbpn) + return PrecessionNutationAnglesAndMatrices(dpsi, deps, epsa, rb, rp, rbp, rn, rbpn) } typealias StarDirectionCosines = DoubleArray @@ -2699,3 +2658,1052 @@ inline fun eraTtUt1(tt1: Double, tt2: Double, ttMinusUt1: Double): DoubleArray { } const val DBL_EPSILON = 2.220446049250313E-16 + +/** + * For a terrestrial observer, prepare star-independent astrometry + * parameters for transformations between ICRS and geocentric CIRS + * coordinates. The Earth ephemeris and CIP/CIO are supplied by the + * caller. + * + * The parameters produced by this function are required in the + * parallax, light deflection, aberration, and bias-precession-nutation + * parts of the astrometric transformation chain. + * + * @param tdb1 TDB as a 2-part... + * @param tdb2 ...Julian Date (Note 1) + * @param ebp Earth barycentric position (au) + * @param ebv Earth barycentric velocity (au/day) + * @param ehp Earth heliocentric position (au) + * @param x CIP X (components of unit vector) + * @param y CIP Y (components of unit vector) + * @param s the CIO locator s (radians) + * + * @return Star-independent astrometry parameters. + */ +fun eraApci( + tdb1: Double, tdb2: Double, + ebp: Vector3D, ebv: Vector3D, ehp: Vector3D, + x: Double, y: Double, s: Double, +): AstrometryParameters { + val astrom = eraApcg(tdb1, tdb2, ebp, ebv, ehp) + val bpn = eraC2ixys(x, y, s) + return astrom.copy(bpn = bpn) +} + +/** + * For a terrestrial observer, prepare star-independent astrometry + * parameters for transformations between ICRS and geocentric CIRS + * coordinates. The caller supplies the date, and ERFA models are used + * to predict the Earth ephemeris and CIP/CIO. + * + * The parameters produced by this function are required in the + * parallax, light deflection, aberration, and bias-precession-nutation + * parts of the astrometric transformation chain. + * + * @return star-independent astrometry parameters and + * equation of the origins (ERA-GST, radians). + */ +fun eraApci13(tdb1: Double, tdb2: Double): Pair { + // Earth barycentric & heliocentric position/velocity (au, au/d). + val (ehpv, ebpv) = eraEpv00(tdb1, tdb2) + + // Form the equinox based BPN matrix, IAU 2006/2000A. + val r = eraPnm06a(tdb1, tdb2) + + // Extract CIP X,Y. + val (x, y) = eraBpn2xy(r) + + // Obtain CIO locator s. + val s = eraS06(tdb1, tdb2, x, y) + + // Compute the star-independent astrometry parameters. + val astrom = eraApci(tdb1, tdb2, ebpv.position, ebpv.velocity, ehpv.position, x, y, s) + + // Equation of the origins. + val eo = eraEors(r, s) + + return astrom to eo +} + +/** + * Transform ICRS star data, epoch J2000.0, to CIRS. + */ +fun eraAtci13( + rightAscension: Angle, declination: Angle, + pmRA: Angle, pmDEC: Angle, parallax: Angle, rv: Velocity, + tdb1: Double, tdb2: Double +): DoubleArray { + // The transformation parameters. + val (astrom, eo) = eraApci13(tdb1, tdb2) + // ICRS (epoch J2000.0) to CIRS. + val (ri, di) = eraAtciq(rightAscension, declination, pmRA, pmDEC, parallax, rv, astrom) + + return doubleArrayOf(ri, di, eo) +} + +/** + * Quick ICRS, epoch J2000.0, to CIRS transformation, given precomputed + * star-independent astrometry parameters. + * + * Use of this function is appropriate when efficiency is important and + * where many star positions are to be transformed for one date. The + * star-independent parameters can be obtained by calling one of the + * functions [eraApci13], [eraApcg13], [eraApco13] or [eraApcs13]. + * + * If the parallax and proper motions are zero the [eraAtciqz] function + * can be used instead. + * + * @return CIRS RA,Dec (radians) + */ +fun eraAtciq( + rightAscension: Angle, declination: Angle, + pmRA: Angle, pmDEC: Angle, parallax: Angle, rv: Velocity, + astrom: AstrometryParameters, +): DoubleArray { + // Proper motion and parallax, giving BCRS coordinate direction. + val pco = eraPmpx(rightAscension, declination, pmRA, pmDEC, parallax, rv, astrom.pmt, astrom.eb) + + // Light deflection by the Sun, giving BCRS natural direction. + val pnat = eraLdsun(pco, astrom.eh, astrom.em) + + // Aberration, giving GCRS proper direction. + val ppr = eraAb(pnat, astrom.v, astrom.em, astrom.bm1) + + // Bias-precession-nutation, giving CIRS proper direction. + val pi = astrom.bpn * ppr + + // CIRS RA,Dec. + val (ri, di) = eraC2s(pi[0], pi[1], pi[2]) + + return doubleArrayOf(ri.normalized, di) +} + +/** + * Quick ICRS to CIRS transformation, given precomputed star- + * independent astrometry parameters, and assuming zero parallax and + * proper motion. + * + * Use of this function is appropriate when efficiency is important and + * where many star positions are to be transformed for one date. The + * star-independent parameters can be obtained by calling one of the + * functions eraApci[13], eraApcg[13], eraApco[13] or eraApcs[13]. + * + * The corresponding function for the case of non-zero parallax and + * proper motion is [eraAtciq]. + * + * @return CIRS RA,Dec (radians) + */ +fun eraAtciqz(rightAscension: Angle, declination: Angle, astrom: AstrometryParameters): DoubleArray { + // BCRS coordinate direction (unit vector). + val pco = eraS2c(rightAscension, declination) + + // Light deflection by the Sun, giving BCRS natural direction. + val pnat = eraLdsun(pco, astrom.eh, astrom.em) + + // Aberration, giving GCRS proper direction. + val ppr = eraAb(pnat, astrom.v, astrom.em, astrom.bm1) + + // Bias-precession-nutation, giving CIRS proper direction. + val pi = astrom.bpn * ppr + + // CIRS RA,Dec. + val (ri, di) = eraC2s(pi[0], pi[1], pi[2]) + + return doubleArrayOf(ri.normalized, di) +} + +/** + * Deflection of starlight by the Sun. + * + * @param p Direction from observer to star (unit vector) + * @param e Direction from Sun to observer (unit vector) + * @param em Distance from Sun to observer (au) + * + * @return Observer to deflected star (unit vector) + */ +fun eraLdsun(p: Vector3D, e: Vector3D, em: Distance): Vector3D { + // Deflection limiter (smaller for distant observers). + val em2 = max(1.0, em * em) + return eraLd(1.0, p, p, e, em, 1e-6 / em2) +} + +/** + * Apply light deflection by a solar-system body, as part of + * transforming coordinate direction into natural direction. + * + * @param bm Mass of the gravitating body (solar masses) + * @param p Direction from observer to source (unit vector) + * @param q Direction from body to source (unit vector) + * @param e Direction from body to observer (unit vector) + * @param em Distance from body to observer (au) + * @param dlim Deflection limiter (Note 4) + * + * @return Observer to deflected source (unit vector) + */ +fun eraLd(bm: Double, p: Vector3D, q: Vector3D, e: Vector3D, em: Distance, dlim: Double): Vector3D { + val qpe = DoubleArray(3) { q[it] + e[it] } + val w = bm * SCHWARZSCHILD_RADIUS_OF_THE_SUN / em / max(q.dot(qpe), dlim) + // p x (e x q). + val eq = e.cross(q) + val peq = p.cross(eq) + + // Apply the deflection. + repeat(3) { + qpe[it] = p[it] + w * peq[it] + } + + return Vector3D(qpe) +} + +/** + * Proper motion and parallax. + * + * @param rightAscension ICRS RA at catalog epoch (radians) + * @param declination ICRS Dec at catalog epoch (radians) + * @param pmRA RA proper motion (radians/year, Note 1) + * @param pmDEC Dec proper motion (radians/year) + * @param parallax parallax (arcsec) + * @param rv radial velocity (km/s, +ve if receding) + * @param pmt proper motion time interval (SSB, Julian years) + * @param pob SSB to observer vector (au) + * + * @return Coordinate direction (BCRS unit vector) + */ +fun eraPmpx( + rightAscension: Angle, declination: Angle, + pmRA: Angle, pmDEC: Angle, parallax: Angle, rv: Velocity, + pmt: Double, pob: Vector3D +): Vector3D { + val p = DoubleArray(3) + val pm = DoubleArray(3) + + // Spherical coordinates to unit vector (and useful functions). + val sr = sin(rightAscension) + val cr = cos(rightAscension) + val sd = sin(declination) + val cd = cos(declination) + p[0] = cr * cd + p[1] = sr * cd + p[2] = sd + + // Proper motion time interval (y) including Roemer effect. + val dt = pmt + pob.dot(p) * (LIGHT_TIME_AU / DAYSEC / DAYSPERJY) + + // Space motion (radians per year). + val pxr = parallax * ASEC2RAD + val w = (DAYSEC * DAYSPERJM / AU_M) * rv * pxr + val pdz = pmDEC * p[2] + pm[0] = -pmRA * p[1] - pdz * cr + w * p[0] + pm[1] = pmRA * p[0] - pdz * sr + w * p[1] + pm[2] = pmDEC * cd + w * p[2] + + // Coordinate direction of star (unit vector, BCRS). + repeat(3) { + p[it] += dt * pm[it] - pxr * pob[it] + } + + return Vector3D(p).normalized +} + +/** + * Extract from the bias-precession-nutation matrix the X,Y coordinates + * of the Celestial Intermediate Pole. + */ +inline fun eraBpn2xy(rbpn: Matrix3D): DoubleArray { + return doubleArrayOf(rbpn[2, 0], rbpn[2, 1]) +} + +/** + * Precession-nutation, IAU 2000B model: a multi-purpose function, + * supporting classical (equinox-based) use directly and CIO-based + * use indirectly. + */ +fun eraPn00b(tt1: Double, tt2: Double): PrecessionNutationAnglesAndMatrices { + val (dpsi, deps) = eraNut00b(tt1, tt2) + val (_, _, epsa, rb, rp, rbp, rn, rbpn) = eraPn00(tt1, tt2, dpsi, deps) + return PrecessionNutationAnglesAndMatrices(dpsi, deps, epsa, rb, rp, rbp, rn, rbpn) +} + +/** + * Form the matrix of precession-nutation for a given date (including + * frame bias), equinox-based, IAU 2000B model. + */ +inline fun eraPnm00b(tt1: Double, tt2: Double): Matrix3D { + return eraPn00b(tt1, tt2).gcrsToTrue +} + +/** + * The CIO locator s, positioning the Celestial Intermediate Origin on + * the equator of the Celestial Intermediate Pole, using the IAU 2000B + * precession-nutation model. + */ +fun eraS00b(tt1: Double, tt2: Double): Angle { + // Bias-precession-nutation-matrix, IAU 2000B. + val rbpn = eraPnm00b(tt1, tt2) + + // Extract the CIP coordinates. + val (x, y) = eraBpn2xy(rbpn) + + // Compute the CIO locator s, given the CIP coordinates. + return eraS00(tt1, tt2, x, y) +} + +internal object IAU2000 { + + // Polynomial coefficients + @JvmField internal val SP = doubleArrayOf(94.00e-6, 3808.35e-6, -119.94e-6, -72574.09e-6, 27.70e-6, 15.61e-6) + + // Terms of order t^0 + @JvmField internal val S0 = arrayOf( + // 1-10 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), -2640.73e-6, 0.39e-6), + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -63.53e-6, 0.02e-6), + Term(intArrayOf(0, 0, 2, -2, 3, 0, 0, 0), -11.75e-6, -0.01e-6), + Term(intArrayOf(0, 0, 2, -2, 1, 0, 0, 0), -11.21e-6, -0.01e-6), + Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), 4.57e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 3, 0, 0, 0), -2.02e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 1, 0, 0, 0), -1.98e-6, 0.00e-6), + Term(intArrayOf(0, 0, 0, 0, 3, 0, 0, 0), 1.72e-6, 0.00e-6), + Term(intArrayOf(0, 1, 0, 0, 1, 0, 0, 0), 1.41e-6, 0.01e-6), + Term(intArrayOf(0, 1, 0, 0, -1, 0, 0, 0), 1.26e-6, 0.01e-6), + + // 11-20 + Term(intArrayOf(1, 0, 0, 0, -1, 0, 0, 0), 0.63e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, 0, 1, 0, 0, 0), 0.63e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 3, 0, 0, 0), -0.46e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 1, 0, 0, 0), -0.45e-6, 0.00e-6), + Term(intArrayOf(0, 0, 4, -4, 4, 0, 0, 0), -0.36e-6, 0.00e-6), + Term(intArrayOf(0, 0, 1, -1, 1, -8, 12, 0), 0.24e-6, 0.12e-6), + Term(intArrayOf(0, 0, 2, 0, 0, 0, 0, 0), -0.32e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), -0.28e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 3, 0, 0, 0), -0.27e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 1, 0, 0, 0), -0.26e-6, 0.00e-6), + + // 21-30 + Term(intArrayOf(0, 0, 2, -2, 0, 0, 0, 0), 0.21e-6, 0.00e-6), + Term(intArrayOf(0, 1, -2, 2, -3, 0, 0, 0), -0.19e-6, 0.00e-6), + Term(intArrayOf(0, 1, -2, 2, -1, 0, 0, 0), -0.18e-6, 0.00e-6), + Term(intArrayOf(0, 0, 0, 0, 0, 8, -13, -1), 0.10e-6, -0.05e-6), + Term(intArrayOf(0, 0, 0, 2, 0, 0, 0, 0), -0.15e-6, 0.00e-6), + Term(intArrayOf(2, 0, -2, 0, -1, 0, 0, 0), 0.14e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 2, 0, 0, 0), 0.14e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, -2, 1, 0, 0, 0), -0.14e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, -2, -1, 0, 0, 0), -0.14e-6, 0.00e-6), + Term(intArrayOf(0, 0, 4, -2, 4, 0, 0, 0), -0.13e-6, 0.00e-6), + + // 31-33 + Term(intArrayOf(0, 0, 2, -2, 4, 0, 0, 0), 0.11e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, 0, -3, 0, 0, 0), -0.11e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, 0, -1, 0, 0, 0), -0.11e-6, 0.00e-6), + ) + + // Terms of order t^1 + @JvmField internal val S1 = arrayOf( + // 1 - 3 + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -0.07e-6, 3.57e-6), + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 1.71e-6, -0.03e-6), + Term(intArrayOf(0, 0, 2, -2, 3, 0, 0, 0), 0.00e-6, 0.48e-6), + ) + + // Terms of order t^2 + @JvmField internal val S2 = arrayOf( + // 1-10 + // 1-10 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 743.53e-6, -0.17e-6), + Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), 56.91e-6, 0.06e-6), + Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), 9.84e-6, -0.01e-6), + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), -8.85e-6, 0.01e-6), + Term(intArrayOf(0, 1, 0, 0, 0, 0, 0, 0), -6.38e-6, -0.05e-6), + Term(intArrayOf(1, 0, 0, 0, 0, 0, 0, 0), -3.07e-6, 0.00e-6), + Term(intArrayOf(0, 1, 2, -2, 2, 0, 0, 0), 2.23e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 1, 0, 0, 0), 1.67e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 2, 0, 0, 0), 1.30e-6, 0.00e-6), + Term(intArrayOf(0, 1, -2, 2, -2, 0, 0, 0), 0.93e-6, 0.00e-6), + + // 11-20 + Term(intArrayOf(1, 0, 0, -2, 0, 0, 0, 0), 0.68e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, -2, 1, 0, 0, 0), -0.55e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, 0, -2, 0, 0, 0), 0.53e-6, 0.00e-6), + Term(intArrayOf(0, 0, 0, 2, 0, 0, 0, 0), -0.27e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, 0, 1, 0, 0, 0), -0.27e-6, 0.00e-6), + Term(intArrayOf(1, 0, -2, -2, -2, 0, 0, 0), -0.26e-6, 0.00e-6), + Term(intArrayOf(1, 0, 0, 0, -1, 0, 0, 0), -0.25e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, 0, 1, 0, 0, 0), 0.22e-6, 0.00e-6), + Term(intArrayOf(2, 0, 0, -2, 0, 0, 0, 0), -0.21e-6, 0.00e-6), + Term(intArrayOf(2, 0, -2, 0, -1, 0, 0, 0), 0.20e-6, 0.00e-6), + + // 21-25 + Term(intArrayOf(0, 0, 2, 2, 2, 0, 0, 0), 0.17e-6, 0.00e-6), + Term(intArrayOf(2, 0, 2, 0, 2, 0, 0, 0), 0.13e-6, 0.00e-6), + Term(intArrayOf(2, 0, 0, 0, 0, 0, 0, 0), -0.13e-6, 0.00e-6), + Term(intArrayOf(1, 0, 2, -2, 2, 0, 0, 0), -0.12e-6, 0.00e-6), + Term(intArrayOf(0, 0, 2, 0, 0, 0, 0, 0), -0.11e-6, 0.00e-6), + ) + + // Terms of order t^3 + @JvmField internal val S3 = arrayOf( + // 1-4 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), 0.30e-6, -23.51e-6), + Term(intArrayOf(0, 0, 2, -2, 2, 0, 0, 0), -0.03e-6, -1.39e-6), + Term(intArrayOf(0, 0, 2, 0, 2, 0, 0, 0), -0.01e-6, -0.24e-6), + Term(intArrayOf(0, 0, 0, 0, 2, 0, 0, 0), 0.00e-6, 0.22e-6), + ) + + // Terms of order t^4 + @JvmField internal val S4 = arrayOf( + // 1-1 + Term(intArrayOf(0, 0, 0, 0, 1, 0, 0, 0), -0.26e-6, -0.01e-6), + ) + + @JvmField internal val S = arrayOf(S0, S1, S2, S3, S4) +} + +/** + * The CIO locator s, positioning the Celestial Intermediate Origin on + * the equator of the Celestial Intermediate Pole, given the CIP's X,Y + * coordinates. Compatible with IAU 2000A precession-nutation. + */ +fun eraS00(tt1: Double, tt2: Double, x: Double, y: Double): Angle { + // Interval between fundamental epoch J2000.0 and current date (JC). + val t = (tt1 - J2000 + tt2) / DAYSPERJC + + // Fundamental Arguments (from IERS Conventions 2003) + val fa = DoubleArray(8) + + // Mean anomaly of the Moon. + fa[0] = eraFal03(t) + // Mean anomaly of the Sun. + fa[1] = eraFalp03(t) + // Mean longitude of the Moon minus that of the ascending node. + fa[2] = eraFaf03(t) + // Mean elongation of the Moon from the Sun. + fa[3] = eraFad03(t) + // Mean longitude of the ascending node of the Moon. + fa[4] = eraFaom03(t) + // Mean longitude of Venus. + fa[5] = eraFave03(t) + // Mean longitude of Earth. + fa[6] = eraFae03(t) + // General precession in longitude. + fa[7] = eraFapa03(t) + + // Evalutate s. + val w = DoubleArray(6) { IAU2000.SP[it] } + + for (k in IAU2000.S.indices) { + for (i in IAU2000.S[k].indices.reversed()) { + var a = 0.0 + + repeat(8) { + a += IAU2000.S[k][i].nfa[it] * fa[it] + } + + w[k] += IAU2000.S[k][i].s * sin(a) + IAU2000.S[k][i].c * cos(a) + } + } + + return (w[0] + (w[1] + (w[2] + (w[3] + (w[4] + w[5] * t) * t) * t) * t) * t).arcsec - x * y / 2.0 +} + +/** + * Form the matrix of precession-nutation for a given date (including + * frame bias), equinox based, IAU 2000A model. + */ +inline fun eraPnm00a(tt1: Double, tt2: Double): Matrix3D { + return eraPn00a(tt1, tt2).gcrsToTrue +} + +/** + * Precession-nutation, IAU 2000A model: a multi-purpose function, + * supporting classical (equinox-based) use directly and CIO-based + * use indirectly. + */ +fun eraPn00a(tt1: Double, tt2: Double): PrecessionNutationAnglesAndMatrices { + val (dpsi, deps) = eraNut00a(tt1, tt2) + val (_, _, epsa, rb, rp, rbp, rn, rbpn) = eraPn00(tt1, tt2, dpsi, deps) + return PrecessionNutationAnglesAndMatrices(dpsi, deps, epsa, rb, rp, rbp, rn, rbpn) +} + +/** + * The CIO locator s, positioning the Celestial Intermediate Origin on + * the equator of the Celestial Intermediate Pole, using the IAU 2000A + * precession-nutation model. + */ +fun eraS00a(tt1: Double, tt2: Double): Angle { + // Bias-precession-nutation-matrix, IAU 2000A. + val rbpn = eraPnm00a(tt1, tt2) + + // Extract the CIP coordinates. + val (x, y) = eraBpn2xy(rbpn) + + // Compute the CIO locator s, given the CIP coordinates. + return eraS00(tt1, tt2, x, y) +} + +/** + * For a terrestrial observer, prepare star-independent astrometry + * parameters for transformations between ICRS and observed + * coordinates. The caller supplies UTC, site coordinates, ambient air + * conditions and observing wavelength, and ERFA models are used to + * obtain the Earth ephemeris, CIP/CIO and refraction constants. + * + * The parameters produced by this function are required in the + * parallax, light deflection, aberration, and bias-precession-nutation + * parts of the ICRS/CIRS transformations. + * + * @param utc1 UTC as a 2-part... + * @param utc2 ...Julian Date + * @param dut1 UT1-UTC (seconds) + * @param elong Longitude (radians, east +ve) + * @param phi Latitude (geodetic, radians) + * @param hm Height above ellipsoid (m, geodetic) + * @param xp Polar motion coordinates (radians) + * @param yp Polar motion coordinates (radians) + * @param phpa Pressure at the observer (hPa = mBar) + * @param tc Ambient temperature at the observer (deg C) + * @param rh Relative humidity at the observer (range 0-1) + * @param wl Wavelength (micrometers) + */ +fun eraApco13( + utc1: Double, utc2: Double, dut1: Double, + elong: Angle, phi: Angle, hm: Distance, xp: Angle, yp: Angle, + phpa: Pressure, tc: Temperature, rh: Double, wl: Double +): Pair { + val (tai1, tai2) = eraUtcTai(utc1, utc2) + val (tt1, tt2) = eraTaiTt(tai1, tai2) + val (ut11, ut12) = eraUtcUt1(utc1, utc2, dut1) + + // Earth barycentric & heliocentric position/velocity (au, au/d). + val (ehpv, ebpv) = eraEpv00(tt1, tt2) + val (ebp, ebv) = ebpv + val ehp = ehpv.position + + // Form the equinox based BPN matrix, IAU 2006/2000A. + val r = eraPnm06a(tt1, tt2) + + // Extract CIP X,Y. + val (x, y) = eraBpn2xy(r) + + // Obtain CIO locator s. + val s = eraS06(tt1, tt2, x, y) + + // Earth rotation angle. + val theta = eraEra00(ut11, ut12) + + // TIO locator s'. + val sp = eraSp00(tt1, tt2) + + // Refraction constants A and B. + val (refa, refb) = eraRefco(phpa, tc, rh, wl) + + // Compute the star-independent astrometry parameters. + val astrom = eraApco( + tt1, tt2, ebp, ebv, ehp, + x, y, s, theta, + elong, phi, hm, xp, yp, sp, refa, refb + ) + + // Equation of the origins. + val eo = eraEors(r, s) + + return astrom to eo +} + +/** + * For an observer whose geocentric position and velocity are known, + * prepare star-independent astrometry parameters for transformations + * between ICRS and GCRS. The Earth ephemeris is from ERFA models. + * + * The parameters produced by this function are required in the space + * motion, parallax, light deflection and aberration parts of the + * astrometric transformation chain. + * + * @param p observer's geocentric position with respect to BCRS axes (m) + * @param v observer's geocentric velocity with respect to BCRS axes (m/s) + */ +fun eraApcs13(tdb1: Double, tdb2: Double, p: Vector3D, v: Vector3D): AstrometryParameters { + // Earth barycentric & heliocentric position/velocity (au, au/d). + val (ehpv, ebpv) = eraEpv00(tdb1, tdb2) + // Compute the star-independent astrometry parameters. + return eraApcs(tdb1, tdb2, p, v, ebpv.position, ebpv.velocity, ehpv.position) +} + +/** + * For a terrestrial observer, prepare star-independent astrometry + * parameters for transformations between CIRS and observed + * coordinates. The caller supplies the Earth orientation information + * and the refraction constants as well as the site coordinates. + * + * @param sp The TIO locator s (radians) + * @param theta Earth rotation angle (radians) + * @param elong Longitude (radians, east +ve) + * @param phi Latitude (geodetic, radians) + * @param hm Height above ellipsoid (m, geodetic) + * @param xp Polar motion coordinates (radians) + * @param yp Polar motion coordinates (radians) + * @param refa Refraction constant A (radians) + * @param refb Refraction constant B (radians) + */ +fun eraApio( + sp: Angle, theta: Angle, elong: Angle, phi: Angle, + hm: Double, xp: Angle, yp: Angle, refa: Angle, refb: Angle, + astrom: AstrometryParameters? = null, +): AstrometryParameters { + // Form the rotation matrix, CIRS to apparent [HA,Dec]. + val r = Matrix3D.rotZ(theta + sp).rotateY(-xp).rotateX(-yp).rotateZ(elong) + + // Solve for local Earth rotation angle. + val eral = atan2(r[0, 1], r[0, 0]) + + // Solve for polar motion [X,Y] with respect to local meridian. + val xpl = atan2(r[0, 2], hypot(r[0, 0], r[0, 1])) + val ypl = -atan2(r[1, 2], r[2, 2]) + + // Adjusted longitude. + val along = eraAnpm(eral - theta) + + // Functions of latitude. + val sphi = sin(phi) + val cphi = cos(phi) + + // Observer's geocentric position and velocity (m, m/s, CIRS). + val pv = eraPvtob(elong, phi, hm, xp, yp, sp, theta) + + // Magnitude of diurnal aberration vector. + val diurab = hypot(pv.velocity[0], pv.velocity[1]) / SPEED_OF_LIGHT + + // Refraction constants. + return astrom?.copy( + xpl = xpl, ypl = ypl, along = along, sphi = sphi, cphi = cphi, + diurab = diurab, refa = refa, refb = refb, eral = eral, + ) ?: AstrometryParameters( + xpl = xpl, ypl = ypl, along = along, sphi = sphi, cphi = cphi, + diurab = diurab, refa = refa, refb = refb, eral = eral, + ) +} + +/** + * For a terrestrial observer, prepare star-independent astrometry + * parameters for transformations between CIRS and observed + * coordinates. The caller supplies UTC, site coordinates, ambient air + * conditions and observing wavelength. + * + * @param utc1 UTC as a 2-part... + * @param utc2 ...Julian Date + * @param dut1 UT1-UTC (seconds) + * @param elong Longitude (radians, east +ve) + * @param phi Latitude (geodetic, radians) + * @param hm Height above ellipsoid (m, geodetic) + * @param xp Polar motion coordinates (radians) + * @param yp Polar motion coordinates (radians) + * @param phpa Pressure at the observer (hPa = mBar) + * @param tc Ambient temperature at the observer (deg C) + * @param rh Relative humidity at the observer (range 0-1) + * @param wl Wavelength (micrometers) + */ +fun eraApio13( + utc1: Double, utc2: Double, dut1: Double, + elong: Angle, phi: Angle, hm: Distance, xp: Angle, yp: Angle, + phpa: Pressure, tc: Temperature, rh: Double, wl: Double +): AstrometryParameters { + val (tai1, tai2) = eraUtcTai(utc1, utc2) + val (tt1, tt2) = eraTaiTt(tai1, tai2) + val (ut11, ut12) = eraUtcUt1(utc1, utc2, dut1) + + // TIO locator s'. + val sp = eraSp00(tt1, tt2) + + // Earth rotation angle. + val theta = eraEra00(ut11, ut12) + + // Refraction constants A and B. + val (refa, refb) = eraRefco(phpa, tc, rh, wl) + + // CIRS <-> observed astrometry parameters. + return eraApio(sp, theta, elong, phi, hm, xp, yp, refa, refb) +} + +/** + * Quick CIRS to observed place transformation. + * + * Use of this function is appropriate when efficiency is important and + * where many star positions are all to be transformed for one date. + * The star-independent astrometry parameters can be obtained by + * calling [eraApio13] or [eraApco13]. + * + * @param rightAscension CIRS right ascension + * @param declination CIRS declination + * @param astrom star-independent astrometry parameters. + * + * @return observed azimuth (radians: N=0,E=90), observed zenith distance (radians), + * observed hour angle (radians), observed declination (radians), observed right ascension (CIO-based, radians) + */ +fun eraAtioq(rightAscension: Angle, declination: Angle, astrom: AstrometryParameters): DoubleArray { + // CIRS RA,Dec to Cartesian -HA,Dec. + val v = eraS2c(rightAscension - astrom.eral, declination) + + // Polar motion. + val sx = sin(astrom.xpl) + val cx = cos(astrom.xpl) + val sy = sin(astrom.ypl) + val cy = cos(astrom.ypl) + val xhd = cx * v[0] + sx * v[2] + val yhd = sx * sy * v[0] + cy * v[1] - cx * sy * v[2] + val zhd = -sx * cy * v[0] + sy * v[1] + cx * cy * v[2] + + // Diurnal aberration. + var f = (1.0 - astrom.diurab * yhd) + val xhdt = f * xhd + val yhdt = f * (yhd + astrom.diurab) + val zhdt = f * zhd + + // Cartesian -HA,Dec to Cartesian Az,El (S=0,E=90). + val xaet = astrom.sphi * xhdt - astrom.cphi * zhdt + val zaet = astrom.cphi * xhdt + astrom.sphi * zhdt + + // Azimuth (N=0,E=90). + val azobs = if (xaet != 0.0 || yhdt != 0.0) atan2(yhdt, -xaet) else 0.0 + + // Cosine and sine of altitude, with precautions. + val r = max(sqrt(xaet * xaet + yhdt * yhdt), 1e-6) + val z = max(zaet, 0.05) + + // A*tan(z)+B*tan^3(z) model, with Newton-Raphson correction. + val tz = r / z + val w = astrom.refb * tz * tz + val del = (astrom.refa + w) * tz / (1.0 + (astrom.refa + 3.0 * w) / (z * z)) + + // Apply the change, giving observed vector. + val cosdel = 1.0 - del * del / 2.0 + f = cosdel - del * z / r + val xaeo = xaet * f + val yaeo = yhdt * f + val zaeo = cosdel * zaet + del * r + + // Observed ZD. + val zdobs = atan2(sqrt(xaeo * xaeo + yaeo * yaeo), zaeo) + + // Az/El vector to HA,Dec vector (both right-handed). + val vx = astrom.sphi * xaeo + astrom.cphi * zaeo + val vz = -astrom.cphi * xaeo + astrom.sphi * zaeo + + // To spherical -HA,Dec. + val (hmobs, dcobs) = eraC2s(vx, yaeo, vz) + + // Right ascension (with respect to CIO). + val raobs = astrom.eral + hmobs + + // Return the results. + return doubleArrayOf(azobs.normalized, zdobs, -hmobs, dcobs, raobs.normalized) +} + +/** + * ICRS RA,Dec to observed place. The caller supplies UTC, site + * coordinates, ambient air conditions and observing wavelength. + * + * ERFA models are used for the Earth ephemeris, bias-precession- + * nutation, Earth orientation and refraction. + * + * @param rightAscension ICRS RA at catalog epoch (radians) + * @param declination ICRS Dec at catalog epoch (radians) + * @param pmRA RA proper motion (radians/year, Note 1) + * @param pmDEC Dec proper motion (radians/year) + * @param parallax parallax (arcsec) + * @param rv radial velocity (km/s, +ve if receding) + * @param utc1 UTC as a 2-part... + * @param utc2 ...Julian Date + * @param dut1 UT1-UTC (seconds) + * @param elong Longitude (radians, east +ve) + * @param phi Latitude (geodetic, radians) + * @param hm Height above ellipsoid (m, geodetic) + * @param xp Polar motion coordinates (radians) + * @param yp Polar motion coordinates (radians) + * @param phpa Pressure at the observer (hPa = mBar) + * @param tc Ambient temperature at the observer (deg C) + * @param rh Relative humidity at the observer (range 0-1) + * @param wl Wavelength (micrometers) + * + * @return observed azimuth (radians: N=0,E=90), observed zenith distance (radians), + * observed hour angle (radians), observed declination (radians), + * observed right ascension (CIO-based, radians) and equation of the origins (ERA-GST, radians). + */ +fun eraAtco13( + rightAscension: Angle, declination: Angle, + pmRA: Angle, pmDEC: Angle, parallax: Angle, rv: Velocity, + utc1: Double, utc2: Double, dut1: Double, + elong: Angle, phi: Angle, hm: Double, xp: Angle, yp: Angle, + phpa: Double, tc: Temperature, rh: Double, wl: Double, +): Pair { + // Star-independent astrometry parameters. + val (astrom, eo) = eraApco13(utc1, utc2, dut1, elong, phi, hm, xp, yp, phpa, tc, rh, wl) + // Transform ICRS to CIRS. + val (ri, di) = eraAtciq(rightAscension, declination, pmRA, pmDEC, parallax, rv, astrom) + // Transform CIRS to observed. + return eraAtioq(ri, di, astrom) to eo +} + +/** + * Quick CIRS RA,Dec to ICRS astrometric place, given the star- + * independent astrometry parameters. + * + * Use of this function is appropriate when efficiency is important and + * where many star positions are all to be transformed for one date. + * The star-independent astrometry parameters can be obtained by + * calling one of the functions [eraApci13], [eraApcg13], [eraApco13] + * or [eraApcs13]. + * + * @return ICRS astrometric RA,Dec (radians) + */ +fun eraAticq(rightAscension: Angle, declination: Angle, astrom: AstrometryParameters): DoubleArray { + // CIRS RA,Dec to Cartesian. + val pi = eraS2c(rightAscension, declination) + + // Bias-precession-nutation, giving GCRS proper direction. + val ppr = astrom.bpn.transposed * pi + + // Aberration, giving GCRS natural direction + val d = DoubleArray(3) + val pnat = Vector3D() + + repeat(3) { + pnat.unsafe { + var r2 = 0.0 + + for (i in 0..2) { + val w = ppr[i] - d[i] + this[i] = w + r2 += w * w + } + + val r = sqrt(r2) + + for (i in 0..2) { + this[i] /= r + } + } + + val after = eraAb(pnat, astrom.v, astrom.em, astrom.bm1) + + pnat.unsafe { + var r2 = 0.0 + + for (i in 0..2) { + d[i] = after[i] - this[i] + val w = ppr[i] - d[i] + this[i] = w + r2 += w * w + } + + val r = sqrt(r2) + + for (i in 0..2) { + this[i] /= r + } + } + } + + // Light deflection by the Sun, giving BCRS coordinate direction. + d.fill(0.0) + + val pco = Vector3D() + + repeat(5) { + pco.unsafe { + var r2 = 0.0 + + for (i in 0..2) { + val w = pnat[i] - d[i] + this[i] = w + r2 += w * w + } + + val r = sqrt(r2) + + for (i in 0..2) { + this[i] /= r + } + } + + val after = eraLdsun(pco, astrom.eh, astrom.em) + + pco.unsafe { + var r2 = 0.0 + + for (i in 0..2) { + d[i] = after[i] - this[i] + val w = pnat[i] - d[i] + this[i] = w + r2 += w * w + } + + val r = sqrt(r2) + + for (i in 0..2) { + this[i] /= r + } + } + } + + return eraC2s(pco[0], pco[1], pco[2]) +} + +/** + * Transform star RA,Dec from geocentric CIRS to ICRS astrometric. + * + * @return ICRS astrometric RA,Dec (radians) and equation of the origins (ERA-GST, radians). + */ +fun eraAtic13(rightAscension: Angle, declination: Angle, tdb1: Double, tdb2: Double): DoubleArray { + // Star-independent astrometry parameters. + val (astrom, eo) = eraApci13(tdb1, tdb2) + + // CIRS to ICRS astrometric. + val (ri, di) = eraAticq(rightAscension, declination, astrom) + + return doubleArrayOf(ri, di, eo) +} + +/** + * Convert position/velocity from spherical to Cartesian coordinates. + * + * theta longitude angle (radians) + * phi latitude angle (radians) + * r radial distance + * td rate of change of theta + * pd rate of change of phi + * rd rate of change of r + */ +fun eraS2pv( + theta: Angle, phi: Angle, r: Double, + td: Double, pd: Double, rd: Double +): PositionAndVelocity { + val st = sin(theta) + val ct = cos(theta) + val sp = sin(phi) + val cp = cos(phi) + val rcp = r * cp + val x = rcp * ct + val y = rcp * st + val rpd = r * pd + val w = rpd * sp - cp * rd + + return PositionAndVelocity(Vector3D(x, y, r * sp), Vector3D(-y * td - w * ct, x * td - w * st, rpd * cp + sp * rd)) +} + +/** + * Convert star catalog coordinates to position+velocity vector. + * + * @param rightAscension Right ascension (radians) + * @param declination Declination (radians) + * @param pmRA RA proper motion (radians/year) + * @param pmDEC Dec proper motion (radians/year) + * @param parallax Parallax (arcseconds) + * @param rv Radial velocity (km/s, positive = receding) + * + * @return pv-vector (au, au/day). + */ +fun eraStarpv( + rightAscension: Angle, declination: Angle, + pmRA: Angle, pmDEC: Angle, parallax: Double, rv: Double, +): PositionAndVelocity { + // Distance (au). + var w = max(parallax, 1e-7) + val r = (180.0 * 3600.0 / PI) / w + + val rd = DAYSEC * rv / AU_KM + + // Proper motion (radian/day). + val rad = pmRA / DAYSPERJY + val decd = pmDEC / DAYSPERJY + + // To pv-vector (au,au/day). + val pv = eraS2pv(rightAscension, declination, r, rad, decd, rd) + + return eraStarpv(pv) +} + +/** + * Convert star catalog coordinates to position+velocity vector. + * + * Modified to accept radians and au/day instead of arcseconds and km/s. + * + * @param rightAscension Right ascension (radians) + * @param declination Declination (radians) + * @param pmRA RA proper motion (radians/year) + * @param pmDEC Dec proper motion (radians/year) + * @param parallax Parallax (radians) + * @param rv Radial velocity (au/day, positive = receding) + * + * @return pv-vector (au, au/day). + */ +fun eraStarpvMod( + rightAscension: Angle, declination: Angle, + pmRA: Angle, pmDEC: Angle, parallax: Angle, rv: Velocity, +): PositionAndVelocity { + // Distance (au). + val r = 1 / max(parallax, 1e-13) + + // Proper motion (radian/day). + val rad = pmRA / DAYSPERJY + val decd = pmDEC / DAYSPERJY + + // To pv-vector (au,au/day). + val pv = eraS2pv(rightAscension, declination, r, rad, decd, rv) + + return eraStarpv(pv) +} + +private fun eraStarpv(pv: PositionAndVelocity): PositionAndVelocity { + // Isolate the radial component of the velocity (au/day). + val pu = pv.position.normalized + val vsr = pu.dot(pv.velocity) + val usr = pu * vsr + + // Isolate the transverse component of the velocity (au/day). + val ust = pv.velocity - usr + val vst = ust.length + + // Special-relativity dimensionless parameters. + val betsr = vsr / SPEED_OF_LIGHT_AU_DAY + val betst = vst / SPEED_OF_LIGHT_AU_DAY + + // Determine the observed-to-inertial correction terms. + var bett = betst + var betr = betsr + + var d = 0.0 + var del = 0.0 + var odd = 0.0 + var oddel = 0.0 + var od = 0.0 + var odel = 0.0 + + for (i in 0..99) { + d = 1.0 + betr + val w = betr * betr + bett * bett + del = -w / (sqrt(1.0 - w) + 1.0) + betr = d * betsr + del + bett = d * betst + + if (i > 0) { + val dd = abs(d - od) + val ddel = abs(del - odel) + if (i > 1 && dd >= odd && ddel >= oddel) break + odd = dd + oddel = ddel + } + + od = d + odel = del + } + + // Scale observed tangential velocity vector into inertial (au/d). + val ut = ust * d + + // Compute inertial radial velocity vector (au/d). + val ur = pu * (SPEED_OF_LIGHT_AU_DAY * (d * betsr + del)) + + // Combine the two to obtain the inertial space velocity vector. + val v = ur + ut + + return PositionAndVelocity(pv.position, v) +} diff --git a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/PrecessionNutationAnglesAndMatrices.kt b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/PrecessionNutationAnglesAndMatrices.kt new file mode 100644 index 000000000..339adf701 --- /dev/null +++ b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/PrecessionNutationAnglesAndMatrices.kt @@ -0,0 +1,14 @@ +package nebulosa.erfa + +import nebulosa.math.Angle +import nebulosa.math.Matrix3D + +data class PrecessionNutationAnglesAndMatrices( + @JvmField val dpsi: Angle, @JvmField val deps: Angle, // nutation + @JvmField val meanObliquity: Angle, // epsa + @JvmField val frameBias: Matrix3D, // rb + @JvmField val precession: Matrix3D, // rp + @JvmField val biasPrecession: Matrix3D, // rbp + @JvmField val nutation: Matrix3D, // rn + @JvmField val gcrsToTrue: Matrix3D, // rbpn +) diff --git a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/PrecessionNutationMatrices.kt b/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/PrecessionNutationMatrices.kt deleted file mode 100644 index 30908f6bb..000000000 --- a/nebulosa-erfa/src/main/kotlin/nebulosa/erfa/PrecessionNutationMatrices.kt +++ /dev/null @@ -1,13 +0,0 @@ -package nebulosa.erfa - -import nebulosa.math.Angle -import nebulosa.math.Matrix3D - -data class PrecessionNutationMatrices( - @JvmField val meanObliquity: Angle, - @JvmField val frameBias: Matrix3D, - @JvmField val precession: Matrix3D, - @JvmField val biasPrecession: Matrix3D, - @JvmField val nutation: Matrix3D, - @JvmField val gcrsToTrue: Matrix3D, -) diff --git a/nebulosa-erfa/src/test/kotlin/ErfaTest.kt b/nebulosa-erfa/src/test/kotlin/ErfaTest.kt index 324e167a9..8ab08d14a 100644 --- a/nebulosa-erfa/src/test/kotlin/ErfaTest.kt +++ b/nebulosa-erfa/src/test/kotlin/ErfaTest.kt @@ -81,6 +81,12 @@ class ErfaTest : StringSpec() { theta shouldBe (-0.4636476090008061162 plusOrMinus 1e-14) phi shouldBe (0.2199879773954594463 plusOrMinus 1e-14) } + "eraS2c" { + val c = eraS2c(3.0123, -0.999) + c[0] shouldBe (-0.5366267667260523906 plusOrMinus 1e-12) + c[1] shouldBe (0.0697711109765145365 plusOrMinus 1e-12) + c[2] shouldBe (-0.8409302618566214041 plusOrMinus 1e-12) + } "eraP2s" { val (theta, phi, r) = eraP2s(100.0.au, (-50.0).au, 25.0.au) theta shouldBe (-0.4636476090008061162 plusOrMinus 1e-12) @@ -91,36 +97,36 @@ class ErfaTest : StringSpec() { eraAnpm((-4.0).rad) shouldBe (2.283185307179586477 plusOrMinus 1e-12) } "eraGc2Gde" { - val (e1, p1, h1) = eraGc2Gde(6378137.0.m, 1.0 / 298.257223563, 2e6.m, 3e6.m, 5.244e6.m) + val (e1, p1, h1) = eraGc2Gde(6378137.0, 1.0 / 298.257223563, 2e6, 3e6, 5.244e6) e1 shouldBe (0.9827937232473290680 plusOrMinus 1e-14) p1 shouldBe (0.97160184819075459 plusOrMinus 1e-14) - h1.toMeters shouldBe (331.4172461426059892 plusOrMinus 1e-8) + h1 shouldBe (331.4172461426059892 plusOrMinus 1e-8) - val (e2, p2, h2) = eraGc2Gde(6378137.0.m, 1.0 / 298.257222101, 2e6.m, 3e6.m, 5.244e6.m) + val (e2, p2, h2) = eraGc2Gde(6378137.0, 1.0 / 298.257222101, 2e6, 3e6, 5.244e6) e2 shouldBe (0.9827937232473290680 plusOrMinus 1e-14) p2 shouldBe (0.97160184820607853 plusOrMinus 1e-14) - h2.toMeters shouldBe (331.41731754844348 plusOrMinus 1e-8) + h2 shouldBe (331.41731754844348 plusOrMinus 1e-8) - val (e3, p3, h3) = eraGc2Gde(6378135.0.m, 1.0 / 298.26, 2e6.m, 3e6.m, 5.244e6.m) + val (e3, p3, h3) = eraGc2Gde(6378135.0, 1.0 / 298.26, 2e6, 3e6, 5.244e6) e3 shouldBe (0.9827937232473290680 plusOrMinus 1e-14) p3 shouldBe (0.9716018181101511937 plusOrMinus 1e-14) - h3.toMeters shouldBe (333.2770726130318123 plusOrMinus 1e-8) + h3 shouldBe (333.2770726130318123 plusOrMinus 1e-8) } "eraGd2Gc" { - val (x1, y1, z1) = eraGd2Gce(6378137.0.m, 1.0 / 298.257223563, 3.1.rad, (-0.5).rad, 2500.0.m) - x1.toMeters shouldBe (-5599000.5577049947 plusOrMinus 1e-7) - y1.toMeters shouldBe (233011.67223479203 plusOrMinus 1e-7) - z1.toMeters shouldBe (-3040909.4706983363 plusOrMinus 1e-7) + val (x1, y1, z1) = eraGd2Gce(6378137.0, 1.0 / 298.257223563, 3.1.rad, (-0.5).rad, 2500.0) + x1 shouldBe (-5599000.5577049947 plusOrMinus 1e-7) + y1 shouldBe (233011.67223479203 plusOrMinus 1e-7) + z1 shouldBe (-3040909.4706983363 plusOrMinus 1e-7) - val (x2, y2, z2) = eraGd2Gce(6378137.0.m, 1.0 / 298.257222101, 3.1.rad, (-0.5).rad, 2500.0.m) - x2.toMeters shouldBe (-5599000.5577260984 plusOrMinus 1e-7) - y2.toMeters shouldBe (233011.6722356702949 plusOrMinus 1e-7) - z2.toMeters shouldBe (-3040909.4706095476 plusOrMinus 1e-7) + val (x2, y2, z2) = eraGd2Gce(6378137.0, 1.0 / 298.257222101, 3.1.rad, (-0.5).rad, 2500.0) + x2 shouldBe (-5599000.5577260984 plusOrMinus 1e-7) + y2 shouldBe (233011.6722356702949 plusOrMinus 1e-7) + z2 shouldBe (-3040909.4706095476 plusOrMinus 1e-7) - val (x3, y3, z3) = eraGd2Gce(6378135.0.m, 1.0 / 298.26, 3.1.rad, (-0.5).rad, 2500.0.m) - x3.toMeters shouldBe (-5598998.7626301490 plusOrMinus 1e-7) - y3.toMeters shouldBe (233011.5975297822211 plusOrMinus 1e-7) - z3.toMeters shouldBe (-3040908.6861467111 plusOrMinus 1e-7) + val (x3, y3, z3) = eraGd2Gce(6378135.0, 1.0 / 298.26, 3.1.rad, (-0.5).rad, 2500.0) + x3 shouldBe (-5598998.7626301490 plusOrMinus 1e-7) + y3 shouldBe (233011.5975297822211 plusOrMinus 1e-7) + z3 shouldBe (-3040908.6861467111 plusOrMinus 1e-7) } "eraC2ixys" { val m = eraC2ixys(0.5791308486706011000e-3, 0.4020579816732961219e-4, (-0.1220040848472271978e-7).rad) @@ -149,35 +155,35 @@ class ErfaTest : StringSpec() { "eraApcs" { val astro = eraApcs( 2456384.5, 0.970031644, - (-1836024.09).m, 1056607.72.m, (-5998795.26).m, - -77.0361767, -133.310856, 0.0971855934, - (-0.974170438).au, (-0.211520082).au, (-0.0917583024).au, - 0.00364365824.auDay, (-0.0154287319).auDay, (-0.00668922024).auDay, - (-0.973458265).au, (-0.209215307).au, (-0.0906996477).au, + Vector3D(-1836024.09, 1056607.7, -5998795.26), + Vector3D(-77.0361767, -133.310856, 0.0971855934), + Vector3D(-0.974170438, -0.211520082, -0.0917583024), + Vector3D(0.00364365824, -0.0154287319, -0.00668922024), + Vector3D(-0.973458265, -0.209215307, -0.0906996477), ) astro.pmt shouldBe (13.25248468622587269 plusOrMinus 1e-11) astro.eb.x shouldBe (-0.9741827110629881886 plusOrMinus 1e-12) astro.eb.y shouldBe (-0.2115130190136415986 plusOrMinus 1e-12) astro.eb.z shouldBe (-0.09179840186954412099 plusOrMinus 1e-12) - astro.ehx shouldBe (-0.9736425571689454706 plusOrMinus 1e-12) - astro.ehy shouldBe (-0.2092452125850435930 plusOrMinus 1e-12) - astro.ehz shouldBe (-0.09075578152248299218 plusOrMinus 1e-12) + astro.eh.x shouldBe (-0.9736425571689454706 plusOrMinus 1e-12) + astro.eh.y shouldBe (-0.2092452125850435930 plusOrMinus 1e-12) + astro.eh.z shouldBe (-0.09075578152248299218 plusOrMinus 1e-12) astro.em shouldBe (0.9998233241709796859 plusOrMinus 1e-12) - astro.vx shouldBe (0.2078704993282685510e-4 plusOrMinus 1e-16) - astro.vy shouldBe (-0.8955360106989405683e-4 plusOrMinus 1e-16) - astro.vz shouldBe (-0.3863338994289409097e-4 plusOrMinus 1e-16) + astro.v.x shouldBe (0.2078704993282685510e-4 plusOrMinus 1e-16) + astro.v.y shouldBe (-0.8955360106989405683e-4 plusOrMinus 1e-16) + astro.v.z shouldBe (-0.3863338994289409097e-4 plusOrMinus 1e-16) astro.bm1 shouldBe (0.9999999950277561237 plusOrMinus 1e-12) } "eraApco" { val astro = eraApco( 2456384.5, 0.970031644, - (-0.974170438).au, (-0.211520082).au, (-0.0917583024).au, - 0.00364365824.auDay, (-0.0154287319).auDay, (-0.00668922024).auDay, - (-0.973458265).au, (-0.209215307).au, (-0.0906996477).au, + Vector3D(-0.974170438, -0.211520082, -0.0917583024), + Vector3D(0.00364365824, -0.0154287319, -0.00668922024), + Vector3D(-0.973458265, -0.209215307, -0.0906996477), 0.0013122272, -2.92808623e-5, 3.05749468e-8.rad, 3.14540971.rad, (-0.527800806).rad, (-1.2345856).rad, - 2738.0.m, + 2738.0, 2.47230737e-7.rad, 1.82640464e-6.rad, (-3.01974337e-11).rad, 0.000201418779.rad, (-2.36140831e-7).rad, ) @@ -186,13 +192,13 @@ class ErfaTest : StringSpec() { astro.eb.x shouldBe (-0.9741827110630322720 plusOrMinus 1e-12) astro.eb.y shouldBe (-0.2115130190135344832 plusOrMinus 1e-12) astro.eb.z shouldBe (-0.09179840186949532298 plusOrMinus 1e-12) - astro.ehx shouldBe (-0.9736425571689739035 plusOrMinus 1e-12) - astro.ehy shouldBe (-0.2092452125849330936 plusOrMinus 1e-12) - astro.ehz shouldBe (-0.09075578152243272599 plusOrMinus 1e-12) + astro.eh.x shouldBe (-0.9736425571689739035 plusOrMinus 1e-12) + astro.eh.y shouldBe (-0.2092452125849330936 plusOrMinus 1e-12) + astro.eh.z shouldBe (-0.09075578152243272599 plusOrMinus 1e-12) astro.em shouldBe (0.9998233241709957653 plusOrMinus 1e-12) - astro.vx shouldBe (0.2078704992916728762e-4 plusOrMinus 1e-16) - astro.vy shouldBe (-0.8955360107151952319e-4 plusOrMinus 1e-16) - astro.vz shouldBe (-0.3863338994288951082e-4 plusOrMinus 1e-16) + astro.v.x shouldBe (0.2078704992916728762e-4 plusOrMinus 1e-16) + astro.v.y shouldBe (-0.8955360107151952319e-4 plusOrMinus 1e-16) + astro.v.z shouldBe (-0.3863338994288951082e-4 plusOrMinus 1e-16) astro.bm1 shouldBe (0.9999999950277561236 plusOrMinus 1e-12) astro.bpn[0, 0] shouldBe (0.9999991390295159156 plusOrMinus 1e-12) astro.bpn[1, 0] shouldBe (0.4978650072505016932e-7 plusOrMinus 1e-12) @@ -282,22 +288,22 @@ class ErfaTest : StringSpec() { "eraApcg" { val astrom = eraApcg( 2456165.5, 0.401182685, - 0.901310875.au, (-0.417402664).au, (-0.180982288).au, - 0.00742727954.auDay, 0.0140507459.auDay, 0.00609045792.auDay, - 0.903358544.au, (-0.415395237).au, (-0.180084014).au + Vector3D(0.901310875, -0.417402664, -0.180982288), + Vector3D(0.00742727954, 0.0140507459, 0.00609045792), + Vector3D(0.903358544, -0.415395237, -0.180084014), ) astrom.pmt shouldBe (12.65133794027378508 plusOrMinus 1e-11) astrom.eb.x shouldBe (0.901310875 plusOrMinus 1e-12) astrom.eb.y shouldBe (-0.417402664 plusOrMinus 1e-12) astrom.eb.z shouldBe (-0.180982288 plusOrMinus 1e-12) - astrom.ehx shouldBe (0.8940025429324143045 plusOrMinus 1e-12) - astrom.ehy shouldBe (-0.4110930268679817955 plusOrMinus 1e-12) - astrom.ehz shouldBe (-0.1782189004872870264 plusOrMinus 1e-12) + astrom.eh.x shouldBe (0.8940025429324143045 plusOrMinus 1e-12) + astrom.eh.y shouldBe (-0.4110930268679817955 plusOrMinus 1e-12) + astrom.eh.z shouldBe (-0.1782189004872870264 plusOrMinus 1e-12) astrom.em shouldBe (1.010465295811013146 plusOrMinus 1e-12) - astrom.vx shouldBe (0.4289638913597693554e-4 plusOrMinus 1e-16) - astrom.vy shouldBe (0.8115034051581320575e-4 plusOrMinus 1e-16) - astrom.vz shouldBe (0.3517555136380563427e-4 plusOrMinus 1e-16) + astrom.v.x shouldBe (0.4289638913597693554e-4 plusOrMinus 1e-16) + astrom.v.y shouldBe (0.8115034051581320575e-4 plusOrMinus 1e-16) + astrom.v.z shouldBe (0.3517555136380563427e-4 plusOrMinus 1e-16) astrom.bm1 shouldBe (0.9999999951686012981 plusOrMinus 1e-12) } "eraEpv00" { @@ -325,13 +331,13 @@ class ErfaTest : StringSpec() { astrom.eb.x shouldBe (0.9013108747340644755 plusOrMinus 1e-12) astrom.eb.y shouldBe (-0.4174026640406119957 plusOrMinus 1e-12) astrom.eb.z shouldBe (-0.1809822877867817771 plusOrMinus 1e-12) - astrom.ehx shouldBe (0.8940025429255499549 plusOrMinus 1e-12) - astrom.ehy shouldBe (-0.4110930268331896318 plusOrMinus 1e-12) - astrom.ehz shouldBe (-0.1782189006019749850 plusOrMinus 1e-12) + astrom.eh.x shouldBe (0.8940025429255499549 plusOrMinus 1e-12) + astrom.eh.y shouldBe (-0.4110930268331896318 plusOrMinus 1e-12) + astrom.eh.z shouldBe (-0.1782189006019749850 plusOrMinus 1e-12) astrom.em shouldBe (1.010465295964664178 plusOrMinus 1e-12) - astrom.vx shouldBe (0.4289638912941341125e-4 plusOrMinus 1e-16) - astrom.vy shouldBe (0.8115034032405042132e-4 plusOrMinus 1e-16) - astrom.vz shouldBe (0.3517555135536470279e-4 plusOrMinus 1e-16) + astrom.v.x shouldBe (0.4289638912941341125e-4 plusOrMinus 1e-16) + astrom.v.y shouldBe (0.8115034032405042132e-4 plusOrMinus 1e-16) + astrom.v.z shouldBe (0.3517555135536470279e-4 plusOrMinus 1e-16) astrom.bm1 shouldBe (0.9999999951686013142 plusOrMinus 1e-12) } "eraAe2hd" { @@ -365,7 +371,7 @@ class ErfaTest : StringSpec() { dt shouldBe (-0.1280368005936998991e-2 plusOrMinus 1E-15) } "eraPvtob" { - val (p, v) = eraPvtob(2.0.rad, 0.5.rad, 3000.0.m, 1e-6.rad, (-0.5e-6).rad, 1e-8.rad, 5.0.rad) + val (p, v) = eraPvtob(2.0.rad, 0.5.rad, 3000.0, 1e-6.rad, (-0.5e-6).rad, 1e-8.rad, 5.0.rad) p[0] shouldBe (4225081.367071159207 plusOrMinus 1e-5) p[1] shouldBe (3681943.215856198144 plusOrMinus 1e-5) p[2] shouldBe (3041149.399241260785 plusOrMinus 1e-5) @@ -776,7 +782,7 @@ class ErfaTest : StringSpec() { rbp[2, 2] shouldBe (0.9999999285680153377 plusOrMinus 1e-12) } "eraPn00" { - val (epsa, rb, rp, rbp, rn, rbpn) = eraPn00(2400000.5, 53736.0, -0.9632552291149335877e-5, 0.4063197106621141414e-4) + val (_, _, epsa, rb, rp, rbp, rn, rbpn) = eraPn00(2400000.5, 53736.0, -0.9632552291149335877e-5, 0.4063197106621141414e-4) epsa shouldBe (0.4090791789404229916 plusOrMinus 1e-12) @@ -856,5 +862,272 @@ class ErfaTest : StringSpec() { rc2t[2, 1] shouldBe (0.3977631855605078674 plusOrMinus 1e-12) rc2t[2, 2] shouldBe (0.9174875068792735362 plusOrMinus 1e-12) } + "eraS00" { + val s = eraS00(2400000.5, 53736.0, 0.5791308486706011000e-3, 0.4020579816732961219e-4) + s shouldBe (-0.1220036263270905693e-7 plusOrMinus 1e-18) + } + "eraS00b" { + val s = eraS00b(2400000.5, 52541.0) + s shouldBe (-0.1340695782951026584e-7 plusOrMinus 1e-18) + } + "eraS00a" { + val s = eraS00a(2400000.5, 52541.0) + s shouldBe (-0.1340684448919163584e-7 plusOrMinus 1e-18) + } + "eraApco13" { + val (astrom, eo) = eraApco13( + 2456384.5, 0.969254051, 0.1550675, + -0.527800806, -1.2345856, 2738.0, + 2.47230737e-7, 1.82640464e-6, + 731.0, 12.8, 0.59, 0.55 + ) + + astrom.pmt shouldBe (13.25248468622475727 plusOrMinus 1e-11) + astrom.eb.x shouldBe (-0.9741827107320875162 plusOrMinus 1e-12) + astrom.eb.y shouldBe (-0.2115130190489716682 plusOrMinus 1e-12) + astrom.eb.z shouldBe (-0.09179840189496755339 plusOrMinus 1e-12) + astrom.eh.x shouldBe (-0.9736425572586935247 plusOrMinus 1e-12) + astrom.eh.y shouldBe (-0.2092452121603336166 plusOrMinus 1e-12) + astrom.eh.z shouldBe (-0.09075578153885665295 plusOrMinus 1e-12) + astrom.em shouldBe (0.9998233240913898141 plusOrMinus 1e-12) + astrom.v.x shouldBe (0.2078704994520489246e-4 plusOrMinus 1e-16) + astrom.v.y shouldBe (-0.8955360133238868938e-4 plusOrMinus 1e-16) + astrom.v.z shouldBe (-0.3863338993055887398e-4 plusOrMinus 1e-16) + astrom.bm1 shouldBe (0.9999999950277561004 plusOrMinus 1e-12) + astrom.bpn[0, 0] shouldBe (0.9999991390295147999 plusOrMinus 1e-12) + astrom.bpn[1, 0] shouldBe (0.4978650075315529277e-7 plusOrMinus 1e-12) + astrom.bpn[2, 0] shouldBe (0.001312227200850293372 plusOrMinus 1e-12) + astrom.bpn[0, 1] shouldBe (-0.1136336652812486604e-7 plusOrMinus 1e-12) + astrom.bpn[1, 1] shouldBe (0.9999999995713154865 plusOrMinus 1e-12) + astrom.bpn[2, 1] shouldBe (-0.2928086230975367296e-4 plusOrMinus 1e-12) + astrom.bpn[0, 2] shouldBe (-0.001312227201745553566 plusOrMinus 1e-12) + astrom.bpn[1, 2] shouldBe (0.2928082218847679162e-4 plusOrMinus 1e-12) + astrom.bpn[2, 2] shouldBe (0.9999991386008312212 plusOrMinus 1e-12) + astrom.along shouldBe (-0.5278008060295995733 plusOrMinus 1e-12) + astrom.xpl shouldBe (0.1133427418130752958e-5 plusOrMinus 1e-17) + astrom.ypl shouldBe (0.1453347595780646207e-5 plusOrMinus 1e-17) + astrom.sphi shouldBe (-0.9440115679003211329 plusOrMinus 1e-12) + astrom.cphi shouldBe (0.3299123514971474711 plusOrMinus 1e-12) + astrom.diurab shouldBeExactly 0.0 + astrom.eral shouldBe (2.617608909189664000 plusOrMinus 1e-12) + astrom.refa shouldBe (0.2014187785940396921e-3 plusOrMinus 1e-15) + astrom.refb shouldBe (-0.2361408314943696227e-6 plusOrMinus 1e-18) + eo shouldBe (-0.003020548354802412839 plusOrMinus 1e-14) + } + "eraPmpx" { + val pco = eraPmpx(1.234, 0.789, 1e-5, -2e-5, 1e-2, 10.0, 8.75, Vector3D(0.9, 0.4, 0.1)) + pco[0] shouldBe (0.2328137623960308438 plusOrMinus 1e-12) + pco[1] shouldBe (0.6651097085397855328 plusOrMinus 1e-12) + pco[2] shouldBe (0.7095257765896359837 plusOrMinus 1e-12) + } + "eraAtciq" { + val (astrom) = eraApci13(2456165.5, 0.401182685) + val (ri, di) = eraAtciq(2.71, 0.174, 1e-5, 5e-6, 0.1, 55.0, astrom) + ri shouldBe (2.710121572968696744 plusOrMinus 1e-12) + di shouldBe (0.1729371367219539137 plusOrMinus 1e-12) + } + "eraAtciqz" { + val (astrom) = eraApci13(2456165.5, 0.401182685) + val (ri, di) = eraAtciqz(2.71, 0.174, astrom) + ri shouldBe (2.709994899247256984 plusOrMinus 1e-12) + di shouldBe (0.1728740720984931891 plusOrMinus 1e-12) + } + "eraAtci13" { + val (ri, di, eo) = eraAtci13(2.71, 0.174, 1e-5, 5e-6, 0.1, 55.0, 2456165.5, 0.401182685) + ri shouldBe (2.710121572968696744 plusOrMinus 1e-12) + di shouldBe (0.1729371367219539137 plusOrMinus 1e-12) + eo shouldBe (-0.002900618712657375647 plusOrMinus 1e-14) + } + "eraApci13" { + val (astrom, eo) = eraApci13(2456165.5, 0.401182685) + + astrom.pmt shouldBe (12.65133794027378508 plusOrMinus 1e-11) + astrom.eb[0] shouldBe (0.9013108747340644755 plusOrMinus 1e-12) + astrom.eb[1] shouldBe (-0.4174026640406119957 plusOrMinus 1e-12) + astrom.eb[2] shouldBe (-0.1809822877867817771 plusOrMinus 1e-12) + astrom.eh[0] shouldBe (0.8940025429255499549 plusOrMinus 1e-12) + astrom.eh[1] shouldBe (-0.4110930268331896318 plusOrMinus 1e-12) + astrom.eh[2] shouldBe (-0.1782189006019749850 plusOrMinus 1e-12) + astrom.em shouldBe (1.010465295964664178 plusOrMinus 1e-12) + astrom.v[0] shouldBe (0.4289638912941341125e-4 plusOrMinus 1e-16) + astrom.v[1] shouldBe (0.8115034032405042132e-4 plusOrMinus 1e-16) + astrom.v[2] shouldBe (0.3517555135536470279e-4 plusOrMinus 1e-16) + astrom.bm1 shouldBe (0.9999999951686013142 plusOrMinus 1e-12) + astrom.bpn[0, 0] shouldBe (0.9999992060376761710 plusOrMinus 1e-12) + astrom.bpn[1, 0] shouldBe (0.4124244860106037157e-7 plusOrMinus 1e-12) + astrom.bpn[2, 0] shouldBe (0.1260128571051709670e-2 plusOrMinus 1e-12) + astrom.bpn[0, 1] shouldBe (-0.1282291987222130690e-7 plusOrMinus 1e-12) + astrom.bpn[1, 1] shouldBe (0.9999999997456835325 plusOrMinus 1e-12) + astrom.bpn[2, 1] shouldBe (-0.2255288829420524935e-4 plusOrMinus 1e-12) + astrom.bpn[0, 2] shouldBe (-0.1260128571661374559e-2 plusOrMinus 1e-12) + astrom.bpn[1, 2] shouldBe (0.2255285422953395494e-4 plusOrMinus 1e-12) + astrom.bpn[2, 2] shouldBe (0.9999992057833604343 plusOrMinus 1e-12) + eo shouldBe (-0.2900618712657375647e-2 plusOrMinus 1e-12) + } + "eraLdsun" { + val p = Vector3D(-0.763276255, -0.608633767, -0.216735543) + val e = Vector3D(-0.973644023, -0.20925523, -0.0907169552) + val p1 = eraLdsun(p, e, 0.999809214) + p1[0] shouldBe (-0.7632762580731413169 plusOrMinus 1e-12) + p1[1] shouldBe (-0.6086337635262647900 plusOrMinus 1e-12) + p1[2] shouldBe (-0.2167355419322321302 plusOrMinus 1e-12) + } + "eraLd" { + val p = Vector3D(-0.763276255, -0.608633767, -0.216735543) + val q = Vector3D(-0.763276255, -0.608633767, -0.216735543) + val e = Vector3D(0.76700421, 0.605629598, 0.211937094) + val p1 = eraLd(0.00028574, p, q, e, 8.91276983, 3e-10) + p1[0] shouldBe (-0.7632762548968159627 plusOrMinus 1e-12) + p1[1] shouldBe (-0.6086337670823762701 plusOrMinus 1e-12) + p1[2] shouldBe (-0.2167355431320546947 plusOrMinus 1e-12) + } + "eraApci" { + val ebp = Vector3D(0.901310875, -0.417402664, -0.180982288) + val ebv = Vector3D(0.00742727954, 0.0140507459, 0.00609045792) + val ehp = Vector3D(0.903358544, -0.415395237, -0.180084014) + val astrom = eraApci(2456165.5, 0.401182685, ebp, ebv, ehp, 0.0013122272, -2.92808623e-5, 3.05749468e-8) + + astrom.pmt shouldBe (12.65133794027378508 plusOrMinus 1e-11) + astrom.eb[0] shouldBe (0.901310875 plusOrMinus 1e-12) + astrom.eb[1] shouldBe (-0.417402664 plusOrMinus 1e-12) + astrom.eb[2] shouldBe (-0.180982288 plusOrMinus 1e-12) + astrom.eh[0] shouldBe (0.8940025429324143045 plusOrMinus 1e-12) + astrom.eh[1] shouldBe (-0.4110930268679817955 plusOrMinus 1e-12) + astrom.eh[2] shouldBe (-0.1782189004872870264 plusOrMinus 1e-12) + astrom.em shouldBe (1.010465295811013146 plusOrMinus 1e-12) + astrom.v[0] shouldBe (0.4289638913597693554e-4 plusOrMinus 1e-16) + astrom.v[1] shouldBe (0.8115034051581320575e-4 plusOrMinus 1e-16) + astrom.v[2] shouldBe (0.3517555136380563427e-4 plusOrMinus 1e-16) + astrom.bm1 shouldBe (0.9999999951686012981 plusOrMinus 1e-12) + astrom.bpn[0, 0] shouldBe (0.9999991390295159156 plusOrMinus 1e-12) + astrom.bpn[1, 0] shouldBe (0.4978650072505016932e-7 plusOrMinus 1e-12) + astrom.bpn[2, 0] shouldBe (0.1312227200000000000e-2 plusOrMinus 1e-12) + astrom.bpn[0, 1] shouldBe (-0.1136336653771609630e-7 plusOrMinus 1e-12) + astrom.bpn[1, 1] shouldBe (0.9999999995713154868 plusOrMinus 1e-12) + astrom.bpn[2, 1] shouldBe (-0.2928086230000000000e-4 plusOrMinus 1e-12) + astrom.bpn[0, 2] shouldBe (-0.1312227200895260194e-2 plusOrMinus 1e-12) + astrom.bpn[1, 2] shouldBe (0.2928082217872315680e-4 plusOrMinus 1e-12) + astrom.bpn[2, 2] shouldBe (0.9999991386008323373 plusOrMinus 1e-12) + } + "eraApcs13" { + val p = Vector3D(-6241497.16, 401346.896, -1251136.04) + val v = Vector3D(-29.264597, -455.021831, 0.0266151194) + val astrom = eraApcs13(2456165.5, 0.401182685, p, v) + + astrom.pmt shouldBe (12.65133794027378508 plusOrMinus 1e-11) + astrom.eb[0] shouldBe (0.9012691529025250644 plusOrMinus 1e-12) + astrom.eb[1] shouldBe (-0.4173999812023194317 plusOrMinus 1e-12) + astrom.eb[2] shouldBe (-0.1809906511146429670 plusOrMinus 1e-12) + astrom.eh[0] shouldBe (0.8939939101760130792 plusOrMinus 1e-12) + astrom.eh[1] shouldBe (-0.4111053891734021478 plusOrMinus 1e-12) + astrom.eh[2] shouldBe (-0.1782336880636997374 plusOrMinus 1e-12) + astrom.em shouldBe (1.010428384373491095 plusOrMinus 1e-12) + astrom.v[0] shouldBe (0.4279877294121697570e-4 plusOrMinus 1e-16) + astrom.v[1] shouldBe (0.7963255087052120678e-4 plusOrMinus 1e-16) + astrom.v[2] shouldBe (0.3517564013384691531e-4 plusOrMinus 1e-16) + astrom.bm1 shouldBe (0.9999999952947980978 plusOrMinus 1e-12) + astrom.bpn shouldBe Matrix3D.IDENTITY + } + "eraAtioq" { + val astrom = + eraApio13(2456384.5, 0.969254051, 0.1550675, -0.527800806, -1.2345856, 2738.0, 2.47230737e-7, 1.82640464e-6, 731.0, 12.8, 0.59, 0.55) + val (aob, zob, hob, dob, rob) = eraAtioq(2.710121572969038991, 0.1729371367218230438, astrom) + + aob shouldBe (0.9233952224895122499e-1 plusOrMinus 1e-12) + zob shouldBe (1.407758704513549991 plusOrMinus 1e-12) + hob shouldBe (-0.9247619879881698140e-1 plusOrMinus 1e-12) + dob shouldBe (0.1717653435756234676 plusOrMinus 1e-12) + rob shouldBe (2.710085107988480746 plusOrMinus 1e-12) + } + "eraApio13" { + val astrom = + eraApio13(2456384.5, 0.969254051, 0.1550675, -0.527800806, -1.2345856, 2738.0, 2.47230737e-7, 1.82640464e-6, 731.0, 12.8, 0.59, 0.55) + + astrom.along shouldBe (-0.5278008060295995733 plusOrMinus 1e-12) + astrom.xpl shouldBe (0.1133427418130752958e-5 plusOrMinus 1e-17) + astrom.ypl shouldBe (0.1453347595780646207e-5 plusOrMinus 1e-17) + astrom.sphi shouldBe (-0.9440115679003211329 plusOrMinus 1e-12) + astrom.cphi shouldBe (0.3299123514971474711 plusOrMinus 1e-12) + astrom.diurab shouldBe (0.5135843661699913529e-6 plusOrMinus 1e-12) + astrom.eral shouldBe (2.617608909189664000 plusOrMinus 1e-12) + astrom.refa shouldBe (0.2014187785940396921e-3 plusOrMinus 1e-15) + astrom.refb shouldBe (-0.2361408314943696227e-6 plusOrMinus 1e-18) + } + "eraApio" { + val astrom = + eraApio(-3.01974337e-11, 3.14540971, -0.527800806, -1.2345856, 2738.0, 2.47230737e-7, 1.82640464e-6, 0.000201418779, -2.36140831e-7) + + astrom.along shouldBe (-0.5278008060295995734 plusOrMinus 1e-12) + astrom.xpl shouldBe (0.1133427418130752958e-5 plusOrMinus 1e-17) + astrom.ypl shouldBe (0.1453347595780646207e-5 plusOrMinus 1e-17) + astrom.sphi shouldBe (-0.9440115679003211329 plusOrMinus 1e-12) + astrom.cphi shouldBe (0.3299123514971474711 plusOrMinus 1e-12) + astrom.diurab shouldBe (0.5135843661699913529e-6 plusOrMinus 1e-12) + astrom.eral shouldBe (2.617608903970400427 plusOrMinus 1e-12) + astrom.refa shouldBe (0.2014187790000000000e-3 plusOrMinus 1e-15) + astrom.refb shouldBe (-0.2361408310000000000e-6 plusOrMinus 1e-18) + } + "eraAtco13" { + val (b, eo) = eraAtco13( + 2.71, 0.174, 1e-5, 5e-6, 0.1, 55.0, 2456384.5, 0.969254051, + 0.1550675, -0.527800806, -1.2345856, 2738.0, 2.47230737e-7, 1.82640464e-6, 731.0, 12.8, + 0.59, 0.55 + ) + + val (aob, zob, hob, dob, rob) = b + + aob shouldBe (0.9251774485485515207e-1 plusOrMinus 1e-12) + zob shouldBe (1.407661405256499357 plusOrMinus 1e-12) + hob shouldBe (-0.9265154431529724692e-1 plusOrMinus 1e-12) + dob shouldBe (0.1716626560072526200 plusOrMinus 1e-12) + rob shouldBe (2.710260453504961012 plusOrMinus 1e-12) + eo shouldBe (-0.003020548354802412839 plusOrMinus 1e-14) + } + "eraAticq" { + val (astrom) = eraApci13(2456165.5, 0.401182685) + val (ri, di) = eraAticq(2.710121572969038991, 0.1729371367218230438, astrom) + ri shouldBe (2.710126504531716819 plusOrMinus 1e-12) + di shouldBe (0.1740632537627034482 plusOrMinus 1e-12) + } + "eraAtic13" { + val (rc, dc, eo) = eraAtic13(2.710121572969038991, 0.1729371367218230438, 2456165.5, 0.401182685) + + rc shouldBe (2.710126504531716819 plusOrMinus 1e-12) + dc shouldBe (0.1740632537627034482 plusOrMinus 1e-12) + eo shouldBe (-0.002900618712657375647 plusOrMinus 1e-14) + } + "eraS2pv" { + val pv = eraS2pv(-3.21, 0.123, 0.456, -7.8e-6, 9.01e-6, -1.23e-5) + + pv.position[0] shouldBe (-0.4514964673880165228 plusOrMinus 1e-12) + pv.position[1] shouldBe (0.0309339427734258688 plusOrMinus 1e-12) + pv.position[2] shouldBe (0.0559466810510877933 plusOrMinus 1e-12) + + pv.velocity[0] shouldBe (0.1292270850663260170e-4 plusOrMinus 1e-16) + pv.velocity[1] shouldBe (0.2652814182060691422e-5 plusOrMinus 1e-16) + pv.velocity[2] shouldBe (0.2568431853930292259e-5 plusOrMinus 1e-16) + } + "eraStarpv" { + val pv = eraStarpv(0.01686756, -1.093989828, -1.78323516e-5, 2.336024047e-6, 0.74723, -21.6) + + pv.position[0] shouldBe (126668.5912743160601 plusOrMinus 1e-10) + pv.position[1] shouldBe (2136.792716839935195 plusOrMinus 1e-12) + pv.position[2] shouldBe (-245251.2339876830091 plusOrMinus 1e-10) + + pv.velocity[0] shouldBe (-0.4051854008955659551e-2 plusOrMinus 1e-13) + pv.velocity[1] shouldBe (-0.6253919754414777970e-2 plusOrMinus 1e-15) + pv.velocity[2] shouldBe (0.1189353714588109341e-1 plusOrMinus 1e-13) + } + "eraStarpvMod" { + val pv = eraStarpvMod(0.01686756, -1.093989828, -1.78323516e-5, 2.336024047e-6, 0.74723.arcsec, (-21.6).kms) + + pv.position[0] shouldBe (126668.5912743160601 plusOrMinus 1e-10) + pv.position[1] shouldBe (2136.792716839935195 plusOrMinus 1e-12) + pv.position[2] shouldBe (-245251.2339876830091 plusOrMinus 1e-10) + + pv.velocity[0] shouldBe (-0.4051854008955659551e-2 plusOrMinus 1e-13) + pv.velocity[1] shouldBe (-0.6253919754414777970e-2 plusOrMinus 1e-15) + pv.velocity[2] shouldBe (0.1189353714588109341e-1 plusOrMinus 1e-13) + } } } diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/Fits.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/Fits.kt index 8399a7c1a..4ecdfc0f3 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/Fits.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/Fits.kt @@ -1,27 +1,17 @@ package nebulosa.fits import nebulosa.io.SeekableSource -import nebulosa.io.seekableSource import okio.Sink -import java.io.Closeable import java.io.EOFException -import java.io.File -import java.nio.file.Path +import java.util.* -class Fits private constructor( - val source: SeekableSource, - private val hdus: ArrayList>, -) : List> by hdus, Closeable { +open class Fits : LinkedList> { - constructor(source: SeekableSource) : this(source, ArrayList(4)) + constructor() : super() - constructor(path: File) : this(path.seekableSource()) + constructor(hdus: Collection>) : super(hdus) - constructor(path: Path) : this(path.toFile()) - - constructor(path: String) : this(File(path)) - - fun readHdu(): Hdu<*>? { + fun readHdu(source: SeekableSource): Hdu<*>? { return try { return FitsIO.read(source).also(::add) } catch (ignored: EOFException) { @@ -29,29 +19,13 @@ class Fits private constructor( } } - fun read() { + fun read(source: SeekableSource) { while (true) { - readHdu() ?: break + readHdu(source) ?: break } } - fun add(hdu: Hdu<*>) { - hdus.add(hdu) - } - - fun remove(hdu: Hdu<*>): Boolean { - return hdus.remove(hdu) - } - - fun clear() { - hdus.clear() - } - fun writeTo(sink: Sink) { - hdus.forEach { FitsIO.write(sink, it) } - } - - override fun close() { - source.close() + forEach { FitsIO.write(sink, it) } } } diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt index 71bdd74fc..50dd6a35c 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt @@ -1,24 +1,27 @@ +@file:Suppress("NOTHING_TO_INLINE") + package nebulosa.fits +import nebulosa.io.SeekableSource import nebulosa.math.Angle import nebulosa.math.deg +import java.io.File +import java.nio.file.Path import java.time.Duration import java.time.LocalDateTime -@Suppress("NOTHING_TO_INLINE") inline fun Header.clone() = Header(this) inline val Header.naxis get() = getInt(Standard.NAXIS, -1) -@Suppress("NOTHING_TO_INLINE") inline fun Header.naxis(n: Int) = getInt(Standard.NAXISn.n(n), 0) inline val Header.width - get() = naxis(1) + get() = getInt(Standard.NAXIS1, 0) inline val Header.height - get() = naxis(2) + get() = getInt(Standard.NAXIS2, 0) val Header.rightAscension get() = Angle(getStringOrNull(Standard.RA), isHours = true, decimalIsHours = false).takeIf { it.isFinite() } @@ -73,3 +76,11 @@ inline val Header.frame inline val Header.instrument get() = getStringOrNull(Standard.INSTRUME)?.ifBlank { null }?.trim() + +inline fun SeekableSource.fits() = Fits().also { it.read(this) } + +inline fun String.fits() = FitsPath(this).also(FitsPath::read) + +inline fun Path.fits() = FitsPath(this).also(FitsPath::read) + +inline fun File.fits() = FitsPath(this).also(FitsPath::read) diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsPath.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsPath.kt new file mode 100644 index 000000000..84d4d5704 --- /dev/null +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsPath.kt @@ -0,0 +1,34 @@ +package nebulosa.fits + +import nebulosa.io.seekableSink +import nebulosa.io.seekableSource +import java.io.Closeable +import java.io.File +import java.nio.file.Path + +class FitsPath(path: Path) : Fits(), Closeable { + + private val source = path.seekableSource() + private val sink = path.seekableSink() + + constructor(file: File) : this(file.toPath()) + + constructor(path: String) : this(Path.of(path)) + + fun read() { + return read(source) + } + + fun readHdu(): Hdu<*>? { + return readHdu(source) + } + + fun writeTo() { + writeTo(sink) + } + + override fun close() { + source.close() + sink.close() + } +} diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FloatArrayImageData.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FloatImageData.kt similarity index 65% rename from nebulosa-fits/src/main/kotlin/nebulosa/fits/FloatArrayImageData.kt rename to nebulosa-fits/src/main/kotlin/nebulosa/fits/FloatImageData.kt index cb6733c16..0c62de7fc 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FloatArrayImageData.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FloatImageData.kt @@ -6,10 +6,10 @@ import okio.Sink import java.nio.ByteBuffer @Suppress("ArrayInDataClass") -data class FloatArrayImageData( +data class FloatImageData( override val width: Int, override val height: Int, - val data: FloatArray, + @JvmField val data: FloatArray = FloatArray(width * height), ) : ImageData { override val bitpix = Bitpix.FLOAT @@ -19,22 +19,22 @@ data class FloatArrayImageData( val stride = ByteBuffer.allocate(strideSizeInBytes) repeat(height) { - val offset = it * width + var offset = it * width stride.clear() - for (i in offset until offset + width) stride.putFloat(data[i]) + repeat(width) { stride.putFloat(data[offset++]) } stride.flip() block(stride) } } override fun writeTo(sink: Sink): Long { - return Buffer().use { buffer -> + return Buffer().use { b -> var byteCount = 0L repeat(height) { - val offset = it * width - for (i in offset until offset + width) buffer.writeFloat(data[i]) - byteCount += buffer.readAll(sink) + var offset = it * width + repeat(width) { b.writeFloat(data[offset++]) } + byteCount += b.readAll(sink) } byteCount diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/Header.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/Header.kt index b3279bbeb..0f5132f13 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/Header.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/Header.kt @@ -18,7 +18,7 @@ open class Header internal constructor(@JvmField internal val cards: LinkedList< constructor(header: Header) : this(LinkedList(header.cards)) - fun readOnly(): Header = ReadOnlyHeader(this) + open fun readOnly(): Header = ReadOnlyHeader(this) override fun clear() { cards.clear() @@ -77,11 +77,11 @@ open class Header internal constructor(@JvmField internal val cards: LinkedList< override fun add(key: FitsHeader, value: Boolean): HeaderCard { checkType(key, ValueType.LOGICAL) - val card = HeaderCard.create(key, value) - val index = cards.indexOfFirst { it.key == key.key } - if (index >= 0) cards[index] = card - else cards.add(card) - return card + return HeaderCard.create(key, value).also(::add) + } + + override fun add(key: String, value: Boolean, comment: String): HeaderCard { + return HeaderCard.create(key, value, comment).also(::add) } override fun add(key: FitsHeader, value: Int): HeaderCard { @@ -89,16 +89,28 @@ open class Header internal constructor(@JvmField internal val cards: LinkedList< return HeaderCard.create(key, value).also(::add) } + override fun add(key: String, value: Int, comment: String): HeaderCard { + return HeaderCard.create(key, value, comment).also(::add) + } + override fun add(key: FitsHeader, value: Double): HeaderCard { checkType(key, ValueType.REAL) return HeaderCard.create(key, value).also(::add) } + override fun add(key: String, value: Double, comment: String): HeaderCard { + return HeaderCard.create(key, value, comment).also(::add) + } + override fun add(key: FitsHeader, value: String): HeaderCard { checkType(key, ValueType.STRING) return HeaderCard.create(key, value).also(::add) } + override fun add(key: String, value: String, comment: String): HeaderCard { + return HeaderCard.create(key, value, comment).also(::add) + } + override fun add(card: HeaderCard) { if (!card.isKeyValuePair) cards.add(card) else { diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/HeaderCard.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/HeaderCard.kt index d913d85a3..27d5e392f 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/HeaderCard.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/HeaderCard.kt @@ -31,7 +31,7 @@ data class HeaderCard( val isDecimalType get() = DECIMAL_TYPES.any { it === type } - || BigDecimal::class.java.isAssignableFrom(type) + || BigDecimal::class.java.isAssignableFrom(type) val isIntegerType get() = INTEGET_TYPES.any { it === type } @@ -176,6 +176,36 @@ data class HeaderCard( return HeaderCard(header.key, value, header.comment, String::class.javaObjectType) } + @JvmStatic + fun create(key: String, value: Boolean, comment: String = ""): HeaderCard { + return HeaderCard(key, if (value) "T" else "F", comment, Boolean::class.javaPrimitiveType!!) + } + + @JvmStatic + fun create(key: String, value: Int, comment: String = ""): HeaderCard { + return HeaderCard(key, "$value", comment, Int::class.javaPrimitiveType!!) + } + + @JvmStatic + fun create(key: String, value: Long, comment: String = ""): HeaderCard { + return HeaderCard(key, "$value", comment, Long::class.javaPrimitiveType!!) + } + + @JvmStatic + fun create(key: String, value: Float, comment: String = ""): HeaderCard { + return HeaderCard(key, "$value", comment, Float::class.javaPrimitiveType!!) + } + + @JvmStatic + fun create(key: String, value: Double, comment: String = ""): HeaderCard { + return HeaderCard(key, "$value", comment, Double::class.javaPrimitiveType!!) + } + + @JvmStatic + fun create(key: String, value: String, comment: String = ""): HeaderCard { + return HeaderCard(key, value, comment, String::class.javaObjectType) + } + @JvmStatic internal fun isHierarchKey(key: String): Boolean { return key.uppercase().startsWith(HIERARCH_WITH_DOT) diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/ImageHdu.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/ImageHdu.kt index 4023f383d..43fd30a06 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/ImageHdu.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/ImageHdu.kt @@ -9,10 +9,8 @@ data class ImageHdu( override var data: Array = emptyArray(), ) : Hdu { - val width = header.getInt(Standard.NAXIS1, 0) - - val height = header.getInt(Standard.NAXIS2, 0) - + val width = header.width + val height = header.height val bitpix = Bitpix.from(header) override fun read(source: SeekableSource) { diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/ReadOnlyHeader.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/ReadOnlyHeader.kt index 59476e5cd..522b2e17e 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/ReadOnlyHeader.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/ReadOnlyHeader.kt @@ -3,7 +3,7 @@ package nebulosa.fits import nebulosa.io.SeekableSource import java.util.* -internal class ReadOnlyHeader : Header { +open class ReadOnlyHeader : Header { constructor() : super(LinkedList()) @@ -26,4 +26,6 @@ internal class ReadOnlyHeader : Header { override fun add(card: HeaderCard) = throw UnsupportedOperationException("Header is read-only") override fun delete(key: FitsHeader) = throw UnsupportedOperationException("Header is read-only") + + override fun readOnly() = this } diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt index 2e4a30fc4..85f0ca77e 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt @@ -1,57 +1,53 @@ package nebulosa.fits -import nebulosa.io.Seekable +import nebulosa.io.SeekableSource import nebulosa.io.sink import nebulosa.io.transferFully import okio.Buffer import okio.Sink -import okio.Source import java.nio.ByteBuffer data class SeekableSourceImageData( - private val source: Seekable, + private val source: SeekableSource, private val position: Long, override val width: Int, override val height: Int, override val bitpix: Bitpix, ) : ImageData { - override fun read(block: (ByteBuffer) -> Unit) { - require(source is Source) - - val strideSizeInBytes = (width * bitpix.byteSize).toLong() + private val strideSizeInBytes = (width * bitpix.byteSize).toLong() - val buffer = Buffer() + override fun read(block: (ByteBuffer) -> Unit) { val data = ByteArray(strideSizeInBytes.toInt()) val sink = data.sink() synchronized(source) { source.seek(position) - repeat(height) { - sink.seek(0L) + Buffer().use { b -> + repeat(height) { + sink.seek(0L) - buffer.transferFully(source, sink, strideSizeInBytes) - block(ByteBuffer.wrap(data)) - buffer.clear() + b.transferFully(source, sink, strideSizeInBytes) + block(ByteBuffer.wrap(data)) + b.clear() + } } } } override fun writeTo(sink: Sink): Long { - require(source is Source) - - val buffer = Buffer() - val strideSizeInBytes = (width * bitpix.byteSize).toLong() var byteCount = 0L return synchronized(source) { source.seek(position) - repeat(height) { - buffer.transferFully(source, sink, strideSizeInBytes) - buffer.clear() - byteCount += strideSizeInBytes + Buffer().use { b -> + repeat(height) { + b.transferFully(source, sink, strideSizeInBytes) + b.clear() + byteCount += strideSizeInBytes + } } byteCount diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/WritableHeader.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/WritableHeader.kt index 41abb4210..288459691 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/WritableHeader.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/WritableHeader.kt @@ -12,6 +12,14 @@ interface WritableHeader { fun add(key: FitsHeader, value: String): HeaderCard + fun add(key: String, value: Boolean, comment: String = ""): HeaderCard + + fun add(key: String, value: Int, comment: String = ""): HeaderCard + + fun add(key: String, value: Double, comment: String = ""): HeaderCard + + fun add(key: String, value: String, comment: String = ""): HeaderCard + fun add(card: HeaderCard) fun delete(key: FitsHeader): Boolean diff --git a/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt b/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt index aeaf4449b..95b084b0c 100644 --- a/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt +++ b/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt @@ -1,6 +1,6 @@ import io.kotest.matchers.shouldBe -import nebulosa.fits.Fits import nebulosa.fits.ImageHdu +import nebulosa.fits.fits import nebulosa.io.sink import nebulosa.io.source import nebulosa.test.FitsStringSpec @@ -15,9 +15,8 @@ class FitsWriteTest : FitsStringSpec() { hdu0.write(data.sink()) data.toByteString(2880, 66240).md5().hex() shouldBe "e1735e21c94dc49885fabc429406e573" - val fits = Fits(data.source()).also(Fits::read) + val fits = data.source().use { it.fits() } val hdu1 = fits.filterIsInstance().first() - fits.close() hdu0.header shouldBe hdu1.header } diff --git a/nebulosa-fits/src/test/kotlin/ImageDataTest.kt b/nebulosa-fits/src/test/kotlin/ImageDataTest.kt new file mode 100644 index 000000000..77e75b901 --- /dev/null +++ b/nebulosa-fits/src/test/kotlin/ImageDataTest.kt @@ -0,0 +1,63 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.floats.shouldBeExactly +import io.kotest.matchers.ints.shouldBeExactly +import nebulosa.fits.Bitpix +import nebulosa.fits.FloatImageData +import nebulosa.fits.SeekableSourceImageData +import nebulosa.io.sink +import nebulosa.io.source +import java.nio.ByteBuffer + +class ImageDataTest : StringSpec() { + + init { + "float:read" { + val input = FloatArray(100) { it.toFloat() } + val data = FloatImageData(10, 10, input) + + var i = 0 + + data.read { b -> + repeat(10) { b.getFloat() shouldBeExactly input[i++] } + } + + i shouldBeExactly 100 + } + "float:write" { + val input = FloatArray(100) { it.toFloat() } + val data = FloatImageData(10, 10, input) + val output = ByteArray(100 * 4) + + data.writeTo(output.sink()) + + val buffer = ByteBuffer.wrap(output) + + repeat(100) { + buffer.getFloat() shouldBeExactly input[it] + } + } + "seekable source:read" { + val input = ByteArray(100) { it.toByte() } + val data = SeekableSourceImageData(input.source(), 0L, 10, 10, Bitpix.BYTE) + + var i = 0 + + data.read { b -> + repeat(10) { b.get().toInt() shouldBeExactly input[i++].toInt() } + } + + i shouldBeExactly 100 + } + "seekable source:write" { + val input = ByteArray(100) { it.toByte() } + val data = SeekableSourceImageData(input.source(), 0L, 10, 10, Bitpix.BYTE) + val output = ByteArray(input.size) + + data.writeTo(output.sink()) + + repeat(output.size) { + output[it].toInt() shouldBeExactly input[it].toInt() + } + } + } +} 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 ec6b5d870..54a4e921d 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,25 +1,30 @@ 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 { - - val x: Double - - val y: Double +interface GuidePoint : Point2D { val valid: Boolean - fun dX(point: GuidePoint): Double - - fun dY(point: GuidePoint): Double + fun dX(point: Point2D): Double { + return x - point.x + } - val distance: Double + fun dY(point: Point2D): Double { + return y - point.y + } - fun distance(point: GuidePoint) = hypot(dX(point), dY(point)) + val distance + get() = hypot(x, y) - val angle: Angle + val angle + get() = atan2(y, x).rad - fun angle(point: GuidePoint): Angle + fun angle(point: Point2D): Angle { + return atan2(dY(point), dX(point)).rad + } } diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt index 546e5750a..c1625505a 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt @@ -1,9 +1,6 @@ package nebulosa.guiding.internal -import nebulosa.math.Angle -import nebulosa.math.rad -import kotlin.math.atan2 -import kotlin.math.hypot +import nebulosa.math.Point2D /** * Represents a location on a guide camera image. @@ -32,34 +29,10 @@ open class Point( valid = true } - internal inline fun set(point: GuidePoint) { + internal inline fun set(point: Point2D) { set(point.x, point.y) } - override fun dX(point: GuidePoint): Double { - return x - point.x - } - - override fun dY(point: GuidePoint): Double { - return y - point.y - } - - override val distance - get() = hypot(x, y) - - override fun distance(point: GuidePoint) = hypot(dX(point), dY(point)) - - override val angle - get() = if (x != 0.0 || y != 0.0) atan2(y, x).rad - else 0.0 - - override fun angle(point: GuidePoint): Angle { - val dx = dX(point) - val dy = dY(point) - return if (dx != 0.0 || dy != 0.0) atan2(dy, dx).rad - else 0.0 - } - internal open fun invalidate() { valid = false } diff --git a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt index 95ba77531..2cb26cef0 100644 --- a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt +++ b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt @@ -1,7 +1,7 @@ package nebulosa.guiding.phd2 -import nebulosa.common.concurrency.CancellationToken -import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.common.concurrency.cancel.CancellationToken +import nebulosa.common.concurrency.latch.CountUpDownLatch import nebulosa.guiding.* import nebulosa.log.loggerFor import nebulosa.math.arcsec diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt index 6d8ccc073..8fe3c8eaf 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt @@ -1,6 +1,6 @@ package nebulosa.guiding -import nebulosa.common.concurrency.CancellationToken +import nebulosa.common.concurrency.cancel.CancellationToken import java.io.Closeable import java.time.Duration diff --git a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2Fits.kt b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2Fits.kt index e8a5a9ef1..106e50c09 100644 --- a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2Fits.kt +++ b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2Fits.kt @@ -16,4 +16,19 @@ internal interface Hips2Fits { @Query("coordsys") coordSystem: String, @Query("rotation_angle") rotationAngle: Double, @Query("format") format: String, ): Call + + /** + * MOC Server tool for retrieving as fast as possible the list of astronomical data sets + * (35531 catalogs, surveys, ... harvested from CDS and VO servers) having at least + * one observation in a specifical sky region/and or time range. + * + * The default result is an ID list. MOC Server is based on Multi-Order Coverage maps (MOC) + * described in the IVOA REC. standard. + * + * Query example: `hips_service_url*=*alasky* && dataproduct_type=image && moc_sky_fraction >= 0.99` + * + * @see Web page + */ + @GET("MocServer/query?get=record&fmt=json") + fun availableSurveys(@Query("expr") expr: String): Call> } diff --git a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt index e883dd623..888c00588 100644 --- a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt +++ b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt @@ -22,7 +22,7 @@ class Hips2FitsService( * the center of projection, the type of projection and the field of view. */ fun query( - hips: HipsSurvey, + id: String, ra: Angle, dec: Angle, width: Int = 1200, height: Int = 900, rotation: Angle = 0.0, @@ -31,10 +31,13 @@ class Hips2FitsService( coordSystem: CoordinateFrameType = CoordinateFrameType.ICRS, format: FormatOutputType = FormatOutputType.FITS, ) = service.query( - hips.id, ra.toDegrees, dec.toDegrees, width, height, projection.name, fov.toDegrees, + id, ra.toDegrees, dec.toDegrees, width, height, projection.name, fov.toDegrees, coordSystem.name.lowercase(), rotation.toDegrees, format.name.lowercase(), ) + fun availableSurveys() = service + .availableSurveys("ID=CDS* && hips_service_url*=*alasky* && dataproduct_type=image && moc_sky_fraction >= 0.99 && obs_regime=Optical,Infrared,UV,Radio,X-ray,Gamma-ray") + companion object { const val MAIN_URL = "https://alasky.cds.unistra.fr/" diff --git a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/HipsSurvey.kt b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/HipsSurvey.kt index b107b5889..8bae56328 100644 --- a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/HipsSurvey.kt +++ b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/HipsSurvey.kt @@ -11,4 +11,15 @@ data class HipsSurvey( @field:JsonProperty("bitPix") @field:JsonAlias("hips_pixel_bitpix") val bitPix: Int = 0, @field:JsonProperty("pixelScale") @field:JsonAlias("hips_pixel_scale") val pixelScale: Double = 0.0, @field:JsonProperty("skyFraction") @field:JsonAlias("moc_sky_fraction") val skyFraction: Double = 0.0, -) +) : Comparable { + + override fun compareTo(other: HipsSurvey): Int { + if (regime == other.regime) return -skyFraction.compareTo(other.skyFraction) + return REGIME_SORT_ORDER.indexOf(regime).compareTo(REGIME_SORT_ORDER.indexOf(other.regime)) + } + + companion object { + + @JvmStatic private val REGIME_SORT_ORDER = arrayOf("Optical", "Infrared", "UV", "Radio", "X-ray", "Gamma-ray") + } +} diff --git a/nebulosa-hips2fits/src/test/kotlin/Hips2FitsServiceTest.kt b/nebulosa-hips2fits/src/test/kotlin/Hips2FitsServiceTest.kt index f60e0f160..1f5adf9dc 100644 --- a/nebulosa-hips2fits/src/test/kotlin/Hips2FitsServiceTest.kt +++ b/nebulosa-hips2fits/src/test/kotlin/Hips2FitsServiceTest.kt @@ -1,10 +1,11 @@ import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.collections.shouldNotBeEmpty import io.kotest.matchers.doubles.shouldBeExactly import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.nulls.shouldNotBeNull import nebulosa.fits.* import nebulosa.hips2fits.Hips2FitsService -import nebulosa.hips2fits.HipsSurvey import nebulosa.io.source import nebulosa.math.deg import nebulosa.math.toDegrees @@ -15,18 +16,22 @@ class Hips2FitsServiceTest : StringSpec() { val service = Hips2FitsService() "query" { - val responseBody = service.query(HipsSurvey("CDS/P/DSS2/red"), 201.36506337683.deg, (-43.01911250808).deg) + val responseBody = service + .query("CDS/P/DSS2/red", 201.36506337683.deg, (-43.01911250808).deg) .execute() .body() .shouldNotBeNull() - val fits = responseBody.use { Fits(it.bytes().source()) } - fits.read() + val fits = responseBody.use { it.bytes().source().fits() } val hdu = fits.filterIsInstance().first().header hdu.width shouldBeExactly 1200 hdu.height shouldBeExactly 900 hdu.rightAscension.toDegrees shouldBeExactly 201.36506337683 hdu.declination.toDegrees shouldBeExactly -43.01911250808 - fits.close() + } + "available surveys" { + val surveys = service.availableSurveys().execute().body().shouldNotBeNull() + surveys.shouldNotBeEmpty() + surveys shouldHaveSize 115 } } } diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Float8bitsDataBuffer.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Float8bitsDataBuffer.kt index 6613d5a6a..3b3cb3a53 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Float8bitsDataBuffer.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Float8bitsDataBuffer.kt @@ -2,7 +2,8 @@ package nebulosa.imaging import java.awt.image.DataBuffer -class Float8bitsDataBuffer( +@Suppress("ArrayInDataClass") +data class Float8bitsDataBuffer( @JvmField val mono: Boolean, @JvmField val r: FloatArray, // or gray. @JvmField val g: FloatArray = r, diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Image.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Image.kt index 8b8eae9d3..405896762 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Image.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/Image.kt @@ -207,8 +207,7 @@ class Image( } fun hdu(): Hdu { - val data = Array(numberOfChannels) { FloatArrayImageData(width, height, this.data[it]) } - return ImageHdu(header, data) + return ImageHdu(header, Array(numberOfChannels) { FloatImageData(width, height, data[it]) }) } /** @@ -364,7 +363,7 @@ class Image( val width = bufferedImage.width val height = bufferedImage.height val mono = bufferedImage.type == TYPE_BYTE_GRAY - || bufferedImage.type == TYPE_USHORT_GRAY + || bufferedImage.type == TYPE_USHORT_GRAY header.add(Standard.SIMPLE, true) header.add(Standard.BITPIX, Bitpix.FLOAT.code) diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/AutoScreenTransformFunction.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/AutoScreenTransformFunction.kt index d2290f4b5..b40346143 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/AutoScreenTransformFunction.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/AutoScreenTransformFunction.kt @@ -9,7 +9,7 @@ import nebulosa.log.loggerFor import kotlin.math.max import kotlin.math.min -object AutoScreenTransformFunction : ComputationAlgorithm, TransformAlgorithm { +data object AutoScreenTransformFunction : ComputationAlgorithm, TransformAlgorithm { override fun compute(source: Image): ScreenTransformFunction.Parameters { // Find the median sample. diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/HorizontalFlip.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/HorizontalFlip.kt index 2ff72e943..15dc72492 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/HorizontalFlip.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/HorizontalFlip.kt @@ -3,7 +3,7 @@ package nebulosa.imaging.algorithms.transformation import nebulosa.imaging.Image import nebulosa.imaging.algorithms.TransformAlgorithm -object HorizontalFlip : TransformAlgorithm { +data object HorizontalFlip : TransformAlgorithm { override fun transform(source: Image): Image { for (y in 0 until source.height) { diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/Invert.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/Invert.kt index 3a4051dd6..403595497 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/Invert.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/Invert.kt @@ -3,7 +3,7 @@ package nebulosa.imaging.algorithms.transformation import nebulosa.imaging.Image import nebulosa.imaging.algorithms.TransformAlgorithm -object Invert : TransformAlgorithm { +data object Invert : TransformAlgorithm { override fun transform(source: Image): Image { for (i in source.r.indices) source.r[i] = 1f - source.r[i] diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/VerticalFlip.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/VerticalFlip.kt index c09d29980..a7448fd16 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/VerticalFlip.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/VerticalFlip.kt @@ -3,7 +3,7 @@ package nebulosa.imaging.algorithms.transformation import nebulosa.imaging.Image import nebulosa.imaging.algorithms.TransformAlgorithm -object VerticalFlip : TransformAlgorithm { +data object VerticalFlip : TransformAlgorithm { override fun transform(source: Image): Image { for (y in 0 until source.height / 2) { diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Blur.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Blur.kt index da1133701..d65160672 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Blur.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Blur.kt @@ -1,6 +1,6 @@ package nebulosa.imaging.algorithms.transformation.convolution -object Blur : Convolution( +data object Blur : Convolution( floatArrayOf( 1f, 2f, 3f, 2f, 1f, 2f, 4f, 5f, 4f, 2f, diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Edges.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Edges.kt index 3af628796..938ad6acd 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Edges.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Edges.kt @@ -1,6 +1,6 @@ package nebulosa.imaging.algorithms.transformation.convolution -object Edges : Convolution( +data object Edges : Convolution( floatArrayOf( 0f, -1f, 0f, -1f, 4f, -1f, diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Emboss.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Emboss.kt index 7521878ed..942016324 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Emboss.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Emboss.kt @@ -1,6 +1,6 @@ package nebulosa.imaging.algorithms.transformation.convolution -object Emboss : Convolution( +data object Emboss : Convolution( floatArrayOf( -1f, 0f, 0f, 0f, 0f, 0f, diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Mean.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Mean.kt index 121391001..47605e0e6 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Mean.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Mean.kt @@ -1,6 +1,6 @@ package nebulosa.imaging.algorithms.transformation.convolution -object Mean : Convolution( +data object Mean : Convolution( floatArrayOf( 1f, 1f, 1f, 1f, 1f, 1f, diff --git a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Sharpen.kt b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Sharpen.kt index 5a9e644ad..07f08d63a 100644 --- a/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Sharpen.kt +++ b/nebulosa-imaging/src/main/kotlin/nebulosa/imaging/algorithms/transformation/convolution/Sharpen.kt @@ -1,6 +1,6 @@ package nebulosa.imaging.algorithms.transformation.convolution -object Sharpen : Convolution( +data object Sharpen : Convolution( floatArrayOf( 0f, -1f, 0f, -1f, 5f, -1f, diff --git a/nebulosa-imaging/src/test/kotlin/TransformAlgorithmTest.kt b/nebulosa-imaging/src/test/kotlin/TransformAlgorithmTest.kt index e8964095e..954045d31 100644 --- a/nebulosa-imaging/src/test/kotlin/TransformAlgorithmTest.kt +++ b/nebulosa-imaging/src/test/kotlin/TransformAlgorithmTest.kt @@ -2,12 +2,13 @@ import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.shouldBe -import nebulosa.fits.Fits +import nebulosa.fits.fits import nebulosa.imaging.Image import nebulosa.imaging.ImageChannel import nebulosa.imaging.algorithms.transformation.* import nebulosa.imaging.algorithms.transformation.convolution.* import nebulosa.test.FitsStringSpec +import java.io.File class TransformAlgorithmTest : FitsStringSpec() { @@ -267,13 +268,13 @@ class TransformAlgorithmTest : FitsStringSpec() { nImage.save("color-grayscale-y").second shouldBe "24dd4a7e0fa9e4be34c53c924a78a940" } "color:debayer" { - val fits = Fits("src/test/resources/Debayer.fits").also(Fits::read) + val fits = File("src/test/resources/Debayer.fits").fits() val mImage = Image.open(fits) val nImage = mImage.transform(AutoScreenTransformFunction) nImage.save("color-debayer").second shouldBe "86b5bdd67dfd6bbf5495afae4bf2bc04" } "color:no-debayer" { - val fits = Fits("src/test/resources/Debayer.fits").also(Fits::read) + val fits = File("src/test/resources/Debayer.fits").fits() val mImage = Image.open(fits, false) val nImage = mImage.transform(AutoScreenTransformFunction) nImage.save("color-no-debayer").second shouldBe "958ccea020deec1f0c075042a9ba37c3" diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/DefaultINDIClient.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/DefaultINDIClient.kt deleted file mode 100644 index 165136812..000000000 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/DefaultINDIClient.kt +++ /dev/null @@ -1,112 +0,0 @@ -package nebulosa.indi.client - -import nebulosa.indi.client.connection.INDIProccessConnection -import nebulosa.indi.client.connection.INDISocketConnection -import nebulosa.indi.client.device.DeviceProtocolHandler -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.gps.GPS -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount -import nebulosa.indi.device.thermometer.Thermometer -import nebulosa.indi.protocol.GetProperties -import nebulosa.indi.protocol.INDIProtocol -import nebulosa.indi.protocol.io.INDIConnection -import nebulosa.log.debug -import nebulosa.log.loggerFor - -open class DefaultINDIClient(override val connection: INDIConnection) : DeviceProtocolHandler(), INDIClient { - - constructor( - host: String, - port: Int = INDIProtocol.DEFAULT_PORT, - ) : this(INDISocketConnection(host, port)) - - constructor( - process: Process, - ) : this(INDIProccessConnection(process)) - - override val isClosed - get() = !connection.isOpen - - override val input - get() = connection.input - - override fun start() { - super.start() - sendMessageToServer(GetProperties()) - } - - override fun sendMessageToServer(message: INDIProtocol) { - LOG.debug { "sending message: $message" } - connection.writeINDIProtocol(message) - } - - override fun cameras(): List { - return cameras.values.toList() - } - - override fun camera(name: String): Camera? { - return cameras[name] - } - - override fun mounts(): List { - return mounts.values.toList() - } - - override fun mount(name: String): Mount? { - return mounts[name] - } - - override fun focusers(): List { - return focusers.values.toList() - } - - override fun focuser(name: String): Focuser? { - return focusers[name] - } - - override fun wheels(): List { - return wheels.values.toList() - } - - override fun wheel(name: String): FilterWheel? { - return wheels[name] - } - - override fun gps(): List { - return gps.values.toList() - } - - override fun gps(name: String): GPS? { - return gps[name] - } - - override fun guideOutputs(): List { - return guideOutputs.values.toList() - } - - override fun guideOutput(name: String): GuideOutput? { - return guideOutputs[name] - } - - override fun thermometers(): List { - return thermometers.values.toList() - } - - override fun thermometer(name: String): Thermometer? { - return thermometers[name] - } - - override fun close() { - super.close() - - connection.close() - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/INDIClient.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/INDIClient.kt index ddd342e4e..aff8daf11 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/INDIClient.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/INDIClient.kt @@ -1,6 +1,19 @@ package nebulosa.indi.client -import nebulosa.indi.device.MessageSender +import nebulosa.indi.client.connection.INDIProccessConnection +import nebulosa.indi.client.connection.INDISocketConnection +import nebulosa.indi.client.device.GPSDevice +import nebulosa.indi.client.device.INDIDeviceProtocolHandler +import nebulosa.indi.client.device.cameras.AsiCamera +import nebulosa.indi.client.device.cameras.INDICamera +import nebulosa.indi.client.device.cameras.SVBonyCamera +import nebulosa.indi.client.device.cameras.SimCamera +import nebulosa.indi.client.device.focusers.INDIFocuser +import nebulosa.indi.client.device.mounts.INDIMount +import nebulosa.indi.client.device.mounts.IoptronV3Mount +import nebulosa.indi.client.device.wheels.INDIFilterWheel +import nebulosa.indi.device.Device +import nebulosa.indi.device.INDIDeviceProvider import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.focuser.Focuser @@ -8,40 +21,149 @@ import nebulosa.indi.device.gps.GPS import nebulosa.indi.device.guide.GuideOutput import nebulosa.indi.device.mount.Mount import nebulosa.indi.device.thermometer.Thermometer +import nebulosa.indi.protocol.GetProperties +import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.io.INDIConnection -import nebulosa.indi.protocol.parser.INDIProtocolParser +import nebulosa.log.debug +import nebulosa.log.loggerFor +import java.util.* -interface INDIClient : INDIProtocolParser, MessageSender { +data class INDIClient(val connection: INDIConnection) : INDIDeviceProtocolHandler(), INDIDeviceProvider { - val connection: INDIConnection + constructor( + host: String, + port: Int = INDIProtocol.DEFAULT_PORT, + ) : this(INDISocketConnection(host, port)) - fun start() + constructor( + process: Process, + ) : this(INDIProccessConnection(process)) - fun cameras(): List + override val id = UUID.randomUUID().toString() - fun camera(name: String): Camera? + override val isClosed + get() = !connection.isOpen || super.isClosed - fun mounts(): List + override val input + get() = connection.input - fun mount(name: String): Mount? + override fun newCamera(message: INDIProtocol, executable: String): Camera { + return CAMERAS[executable]?.create(this, message.device) ?: INDICamera(this, message.device) + } - fun focusers(): List + override fun newMount(message: INDIProtocol, executable: String): Mount { + return MOUNTS[executable]?.create(this, message.device) ?: INDIMount(this, message.device) + } - fun focuser(name: String): Focuser? + override fun newFocuser(message: INDIProtocol): Focuser { + return INDIFocuser(this, message.device) + } - fun wheels(): List + override fun newFilterWheel(message: INDIProtocol): FilterWheel { + return INDIFilterWheel(this, message.device) + } - fun wheel(name: String): FilterWheel? + override fun newGPS(message: INDIProtocol): GPS { + return GPSDevice(this, message.device) + } - fun gps(): List + override fun start() { + super.start() + sendMessageToServer(GetProperties()) + } - fun gps(name: String): GPS? + override fun sendMessageToServer(message: INDIProtocol) { + LOG.debug { "sending message: $message" } + connection.writeINDIProtocol(message) + } - fun guideOutputs(): List + override fun onConnectionClosed() { + fireOnConnectionClosed() + } - fun guideOutput(name: String): GuideOutput? + override fun cameras(): List { + return cameras.values.toList() + } - fun thermometers(): List + override fun camera(name: String): Camera? { + return cameras[name] + } - fun thermometer(name: String): Thermometer? + override fun mounts(): List { + return mounts.values.toList() + } + + override fun mount(name: String): Mount? { + return mounts[name] + } + + override fun focusers(): List { + return focusers.values.toList() + } + + override fun focuser(name: String): Focuser? { + return focusers[name] + } + + override fun wheels(): List { + return wheels.values.toList() + } + + override fun wheel(name: String): FilterWheel? { + return wheels[name] + } + + override fun gps(): List { + return gps.values.toList() + } + + override fun gps(name: String): GPS? { + return gps[name] + } + + override fun guideOutputs(): List { + return guideOutputs.values.toList() + } + + override fun guideOutput(name: String): GuideOutput? { + return guideOutputs[name] + } + + override fun thermometers(): List { + return thermometers.values.toList() + } + + override fun thermometer(name: String): Thermometer? { + return thermometers[name] + } + + override fun close() { + super.close() + + connection.close() + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + + @JvmStatic private val CAMERAS = mapOf( + "indi_asi_ccd" to AsiCamera::class.java, + "indi_asi_single_ccd" to AsiCamera::class.java, + "indi_svbony_ccd" to SVBonyCamera::class.java, + "indi_sv305_ccd" to SVBonyCamera::class.java, // legacy name. + "indi_simulator_ccd" to SimCamera::class.java, + "indi_simulator_guide" to SimCamera::class.java, + ) + + @JvmStatic private val MOUNTS = mapOf( + "indi_ioptronv3_telescope" to IoptronV3Mount::class.java, + ) + + @JvmStatic + fun Class.create(handler: INDIClient, name: String): T { + return getConstructor(INDIClient::class.java, String::class.java) + .newInstance(handler, name) + } + } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDIProccessConnection.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDIProccessConnection.kt index bbeb75006..878b27d87 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDIProccessConnection.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDIProccessConnection.kt @@ -3,7 +3,7 @@ package nebulosa.indi.client.connection import nebulosa.indi.client.io.INDIProtocolFactory import nebulosa.indi.protocol.io.INDIConnection -class INDIProccessConnection(val process: Process) : INDIConnection { +data class INDIProccessConnection(private val process: Process) : INDIConnection { override val input = INDIProtocolFactory.createInputStream(process.inputStream) diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDISocketConnection.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDISocketConnection.kt index 2ae819e42..2098f2a50 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDISocketConnection.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/connection/INDISocketConnection.kt @@ -3,19 +3,21 @@ package nebulosa.indi.client.connection import nebulosa.indi.client.io.INDIProtocolFactory import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.io.INDIConnection +import nebulosa.log.loggerFor import java.net.InetSocketAddress import java.net.Socket -class INDISocketConnection(private val socket: Socket) : INDIConnection { +data class INDISocketConnection(private val socket: Socket) : INDIConnection { constructor(host: String, port: Int = INDIProtocol.DEFAULT_PORT) : this(Socket()) { + socket.reuseAddress = false socket.connect(InetSocketAddress(host, port), 30000) } val host: String get() = socket.localAddress.hostName - val port: Int + val port get() = socket.localPort override val input by lazy { INDIProtocolFactory.createInputStream(socket.getInputStream()) } @@ -26,32 +28,15 @@ class INDISocketConnection(private val socket: Socket) : INDIConnection { get() = !socket.isClosed override fun close() { - var thrown: Throwable? = null - - try { - socket.shutdownInput() - } catch (e: Throwable) { - thrown = e - } - - try { - socket.shutdownOutput() - } catch (e: Throwable) { - if (thrown == null) { - thrown = e - } - } - try { socket.close() } catch (e: Throwable) { - if (thrown == null) { - thrown = e - } + LOG.error("socket close error", e) } + } - if (thrown != null) { - throw thrown - } + companion object { + + @JvmStatic private val LOG = loggerFor() } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FilterWheelDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FilterWheelDevice.kt deleted file mode 100644 index 23455c84d..000000000 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FilterWheelDevice.kt +++ /dev/null @@ -1,72 +0,0 @@ -package nebulosa.indi.client.device - -import nebulosa.indi.device.filterwheel.* -import nebulosa.indi.protocol.DefNumberVector -import nebulosa.indi.protocol.INDIProtocol -import nebulosa.indi.protocol.NumberVector -import nebulosa.indi.protocol.PropertyState - -internal open class FilterWheelDevice( - handler: DeviceProtocolHandler, - name: String, -) : AbstractDevice(handler, name), FilterWheel { - - override var count = 0 - override var position = -1 - override var moving = false - - override fun handleMessage(message: INDIProtocol) { - when (message) { - is NumberVector<*> -> { - when (message.name) { - "FILTER_SLOT" -> { - val slot = message["FILTER_SLOT_VALUE"]!! - - if (message is DefNumberVector) { - count = slot.max.toInt() - slot.min.toInt() + 1 - handler.fireOnEventReceived(FilterWheelCountChanged(this)) - } - - if (message.state == PropertyState.ALERT) { - handler.fireOnEventReceived(FilterWheelMoveFailed(this)) - } - - val prevPosition = position - position = slot.value.toInt() - - if (prevPosition != position) { - handler.fireOnEventReceived(FilterWheelPositionChanged(this, prevPosition)) - } - - val prevIsMoving = moving - moving = message.isBusy - - if (prevIsMoving != moving) { - handler.fireOnEventReceived(FilterWheelMovingChanged(this)) - } - } - } - } - else -> Unit - } - - super.handleMessage(message) - } - - override fun moveTo(position: Int) { - if (position in 1..count) { - sendNewNumber("FILTER_SLOT", "FILTER_SLOT_VALUE" to position.toDouble()) - } - } - - override fun syncNames(names: Iterable) { - sendNewText("FILTER_NAME", names.mapIndexed { i, name -> "FILTER_SLOT_NAME_${i + 1}" to name }) - } - - override fun close() = Unit - - override fun toString(): String { - return "FilterWheel(name=$name, slotCount=$count, position=$position," + - " moving=$moving)" - } -} diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt index 4386bff0f..f9616ba1d 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/GPSDevice.kt @@ -1,5 +1,6 @@ package nebulosa.indi.client.device +import nebulosa.indi.client.INDIClient import nebulosa.indi.device.gps.GPS import nebulosa.indi.device.gps.GPSCoordinateChanged import nebulosa.indi.device.gps.GPSTimeChanged @@ -11,16 +12,21 @@ import nebulosa.math.m import java.time.OffsetDateTime import java.time.ZoneOffset -internal class GPSDevice( - handler: DeviceProtocolHandler, - name: String, -) : AbstractDevice(handler, name), GPS { +internal open class GPSDevice( + override val sender: INDIClient, + override val name: String, +) : INDIDevice(), GPS { - override val hasGPS = true - override var longitude = 0.0 - override var latitude = 0.0 - override var elevation = 0.0 - override var dateTime = OffsetDateTime.MIN!! + @Volatile final override var hasGPS = true + private set + @Volatile final override var longitude = 0.0 + private set + @Volatile final override var latitude = 0.0 + private set + @Volatile final override var elevation = 0.0 + private set + @Volatile final override var dateTime = OffsetDateTime.MIN!! + private set override fun handleMessage(message: INDIProtocol) { when (message) { @@ -31,7 +37,7 @@ internal class GPSDevice( longitude = message["LONG"]!!.value.deg elevation = message["ELEV"]!!.value.m - handler.fireOnEventReceived(GPSCoordinateChanged(this)) + sender.fireOnEventReceived(GPSCoordinateChanged(this)) } } } @@ -43,7 +49,7 @@ internal class GPSDevice( dateTime = OffsetDateTime.of(utcTime, ZoneOffset.ofTotalSeconds((utcOffset * 60.0).toInt())) - handler.fireOnEventReceived(GPSTimeChanged(this)) + sender.fireOnEventReceived(GPSTimeChanged(this)) } } } @@ -57,6 +63,6 @@ internal class GPSDevice( override fun toString(): String { return "GPS(hasGPS=$hasGPS, longitude=$longitude, latitude=$latitude," + - " elevation=$elevation, dateTime=$dateTime)" + " elevation=$elevation, dateTime=$dateTime)" } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/AbstractDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDevice.kt similarity index 80% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/AbstractDevice.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDevice.kt index 7112bf2bb..7f37048fc 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/AbstractDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDevice.kt @@ -1,5 +1,7 @@ package nebulosa.indi.client.device +import nebulosa.indi.client.INDIClient +import nebulosa.indi.client.device.cameras.INDICamera import nebulosa.indi.device.* import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.dome.Dome @@ -13,20 +15,28 @@ import nebulosa.indi.protocol.Vector import nebulosa.log.loggerFor import java.util.* -internal abstract class AbstractDevice( - @JvmField internal val handler: DeviceProtocolHandler, - override val name: String, -) : Device { +internal abstract class INDIDevice : Device { - override val properties = linkedMapOf>() + abstract override val sender: INDIClient + override val properties = linkedMapOf>() override val messages = LinkedList() - override var connected = false + override val id = UUID.randomUUID().toString() + + @Volatile override var connected = false protected set - override fun sendMessageToServer(message: INDIProtocol) { - handler.sendMessageToServer(message) + private fun addMessageAndFireEvent(text: String) { + synchronized(messages) { + messages.addFirst(text) + + sender.fireOnEventReceived(DeviceMessageReceived(this, text)) + + if (messages.size > 100) { + messages.removeLast() + } + } } override fun handleMessage(message: INDIProtocol) { @@ -40,28 +50,26 @@ internal abstract class AbstractDevice( if (connected) { this.connected = true - handler.fireOnEventReceived(DeviceConnected(this)) + sender.fireOnEventReceived(DeviceConnected(this)) ask() } else if (this.connected) { this.connected = false - handler.fireOnEventReceived(DeviceDisconnected(this)) + sender.fireOnEventReceived(DeviceDisconnected(this)) } } else if (!connected && message.state == PropertyState.ALERT) { - handler.fireOnEventReceived(DeviceConnectionFailed(this)) + sender.fireOnEventReceived(DeviceConnectionFailed(this)) } } } } is DelProperty -> { val property = properties.remove(message.name) ?: return - handler.fireOnEventReceived(DevicePropertyDeleted(property)) + sender.fireOnEventReceived(DevicePropertyDeleted(property)) } is Message -> { - val text = "[%s]: %s".format(message.timestamp, message.message) - messages.addFirst(text) - handler.fireOnEventReceived(DeviceMessageReceived(this, text)) + addMessageAndFireEvent("[%s]: %s".format(message.timestamp, message.message)) } else -> Unit } @@ -126,7 +134,7 @@ internal abstract class AbstractDevice( properties[property.name] = property - handler.fireOnEventReceived(DevicePropertyChanged(property)) + sender.fireOnEventReceived(DevicePropertyChanged(property)) } is SetVector<*> -> { val property = when (message) { @@ -170,7 +178,7 @@ internal abstract class AbstractDevice( else -> return } - handler.fireOnEventReceived(DevicePropertyChanged(property)) + sender.fireOnEventReceived(DevicePropertyChanged(property)) } else -> return } @@ -207,14 +215,24 @@ internal abstract class AbstractDevice( sendNewSwitch("CONNECTION", "DISCONNECT" to true) } - companion object { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is INDICamera) return false - @JvmStatic private val LOG = loggerFor() + if (sender != other.sender) return false + if (name != other.name) return false - @JvmStatic - fun Class.create(handler: DeviceProtocolHandler, name: String): T { - return getConstructor(DeviceProtocolHandler::class.java, String::class.java) - .newInstance(handler, name) - } + return true + } + + override fun hashCode(): Int { + var result = sender.hashCode() + result = 31 * result + name.hashCode() + return result + } + + companion object { + + @JvmStatic private val LOG = loggerFor() } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDeviceProtocolHandler.kt similarity index 85% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDeviceProtocolHandler.kt index b0b683e50..824c40ee4 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDeviceProtocolHandler.kt @@ -1,12 +1,5 @@ package nebulosa.indi.client.device -import nebulosa.indi.client.device.AbstractDevice.Companion.create -import nebulosa.indi.client.device.camera.AsiCamera -import nebulosa.indi.client.device.camera.CameraDevice -import nebulosa.indi.client.device.camera.SVBonyCamera -import nebulosa.indi.client.device.camera.SimCamera -import nebulosa.indi.client.device.mount.IoptronV3Mount -import nebulosa.indi.client.device.mount.MountDevice import nebulosa.indi.device.* import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraAttached @@ -33,13 +26,14 @@ import nebulosa.indi.protocol.DefTextVector import nebulosa.indi.protocol.DelProperty import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.Message +import nebulosa.indi.protocol.parser.CloseConnectionListener import nebulosa.indi.protocol.parser.INDIProtocolParser import nebulosa.indi.protocol.parser.INDIProtocolReader import nebulosa.log.debug import nebulosa.log.loggerFor import java.util.concurrent.LinkedBlockingQueue -abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { +abstract class INDIDeviceProtocolHandler : MessageSender, INDIProtocolParser, CloseConnectionListener { @JvmField protected val cameras = HashMap(2) @JvmField protected val mounts = HashMap(1) @@ -52,10 +46,20 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { private val notRegisteredDevices = HashSet() @Volatile private var protocolReader: INDIProtocolReader? = null private val messageQueueCounter = HashMap(2048) - private val handlers = ArrayList() + private val handlers = LinkedHashSet() - val isRunning - get() = protocolReader != null + override val isClosed + get() = protocolReader == null || !protocolReader!!.isRunning + + protected abstract fun newCamera(message: INDIProtocol, executable: String): Camera + + protected abstract fun newMount(message: INDIProtocol, executable: String): Mount + + protected abstract fun newFocuser(message: INDIProtocol): Focuser + + protected abstract fun newFilterWheel(message: INDIProtocol): FilterWheel + + protected abstract fun newGPS(message: INDIProtocol): GPS fun registerDeviceEventHandler(handler: DeviceEventHandler) { handlers.add(handler) @@ -69,9 +73,15 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { handlers.forEach { it.onEventReceived(event) } } + fun fireOnConnectionClosed() { + handlers.forEach { it.onConnectionClosed() } + } + internal fun registerGPS(device: GPS) { - gps[device.name] = device - fireOnEventReceived(GPSAttached(device)) + if (device.name !in gps) { + gps[device.name] = device + fireOnEventReceived(GPSAttached(device)) + } } internal fun unregisterGPS(device: GPS) { @@ -82,8 +92,10 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { } internal fun registerGuideOutput(device: GuideOutput) { - guideOutputs[device.name] = device - fireOnEventReceived(GuideOutputAttached(device)) + if (device.name !in guideOutputs) { + guideOutputs[device.name] = device + fireOnEventReceived(GuideOutputAttached(device)) + } } internal fun unregisterGuideOutput(device: GuideOutput) { @@ -94,8 +106,10 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { } internal fun registerThermometer(device: Thermometer) { - thermometers[device.name] = device - fireOnEventReceived(ThermometerAttached(device)) + if (device.name !in thermometers) { + thermometers[device.name] = device + fireOnEventReceived(ThermometerAttached(device)) + } } internal fun unregisterThermometer(device: Thermometer) { @@ -108,6 +122,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { open fun start() { if (protocolReader == null) { protocolReader = INDIProtocolReader(this, Thread.MIN_PRIORITY) + protocolReader!!.registerCloseConnectionListener(this) protocolReader!!.start() } } @@ -155,6 +170,8 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { wheels.clear() focusers.clear() gps.clear() + guideOutputs.clear() + thermometers.clear() notRegisteredDevices.clear() messageQueueCounter.clear() @@ -197,8 +214,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { registered = true if (message.device !in cameras) { - val device = CAMERAS[executable]?.create(this, message.device) - ?: CameraDevice(this, message.device) + val device = newCamera(message, executable) cameras[message.device] = device takeMessageFromReorderingQueue(device) LOG.info("camera attached: {}", device.name) @@ -210,8 +226,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { registered = true if (message.device !in mounts) { - val device = MOUNTS[executable]?.create(this, message.device) - ?: MountDevice(this, message.device) + val device = newMount(message, executable) mounts[message.device] = device takeMessageFromReorderingQueue(device) LOG.info("mount attached: {}", device.name) @@ -223,7 +238,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { registered = true if (message.device !in wheels) { - val device = FilterWheelDevice(this, message.device) + val device = newFilterWheel(message) wheels[message.device] = device takeMessageFromReorderingQueue(device) LOG.info("filter wheel attached: {}", device.name) @@ -235,7 +250,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { registered = true if (message.device !in focusers) { - val device = FocuserDevice(this, message.device) + val device = newFocuser(message) focusers[message.device] = device takeMessageFromReorderingQueue(device) LOG.info("focuser attached: {}", device.name) @@ -247,7 +262,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { registered = true if (message.device !in gps) { - val device = GPSDevice(this, message.device) + val device = newGPS(message) gps[message.device] = device takeMessageFromReorderingQueue(device) LOG.info("gps attached: {}", device.name) @@ -353,19 +368,6 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { companion object { - @JvmStatic private val LOG = loggerFor() - - @JvmStatic private val CAMERAS = mapOf( - "indi_asi_ccd" to AsiCamera::class.java, - "indi_asi_single_ccd" to AsiCamera::class.java, - "indi_svbony_ccd" to SVBonyCamera::class.java, - "indi_sv305_ccd" to SVBonyCamera::class.java, // legacy name. - "indi_simulator_ccd" to SimCamera::class.java, - "indi_simulator_guide" to SimCamera::class.java, - ) - - @JvmStatic private val MOUNTS = mapOf( - "indi_ioptronv3_telescope" to IoptronV3Mount::class.java, - ) + @JvmStatic private val LOG = loggerFor() } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/AsiCamera.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/AsiCamera.kt similarity index 85% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/AsiCamera.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/AsiCamera.kt index 344df9e8a..a5834d09e 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/AsiCamera.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/AsiCamera.kt @@ -1,13 +1,13 @@ -package nebulosa.indi.client.device.camera +package nebulosa.indi.client.device.cameras -import nebulosa.indi.client.device.DeviceProtocolHandler +import nebulosa.indi.client.INDIClient import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.NumberVector internal class AsiCamera( - handler: DeviceProtocolHandler, + provider: INDIClient, name: String, -) : CameraDevice(handler, name) { +) : INDICamera(provider, name) { override fun handleMessage(message: INDIProtocol) { when (message) { diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/INDICamera.kt similarity index 57% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/INDICamera.kt index 5d96adf67..d3b5620a4 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/INDICamera.kt @@ -1,70 +1,119 @@ -package nebulosa.indi.client.device.camera +package nebulosa.indi.client.device.cameras import nebulosa.imaging.algorithms.transformation.CfaPattern -import nebulosa.indi.client.device.AbstractDevice -import nebulosa.indi.client.device.DeviceProtocolHandler +import nebulosa.indi.client.INDIClient +import nebulosa.indi.client.device.INDIDevice import nebulosa.indi.device.camera.* +import nebulosa.indi.device.camera.Camera.Companion.NANO_SECONDS import nebulosa.indi.device.guide.GuideOutputPulsingChanged import nebulosa.indi.protocol.* import nebulosa.io.Base64InputStream import nebulosa.log.loggerFor import java.time.Duration -internal open class CameraDevice( - handler: DeviceProtocolHandler, - name: String, -) : AbstractDevice(handler, name), Camera { - - override var exposuring = false - override var hasCoolerControl = false - override var coolerPower = 0.0 - override var cooler = false - override var hasDewHeater = false - override var dewHeater = false - override var frameFormats = emptyList() - override var canAbort = false - override var cfaOffsetX = 0 - override var cfaOffsetY = 0 - override var cfaType = CfaPattern.RGGB - override var exposureMin = Duration.ZERO - override var exposureMax = Duration.ZERO - override var exposureState = PropertyState.IDLE - override var exposureTime = Duration.ZERO - override var hasCooler = false - override var canSetTemperature = false - override var canSubFrame = false - override var x = 0 - override var minX = 0 - override var maxX = 0 - override var y = 0 - override var minY = 0 - override var maxY = 0 - override var width = 0 - override var minWidth = 0 - override var maxWidth = 0 - override var height = 0 - override var minHeight = 0 - override var maxHeight = 0 - override var canBin = false - override var maxBinX = 1 - override var maxBinY = 1 - override var binX = 1 - override var binY = 1 - override var gain = 0 - override var gainMin = 0 - override var gainMax = 0 - override var offset = 0 - override var offsetMin = 0 - override var offsetMax = 0 - override var hasGuiderHead = false // TODO: Handle guider head. - override var pixelSizeX = 0.0 - override var pixelSizeY = 0.0 - - override var hasThermometer = false - override var temperature = 0.0 - - override var canPulseGuide = false - override var pulseGuiding = false +internal open class INDICamera( + override val sender: INDIClient, + override val name: String, +) : INDIDevice(), Camera { + + @Volatile final override var exposuring = false + private set + @Volatile final override var hasCoolerControl = false + private set + @Volatile final override var coolerPower = 0.0 + private set + @Volatile final override var cooler = false + private set + @Volatile final override var hasDewHeater = false + private set + @Volatile final override var dewHeater = false + private set + @Volatile final override var frameFormats = emptyList() + private set + @Volatile final override var canAbort = false + private set + @Volatile final override var cfaOffsetX = 0 + private set + @Volatile final override var cfaOffsetY = 0 + private set + @Volatile final override var cfaType = CfaPattern.RGGB + private set + @Volatile final override var exposureMin: Duration = Duration.ZERO + private set + @Volatile final override var exposureMax: Duration = Duration.ZERO + private set + @Volatile final override var exposureState = PropertyState.IDLE + private set + @Volatile final override var exposureTime: Duration = Duration.ZERO + private set + @Volatile final override var hasCooler = false + private set + @Volatile final override var canSetTemperature = false + private set + @Volatile final override var canSubFrame = false + private set + @Volatile final override var x = 0 + private set + @Volatile final override var minX = 0 + private set + @Volatile final override var maxX = 0 + private set + @Volatile final override var y = 0 + private set + @Volatile final override var minY = 0 + private set + @Volatile final override var maxY = 0 + private set + @Volatile final override var width = 0 + private set + @Volatile final override var minWidth = 0 + private set + @Volatile final override var maxWidth = 0 + private set + @Volatile final override var height = 0 + private set + @Volatile final override var minHeight = 0 + private set + @Volatile final override var maxHeight = 0 + private set + @Volatile final override var canBin = false + private set + @Volatile final override var maxBinX = 1 + private set + @Volatile final override var maxBinY = 1 + private set + @Volatile final override var binX = 1 + private set + @Volatile final override var binY = 1 + private set + @Volatile final override var gain = 0 + private set + @Volatile final override var gainMin = 0 + private set + @Volatile final override var gainMax = 0 + private set + @Volatile final override var offset = 0 + private set + @Volatile final override var offsetMin = 0 + private set + @Volatile final override var offsetMax = 0 + private set + @Volatile final override var hasGuiderHead = false // TODO: Handle guider head. + private set + @Volatile final override var pixelSizeX = 0.0 + private set + @Volatile final override var pixelSizeY = 0.0 + private set + + @Volatile final override var hasThermometer = false + private set + @Volatile final override var temperature = 0.0 + private set + + @Volatile final override var canPulseGuide = false + private set + @Volatile final override var pulseGuiding = false + private set override fun handleMessage(message: INDIProtocol) { when (message) { @@ -74,23 +123,23 @@ internal open class CameraDevice( if (message is DefSwitchVector) { hasCoolerControl = true - handler.fireOnEventReceived(CameraCoolerControlChanged(this)) + sender.fireOnEventReceived(CameraCoolerControlChanged(this)) } cooler = message["COOLER_ON"]?.value ?: false - handler.fireOnEventReceived(CameraCoolerChanged(this)) + sender.fireOnEventReceived(CameraCoolerChanged(this)) } "CCD_CAPTURE_FORMAT" -> { if (message is DefSwitchVector && message.isNotEmpty()) { frameFormats = message.map { it.name } - handler.fireOnEventReceived(CameraFrameFormatsChanged(this)) + sender.fireOnEventReceived(CameraFrameFormatsChanged(this)) } } "CCD_ABORT_EXPOSURE" -> { if (message is DefSwitchVector) { canAbort = message.isNotReadOnly - handler.fireOnEventReceived(CameraCanAbortChanged(this)) + sender.fireOnEventReceived(CameraCanAbortChanged(this)) } } } @@ -101,7 +150,7 @@ internal open class CameraDevice( cfaOffsetX = message["CFA_OFFSET_X"]!!.value.toInt() cfaOffsetY = message["CFA_OFFSET_Y"]!!.value.toInt() cfaType = CfaPattern.valueOf(message["CFA_TYPE"]!!.value) - handler.fireOnEventReceived(CameraCfaChanged(this)) + sender.fireOnEventReceived(CameraCfaChanged(this)) } } } @@ -111,72 +160,70 @@ internal open class CameraDevice( pixelSizeX = message["CCD_PIXEL_SIZE_X"]?.value ?: 0.0 pixelSizeY = message["CCD_PIXEL_SIZE_Y"]?.value ?: 0.0 - handler.fireOnEventReceived(CameraPixelSizeChanged(this)) + sender.fireOnEventReceived(CameraPixelSizeChanged(this)) } "CCD_EXPOSURE" -> { val element = message["CCD_EXPOSURE_VALUE"]!! if (element is DefNumber) { - exposureMin = Duration.ofNanos((element.min * 1000000000.0).toLong()) - exposureMax = Duration.ofNanos((element.max * 1000000000.0).toLong()) - handler.fireOnEventReceived(CameraExposureMinMaxChanged(this)) + exposureMin = Duration.ofNanos((element.min * NANO_SECONDS).toLong()) + exposureMax = Duration.ofNanos((element.max * NANO_SECONDS).toLong()) + sender.fireOnEventReceived(CameraExposureMinMaxChanged(this)) } val prevExposureState = exposureState exposureState = message.state if (exposureState == PropertyState.BUSY || exposureState == PropertyState.OK) { - exposureTime = Duration.ofNanos((element.value * 1000000000.0).toLong()) + exposureTime = Duration.ofNanos((element.value * NANO_SECONDS).toLong()) - handler.fireOnEventReceived(CameraExposureProgressChanged(this)) + sender.fireOnEventReceived(CameraExposureProgressChanged(this)) } val prevIsExposuring = exposuring exposuring = exposureState == PropertyState.BUSY if (prevIsExposuring != exposuring) { - handler.fireOnEventReceived(CameraExposuringChanged(this)) + sender.fireOnEventReceived(CameraExposuringChanged(this)) } - if (exposureState == PropertyState.IDLE - && (prevExposureState == PropertyState.BUSY || exposuring) - ) { - handler.fireOnEventReceived(CameraExposureAborted(this)) + if (exposureState == PropertyState.IDLE && (prevExposureState == PropertyState.BUSY || exposuring)) { + sender.fireOnEventReceived(CameraExposureAborted(this)) } else if (exposureState == PropertyState.OK && prevExposureState == PropertyState.BUSY) { - handler.fireOnEventReceived(CameraExposureFinished(this)) + sender.fireOnEventReceived(CameraExposureFinished(this)) } else if (exposureState == PropertyState.ALERT && prevExposureState != PropertyState.ALERT) { - handler.fireOnEventReceived(CameraExposureFailed(this)) + sender.fireOnEventReceived(CameraExposureFailed(this)) } if (prevExposureState != exposureState) { - handler.fireOnEventReceived(CameraExposureStateChanged(this, prevExposureState)) + sender.fireOnEventReceived(CameraExposureStateChanged(this, prevExposureState)) } } "CCD_COOLER_POWER" -> { coolerPower = message.first().value - handler.fireOnEventReceived(CameraCoolerPowerChanged(this)) + sender.fireOnEventReceived(CameraCoolerPowerChanged(this)) } "CCD_TEMPERATURE" -> { if (message is DefNumberVector) { hasCooler = true canSetTemperature = message.isNotReadOnly - handler.fireOnEventReceived(CameraHasCoolerChanged(this)) - handler.fireOnEventReceived(CameraCanSetTemperatureChanged(this)) + sender.fireOnEventReceived(CameraHasCoolerChanged(this)) + sender.fireOnEventReceived(CameraCanSetTemperatureChanged(this)) if (!hasThermometer) { hasThermometer = true - handler.registerThermometer(this) + sender.registerThermometer(this) } } temperature = message["CCD_TEMPERATURE_VALUE"]!!.value - handler.fireOnEventReceived(CameraTemperatureChanged(this)) + sender.fireOnEventReceived(CameraTemperatureChanged(this)) } "CCD_FRAME" -> { if (message is DefNumberVector) { canSubFrame = message.isNotReadOnly - handler.fireOnEventReceived(CameraCanSubFrameChanged(this)) + sender.fireOnEventReceived(CameraCanSubFrameChanged(this)) val minX = message["X"]!!.min.toInt() val maxX = message["X"]!!.max.toInt() @@ -207,7 +254,7 @@ internal open class CameraDevice( this.width = width this.height = height - handler.fireOnEventReceived(CameraFrameChanged(this)) + sender.fireOnEventReceived(CameraFrameChanged(this)) } "CCD_BINNING" -> { if (message is DefNumberVector) { @@ -215,20 +262,20 @@ internal open class CameraDevice( maxBinX = message["HOR_BIN"]!!.max.toInt() maxBinY = message["VER_BIN"]!!.max.toInt() - handler.fireOnEventReceived(CameraCanBinChanged(this)) + sender.fireOnEventReceived(CameraCanBinChanged(this)) } binX = message["HOR_BIN"]!!.value.toInt() binY = message["VER_BIN"]!!.value.toInt() - handler.fireOnEventReceived(CameraBinChanged(this)) + sender.fireOnEventReceived(CameraBinChanged(this)) } "TELESCOPE_TIMED_GUIDE_NS", "TELESCOPE_TIMED_GUIDE_WE" -> { if (!canPulseGuide && message is DefNumberVector) { canPulseGuide = true - handler.registerGuideOutput(this) + sender.registerGuideOutput(this) LOG.info("guide output attached: {}", name) } else { @@ -236,7 +283,7 @@ internal open class CameraDevice( pulseGuiding = message.isBusy if (pulseGuiding != prevIsPulseGuiding) { - handler.fireOnEventReceived(GuideOutputPulsingChanged(this)) + sender.fireOnEventReceived(GuideOutputPulsingChanged(this)) } } } @@ -248,7 +295,7 @@ internal open class CameraDevice( val ccd1 = message["CCD1"]!! val fits = Base64InputStream(ccd1.value) val compressed = COMPRESSION_FORMATS.any { ccd1.format.endsWith(it, true) } - handler.fireOnEventReceived(CameraFrameCaptured(this, fits, compressed)) + sender.fireOnEventReceived(CameraFrameCaptured(this, fits, null, compressed)) } "CCD2" -> { // TODO: Handle Guider Head frame. @@ -303,7 +350,7 @@ internal open class CameraDevice( override fun offset(value: Int) = Unit override fun startCapture(exposureTime: Duration) { - val exposureInSeconds = exposureTime.toNanos() / 1000000000.0 + val exposureInSeconds = exposureTime.toNanos() / NANO_SECONDS sendNewNumber("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE" to exposureInSeconds) } @@ -339,13 +386,13 @@ internal open class CameraDevice( override fun close() { if (hasThermometer) { - handler.unregisterThermometer(this) + sender.unregisterThermometer(this) hasThermometer = false LOG.info("thermometer detached: {}", name) } if (canPulseGuide) { - handler.unregisterGuideOutput(this) + sender.unregisterGuideOutput(this) canPulseGuide = false LOG.info("guide output detached: {}", name) } @@ -356,12 +403,12 @@ internal open class CameraDevice( gainMin = element.min.toInt() gainMax = element.max.toInt() - handler.fireOnEventReceived(CameraGainMinMaxChanged(this)) + sender.fireOnEventReceived(CameraGainMinMaxChanged(this)) } gain = element.value.toInt() - handler.fireOnEventReceived(CameraGainChanged(this)) + sender.fireOnEventReceived(CameraGainChanged(this)) } protected fun processOffset(message: NumberVector<*>, element: NumberElement) { @@ -369,35 +416,35 @@ internal open class CameraDevice( offsetMin = element.min.toInt() offsetMax = element.max.toInt() - handler.fireOnEventReceived(CameraOffsetMinMaxChanged(this)) + sender.fireOnEventReceived(CameraOffsetMinMaxChanged(this)) } offset = element.value.toInt() - handler.fireOnEventReceived(CameraOffsetChanged(this)) + sender.fireOnEventReceived(CameraOffsetChanged(this)) } 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)" companion object { @JvmStatic private val COMPRESSION_FORMATS = arrayOf(".fz", ".gz") - @JvmStatic private val LOG = loggerFor() + @JvmStatic private val LOG = loggerFor() } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/SVBonyCamera.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/SVBonyCamera.kt similarity index 90% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/SVBonyCamera.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/SVBonyCamera.kt index 71df208b5..f8e2f8f45 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/SVBonyCamera.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/SVBonyCamera.kt @@ -1,13 +1,13 @@ -package nebulosa.indi.client.device.camera +package nebulosa.indi.client.device.cameras -import nebulosa.indi.client.device.DeviceProtocolHandler +import nebulosa.indi.client.INDIClient import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.NumberVector internal class SVBonyCamera( - handler: DeviceProtocolHandler, + provider: INDIClient, name: String, -) : CameraDevice(handler, name) { +) : INDICamera(provider, name) { @Volatile private var legacyProperties = false diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/SimCamera.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/SimCamera.kt similarity index 83% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/SimCamera.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/SimCamera.kt index b505b8560..4a674e898 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/SimCamera.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/SimCamera.kt @@ -1,13 +1,13 @@ -package nebulosa.indi.client.device.camera +package nebulosa.indi.client.device.cameras -import nebulosa.indi.client.device.DeviceProtocolHandler +import nebulosa.indi.client.INDIClient import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.NumberVector internal class SimCamera( - handler: DeviceProtocolHandler, + provider: INDIClient, name: String, -) : CameraDevice(handler, name) { +) : INDICamera(provider, name) { override fun handleMessage(message: INDIProtocol) { when (message) { diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FocuserDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/focusers/INDIFocuser.kt similarity index 62% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FocuserDevice.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/focusers/INDIFocuser.kt index 0fe613cb2..3dfa9d22e 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/FocuserDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/focusers/INDIFocuser.kt @@ -1,29 +1,43 @@ -package nebulosa.indi.client.device +package nebulosa.indi.client.device.focusers +import nebulosa.indi.client.INDIClient +import nebulosa.indi.client.device.INDIDevice import nebulosa.indi.device.firstOnSwitch import nebulosa.indi.device.focuser.* import nebulosa.indi.device.thermometer.ThermometerAttached import nebulosa.indi.device.thermometer.ThermometerDetached import nebulosa.indi.protocol.* -internal open class FocuserDevice( - handler: DeviceProtocolHandler, - name: String, -) : AbstractDevice(handler, name), Focuser { - - override var moving = false - override var position = 0 - override var canAbsoluteMove = false - override var canRelativeMove = false - override var canAbort = false - override var canReverse = false - override var reverse = false - override var canSync = false - override var hasBacklash = false - override var maxPosition = 0 - - override var hasThermometer = false - override var temperature = 0.0 +internal open class INDIFocuser( + override val sender: INDIClient, + override val name: String, +) : INDIDevice(), Focuser { + + @Volatile final override var moving = false + private set + @Volatile final override var position = 0 + private set + @Volatile final override var canAbsoluteMove = false + private set + @Volatile final override var canRelativeMove = false + private set + @Volatile final override var canAbort = false + private set + @Volatile final override var canReverse = false + private set + @Volatile final override var reversed = false + private set + @Volatile final override var canSync = false + private set + @Volatile final override var hasBacklash = false + private set + @Volatile final override var maxPosition = 0 + private set + + @Volatile final override var hasThermometer = false + private set + @Volatile final override var temperature = 0.0 + private set override fun handleMessage(message: INDIProtocol) { when (message) { @@ -33,19 +47,19 @@ internal open class FocuserDevice( if (message is DefSwitchVector) { canAbort = true - handler.fireOnEventReceived(FocuserCanAbortChanged(this)) + sender.fireOnEventReceived(FocuserCanAbortChanged(this)) } } "FOCUS_REVERSE_MOTION" -> { if (message is DefSwitchVector) { canReverse = true - handler.fireOnEventReceived(FocuserCanReverseChanged(this)) + sender.fireOnEventReceived(FocuserCanReverseChanged(this)) } - reverse = message.firstOnSwitch().name == "INDI_ENABLED" + reversed = message.firstOnSwitch().name == "INDI_ENABLED" - handler.fireOnEventReceived(FocuserReverseChanged(this)) + sender.fireOnEventReceived(FocuserReverseChanged(this)) } "FOCUS_BACKLASH_TOGGLE" -> { @@ -60,22 +74,22 @@ internal open class FocuserDevice( val prevMaxPosition = maxPosition maxPosition = message["FOCUS_RELATIVE_POSITION"]!!.max.toInt() - handler.fireOnEventReceived(FocuserCanRelativeMoveChanged(this)) + sender.fireOnEventReceived(FocuserCanRelativeMoveChanged(this)) if (prevMaxPosition != maxPosition) { - handler.fireOnEventReceived(FocuserMaxPositionChanged(this)) + sender.fireOnEventReceived(FocuserMaxPositionChanged(this)) } } if (message.state == PropertyState.ALERT) { - handler.fireOnEventReceived(FocuserMoveFailed(this)) + sender.fireOnEventReceived(FocuserMoveFailed(this)) } val prevIsMoving = moving moving = message.isBusy if (prevIsMoving != moving) { - handler.fireOnEventReceived(FocuserMovingChanged(this)) + sender.fireOnEventReceived(FocuserMovingChanged(this)) } } "ABS_FOCUS_POSITION" -> { @@ -84,36 +98,36 @@ internal open class FocuserDevice( val prevMaxPosition = maxPosition maxPosition = message["FOCUS_ABSOLUTE_POSITION"]!!.max.toInt() - handler.fireOnEventReceived(FocuserCanAbsoluteMoveChanged(this)) + sender.fireOnEventReceived(FocuserCanAbsoluteMoveChanged(this)) if (prevMaxPosition != maxPosition) { - handler.fireOnEventReceived(FocuserMaxPositionChanged(this)) + sender.fireOnEventReceived(FocuserMaxPositionChanged(this)) } } if (message.state == PropertyState.ALERT) { - handler.fireOnEventReceived(FocuserMoveFailed(this)) + sender.fireOnEventReceived(FocuserMoveFailed(this)) } val prevPosition = position position = message["FOCUS_ABSOLUTE_POSITION"]!!.value.toInt() if (prevPosition != position) { - handler.fireOnEventReceived(FocuserPositionChanged(this)) + sender.fireOnEventReceived(FocuserPositionChanged(this)) } val prevIsMoving = moving moving = message.isBusy if (prevIsMoving != moving) { - handler.fireOnEventReceived(FocuserMovingChanged(this)) + sender.fireOnEventReceived(FocuserMovingChanged(this)) } } "FOCUS_SYNC" -> { if (message is DefNumberVector) { canSync = true - handler.fireOnEventReceived(FocuserCanSyncChanged(this)) + sender.fireOnEventReceived(FocuserCanSyncChanged(this)) } } "FOCUS_BACKLASH_STEPS" -> { @@ -122,12 +136,12 @@ internal open class FocuserDevice( "FOCUS_TEMPERATURE" -> { if (message is DefNumberVector) { hasThermometer = true - handler.fireOnEventReceived(ThermometerAttached(this)) + sender.fireOnEventReceived(ThermometerAttached(this)) } temperature = message["TEMPERATURE"]!!.value - handler.fireOnEventReceived(FocuserTemperatureChanged(this)) + sender.fireOnEventReceived(FocuserTemperatureChanged(this)) } } } @@ -178,16 +192,16 @@ internal open class FocuserDevice( override fun close() { if (hasThermometer) { hasThermometer = false - handler.fireOnEventReceived(ThermometerDetached(this)) + sender.fireOnEventReceived(ThermometerDetached(this)) } } override fun toString(): String { return "Focuser(name=$name, moving=$moving, position=$position," + - " canAbsoluteMove=$canAbsoluteMove, canRelativeMove=$canRelativeMove," + - " canAbort=$canAbort, canReverse=$canReverse, reverse=$reverse," + - " canSync=$canSync, hasBacklash=$hasBacklash," + - " maxPosition=$maxPosition, hasThermometer=$hasThermometer," + - " temperature=$temperature)" + " canAbsoluteMove=$canAbsoluteMove, canRelativeMove=$canRelativeMove," + + " canAbort=$canAbort, canReverse=$canReverse, reversed=$reversed," + + " canSync=$canSync, hasBacklash=$hasBacklash," + + " maxPosition=$maxPosition, hasThermometer=$hasThermometer," + + " temperature=$temperature)" } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mounts/INDIMount.kt similarity index 68% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mounts/INDIMount.kt index bd7c9fa8e..84fe447bf 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mounts/INDIMount.kt @@ -1,7 +1,7 @@ -package nebulosa.indi.client.device.mount +package nebulosa.indi.client.device.mounts -import nebulosa.indi.client.device.AbstractDevice -import nebulosa.indi.client.device.DeviceProtocolHandler +import nebulosa.indi.client.INDIClient +import nebulosa.indi.client.device.INDIDevice import nebulosa.indi.device.firstOnSwitch import nebulosa.indi.device.firstOnSwitchOrNull import nebulosa.indi.device.gps.GPS @@ -15,39 +15,65 @@ import java.time.Duration import java.time.OffsetDateTime import java.time.ZoneOffset -internal open class MountDevice( - handler: DeviceProtocolHandler, - name: String, -) : AbstractDevice(handler, name), Mount { - - override var slewing = false - override var tracking = false - override var parking = false - override var parked = false - override var canAbort = false - override var canSync = false - override var canGoTo = false - override var canPark = false - override var canHome = false - override var slewRates = emptyList() - override var slewRate: SlewRate? = null - override var mountType = MountType.EQ_GEM // TODO: Ver os telescรณpios possui tipos. - override var trackModes = emptyList() - override var trackMode = TrackMode.SIDEREAL - override var pierSide = PierSide.NEITHER - override var guideRateWE = 0.0 // TODO: Tratar para cada driver. iOptronV3 tem RA/DE. LX200 tem 1.0x, 0.8x, 0.6x, 0.4x. - override var guideRateNS = 0.0 - override var rightAscension = 0.0 - override var declination = 0.0 - - override var canPulseGuide = false - override var pulseGuiding = false - - override var hasGPS = false - override var longitude = 0.0 - override var latitude = 0.0 - override var elevation = 0.0 - override var dateTime = OffsetDateTime.now()!! +internal open class INDIMount( + override val sender: INDIClient, + override val name: String, +) : INDIDevice(), Mount { + + @Volatile final override var slewing = false + private set + @Volatile final override var tracking = false + private set + @Volatile final override var parking = false + private set + @Volatile final override var parked = false + private set + @Volatile final override var canAbort = false + private set + @Volatile final override var canSync = false + private set + @Volatile final override var canGoTo = false + private set + @Volatile final override var canPark = false + private set + @Volatile final override var canHome = false + protected set + @Volatile final override var slewRates = emptyList() + private set + @Volatile final override var slewRate: SlewRate? = null + private set + @Volatile final override var mountType = MountType.EQ_GEM // TODO: Ver os telescรณpios possui tipos. + private set + @Volatile final override var trackModes = emptyList() + private set + @Volatile final override var trackMode = TrackMode.SIDEREAL + private set + @Volatile final override var pierSide = PierSide.NEITHER + private set + @Volatile final override var guideRateWE = 0.0 // TODO: Tratar para cada driver. iOptronV3 tem RA/DE. LX200 tem 1.0x, 0.8x, 0.6x, 0.4x. + private set + @Volatile final override var guideRateNS = 0.0 + private set + @Volatile final override var rightAscension = 0.0 + private set + @Volatile final override var declination = 0.0 + private set + + @Volatile final override var canPulseGuide = false + private set + @Volatile final override var pulseGuiding = false + private set + + @Volatile final override var hasGPS = false + private set + @Volatile final override var longitude = 0.0 + private set + @Volatile final override var latitude = 0.0 + private set + @Volatile final override var elevation = 0.0 + private set + @Volatile final override var dateTime = OffsetDateTime.now()!! + private set override fun handleMessage(message: INDIProtocol) { when (message) { @@ -57,36 +83,36 @@ internal open class MountDevice( if (message is DefSwitchVector) { slewRates = message.map { SlewRate(it.name, it.label) } - handler.fireOnEventReceived(MountSlewRatesChanged(this)) + sender.fireOnEventReceived(MountSlewRatesChanged(this)) } val name = message.firstOnSwitch().name if (slewRate?.name != name) { slewRate = slewRates.firstOrNull { it.name == name } - handler.fireOnEventReceived(MountSlewRateChanged(this)) + sender.fireOnEventReceived(MountSlewRateChanged(this)) } } // "MOUNT_TYPE" -> { // mountType = MountType.valueOf(message.firstOnSwitch().name) // - // handler.fireOnEventReceived(MountTypeChanged(this)) + // provider.fireOnEventReceived(MountTypeChanged(this)) // } "TELESCOPE_TRACK_MODE" -> { if (message is DefSwitchVector) { trackModes = message.map { TrackMode.valueOf(it.name.replace("TRACK_", "")) } - handler.fireOnEventReceived(MountTrackModesChanged(this)) + sender.fireOnEventReceived(MountTrackModesChanged(this)) } trackMode = TrackMode.valueOf(message.firstOnSwitch().name.replace("TRACK_", "")) - handler.fireOnEventReceived(MountTrackModeChanged(this)) + sender.fireOnEventReceived(MountTrackModeChanged(this)) } "TELESCOPE_TRACK_STATE" -> { tracking = message.firstOnSwitch().name == "TRACK_ON" - handler.fireOnEventReceived(MountTrackingChanged(this)) + sender.fireOnEventReceived(MountTrackingChanged(this)) } "TELESCOPE_PIER_SIDE" -> { val side = message.firstOnSwitchOrNull() @@ -95,31 +121,31 @@ internal open class MountDevice( else if (side.name == "PIER_WEST") PierSide.WEST else PierSide.EAST - handler.fireOnEventReceived(MountPierSideChanged(this)) + sender.fireOnEventReceived(MountPierSideChanged(this)) } "TELESCOPE_PARK" -> { if (message is DefSwitchVector) { canPark = message.isNotReadOnly - handler.fireOnEventReceived(MountCanParkChanged(this)) + sender.fireOnEventReceived(MountCanParkChanged(this)) } parking = message.isBusy parked = message.firstOnSwitchOrNull()?.name == "PARK" - handler.fireOnEventReceived(MountParkChanged(this)) + sender.fireOnEventReceived(MountParkChanged(this)) } "TELESCOPE_ABORT_MOTION" -> { canAbort = true - handler.fireOnEventReceived(MountCanAbortChanged(this)) + sender.fireOnEventReceived(MountCanAbortChanged(this)) } "ON_COORD_SET" -> { canSync = message.any { it.name == "SYNC" } canGoTo = message.any { it.name == "TRACK" } - handler.fireOnEventReceived(MountCanSyncChanged(this)) - handler.fireOnEventReceived(MountCanGoToChanged(this)) + sender.fireOnEventReceived(MountCanSyncChanged(this)) + sender.fireOnEventReceived(MountCanGoToChanged(this)) } } } @@ -129,31 +155,31 @@ internal open class MountDevice( // guideRateWE = message["GUIDE_RATE_WE"]!!.value // guideRateNS = message["GUIDE_RATE_NS"]!!.value // - // handler.fireOnEventReceived(MountGuideRateChanged(this)) + // provider.fireOnEventReceived(MountGuideRateChanged(this)) // } "EQUATORIAL_EOD_COORD" -> { if (message.state == PropertyState.ALERT) { - handler.fireOnEventReceived(MountSlewFailed(this)) + sender.fireOnEventReceived(MountSlewFailed(this)) } val prevIsIslewing = slewing slewing = message.isBusy if (slewing != prevIsIslewing) { - handler.fireOnEventReceived(MountSlewingChanged(this)) + sender.fireOnEventReceived(MountSlewingChanged(this)) } rightAscension = message["RA"]!!.value.hours declination = message["DEC"]!!.value.deg - handler.fireOnEventReceived(MountEquatorialCoordinatesChanged(this)) + sender.fireOnEventReceived(MountEquatorialCoordinatesChanged(this)) } "TELESCOPE_TIMED_GUIDE_NS", "TELESCOPE_TIMED_GUIDE_WE" -> { if (!canPulseGuide && message is DefNumberVector) { canPulseGuide = true - handler.registerGuideOutput(this) + sender.registerGuideOutput(this) LOG.info("guide output attached: {}", name) } @@ -163,7 +189,7 @@ internal open class MountDevice( pulseGuiding = message.isBusy if (pulseGuiding != prevIsPulseGuiding) { - handler.fireOnEventReceived(GuideOutputPulsingChanged(this)) + sender.fireOnEventReceived(GuideOutputPulsingChanged(this)) } } } @@ -172,7 +198,7 @@ internal open class MountDevice( longitude = message["LONG"]!!.value.deg elevation = message["ELEV"]!!.value.m - handler.fireOnEventReceived(MountGeographicCoordinateChanged(this)) + sender.fireOnEventReceived(MountGeographicCoordinateChanged(this)) } } } @@ -184,7 +210,7 @@ internal open class MountDevice( dateTime = OffsetDateTime.of(utcTime, ZoneOffset.ofTotalSeconds((utcOffset * 3600.0).toInt())) - handler.fireOnEventReceived(MountTimeChanged(this)) + sender.fireOnEventReceived(MountTimeChanged(this)) } } } @@ -315,30 +341,30 @@ internal open class MountDevice( override fun close() { if (canPulseGuide) { canPulseGuide = false - handler.unregisterGuideOutput(this) + sender.unregisterGuideOutput(this) LOG.info("guide output detached: {}", name) } if (hasGPS) { hasGPS = false - handler.unregisterGPS(this) + sender.unregisterGPS(this) LOG.info("GPS detached: {}", name) } } override fun toString(): String { return "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)" + " 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)" } companion object { - @JvmStatic private val LOG = loggerFor() + @JvmStatic private val LOG = loggerFor() } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/IoptronV3Mount.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mounts/IoptronV3Mount.kt similarity index 72% rename from nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/IoptronV3Mount.kt rename to nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mounts/IoptronV3Mount.kt index 57d90e4b5..4ef2fc209 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/IoptronV3Mount.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mounts/IoptronV3Mount.kt @@ -1,14 +1,14 @@ -package nebulosa.indi.client.device.mount +package nebulosa.indi.client.device.mounts -import nebulosa.indi.client.device.DeviceProtocolHandler +import nebulosa.indi.client.INDIClient import nebulosa.indi.device.mount.MountCanHomeChanged import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.SwitchVector internal class IoptronV3Mount( - handler: DeviceProtocolHandler, + provider: INDIClient, name: String, -) : MountDevice(handler, name) { +) : INDIMount(provider, name) { override fun handleMessage(message: INDIProtocol) { when (message) { @@ -16,7 +16,7 @@ internal class IoptronV3Mount( when (message.name) { "HOME" -> { canHome = true - handler.fireOnEventReceived(MountCanHomeChanged(this)) + sender.fireOnEventReceived(MountCanHomeChanged(this)) } } } diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/wheels/INDIFilterWheel.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/wheels/INDIFilterWheel.kt new file mode 100644 index 000000000..cb0cd3658 --- /dev/null +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/wheels/INDIFilterWheel.kt @@ -0,0 +1,93 @@ +package nebulosa.indi.client.device.wheels + +import nebulosa.indi.client.INDIClient +import nebulosa.indi.client.device.INDIDevice +import nebulosa.indi.device.filterwheel.* +import nebulosa.indi.protocol.* + +internal open class INDIFilterWheel( + override val sender: INDIClient, + override val name: String, +) : INDIDevice(), FilterWheel { + + @Volatile final override var count = 0 + private set + @Volatile final override var position = 0 + private set + @Volatile final override var moving = false + private set + + final override val names = ArrayList(12) + + override fun handleMessage(message: INDIProtocol) { + when (message) { + is NumberVector<*> -> { + when (message.name) { + "FILTER_SLOT" -> { + val slot = message["FILTER_SLOT_VALUE"]!! + + if (message is DefNumberVector) { + count = slot.max.toInt() - slot.min.toInt() + 1 + sender.fireOnEventReceived(FilterWheelCountChanged(this)) + } + + if (message.state == PropertyState.ALERT) { + sender.fireOnEventReceived(FilterWheelMoveFailed(this)) + } + + val prevPosition = position + position = slot.value.toInt() + + if (prevPosition != position) { + sender.fireOnEventReceived(FilterWheelPositionChanged(this, prevPosition)) + } + + val prevIsMoving = moving + moving = message.isBusy + + if (prevIsMoving != moving) { + sender.fireOnEventReceived(FilterWheelMovingChanged(this)) + } + } + } + } + is TextVector<*> -> { + when (message.name) { + "FILTER_NAME" -> { + names.clear() + + repeat(16) { + val key = "FILTER_SLOT_NAME_${it + 1}" + + if (key in message) { + names.add(message[key]!!.value) + } + } + + sender.fireOnEventReceived(FilterWheelNamesChanged(this)) + } + } + } + else -> Unit + } + + super.handleMessage(message) + } + + override fun moveTo(position: Int) { + if (position in 1..count) { + sendNewNumber("FILTER_SLOT", "FILTER_SLOT_VALUE" to position.toDouble()) + } + } + + override fun names(names: Iterable) { + sendNewText("FILTER_NAME", names.mapIndexed { i, name -> "FILTER_SLOT_NAME_${i + 1}" to name }) + } + + override fun close() = Unit + + override fun toString(): String { + return "FilterWheel(name=$name, slotCount=$count, position=$position," + + " moving=$moving)" + } +} diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/ConnectionEvent.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/ConnectionEvent.kt deleted file mode 100644 index 82aebd42b..000000000 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/ConnectionEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.indi.device - -interface ConnectionEvent : DeviceEvent, PropertyChangedEvent diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt index 59c0a6910..2e575b3cf 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt @@ -6,6 +6,10 @@ import java.io.Closeable interface Device : INDIProtocolHandler, Closeable { + val sender: MessageSender + + val id: String + val name: String val connected: Boolean @@ -18,7 +22,9 @@ interface Device : INDIProtocolHandler, Closeable { fun disconnect() - fun sendMessageToServer(message: INDIProtocol) + fun sendMessageToServer(message: INDIProtocol) { + sender.sendMessageToServer(message) + } fun ask() { sendMessageToServer(GetProperties().also { it.device = name }) diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceAttached.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceAttached.kt index 7e3dfab1b..040e7f263 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceAttached.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceAttached.kt @@ -1,3 +1,6 @@ package nebulosa.indi.device -interface DeviceAttached : DeviceEvent +interface DeviceAttached : DeviceEvent { + + override val device: T +} diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceConnected.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceConnected.kt index dc3b55515..338625b9b 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceConnected.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceConnected.kt @@ -1,3 +1,3 @@ package nebulosa.indi.device -data class DeviceConnected(override val device: Device) : DeviceEvent, ConnectionEvent +data class DeviceConnected(override val device: Device) : DeviceEvent, DeviceConnectionEvent diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceConnectionEvent.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceConnectionEvent.kt new file mode 100644 index 000000000..74c66b958 --- /dev/null +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceConnectionEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.indi.device + +interface DeviceConnectionEvent : DeviceEvent, PropertyChangedEvent diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDetached.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDetached.kt index e47619725..b36e97fb8 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDetached.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDetached.kt @@ -1,3 +1,6 @@ package nebulosa.indi.device -interface DeviceDetached : DeviceEvent +interface DeviceDetached : DeviceEvent { + + override val device: T +} diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDisconnected.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDisconnected.kt index edafc4a3d..94a5f9a9b 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDisconnected.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceDisconnected.kt @@ -1,3 +1,3 @@ package nebulosa.indi.device -data class DeviceDisconnected(override val device: Device) : DeviceEvent, ConnectionEvent +data class DeviceDisconnected(override val device: Device) : DeviceEvent, DeviceConnectionEvent diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceEventHandler.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceEventHandler.kt index 3300fcd3d..4b6e11806 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceEventHandler.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceEventHandler.kt @@ -1,6 +1,18 @@ package nebulosa.indi.device -fun interface DeviceEventHandler { +interface DeviceEventHandler { fun onEventReceived(event: DeviceEvent<*>) + + fun onConnectionClosed() + + fun interface EventReceived : DeviceEventHandler { + + override fun onConnectionClosed() = Unit + } + + fun interface ConnectionClosed : DeviceEventHandler { + + override fun onEventReceived(event: DeviceEvent<*>) = Unit + } } diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/INDIDeviceProvider.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/INDIDeviceProvider.kt new file mode 100644 index 000000000..2f6083188 --- /dev/null +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/INDIDeviceProvider.kt @@ -0,0 +1,45 @@ +package nebulosa.indi.device + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.gps.GPS +import nebulosa.indi.device.guide.GuideOutput +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.thermometer.Thermometer +import java.io.Closeable + +interface INDIDeviceProvider : MessageSender, Closeable { + + fun registerDeviceEventHandler(handler: DeviceEventHandler) + + fun unregisterDeviceEventHandler(handler: DeviceEventHandler) + + fun cameras(): List + + fun camera(name: String): Camera? + + fun mounts(): List + + fun mount(name: String): Mount? + + fun focusers(): List + + fun focuser(name: String): Focuser? + + fun wheels(): List + + fun wheel(name: String): FilterWheel? + + fun gps(): List + + fun gps(name: String): GPS? + + fun guideOutputs(): List + + fun guideOutput(name: String): GuideOutput? + + fun thermometers(): List + + fun thermometer(name: String): Thermometer? +} diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/MessageSender.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/MessageSender.kt index 6cdfc6b31..ba4326939 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/MessageSender.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/MessageSender.kt @@ -4,5 +4,7 @@ import nebulosa.indi.protocol.INDIProtocol interface MessageSender { + val id: String + fun sendMessageToServer(message: INDIProtocol) } diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt index d1de55e98..6ec7efae8 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt @@ -120,6 +120,8 @@ interface Camera : GuideOutput, Thermometer { companion object { + const val NANO_SECONDS = 1_000_000_000.0 + @JvmStatic val DRIVERS = setOf( "indi_altair_ccd", "indi_apogee_ccd", diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraFrameCaptured.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraFrameCaptured.kt index a97f35c21..d75cce06d 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraFrameCaptured.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraFrameCaptured.kt @@ -1,9 +1,11 @@ package nebulosa.indi.device.camera +import nebulosa.fits.Fits import java.io.InputStream data class CameraFrameCaptured( override val device: Camera, - val fits: InputStream, - val compressed: Boolean, + @JvmField val stream: InputStream?, + @JvmField val fits: Fits?, + @JvmField val compressed: Boolean, ) : CameraEvent diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt index 3688079b8..f6bb25e2b 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/FrameType.kt @@ -1,8 +1,8 @@ package nebulosa.indi.device.camera -enum class FrameType { - LIGHT, - DARK, - FLAT, - BIAS, +enum class FrameType(@JvmField val description: String) { + LIGHT("Light"), + DARK("Dark"), + FLAT("Flat"), + BIAS("Bias"), } diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/filterwheel/FilterWheel.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/filterwheel/FilterWheel.kt index f416c412c..ff8eee139 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/filterwheel/FilterWheel.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/filterwheel/FilterWheel.kt @@ -10,9 +10,11 @@ interface FilterWheel : Device { val moving: Boolean + val names: List + fun moveTo(position: Int) - fun syncNames(names: Iterable) + fun names(names: Iterable) companion object { diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/filterwheel/FilterWheelNamesChanged.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/filterwheel/FilterWheelNamesChanged.kt new file mode 100644 index 000000000..941e480a3 --- /dev/null +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/filterwheel/FilterWheelNamesChanged.kt @@ -0,0 +1,5 @@ +package nebulosa.indi.device.filterwheel + +import nebulosa.indi.device.PropertyChangedEvent + +data class FilterWheelNamesChanged(override val device: FilterWheel) : FilterWheelEvent, PropertyChangedEvent diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt index 12d4b4787..a18b1e2c9 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/focuser/Focuser.kt @@ -17,7 +17,7 @@ interface Focuser : Device, Thermometer { val canReverse: Boolean - val reverse: Boolean + val reversed: Boolean val canSync: Boolean diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/MountEquatorialJ2000CoordinatesChanged.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/MountEquatorialJ2000CoordinatesChanged.kt deleted file mode 100644 index 930eecb31..000000000 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/MountEquatorialJ2000CoordinatesChanged.kt +++ /dev/null @@ -1,5 +0,0 @@ -package nebulosa.indi.device.mount - -import nebulosa.indi.device.PropertyChangedEvent - -data class MountEquatorialJ2000CoordinatesChanged(override val device: Mount) : MountEvent, PropertyChangedEvent diff --git a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/CloseConnectionListener.kt b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/CloseConnectionListener.kt new file mode 100644 index 000000000..951757920 --- /dev/null +++ b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/CloseConnectionListener.kt @@ -0,0 +1,6 @@ +package nebulosa.indi.protocol.parser + +fun interface CloseConnectionListener { + + fun onConnectionClosed() +} diff --git a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt index 0d9051d3e..2ccd92661 100644 --- a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt +++ b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt @@ -8,6 +8,8 @@ class INDIProtocolReader( priority: Int = NORM_PRIORITY, ) : Thread(), Closeable { + private val listeners = HashSet(1) + @Volatile private var running = false init { @@ -17,21 +19,37 @@ class INDIProtocolReader( val isRunning get() = running - override fun run() { - val input = parser.input ?: return parser.close() + fun registerCloseConnectionListener(listener: CloseConnectionListener) { + listeners.add(listener) + } + + fun unregisterCloseConnectionListener(listener: CloseConnectionListener) { + listeners.remove(listener) + } + override fun start() { running = true + super.start() + } + + override fun run() { + val input = parser.input try { while (running) { - val message = input.readINDIProtocol() ?: break + val message = input?.readINDIProtocol() ?: break parser.handleMessage(message) } LOG.info("protocol parser finished") + listeners.onEach { it.onConnectionClosed() }.clear() + parser.close() } catch (_: InterruptedException) { - LOG.info("protocol parser interrupted") + running = false + LOG.error("protocol parser interrupted") } catch (e: Throwable) { + running = false + listeners.onEach { it.onConnectionClosed() }.clear() LOG.error("protocol parser error", e) parser.close() } diff --git a/nebulosa-io/src/main/kotlin/nebulosa/io/Io.kt b/nebulosa-io/src/main/kotlin/nebulosa/io/Io.kt index ebe3536d8..73893ff4a 100644 --- a/nebulosa-io/src/main/kotlin/nebulosa/io/Io.kt +++ b/nebulosa-io/src/main/kotlin/nebulosa/io/Io.kt @@ -158,6 +158,9 @@ inline fun Buffer.read(source: Source, byteCount: Long, block: (Buffer) -> T } } +/** + * Reads [byteCount] bytes from [source] to this buffer. + */ fun Buffer.readFully(source: Source, byteCount: Long) { var remainingCount = byteCount @@ -168,6 +171,9 @@ fun Buffer.readFully(source: Source, byteCount: Long) { } } +/** + * Transfers [byteCount] bytes from [source] to [sink] using this buffer as intermediate. + */ fun Buffer.transferFully(source: Source, sink: Sink, byteCount: Long) { var remainingCount = byteCount diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt index c78a1106e..f1ecf6057 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt @@ -110,6 +110,12 @@ fun Angle.dms(): DoubleArray { inline fun Angle.format(formatter: AngleFormatter) = formatter.format(this) +inline fun Angle.formatHMS() = format(AngleFormatter.HMS) + +inline fun Angle.formatDMS() = format(AngleFormatter.DMS) + +inline fun Angle.formatSignedDMS() = format(AngleFormatter.SIGNED_DMS) + inline fun HMS(hour: Int, minute: Int, second: Double = 0.0) = (hour + minute / 60.0 + second / 3600.0).hours inline fun DMS(degrees: Int, minute: Int, second: Double = 0.0, negative: Boolean = degrees < 0) = diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Matrix3D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Matrix3D.kt index 909f93f42..0304ba8cc 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Matrix3D.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Matrix3D.kt @@ -1,7 +1,7 @@ package nebulosa.math @Suppress("NOTHING_TO_INLINE") -open class Matrix3D(@PublishedApi @JvmField internal val matrix: DoubleArray) { +open class Matrix3D(@PublishedApi @JvmField internal val matrix: DoubleArray) : Cloneable { constructor( a11: Double = 0.0, a12: Double = 0.0, a13: Double = 0.0, @@ -37,7 +37,7 @@ open class Matrix3D(@PublishedApi @JvmField internal val matrix: DoubleArray) { get() = matrix[8] // TODO: Potentially less stable than using LU decomposition - inline val determinant: Double + val determinant: Double get() { val a = a11 * (a22 * a33 - a23 * a32) val b = a12 * (a21 * a33 - a23 * a31) @@ -107,9 +107,9 @@ open class Matrix3D(@PublishedApi @JvmField internal val matrix: DoubleArray) { ) inline operator fun times(other: Vector3D) = Vector3D( - a11 * other.x + a12 * other.y + a13 * other.z, - a21 * other.x + a22 * other.y + a23 * other.z, - a31 * other.x + a32 * other.y + a33 * other.z, + a11 * other.vector[0] + a12 * other.vector[1] + a13 * other.vector[2], + a21 * other.vector[0] + a22 * other.vector[1] + a23 * other.vector[2], + a31 * other.vector[0] + a32 * other.vector[1] + a33 * other.vector[2], ) inline operator fun times(scalar: Double) = Matrix3D( @@ -166,9 +166,11 @@ open class Matrix3D(@PublishedApi @JvmField internal val matrix: DoubleArray) { inline fun flipY() = Matrix3D(a13, a12, a11, a23, a22, a21, a33, a32, a31) + override fun clone() = Matrix3D(matrix.copyOf()) + fun isEmpty() = a11 == 0.0 && a12 == 0.0 && a13 == 0.0 && - a21 == 0.0 && a22 == 0.0 && a23 == 0.0 && - a31 == 0.0 && a32 == 0.0 && a33 == 0.0 + a21 == 0.0 && a22 == 0.0 && a23 == 0.0 && + a31 == 0.0 && a32 == 0.0 && a33 == 0.0 override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt new file mode 100644 index 000000000..ea18cf534 --- /dev/null +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Point2D.kt @@ -0,0 +1,18 @@ +package nebulosa.math + +import kotlin.math.hypot + +interface Point2D { + + val x: Double + + val y: Double + + operator fun component1() = x + + operator fun component2() = y + + fun distance(other: Point2D): Double { + return hypot(x - other.x, y - other.y) + } +} diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Point3D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Point3D.kt new file mode 100644 index 000000000..996d2a2ee --- /dev/null +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Point3D.kt @@ -0,0 +1,17 @@ +package nebulosa.math + +import kotlin.math.sqrt + +interface Point3D : Point2D { + + val z: Double + + operator fun component3() = 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) + } +} diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Pressure.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Pressure.kt index 445695c67..fa24cd4c5 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Pressure.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Pressure.kt @@ -6,7 +6,7 @@ import kotlin.math.exp import kotlin.math.pow /** - * Represents a pressure value in millibars. + * Represents a pressure value in millibars/hPa. */ typealias Pressure = Double @@ -47,7 +47,7 @@ inline val Int.atm * * https://www.mide.com/air-pressure-at-altitude-calculator */ -fun Distance.pressure(temperature: Temperature = 10.0.celsius): Pressure { +fun Distance.pressure(temperature: Temperature = 15.0.celsius): Pressure { val e = 9.80665 * 0.0289644 / (8.31432 * -0.0065) val k = temperature.toKelvin val m = toMeters diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Unsafe.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Unsafe.kt new file mode 100644 index 000000000..39cc3f3ef --- /dev/null +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Unsafe.kt @@ -0,0 +1,59 @@ +package nebulosa.math + +import kotlin.annotation.AnnotationTarget.* + +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Retention(AnnotationRetention.BINARY) +@Target(CLASS, ANNOTATION_CLASS, PROPERTY, FIELD, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS) +@MustBeDocumented +annotation class Unsafe + +/** + * Allows manipulate Vector's array in unsafe mode. + */ +@Unsafe +inline fun Vector3D.unsafe(block: UnsafeVector3D.() -> T) = with(UnsafeVector3D(this), block) + +/** + * Allows manipulate Vector's array in unsafe mode. + */ +@Unsafe +inline fun Vector3D.unsafe(block: UnsafeVector3D.() -> Unit) = UnsafeVector3D(this).also(block).vector + +/** + * Allows manipulate Matrix's array in unsafe mode. + */ +@Unsafe +inline fun Matrix3D.unsafe(block: UnsafeMatrix3D.() -> T) = with(UnsafeMatrix3D(this), block) + +/** + * Allows manipulate Matrix's array in unsafe mode. + */ +@Unsafe +inline fun Matrix3D.unsafe(block: UnsafeMatrix3D.() -> Unit) = UnsafeMatrix3D(this).also(block).matrix + +@JvmInline +@Suppress("NOTHING_TO_INLINE") +value class UnsafeVector3D(@JvmField @PublishedApi internal val vector: Vector3D) { + + inline operator fun get(index: Int): Double { + return vector.vector[index] + } + + inline operator fun set(index: Int, value: Double) { + vector.vector[index] = value + } +} + +@JvmInline +@Suppress("NOTHING_TO_INLINE") +value class UnsafeMatrix3D(@JvmField @PublishedApi internal val matrix: Matrix3D) { + + inline operator fun get(index: Int): Double { + return matrix.matrix[index] + } + + inline operator fun set(index: Int, value: Double) { + matrix.matrix[index] = value + } +} diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt index 9dc0d4a65..b641ed4ed 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Vector3D.kt @@ -1,11 +1,12 @@ package nebulosa.math -import kotlin.math.asin +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) { +open class Vector3D protected constructor(@PublishedApi @JvmField internal val vector: DoubleArray) : Point3D, Cloneable { constructor(x: Double = 0.0, y: Double = 0.0, z: Double = 0.0) : this(doubleArrayOf(x, y, z)) @@ -13,36 +14,47 @@ open class Vector3D protected constructor(@PublishedApi @JvmField internal val v inline operator fun get(index: Int) = vector[index] - inline operator fun plus(other: Vector3D) = Vector3D(x + other.x, y + other.y, z + other.z) + inline operator fun plus(other: Vector3D) = Vector3D(vector[0] + other[0], vector[1] + other[1], vector[2] + other[2]) - inline operator fun minus(other: Vector3D) = Vector3D(x - other.x, y - other.y, z - other.z) + inline operator fun minus(other: Vector3D) = Vector3D(vector[0] - other[0], vector[1] - other[1], vector[2] - other[2]) - inline operator fun times(scalar: Double) = Vector3D(x * scalar, y * scalar, z * scalar) + inline operator fun times(scalar: Double) = Vector3D(vector[0] * scalar, vector[1] * scalar, vector[2] * scalar) - inline operator fun times(vector: Vector3D) = Vector3D(x * vector.x, y * vector.y, z * vector.z) + inline operator fun times(vector: Vector3D) = Vector3D(vector[0] * vector[0], vector[1] * vector[1], vector[2] * vector[2]) - inline operator fun div(scalar: Double) = Vector3D(x / scalar, y / scalar, z / scalar) + inline operator fun div(scalar: Double) = Vector3D(vector[0] / scalar, vector[1] / scalar, vector[2] / scalar) - inline operator fun unaryMinus() = Vector3D(-x, -y, -z) + inline operator fun unaryMinus() = Vector3D(-vector[0], -vector[1], -vector[2]) - inline val x + override val x get() = vector[0] - inline val y + override val y get() = vector[1] - inline val z + override val z get() = vector[2] - inline operator fun component1() = x + inline fun array() = vector.copyOf() - inline operator fun component2() = y - - inline operator fun component3() = z + /** + * Scalar product between this vector and [other]. + */ + inline fun dot(other: Vector3D) = dot(other.vector) - inline fun dot(other: Vector3D) = x * other.x + y * other.y + z * other.z + /** + * Scalar product between this vector and [other]. + */ + inline fun dot(other: DoubleArray) = vector[0] * other[0] + vector[1] * other[1] + vector[2] * other[2] - inline fun cross(other: Vector3D) = Vector3D(y * other.z - z * other.y, z * other.x - x * other.z, x * other.y - y * other.x) + /** + * Cross product between this vector and [other]. + */ + inline fun cross(other: Vector3D) = Vector3D( + vector[1] * other[2] - vector[2] * other[1], + vector[2] * other[0] - vector[0] * other[2], + vector[0] * other[1] - vector[1] * other[0] + ) inline val length get() = sqrt(dot(this)) @@ -51,20 +63,34 @@ open class Vector3D protected constructor(@PublishedApi @JvmField internal val v get() = length.let { if (it == 0.0) this else this / it } inline val latitude - get() = asin(z / length).rad + get() = acos(vector[2]).rad inline val longitude - get() = atan2(y, x).rad.normalized + get() = atan2(vector[1], vector[0]).rad.normalized - inline fun isEmpty() = x == 0.0 && y == 0.0 && z == 0.0 + inline fun isEmpty() = vector[0] == 0.0 && vector[1] == 0.0 && vector[2] == 0.0 /** * Computes the angle between this vector and [vector]. */ - fun angle(vector: Vector3D): Angle { - val a = this * vector.length - val b = vector * length - return (2.0 * atan2((a - b).length, (a + b).length)).rad + fun angle(coordinate: Vector3D): Angle { + // val a = this * vector.length + // val b = vector * length + // return (2.0 * atan2((a - b).length, (a + b).length)).rad + + val dot = dot(coordinate) + val v = dot / (length * coordinate.length) + return if (abs(v) > 1.0) if (v < 0.0) SEMICIRCLE else 0.0 + else acos(v).rad + } + + override fun clone() = Vector3D(vector.copyOf()) + + /** + * Rotates this vector given an [axis] and [angle] of rotation. + */ + fun rotate(axis: Vector3D, angle: Angle): Vector3D { + return rotateByRodrigues(this, axis, angle) } override fun equals(other: Any?): Boolean { @@ -76,9 +102,7 @@ open class Vector3D protected constructor(@PublishedApi @JvmField internal val v return true } - override fun hashCode(): Int { - return vector.contentHashCode() - } + override fun hashCode() = vector.contentHashCode() override fun toString() = "${javaClass.simpleName}(x=$x, y=$y, z=$z)" @@ -88,5 +112,30 @@ open class Vector3D protected constructor(@PublishedApi @JvmField internal val v @JvmStatic val X = Vector3D(x = 1.0) @JvmStatic val Y = Vector3D(y = 1.0) @JvmStatic val Z = Vector3D(z = 1.0) + + /** + * Efficient algorithm for rotating a vector in space, given an [axis] and [angle] of rotation. + * + * @param v A vector in Rยณ. + * @param axis A vector describing an axis of rotation about which [v] rotates. + * @param angle The angle that [v] should rotate by. + * + * @see Wiki + */ + @JvmStatic + fun rotateByRodrigues(v: Vector3D, axis: Vector3D, angle: Angle): Vector3D { + val cosa = angle.cos + val k = axis.normalized + return v * cosa + k.cross(v) * angle.sin + k * k.dot(v) * (1.0 - cosa) + } + + /** + * Determines the plane that goes through the three points [a], [b] and [c] + * and its defining vector. + */ + @JvmStatic + fun plane(a: Vector3D, b: Vector3D, c: Vector3D): Vector3D { + return (b - a).cross(c - b) + } } } diff --git a/nebulosa-math/src/test/kotlin/AngleTest.kt b/nebulosa-math/src/test/kotlin/AngleTest.kt index bb7dc62cf..e648f136d 100644 --- a/nebulosa-math/src/test/kotlin/AngleTest.kt +++ b/nebulosa-math/src/test/kotlin/AngleTest.kt @@ -164,15 +164,11 @@ class AngleTest : StringSpec() { .build() .format(negativeAngle) shouldBe "-043 0000 45.00000" - AngleFormatter.HMS - .format(0.0) shouldBe "00h00m00.0s" - - AngleFormatter.HMS - .format(CIRCLE) shouldBe "00h00m00.0s" + AngleFormatter.HMS.format(0.0) shouldBe "00h00m00.0s" + AngleFormatter.HMS.format(CIRCLE) shouldBe "00h00m00.0s" } "bug on round seconds" { - "23h59m60.0s".hours - .format(AngleFormatter.HMS) shouldBe "00h00m00.0s" + "23h59m60.0s".hours.formatHMS() shouldBe "00h00m00.0s" AngleFormatter.HMS .format(Radians(6.283182643402501)) shouldBe "23h59m59.9s" diff --git a/nebulosa-math/src/test/kotlin/Vector3DTest.kt b/nebulosa-math/src/test/kotlin/Vector3DTest.kt index 5ed4a4569..bb9e95324 100644 --- a/nebulosa-math/src/test/kotlin/Vector3DTest.kt +++ b/nebulosa-math/src/test/kotlin/Vector3DTest.kt @@ -2,36 +2,28 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.doubles.shouldBeExactly +import io.kotest.matchers.shouldBe +import nebulosa.constants.PI +import nebulosa.constants.PIOVERTWO import nebulosa.math.Vector3D +import nebulosa.math.deg +import nebulosa.math.toDegrees +import nebulosa.test.plusOrMinus class Vector3DTest : StringSpec() { init { "plus vector" { - val m = Vector3D(2.0, 3.0, 2.0) - val n = Vector3D(2.0, 3.0, 2.0) - val r = m + n - - r[0] shouldBeExactly 4.0 - r[1] shouldBeExactly 6.0 - r[2] shouldBeExactly 4.0 + Vector3D(2.0, 3.0, 2.0) + Vector3D(2.0, 3.0, 2.0) shouldBe Vector3D(4.0, 6.0, 4.0) } "minus vector" { - val m = Vector3D(2.0, 3.0, 2.0) - val n = Vector3D(1.0, 1.0, 5.0) - val r = m - n - - r[0] shouldBeExactly 1.0 - r[1] shouldBeExactly 2.0 - r[2] shouldBeExactly -3.0 + Vector3D(2.0, 3.0, 2.0) - Vector3D(1.0, 1.0, 5.0) shouldBe Vector3D(1.0, 2.0, -3.0) } "times scalar" { - val m = Vector3D(2.0, 3.0, 2.0) - val r = m * 5.0 - - r[0] shouldBeExactly 10.0 - r[1] shouldBeExactly 15.0 - r[2] shouldBeExactly 10.0 + Vector3D(2.0, 3.0, 2.0) * 5.0 shouldBe Vector3D(10.0, 15.0, 10.0) + } + "divide by scalar" { + Vector3D(2.0, 3.0, 2.0) / 2.0 shouldBe Vector3D(1.0, 1.5, 1.0) } "dot" { val m = Vector3D(2.0, 3.0, 2.0) @@ -40,13 +32,7 @@ class Vector3DTest : StringSpec() { m.dot(-v) shouldBeExactly -17.0 } "cross" { - val m = Vector3D(2.0, 3.0, 2.0) - val v = Vector3D(3.0, 2.0, 3.0) - val r = m.cross(v) - - r[0] shouldBeExactly 5.0 - r[1] shouldBeExactly 0.0 - r[2] shouldBeExactly -5.0 + Vector3D(2.0, 3.0, 2.0).cross(Vector3D(3.0, 2.0, 3.0)) shouldBe Vector3D(5.0, 0.0, -5.0) } "is empty" { Vector3D(2.0, 3.0, 2.0).isEmpty().shouldBeFalse() @@ -55,5 +41,49 @@ class Vector3DTest : StringSpec() { Vector3D(0.0, 0.0, 4.0).isEmpty().shouldBeFalse() Vector3D(0.0, 0.0, 0.0).isEmpty().shouldBeTrue() } + "right angle" { + Vector3D.X.angle(Vector3D.Y) shouldBeExactly PIOVERTWO + } + "opposite" { + Vector3D(1.0, 2.0, 3.0).angle(Vector3D(-1.0, -2.0, -3.0)) shouldBeExactly PI + } + "collinear" { + Vector3D(2.0, -3.0, 1.0).angle(Vector3D(4.0, -6.0, 2.0)) shouldBeExactly 0.0 + } + "general" { + Vector3D(3.0, 4.0, 5.0).angle(Vector3D(1.0, 2.0, 2.0)).toDegrees shouldBeExactly 8.130102354156005 + } + "rotate around x axis" { + Vector3D.X.rotate(Vector3D.X, 90.0.deg) shouldBe Vector3D.X + } + "rotate around y axis" { + Vector3D.Y.rotate(Vector3D.Y, 90.0.deg) shouldBe Vector3D.Y + } + "rotate around z axis" { + Vector3D.Z.rotate(Vector3D.Z, 90.0.deg) shouldBe Vector3D.Z + } + "rotate" { + val v = Vector3D(1.0, 2.0, 3.0) + v.rotate(Vector3D.X, PI / 4.0) shouldBe (Vector3D(1.0, -0.707107, 3.535534) plusOrMinus 1e-6) + v.rotate(Vector3D.Y, PI / 4.0) shouldBe (Vector3D(2.828427, 2.0, 1.414213) plusOrMinus 1e-6) + v.rotate(Vector3D.Z, PI / 4.0) shouldBe (Vector3D(-0.707107, 2.12132, 3.0) plusOrMinus 1e-6) + val axis = Vector3D(3.0, 4.0, 5.0) + v.rotate(axis, 29.6512852.deg) shouldBe (Vector3D(1.21325856, 1.73061994, 3.08754891) plusOrMinus 1e-8) + v.rotate(axis, 120.3053274.deg) shouldBe (Vector3D(2.08677229, 1.63198489, 2.64234871) plusOrMinus 1e-8) + v.rotate(axis, 230.6512852.deg) shouldBe (Vector3D(1.69633894, 2.56816842, 2.12766190) plusOrMinus 1e-8) + v.rotate(axis, 359.6139797.deg) shouldBe (Vector3D(0.99810712, 2.00381299, 2.99808533) plusOrMinus 1e-8) + } + "no rotation" { + Vector3D(1.0, 2.0, 3.0).rotate(Vector3D.Y, 0.0) shouldBe Vector3D(1.0, 2.0, 3.0) + } + "plane" { + val a = Vector3D(1.0, -2.0, 1.0) + val b = Vector3D(4.0, -2.0, -2.0) + val c = Vector3D(4.0, 1.0, 4.0) + val d = Vector3D.plane(a, b, c) + d.x shouldBeExactly 9.0 + d.y shouldBeExactly -18.0 + d.z shouldBeExactly 9.0 + } } } 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 de0287cec..b4bf2378f 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/FixedStar.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/astrometry/FixedStar.kt @@ -2,10 +2,15 @@ package nebulosa.nova.astrometry import nebulosa.constants.* import nebulosa.erfa.PositionAndVelocity -import nebulosa.math.* +import nebulosa.erfa.eraStarpvMod +import nebulosa.math.Angle +import nebulosa.math.Vector3D +import nebulosa.math.Velocity +import nebulosa.math.toMetersPerSecond import nebulosa.nova.position.ICRF import nebulosa.time.InstantOfTime -import nebulosa.time.TimeJD +import nebulosa.time.UTC +import kotlin.math.cos import kotlin.math.max import kotlin.math.sin @@ -18,7 +23,7 @@ import kotlin.math.sin * * For objects whose proper motion across the sky has been detected, * you can supply velocities in milliarcseconds (mas) per year, and - * even a [parallax] and [radial] velocity if those are known. + * even a [parallax] and [radialVelocity] velocity if those are known. */ data class FixedStar( val ra: Angle, @@ -26,11 +31,13 @@ data class FixedStar( val pmRA: Angle = 0.0, val pmDEC: Angle = 0.0, val parallax: Angle = 0.0, - val radial: Velocity = 0.0, - val epoch: InstantOfTime = TimeJD.J2000, + val radialVelocity: Velocity = 0.0, + val epoch: InstantOfTime = UTC.J2000, ) : Body { - val positionAndVelocity by lazy { computePositionAndVelocity(ra, dec, pmRA, pmDEC, parallax, radial) } + // val positionAndVelocity by lazy { computePositionAndVelocity(ra, dec, pmRA, pmDEC, parallax, radialVelocity) } + // eraStarpvMod computes wrong values for Polaris ??? + val positionAndVelocity by lazy { eraStarpvMod(ra, dec, pmRA, pmDEC, parallax, radialVelocity) } override val center = 0 @@ -42,15 +49,15 @@ data class FixedStar( // Light-time returned is the projection of vector "pos_obs" onto the // 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.tt.whole + lightTime + observer.time.tdb.fraction - epoch.tt.fraction) - - observer.position + val position = (positionAndVelocity.position + positionAndVelocity.velocity * + (observer.time.tdb.whole - epoch.tdb.whole + lightTime + observer.time.tdb.fraction - epoch.tdb.fraction) - + observer.position) return PositionAndVelocity(position, observer.velocity - positionAndVelocity.velocity) } override fun compute(time: InstantOfTime): PositionAndVelocity { val position = - positionAndVelocity.position + positionAndVelocity.velocity * (time.tdb.whole - epoch.tt.whole + time.tdb.fraction - epoch.tt.fraction) + positionAndVelocity.position + positionAndVelocity.velocity * (time.tdb.whole - epoch.tdb.whole + time.tdb.fraction - epoch.tdb.fraction) return PositionAndVelocity(position, positionAndVelocity.velocity) } @@ -62,15 +69,15 @@ data class FixedStar( private fun computePositionAndVelocity( ra: Angle, dec: Angle, pmRA: Angle = 0.0, pmDEC: Angle = 0.0, - parallax: Angle = 0.0, radial: Velocity = 0.0, + parallax: Angle = 0.0, radialVelocity: Velocity = 0.0, ): PositionAndVelocity { val plx = max(MIN_PARALLAX, parallax) // Computing the star's position as an ICRF position and velocity. val dist = 1.0 / sin(plx) - val cra = ra.cos - val sra = ra.sin - val cdc = dec.cos - val sdc = dec.sin + val cra = cos(ra) + val sra = sin(ra) + val cdc = cos(dec) + val sdc = sin(dec) val position = Vector3D( dist * cdc * cra, @@ -78,15 +85,17 @@ data class FixedStar( dist * sdc, ) + val rvInMetersPerSecond = radialVelocity.toMetersPerSecond + // Compute Doppler factor, which accounts for change in light travel time to star. - val k = 1.0 / (1.0 - radial.toMetersPerSecond / SPEED_OF_LIGHT) + val k = 1.0 / (1.0 - rvInMetersPerSecond / SPEED_OF_LIGHT) // Convert proper motion and radial velocity to orthogonal // components of motion with units of au/day. val pmr = pmRA / (plx * DAYSPERJY) * k val pmd = pmDEC / (plx * DAYSPERJY) * k - val rvl = radial.toKilometersPerSecond * DAYSEC / AU_KM * k + val rvl = rvInMetersPerSecond * DAYSEC / AU_M * k val velocity = Vector3D( -pmr * sra - pmd * sdc * cra + rvl * cdc * cra, diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/frame/ICRS.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/frame/ICRS.kt index 10bbe8c84..121a85d8c 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/frame/ICRS.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/frame/ICRS.kt @@ -3,7 +3,7 @@ package nebulosa.nova.frame import nebulosa.math.Matrix3D /** - * The International Coordinate Reference System (ICRS). + * The International Celestial Reference System (ICRS). * * The ICRS is a permanent reference frame which has replaced J2000, * with which its axes agree to within 0.02 arcseconds (closer than the diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/Barycentric.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/Barycentric.kt index 3820a035f..d325f7421 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/Barycentric.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/Barycentric.kt @@ -8,7 +8,7 @@ import nebulosa.nova.astrometry.Body import nebulosa.nova.astrometry.Observable import nebulosa.nova.frame.Ecliptic import nebulosa.time.InstantOfTime -import nebulosa.time.TimeJD +import nebulosa.time.TT /** * An |xyz| position measured from the Solar System barycenter. @@ -45,7 +45,7 @@ class Barycentric internal constructor( */ fun phaseAngle(target: Body, center: Body): Angle { val pe = -observe(target) // Rotate 180 degrees to point back at Earth. - val ps = target.at(TimeJD(time.tt - pe.lightTime)).observe(center) + val ps = target.at(TT(time.tt.whole - pe.lightTime, time.tt.fraction)).observe(center) return pe.separationFrom(ps) } diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/GeographicPosition.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/GeographicPosition.kt index 6dc0be571..5d6a1e3c6 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/GeographicPosition.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/GeographicPosition.kt @@ -2,14 +2,17 @@ package nebulosa.nova.position import nebulosa.constants.ANGULAR_VELOCITY import nebulosa.constants.DAYSEC -import nebulosa.constants.DEG2RAD +import nebulosa.constants.PIOVERTWO +import nebulosa.erfa.eraRefco import nebulosa.erfa.eraSp00 import nebulosa.math.* import nebulosa.nova.frame.Frame import nebulosa.nova.frame.ITRS import nebulosa.time.IERS import nebulosa.time.InstantOfTime +import kotlin.math.abs import kotlin.math.atan2 +import kotlin.math.pow import kotlin.math.tan class GeographicPosition( @@ -50,19 +53,11 @@ class GeographicPosition( */ fun refract( altitude: Angle, - temperature: Temperature = 10.0.celsius, + temperature: Temperature = 15.0.celsius, pressure: Pressure = elevation.pressure(temperature), - ): Angle { - val a = altitude.toDegrees - - return if (a >= -1.0 && a <= 89.9) { - val r = 0.016667 / tan((a + 7.31 / (a + 4.4)) * DEG2RAD) - val d = r * (0.28 * pressure / temperature.toKelvin) - (a + d).deg - } else { - altitude - } - } + relativeHumidity: Double = 0.0, + waveLength: Double = 0.54, + ) = computeRefractedAltitude(altitude, temperature, pressure, relativeHumidity, waveLength) /** * Computes rotation from GCRS to this locationโ€™s altazimuth system. @@ -116,5 +111,47 @@ class GeographicPosition( companion object { @JvmStatic val EARTH_ANGULAR_VELOCITY_VECTOR = Vector3D(z = DAYSEC * ANGULAR_VELOCITY) + + @JvmStatic + fun computeRefractedAltitude( + altitude: Angle, + temperature: Temperature = 15.0.celsius, + pressure: Pressure = ONE_ATM, + relativeHumidity: Double = 0.5, + waveLength: Double = 0.55, + iterationIncrement: Angle = 1.0.arcsec, + ): Double { + if (altitude < 0.0) { + return altitude + } + + val z = PIOVERTWO - altitude + + val (refa, refb) = eraRefco(pressure, temperature, relativeHumidity, waveLength) + + var roller = iterationIncrement + var iterations = 0 + + while (iterations++ < 10) { + val refractedZenithDistanceRadian = z - roller + + // dZ = A tan Z + B tan^3 Z. + val dZ2 = refa * tan(refractedZenithDistanceRadian) + refb * tan(refractedZenithDistanceRadian).pow(3.0) + + if (dZ2.isNaN()) { + return altitude + } + + val originalZenithDistanceRadian = refractedZenithDistanceRadian + dZ2 + + if (abs(originalZenithDistanceRadian - z) < iterationIncrement) { + return PIOVERTWO - originalZenithDistanceRadian + } + + roller += iterationIncrement + } + + return altitude + } } } diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/ICRF.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/ICRF.kt index e37ea0bf0..1cbc928f5 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/ICRF.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/ICRF.kt @@ -9,15 +9,14 @@ import nebulosa.math.* import nebulosa.nova.astrometry.Body import nebulosa.nova.frame.Frame import nebulosa.nova.frame.ITRS +import nebulosa.time.CurrentTime import nebulosa.time.InstantOfTime -import nebulosa.time.TimeJD -import nebulosa.time.UTC import kotlin.math.atan2 /** * An |xyz| position and velocity oriented to the ICRF axes. * - * The International Coordinate Reference Frame (ICRF) is a permanent + * The International Celestial Reference Frame (ICRF) is a permanent * reference frame that is the replacement for J2000. Their axes agree * to within 0.02 arcseconds. It also supersedes older equinox-based * systems like B1900 and B1950. @@ -78,23 +77,24 @@ open class ICRF protected constructor( /** * Computes the equatorial (RA, declination, distance) * referenced to the dynamical system defined by - * the Earth's true equator and equinox at specific [epoch] time. + * the Earth's true equator and equinox at specific time + * represented by its rotation [matrix]. */ - fun equatorialAtEpoch(epoch: InstantOfTime) = (epoch.m * position).let { SphericalCoordinate.of(it[0].au, it[1].au, it[2].au) } + fun equatorialAtEpoch(matrix: Matrix3D) = (matrix * position).let { SphericalCoordinate.of(it[0].au, it[1].au, it[2].au) } /** * Computes the equatorial (RA, declination, distance) * referenced to the dynamical system defined by - * the Earth's true equator and equinox of [time]. + * the Earth's true equator and equinox at specific [epoch] time. */ - fun equatorialAtDate() = equatorialAtEpoch(time) + fun equatorialAtEpoch(epoch: InstantOfTime) = equatorialAtEpoch(epoch.m) /** * Computes the equatorial (RA, declination, distance) * referenced to the dynamical system defined by - * the Earth's true equator and equinox of J2000.0. + * the Earth's true equator and equinox of [time]. */ - fun equatorialJ2000() = equatorialAtEpoch(TimeJD.J2000) + fun equatorialAtDate() = equatorialAtEpoch(time) /** * Computes hour angle, declination, and distance. @@ -125,7 +125,7 @@ open class ICRF protected constructor( * Computes the altitude, azimuth and distance relative to the observer's horizon. */ fun horizontal( - temperature: Temperature = 10.0.celsius, + temperature: Temperature = 15.0.celsius, pressure: Pressure = ONE_ATM, ): SphericalCoordinate { // TODO: Uncomment when implement apparent method. @@ -214,14 +214,17 @@ open class ICRF protected constructor( val horizontalRotation by lazy { require(target is GeographicPosition || target is PlanetograhicPosition) { "to compute an altazimuth position, you must observe from " + - "a specific Earth location or from a position on another body loaded from a set " + - "of planetary constants" + "a specific Earth location or from a position on another body loaded from a set " + + "of planetary constants" } (target as Frame).rotationAt(time) } - operator fun minus(other: ICRF) = of(position - other.position, velocity - other.velocity, time, other.target, target) + operator fun minus(other: ICRF): ICRF { + require(center == other.center) { "you can only subtract two ICRF vectors if they both start at the same center" } + return of(position - other.position, velocity - other.velocity, time, other.target, target) + } operator fun unaryMinus() = of(-position, -velocity, time, target, center, javaClass) @@ -242,26 +245,21 @@ open class ICRF protected constructor( else -> ICRF(position, velocity, time, center, target) } + @JvmStatic internal fun horizontal( position: ICRF, - temperature: Temperature = 10.0.celsius, - pressure: Pressure = 1013.0.mbar, + temperature: Temperature = 15.0.celsius, + pressure: Pressure = ONE_ATM, ): SphericalCoordinate { - val centerBarycentric = position.centerBarycentric - - val r = centerBarycentric?.horizontalRotation - ?: if (position.center is Frame) { - position.center.rotationAt(position.time) - } else { - throw IllegalArgumentException( - "to compute an altazimuth position, you must observe from " + - "a specific Earth location or from a position on another body loaded from a set " + - "of planetary constants" - ) - } - - val h = r * position.position - val coordinate = SphericalCoordinate.of(h[0].au, h[1].au, h[2].au) + val r = position.centerBarycentric?.horizontalRotation + ?: (position.center as? Frame)?.rotationAt(position.time) + ?: throw IllegalArgumentException( + "to compute an altazimuth position, you must observe from " + + "a specific Earth location or from a position on another body loaded from a set " + + "of planetary constants" + ) + + val coordinate = SphericalCoordinate.of(r * position.position) return if (position.center is GeographicPosition) { val refracted = position.center.refract(coordinate.latitude, temperature, pressure) @@ -274,6 +272,7 @@ open class ICRF protected constructor( /** * Builds a position from two vectors in a reference [frame] at the [time]. */ + @JvmStatic fun frame( time: InstantOfTime, frame: Frame, @@ -309,10 +308,11 @@ open class ICRF protected constructor( * to be in the dynamical system of that particular date. Otherwise, * they will be assumed to be ICRS (the modern replacement for J2000). */ + @JvmStatic fun equatorial( rightAscension: Angle, declination: Angle, distance: Distance = ONE_GIGAPARSEC, - time: InstantOfTime = UTC.now(), + time: InstantOfTime = CurrentTime, epoch: InstantOfTime? = null, center: Number = Int.MIN_VALUE, target: Number = Int.MIN_VALUE, diff --git a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/PlanetograhicPosition.kt b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/PlanetograhicPosition.kt index 5cbe95c9f..e8bfc0c2f 100644 --- a/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/PlanetograhicPosition.kt +++ b/nebulosa-nova/src/main/kotlin/nebulosa/nova/position/PlanetograhicPosition.kt @@ -42,9 +42,7 @@ data class PlanetograhicPosition( // from position, to support situations where we were not // given a latitude and longitude. If that is not feasible, // then at least cache the product of these first two matrices. - val m = Matrix3D.rotY((TAU / 4.0 - latitude).rad) - .rotateZ((TAU / 2.0 - longitude).rad) * - frame.rotationAt(time) + val m = Matrix3D.rotY((TAU / 4.0 - latitude).rad).rotateZ((TAU / 2.0 - longitude).rad) * frame.rotationAt(time) // Turn clockwise into counterclockwise. // Flip the sign of y so that azimuth reads north-east rather than the other direction. diff --git a/nebulosa-nova/src/test/kotlin/FixedStarTest.kt b/nebulosa-nova/src/test/kotlin/FixedStarTest.kt index 1cecd0eb3..73331aa14 100644 --- a/nebulosa-nova/src/test/kotlin/FixedStarTest.kt +++ b/nebulosa-nova/src/test/kotlin/FixedStarTest.kt @@ -1,11 +1,10 @@ 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.math.* -import nebulosa.nasa.daf.RemoteDaf -import nebulosa.nasa.spk.Spk import nebulosa.nova.astrometry.FixedStar -import nebulosa.nova.astrometry.SpiceKernel +import nebulosa.nova.astrometry.VSOP87E import nebulosa.nova.position.Barycentric import nebulosa.time.IERS import nebulosa.time.IERSA @@ -13,6 +12,7 @@ import nebulosa.time.TimeYMDHMS import nebulosa.time.UTC import java.nio.file.Path import kotlin.io.path.inputStream +import kotlin.math.truncate class FixedStarTest : StringSpec() { @@ -28,17 +28,46 @@ class FixedStarTest : StringSpec() { (44.48).mas, (-11.85).mas, (7.54).mas, (-16.42).kms, ) - val spk = Spk(RemoteDaf("https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/a_old_versions/de421.bsp")) - val kernel = SpiceKernel(spk) + val astrometric = VSOP87E.EARTH.at(UTC(TimeYMDHMS(2024, 2, 17, 12, 0, 0.0))) + .observe(star) - val astrometric = kernel[399] - .at(UTC(TimeYMDHMS(2023, 1, 1, 15, 0, 0.0))).observe(star) + val (ra, dec) = astrometric.equatorialAtDate() - val (ra, dec, dist) = astrometric.equatorialAtDate() + with(ra.normalized.hms()) { + truncate(this[0]) shouldBeExactly 3.0 + truncate(this[1]) shouldBeExactly 2.0 + this[2] shouldBe (3.9 plusOrMinus 15.0) + } - ra.normalized.toHours shouldBe (3.0115471963487153 plusOrMinus 1e-8) - dec.toDegrees shouldBe (89.36032606627879 plusOrMinus 1e-8) - dist shouldBe (27355995.0433298 plusOrMinus 1e-6) + with(dec.dms()) { + truncate(this[0]) shouldBeExactly 89.0 + truncate(this[1]) shouldBe (22.0 plusOrMinus 1.0) + this[2] shouldBe (15.8 plusOrMinus 50.0) + } + } + "barnard's star" { + // https://api.noctuasky.com/api/v1/skysources/name/NAME%20Barnard's%20Star + val star = FixedStar( + 269.452082497514.deg, 4.6933642650633.deg, + (-802.803).mas, (10362.542).mas, (547.451).mas, (-110.51).kms, + ) + + val astrometric = VSOP87E.EARTH.at(UTC(TimeYMDHMS(2024, 2, 17, 12, 0, 0.0))) + .observe(star) + + val (ra, dec) = astrometric.equatorialAtDate() + + with(ra.normalized.hms()) { + truncate(this[0]) shouldBeExactly 17.0 + truncate(this[1]) shouldBeExactly 58.0 + this[2] shouldBe (57.8 plusOrMinus 1.0) + } + + with(dec.dms()) { + truncate(this[0]) shouldBeExactly 4.0 + truncate(this[1]) shouldBeExactly 45.0 + this[2] shouldBe (25.5 plusOrMinus 10.0) + } } } } diff --git a/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt b/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt index 0b25b2b39..30a74a811 100644 --- a/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt +++ b/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt @@ -1,9 +1,8 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.shouldBe -import nebulosa.math.AngleFormatter import nebulosa.math.deg -import nebulosa.math.format +import nebulosa.math.formatHMS import nebulosa.math.m import nebulosa.nova.position.Geoid import nebulosa.time.IERS @@ -22,9 +21,9 @@ class GeographicPositionTest : StringSpec() { "lst" { val position = Geoid.IERS2010.lonLat((-45.4227).deg, 0.0) - position.lstAt(UTC(TimeYMDHMS(2022, 1, 1, 12, 0, 0.0))).format(AngleFormatter.HMS) shouldBe "15h42m47.1s" - position.lstAt(UTC(TimeYMDHMS(2024, 1, 1, 12, 0, 0.0))).format(AngleFormatter.HMS) shouldBe "15h40m53.1s" - position.lstAt(UTC(TimeYMDHMS(2025, 1, 1, 12, 0, 0.0))).format(AngleFormatter.HMS) shouldBe "15h43m52.8s" + position.lstAt(UTC(TimeYMDHMS(2022, 1, 1, 12, 0, 0.0))).formatHMS() shouldBe "15h42m47.1s" + position.lstAt(UTC(TimeYMDHMS(2024, 1, 1, 12, 0, 0.0))).formatHMS() shouldBe "15h40m53.1s" + position.lstAt(UTC(TimeYMDHMS(2025, 1, 1, 12, 0, 0.0))).formatHMS() shouldBe "15h43m52.8s" } "xyz" { val latitude = "-23 32 51.00".deg diff --git a/nebulosa-nova/src/test/kotlin/ICRFTest.kt b/nebulosa-nova/src/test/kotlin/ICRFTest.kt index 9df910256..ba3229db0 100644 --- a/nebulosa-nova/src/test/kotlin/ICRFTest.kt +++ b/nebulosa-nova/src/test/kotlin/ICRFTest.kt @@ -4,10 +4,7 @@ import io.kotest.matchers.shouldBe import nebulosa.math.* import nebulosa.nova.position.Geoid import nebulosa.nova.position.ICRF -import nebulosa.time.IERS -import nebulosa.time.IERSA -import nebulosa.time.TimeJD -import nebulosa.time.TimeYMDHMS +import nebulosa.time.* import java.nio.file.Path import kotlin.io.path.inputStream @@ -21,16 +18,16 @@ class ICRFTest : StringSpec() { "equatorial at date to equatorial J2000" { val ra = 2.15105.deg val dec = (-0.4493).deg - val (raNow, decNow) = ICRF.equatorial(ra, dec, epoch = TimeJD(2459950.24436)).equatorialJ2000() - raNow.toDegrees shouldBe (1.85881 plusOrMinus 1e-2) - decNow.toDegrees shouldBe (-0.5762 plusOrMinus 1e-2) + val (raNow, decNow) = ICRF.equatorial(ra, dec, epoch = TT(2459950.0, 0.24436)).equatorial() + raNow.toDegrees shouldBe (1.85881 plusOrMinus 1e-4) + decNow.toDegrees shouldBe (-0.5762 plusOrMinus 1e-4) } "equatorial J2000 to equatorial at date" { val ra = 1.85881.deg val dec = (-0.5762).deg - val (raNow, decNow) = ICRF.equatorial(ra, dec).equatorialAtEpoch(TimeJD(2459950.24436)) - raNow.toDegrees shouldBe (2.15105 plusOrMinus 1e-2) - decNow.toDegrees shouldBe (-0.4493 plusOrMinus 1e-2) + val (raNow, decNow) = ICRF.equatorial(ra, dec).equatorialAtEpoch(TT(2459950.0, 0.24436)) + raNow.toDegrees shouldBe (2.15105 plusOrMinus 1e-4) + decNow.toDegrees shouldBe (-0.4493 plusOrMinus 1e-4) } "horizontal" { // Sirius. diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockPosition.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockPosition.kt index df6b906f9..5118423d9 100644 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockPosition.kt +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockPosition.kt @@ -1,11 +1,13 @@ package nebulosa.phd2.client.commands +import nebulosa.math.Point2D + /** * When [exact] is true, the lock position is moved to the exact given coordinates ([x], [y]). * When false, the current position is moved to the given coordinates and if * a guide star is in range, the lock position is set to the coordinates of the guide star. */ -data class SetLockPosition(val x: Double, val y: Double, val exact: Boolean = true) : PHD2Command { +data class SetLockPosition(override val x: Double, override val y: Double, val exact: Boolean = true) : PHD2Command, Point2D { override val methodName = "set_lock_position" diff --git a/nebulosa-plate-solving/build.gradle.kts b/nebulosa-plate-solving/build.gradle.kts index 861728854..12b716a2d 100644 --- a/nebulosa-plate-solving/build.gradle.kts +++ b/nebulosa-plate-solving/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project(":nebulosa-math")) + api(project(":nebulosa-common")) api(project(":nebulosa-wcs")) api(project(":nebulosa-imaging")) implementation(project(":nebulosa-log")) diff --git a/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolution.kt b/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolution.kt index a262200ec..e092bc4c3 100644 --- a/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolution.kt +++ b/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolution.kt @@ -1,11 +1,12 @@ package nebulosa.plate.solving import nebulosa.fits.Header +import nebulosa.fits.HeaderCard +import nebulosa.fits.ReadOnlyHeader import nebulosa.fits.Standard import nebulosa.log.loggerFor import nebulosa.math.* import nebulosa.wcs.computeCdMatrix -import nebulosa.wcs.hasCd import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.cos @@ -21,11 +22,10 @@ data class PlateSolution( val height: Angle = 0.0, val parity: Parity = Parity.NORMAL, val radius: Angle = hypot(width, height).rad / 2.0, -) : Header() { + private val header: Collection = emptyList(), +) : ReadOnlyHeader(header) { - override fun toString() = "PlateSolution(solved=$solved, orientation=$orientation, scale=$scale, " + - "rightAscension=$rightAscension, declination=$declination, width=$width, " + - "height=$height, parity=$parity, radius=$radius, header=${super.toString()})" + override fun readOnly() = this companion object { @@ -47,13 +47,11 @@ data class PlateSolution( LOG.info( "solution from {}: ORIE={}, SCALE={}, RA={}, DEC={}", - header, crota2.format(AngleFormatter.SIGNED_DMS), cdelt2.toArcsec, - crval1.format(AngleFormatter.HMS), crval2.format(AngleFormatter.SIGNED_DMS), + header, crota2.formatSignedDMS(), cdelt2.toArcsec, + crval1.formatHMS(), crval2.formatSignedDMS(), ) - val solution = PlateSolution(true, crota2, cdelt2, crval1, crval2, abs(cdelt1 * width), abs(cdelt2 * height)) - header.iterator().forEach(solution::add) - return solution + return PlateSolution(true, crota2, cdelt2, crval1, crval2, abs(cdelt1 * width), abs(cdelt2 * height), header = header) } } } diff --git a/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolver.kt b/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolver.kt index 783f8f255..13efcc880 100644 --- a/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolver.kt +++ b/nebulosa-plate-solving/src/main/kotlin/nebulosa/plate/solving/PlateSolver.kt @@ -1,5 +1,6 @@ package nebulosa.plate.solving +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.imaging.Image import nebulosa.math.Angle import java.nio.file.Path @@ -11,5 +12,6 @@ interface PlateSolver { path: Path?, image: Image?, centerRA: Angle = 0.0, centerDEC: Angle = 0.0, radius: Angle = 0.0, downsampleFactor: Int = 0, timeout: Duration? = null, + cancellationToken: CancellationToken = CancellationToken.NONE, ): PlateSolution } diff --git a/nebulosa-plate-solving/src/test/kotlin/PlateSolutionTest.kt b/nebulosa-plate-solving/src/test/kotlin/PlateSolutionTest.kt index d940a1ae1..635c3341c 100644 --- a/nebulosa-plate-solving/src/test/kotlin/PlateSolutionTest.kt +++ b/nebulosa-plate-solving/src/test/kotlin/PlateSolutionTest.kt @@ -3,8 +3,8 @@ import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import nebulosa.fits.Header -import nebulosa.math.AngleFormatter -import nebulosa.math.format +import nebulosa.math.formatHMS +import nebulosa.math.formatSignedDMS import nebulosa.math.toArcsec import nebulosa.math.toDegrees import nebulosa.plate.solving.PlateSolution @@ -41,8 +41,8 @@ class PlateSolutionTest : StringSpec() { val header = Header.from(astrometryNet) val solution = PlateSolution.from(header).shouldNotBeNull() - solution.rightAscension.format(AngleFormatter.HMS) shouldBe "03h19m07.7s" - solution.declination.format(AngleFormatter.SIGNED_DMS) shouldBe "-066ยฐ30'12.2\"" + solution.rightAscension.formatHMS() shouldBe "03h19m07.7s" + solution.declination.formatSignedDMS() shouldBe "-066ยฐ30'12.2\"" solution.orientation.toDegrees shouldBe (-136.9 plusOrMinus 1e-1) solution.scale.toArcsec shouldBe (1.37 plusOrMinus 1e-2) solution.radius.toDegrees shouldBe (0.476 plusOrMinus 1e-3) diff --git a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt index 7a089c55a..6cbd5aed0 100644 --- a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt +++ b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt @@ -10,7 +10,6 @@ import nebulosa.retrofit.CSVRecordListConverterFactory import nebulosa.retrofit.RetrofitService import nebulosa.skycatalog.SkyObject import nebulosa.skycatalog.SkyObjectType -import nebulosa.time.UTC import okhttp3.FormBody import okhttp3.OkHttpClient import retrofit2.Call @@ -52,7 +51,6 @@ class SimbadService( fun search(query: Query): List { val rows = query(query).execute().body() ?: return emptyList() val res = ArrayList() - val currentTime = UTC.now() fun matchName(name: String): String? { for (type in SimbadCatalogType.entries) { @@ -93,7 +91,7 @@ class SimbadService( } val distance = SkyObject.distanceFor(parallax) - val constellation = SkyObject.constellationFor(rightAscensionJ2000, declinationJ2000, currentTime) + val constellation = SkyObject.constellationFor(rightAscensionJ2000, declinationJ2000) val entity = SimbadEntry( id, name.joinToString("") { "[$it]" }, magnitude, diff --git a/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt b/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt index f60ba0852..98cb77fbf 100644 --- a/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt +++ b/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt @@ -10,7 +10,6 @@ import nebulosa.nova.astrometry.Constellation import nebulosa.skycatalog.SkyCatalog import nebulosa.skycatalog.SkyObject import nebulosa.skycatalog.SkyObject.Companion.NAME_SEPARATOR -import nebulosa.time.UTC import java.io.InputStream import java.io.InputStreamReader @@ -27,7 +26,6 @@ class HygDatabase : SkyCatalog(118005) { val reader = CSV_READER.ofNamedCsvRecord(InputStreamReader(stream, Charsets.UTF_8)) val names = ArrayList(7) - val currentTime = UTC.now() for (record in reader) { val id = record.getField("id").toLong() @@ -51,7 +49,7 @@ class HygDatabase : SkyCatalog(118005) { .takeIf { it.isNotEmpty() } ?.uppercase() ?.let(Constellation::valueOf) - ?: SkyObject.constellationFor(rightAscension, declination, currentTime) + ?: SkyObject.constellationFor(rightAscension, declination) names.clear() diff --git a/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt b/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt index 8aace1d6b..db16bb05e 100644 --- a/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt +++ b/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt @@ -7,7 +7,6 @@ import nebulosa.math.rad import nebulosa.skycatalog.SkyCatalog import nebulosa.skycatalog.SkyObject import nebulosa.skycatalog.SkyObject.Companion.NAME_SEPARATOR -import nebulosa.time.UTC import okio.BufferedSource import okio.Source import okio.buffer @@ -25,7 +24,6 @@ class Nebula : SkyCatalog(94661) { source.readString() // Version. source.readString() // Edition. - val currentTime = UTC.now() val commonNames = namesSource?.let(::namesFor) ?: emptyList() val names = ArrayList(8) @@ -87,10 +85,10 @@ class Nebula : SkyCatalog(94661) { if (ngc > 0) "NGC $ngc".findNames() if (ic > 0) "IC $ic".findNames() if (m > 0) "M $m".findNames() - if (mel > 0) "Mellote $mel".findNames() - if (b > 0) "Barnard $b".findNames() - if (c > 0) "Caldwell $c".findNames() - if (cr > 0) "Collinder $cr".findNames() + if (mel > 0) "Mel $mel".findNames() + if (b > 0) "B $b".findNames() + if (c > 0) "C $c".findNames() + if (cr > 0) "Cr $cr".findNames() if (ced.isNotEmpty()) "CED $ced".findNames() if (sh2 > 0) "SH 2-$sh2".findNames() if (rcw > 0) "RCW $rcw".findNames() @@ -106,12 +104,12 @@ class Nebula : SkyCatalog(94661) { if (eso.isNotEmpty()) "ESO $eso".findNames() if (snrg.isNotEmpty()) "SNRG $snrg".findNames() if (dwb > 0) "DWB $dwb".findNames() - if (st > 0) "Stock $st".findNames() + if (st > 0) "St $st".findNames() if (ldn > 0) "LDN $ldn".findNames() if (hcg.isNotEmpty()) "HCG $hcg".findNames() if (vdbh.isNotEmpty()) "VdBH $vdbh".findNames() - if (tr > 0) "Trumpler $tr".findNames() - if (ru > 0) "Ruprecht $ru".findNames() + if (tr > 0) "Tr $tr".findNames() + if (ru > 0) "Ru $ru".findNames() if (vdbha > 0) "VdBHA $vdbha".findNames() val nebula = NebulaEntry( @@ -122,7 +120,7 @@ class Nebula : SkyCatalog(94661) { majorAxis, minorAxis, orientation, parallax = parallax, redshift = redshift, // distance * 3261.5637769, - constellation = SkyObject.constellationFor(ra, dec, currentTime), + constellation = SkyObject.constellationFor(ra, dec), ) add(nebula) diff --git a/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt b/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt index 4840439ac..ad244ed64 100644 --- a/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt +++ b/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt @@ -1,5 +1,7 @@ import io.kotest.core.annotation.EnabledIf import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.ints.shouldBeExactly import nebulosa.math.deg import nebulosa.math.hours @@ -13,15 +15,15 @@ import java.nio.file.Path class NebulaTest : StringSpec() { init { - val nebula = Nebula() - val catalog = Path.of("../data/catalog.dat").source().gzip() - val names = Path.of("../data/names.dat").source() - nebula.load(catalog, names) - "load" { + val catalog = Path.of("../data/catalog.dat").source().gzip() + val names = Path.of("../data/names.dat").source() + + val nebula = Nebula() + nebula.load(catalog, names) + nebula.size shouldBeExactly 94661 - } - "search around" { + nebula .searchAround("05 35 16.8".hours, "-05 23 24".deg, 1.0.deg) .onEach { println(it) } @@ -32,5 +34,10 @@ class NebulaTest : StringSpec() { .onEach { println(it) } .size shouldBeExactly 19 } + "names" { + val names = Path.of("../data/names.dat").source().use(Nebula::namesFor) + val thorHelmet = names.filter { it.id == "NGC 2359" } shouldHaveSize 5 + thorHelmet.map { it.name }.shouldContainAll("Thor's Helmet", "Duck Head Nebula", "Flying Eye Nebula", "Duck Nebula", "Whistle Nebula") + } } } diff --git a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt index de49c236a..124dc2a4f 100644 --- a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt +++ b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt @@ -5,7 +5,7 @@ import nebulosa.math.Distance import nebulosa.math.ONE_PARSEC import nebulosa.nova.astrometry.Constellation import nebulosa.nova.position.ICRF -import nebulosa.time.UTC +import nebulosa.time.InstantOfTime interface SkyObject { @@ -29,8 +29,13 @@ interface SkyObject { @JvmStatic val MAGNITUDE_RANGE = MAGNITUDE_MIN..MAGNITUDE_MAX @JvmStatic - fun constellationFor(rightAscension: Angle, declination: Angle, time: UTC): Constellation { - return Constellation.find(ICRF.equatorial(rightAscension, declination, time = time)) + fun constellationFor(icrf: ICRF): Constellation { + return Constellation.find(icrf) + } + + @JvmStatic + fun constellationFor(rightAscension: Angle, declination: Angle, epoch: InstantOfTime? = null): Constellation { + return constellationFor(ICRF.equatorial(rightAscension, declination, epoch = epoch)) } @JvmStatic diff --git a/nebulosa-star-detection/build.gradle.kts b/nebulosa-star-detection/build.gradle.kts index 24ca9524d..133ae63ca 100644 --- a/nebulosa-star-detection/build.gradle.kts +++ b/nebulosa-star-detection/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } dependencies { + api(project(":nebulosa-math")) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-star-detection/src/main/kotlin/nebulosa/star/detection/ImageStar.kt b/nebulosa-star-detection/src/main/kotlin/nebulosa/star/detection/ImageStar.kt index 348c1720f..fb911bdca 100644 --- a/nebulosa-star-detection/src/main/kotlin/nebulosa/star/detection/ImageStar.kt +++ b/nebulosa-star-detection/src/main/kotlin/nebulosa/star/detection/ImageStar.kt @@ -1,20 +1,12 @@ package nebulosa.star.detection -import kotlin.math.hypot +import nebulosa.math.Point2D -interface ImageStar { - - val x: Double - - val y: Double +interface ImageStar : Point2D { val hfd: Double val snr: Double val flux: Double - - fun distance(star: ImageStar): Double { - return hypot(x - star.x, y - star.y) - } } diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt index ab24cb6af..151665d1d 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt @@ -1,7 +1,7 @@ package nebulosa.test import io.kotest.core.spec.style.StringSpec -import nebulosa.fits.Fits +import nebulosa.fits.fits import nebulosa.io.transferAndCloseOutput import okhttp3.OkHttpClient import okhttp3.Request @@ -13,44 +13,43 @@ import java.util.zip.ZipInputStream import javax.imageio.ImageIO import kotlin.io.path.* - @Suppress("PropertyName") abstract class FitsStringSpec : StringSpec() { protected val FITS_DIR = "../data/fits" - protected val NGC3344_COLOR_8 by lazy { Fits("$FITS_DIR/NGC3344.Color.8.fits").also(Fits::read) } - protected val NGC3344_COLOR_16 by lazy { Fits("$FITS_DIR/NGC3344.Color.16.fits").also(Fits::read) } - protected val NGC3344_COLOR_32 by lazy { Fits("$FITS_DIR/NGC3344.Color.32.fits").also(Fits::read) } - protected val NGC3344_COLOR_F32 by lazy { Fits("$FITS_DIR/NGC3344.Color.F32.fits").also(Fits::read) } - protected val NGC3344_COLOR_F64 by lazy { Fits("$FITS_DIR/NGC3344.Color.F64.fits").also(Fits::read) } - protected val NGC3344_MONO_8 by lazy { Fits("$FITS_DIR/NGC3344.Mono.8.fits").also(Fits::read) } - protected val NGC3344_MONO_16 by lazy { Fits("$FITS_DIR/NGC3344.Mono.16.fits").also(Fits::read) } - protected val NGC3344_MONO_32 by lazy { Fits("$FITS_DIR/NGC3344.Mono.32.fits").also(Fits::read) } - protected val NGC3344_MONO_F32 by lazy { Fits("$FITS_DIR/NGC3344.Mono.F32.fits").also(Fits::read) } - protected val NGC3344_MONO_F64 by lazy { Fits("$FITS_DIR/NGC3344.Mono.F64.fits").also(Fits::read) } - protected val M6707HH by lazy { Fits(download("M6707HH.fits", ASTROPY_PHOTOMETRY_URL)).also(Fits::read) } - protected val STAR_FOCUS_1 by lazy { Fits("$FITS_DIR/STAR_FOCUS_1.fits").also(Fits::read) } - protected val STAR_FOCUS_2 by lazy { Fits("$FITS_DIR/STAR_FOCUS_2.fits").also(Fits::read) } - protected val STAR_FOCUS_3 by lazy { Fits("$FITS_DIR/STAR_FOCUS_3.fits").also(Fits::read) } - protected val STAR_FOCUS_4 by lazy { Fits("$FITS_DIR/STAR_FOCUS_4.fits").also(Fits::read) } - protected val STAR_FOCUS_5 by lazy { Fits("$FITS_DIR/STAR_FOCUS_5.fits").also(Fits::read) } - protected val STAR_FOCUS_6 by lazy { Fits("$FITS_DIR/STAR_FOCUS_6.fits").also(Fits::read) } - protected val STAR_FOCUS_7 by lazy { Fits("$FITS_DIR/STAR_FOCUS_7.fits").also(Fits::read) } - protected val STAR_FOCUS_8 by lazy { Fits("$FITS_DIR/STAR_FOCUS_8.fits").also(Fits::read) } - protected val STAR_FOCUS_9 by lazy { Fits("$FITS_DIR/STAR_FOCUS_9.fits").also(Fits::read) } - protected val STAR_FOCUS_10 by lazy { Fits("$FITS_DIR/STAR_FOCUS_10.fits").also(Fits::read) } - protected val STAR_FOCUS_11 by lazy { Fits("$FITS_DIR/STAR_FOCUS_11.fits").also(Fits::read) } - protected val STAR_FOCUS_12 by lazy { Fits("$FITS_DIR/STAR_FOCUS_12.fits").also(Fits::read) } - protected val STAR_FOCUS_13 by lazy { Fits("$FITS_DIR/STAR_FOCUS_13.fits").also(Fits::read) } - protected val STAR_FOCUS_14 by lazy { Fits("$FITS_DIR/STAR_FOCUS_14.fits").also(Fits::read) } - protected val STAR_FOCUS_15 by lazy { Fits("$FITS_DIR/STAR_FOCUS_15.fits").also(Fits::read) } - protected val STAR_FOCUS_16 by lazy { Fits("$FITS_DIR/STAR_FOCUS_16.fits").also(Fits::read) } - protected val STAR_FOCUS_17 by lazy { Fits("$FITS_DIR/STAR_FOCUS_17.fits").also(Fits::read) } - protected val UNCALIBRATED by lazy { Fits("$FITS_DIR/UNCALIBRATED.fits").also(Fits::read) } - protected val DARK by lazy { Fits("$FITS_DIR/DARK.fits").also(Fits::read) } - protected val FLAT by lazy { Fits("$FITS_DIR/FLAT.fits").also(Fits::read) } - protected val BIAS by lazy { Fits("$FITS_DIR/BIAS.fits").also(Fits::read) } + protected val NGC3344_COLOR_8 by lazy { "$FITS_DIR/NGC3344.Color.8.fits".fits() } + protected val NGC3344_COLOR_16 by lazy { "$FITS_DIR/NGC3344.Color.16.fits".fits() } + protected val NGC3344_COLOR_32 by lazy { "$FITS_DIR/NGC3344.Color.32.fits".fits() } + protected val NGC3344_COLOR_F32 by lazy { "$FITS_DIR/NGC3344.Color.F32.fits".fits() } + protected val NGC3344_COLOR_F64 by lazy { "$FITS_DIR/NGC3344.Color.F64.fits".fits() } + protected val NGC3344_MONO_8 by lazy { "$FITS_DIR/NGC3344.Mono.8.fits".fits() } + protected val NGC3344_MONO_16 by lazy { "$FITS_DIR/NGC3344.Mono.16.fits".fits() } + protected val NGC3344_MONO_32 by lazy { "$FITS_DIR/NGC3344.Mono.32.fits".fits() } + protected val NGC3344_MONO_F32 by lazy { "$FITS_DIR/NGC3344.Mono.F32.fits".fits() } + protected val NGC3344_MONO_F64 by lazy { "$FITS_DIR/NGC3344.Mono.F64.fits".fits() } + protected val M6707HH by lazy { download("M6707HH.fits", ASTROPY_PHOTOMETRY_URL).fits() } + protected val STAR_FOCUS_1 by lazy { "$FITS_DIR/STAR_FOCUS_1.fits".fits() } + protected val STAR_FOCUS_2 by lazy { "$FITS_DIR/STAR_FOCUS_2.fits".fits() } + protected val STAR_FOCUS_3 by lazy { "$FITS_DIR/STAR_FOCUS_3.fits".fits() } + protected val STAR_FOCUS_4 by lazy { "$FITS_DIR/STAR_FOCUS_4.fits".fits() } + protected val STAR_FOCUS_5 by lazy { "$FITS_DIR/STAR_FOCUS_5.fits".fits() } + protected val STAR_FOCUS_6 by lazy { "$FITS_DIR/STAR_FOCUS_6.fits".fits() } + protected val STAR_FOCUS_7 by lazy { "$FITS_DIR/STAR_FOCUS_7.fits".fits() } + protected val STAR_FOCUS_8 by lazy { "$FITS_DIR/STAR_FOCUS_8.fits".fits() } + protected val STAR_FOCUS_9 by lazy { "$FITS_DIR/STAR_FOCUS_9.fits".fits() } + protected val STAR_FOCUS_10 by lazy { "$FITS_DIR/STAR_FOCUS_10.fits".fits() } + protected val STAR_FOCUS_11 by lazy { "$FITS_DIR/STAR_FOCUS_11.fits".fits() } + protected val STAR_FOCUS_12 by lazy { "$FITS_DIR/STAR_FOCUS_12.fits".fits() } + protected val STAR_FOCUS_13 by lazy { "$FITS_DIR/STAR_FOCUS_13.fits".fits() } + protected val STAR_FOCUS_14 by lazy { "$FITS_DIR/STAR_FOCUS_14.fits".fits() } + protected val STAR_FOCUS_15 by lazy { "$FITS_DIR/STAR_FOCUS_15.fits".fits() } + protected val STAR_FOCUS_16 by lazy { "$FITS_DIR/STAR_FOCUS_16.fits".fits() } + protected val STAR_FOCUS_17 by lazy { "$FITS_DIR/STAR_FOCUS_17.fits".fits() } + protected val UNCALIBRATED by lazy { "$FITS_DIR/UNCALIBRATED.fits".fits() } + protected val DARK by lazy { "$FITS_DIR/DARK.fits".fits() } + protected val FLAT by lazy { "$FITS_DIR/FLAT.fits".fits() } + protected val BIAS by lazy { "$FITS_DIR/BIAS.fits".fits() } protected fun BufferedImage.save(name: String): Pair { val path = Path.of("src", "test", "resources", "saved", "$name.png").createParentDirectories() diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt index fdf82a229..89f5715fa 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt @@ -1,6 +1,6 @@ package nebulosa.test -import nebulosa.fits.Fits +import nebulosa.fits.fits import nebulosa.hips2fits.Hips2FitsService import nebulosa.hips2fits.HipsSurvey import nebulosa.io.transferAndCloseOutput @@ -13,9 +13,10 @@ import kotlin.io.path.exists import kotlin.io.path.fileSize import kotlin.io.path.outputStream +@Suppress("PropertyName") abstract class Hips2FitsStringSpec : FitsStringSpec() { - protected val M31 by lazy { Fits(download("00 42 44.3".hours, "41 16 9".deg, 3.deg)).also(Fits::read) } + protected val M31 by lazy { download("00 42 44.3".hours, "41 16 9".deg, 3.deg).fits() } protected fun download(centerRA: Angle, centerDEC: Angle, fov: Angle): Path { val name = "$centerRA@$centerDEC@$fov".toByteArray().toByteString().md5().hex() @@ -26,7 +27,7 @@ abstract class Hips2FitsStringSpec : FitsStringSpec() { } HIPS_SERVICE - .query(CDS_P_DSS2_NIR, centerRA, centerDEC, 1280, 720, 0.0, fov) + .query(CDS_P_DSS2_NIR.id, centerRA, centerDEC, 1280, 720, 0.0, fov) .execute() .body()!! .use { it.byteStream().transferAndCloseOutput(path.outputStream()) } diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/MathMatchers.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/MathMatchers.kt new file mode 100644 index 000000000..f49e4ad40 --- /dev/null +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/MathMatchers.kt @@ -0,0 +1,51 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package nebulosa.test + +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.doubles.ToleranceMatcher +import nebulosa.math.Matrix3D +import nebulosa.math.Vector3D + +inline infix fun Vector3D.plusOrMinus(tolerance: Double) = Vector3DMatcher(this, tolerance) + +class Vector3DMatcher(expected: Vector3D, tolerance: Double) : Matcher { + + private val xMatcher = ToleranceMatcher(expected.x, tolerance) + private val yMatcher = ToleranceMatcher(expected.y, tolerance) + private val zMatcher = ToleranceMatcher(expected.z, tolerance) + + override fun test(value: Vector3D): MatcherResult { + return xMatcher.test(value.x).takeIf { !it.passed() } + ?: yMatcher.test(value.y).takeIf { !it.passed() } + ?: zMatcher.test(value.z) + } +} + +inline infix fun Matrix3D.plusOrMinus(tolerance: Double) = Matrix3DMatcher(this, tolerance) + +class Matrix3DMatcher(expected: Matrix3D, tolerance: Double) : Matcher { + + private val a11Matcher = ToleranceMatcher(expected.a11, tolerance) + private val a12Matcher = ToleranceMatcher(expected.a12, tolerance) + private val a13Matcher = ToleranceMatcher(expected.a13, tolerance) + private val a21Matcher = ToleranceMatcher(expected.a21, tolerance) + private val a22Matcher = ToleranceMatcher(expected.a22, tolerance) + private val a23Matcher = ToleranceMatcher(expected.a23, tolerance) + private val a31Matcher = ToleranceMatcher(expected.a31, tolerance) + private val a32Matcher = ToleranceMatcher(expected.a32, tolerance) + private val a33Matcher = ToleranceMatcher(expected.a33, tolerance) + + override fun test(value: Matrix3D): MatcherResult { + return a11Matcher.test(value.a11).takeIf { !it.passed() } + ?: a12Matcher.test(value.a12).takeIf { !it.passed() } + ?: a13Matcher.test(value.a13).takeIf { !it.passed() } + ?: a21Matcher.test(value.a21).takeIf { !it.passed() } + ?: a22Matcher.test(value.a22).takeIf { !it.passed() } + ?: a23Matcher.test(value.a23).takeIf { !it.passed() } + ?: a31Matcher.test(value.a31).takeIf { !it.passed() } + ?: a32Matcher.test(value.a32).takeIf { !it.passed() } + ?: a33Matcher.test(value.a33) + } +} diff --git a/nebulosa-time/build.gradle.kts b/nebulosa-time/build.gradle.kts index d6f4dca50..53c46ddfe 100644 --- a/nebulosa-time/build.gradle.kts +++ b/nebulosa-time/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } dependencies { + api(project(":nebulosa-common")) api(project(":nebulosa-erfa")) testImplementation(project(":nebulosa-test")) } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/CurrentTime.kt b/nebulosa-time/src/main/kotlin/nebulosa/time/CurrentTime.kt similarity index 79% rename from api/src/main/kotlin/nebulosa/api/atlas/CurrentTime.kt rename to nebulosa-time/src/main/kotlin/nebulosa/time/CurrentTime.kt index 7c5eb5455..42e9ee11a 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/CurrentTime.kt +++ b/nebulosa-time/src/main/kotlin/nebulosa/time/CurrentTime.kt @@ -1,22 +1,24 @@ -package nebulosa.api.atlas +package nebulosa.time -import nebulosa.time.InstantOfTime -import nebulosa.time.TimeDelta -import nebulosa.time.UTC +import nebulosa.common.time.Stopwatch object CurrentTime : InstantOfTime() { - private const val MAX_INTERVAL = 1000L * 30 // 30s. + @JvmField @Volatile var ELAPSED_INTERVAL = 5L - @Volatile private var lastTime = 0L + private val stopwatch = Stopwatch() - private var time = UTC.now() - @Synchronized get() { - val curTime = System.currentTimeMillis() + init { + stopwatch.start() + } - if (curTime - lastTime >= MAX_INTERVAL) { - lastTime = curTime - field = UTC.now() + private var time = UTC.now() + get() { + synchronized(stopwatch) { + if (stopwatch.elapsedSeconds >= ELAPSED_INTERVAL) { + stopwatch.reset() + field = UTC.now() + } } return field diff --git a/nebulosa-time/src/main/kotlin/nebulosa/time/TT.kt b/nebulosa-time/src/main/kotlin/nebulosa/time/TT.kt index 0a404d2a2..e66983337 100644 --- a/nebulosa-time/src/main/kotlin/nebulosa/time/TT.kt +++ b/nebulosa-time/src/main/kotlin/nebulosa/time/TT.kt @@ -33,4 +33,14 @@ class TT : TimeJD, Timescale { override val tdb by lazy { TDB(eraTtTdb(whole, fraction, TDBMinusTT.delta(this)), true) } override val tcb get() = tdb.tcb + + companion object { + + @JvmStatic val J2000 = TT(TimeJD.J2000) + + @JvmStatic val B1950 = TT(TimeJD.B1950) + + @JvmStatic + fun now() = TT(TimeJD.now()) + } } diff --git a/nebulosa-time/src/main/kotlin/nebulosa/time/TimeJD.kt b/nebulosa-time/src/main/kotlin/nebulosa/time/TimeJD.kt index a400cc98c..be0c7ff97 100644 --- a/nebulosa-time/src/main/kotlin/nebulosa/time/TimeJD.kt +++ b/nebulosa-time/src/main/kotlin/nebulosa/time/TimeJD.kt @@ -30,19 +30,19 @@ open class TimeJD internal constructor(private val jd: DoubleArray, normalize: B override fun minus(delta: TimeDelta) = TimeJD(whole, fraction - delta.delta(this)) - override val ut1 get() = UT1(whole, fraction) + override val ut1 by lazy { UT1(whole, fraction) } - override val utc get() = UTC(whole, fraction) + override val utc by lazy { UTC(whole, fraction) } - override val tai get() = TAI(whole, fraction) + override val tai by lazy { TAI(whole, fraction) } - override val tt get() = TT(whole, fraction) + override val tt by lazy { TT(whole, fraction) } - override val tcg get() = TCG(whole, fraction) + override val tcg by lazy { TCG(whole, fraction) } - override val tdb get() = TDB(whole, fraction) + override val tdb by lazy { TDB(whole, fraction) } - override val tcb get() = TCB(whole, fraction) + override val tcb by lazy { TCB(whole, fraction) } companion object { diff --git a/nebulosa-time/src/main/kotlin/nebulosa/time/UTC.kt b/nebulosa-time/src/main/kotlin/nebulosa/time/UTC.kt index 1b0e67435..8d9122513 100644 --- a/nebulosa-time/src/main/kotlin/nebulosa/time/UTC.kt +++ b/nebulosa-time/src/main/kotlin/nebulosa/time/UTC.kt @@ -35,6 +35,10 @@ class UTC : TimeJD, Timescale { companion object { + @JvmStatic val J2000 = UTC(TimeJD.J2000) + + @JvmStatic val B1950 = UTC(TimeJD.B1950) + @JvmStatic fun now() = UTC(TimeJD.now()) } diff --git a/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/WatneyPlateSolver.kt b/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/WatneyPlateSolver.kt index af8d2cdd1..9df007b95 100644 --- a/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/WatneyPlateSolver.kt +++ b/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/WatneyPlateSolver.kt @@ -1,10 +1,11 @@ package nebulosa.watney.plate.solving +import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.erfa.SphericalCoordinate -import nebulosa.fits.Fits import nebulosa.fits.Header import nebulosa.fits.NOAOExt import nebulosa.fits.Standard +import nebulosa.fits.fits import nebulosa.imaging.Image import nebulosa.log.debug import nebulosa.log.loggerFor @@ -44,8 +45,9 @@ data class WatneyPlateSolver( path: Path?, image: Image?, centerRA: Angle, centerDEC: Angle, radius: Angle, downsampleFactor: Int, timeout: Duration?, + cancellationToken: CancellationToken, ): PlateSolution { - val image = image ?: Fits(path!!).also(Fits::read).use(Image::open) + val image = image ?: path!!.fits().use(Image::open) val stars = (starDetector ?: DEFAULT_STAR_DETECTOR).detect(image) LOG.debug { "detected ${stars.size} stars from the image" } @@ -285,7 +287,7 @@ data class WatneyPlateSolver( @JvmStatic private fun isValidSolution(solution: ComputedPlateSolution?): Boolean { return solution != null && solution.centerRA.isFinite() && solution.centerDEC.isFinite() - && solution.orientation.isFinite() && solution.plateConstants.isValid + && solution.orientation.isFinite() && solution.plateConstants.isValid } @JvmStatic diff --git a/nebulosa-wcs/src/main/kotlin/nebulosa/wcs/PixelCoordinates.kt b/nebulosa-wcs/src/main/kotlin/nebulosa/wcs/PixelCoordinates.kt index 92280dab6..e2c06a689 100644 --- a/nebulosa-wcs/src/main/kotlin/nebulosa/wcs/PixelCoordinates.kt +++ b/nebulosa-wcs/src/main/kotlin/nebulosa/wcs/PixelCoordinates.kt @@ -1,5 +1,6 @@ package nebulosa.wcs import nebulosa.math.Angle +import nebulosa.math.Point2D -data class PixelCoordinates(val x: Double, val y: Double, val phi: Angle, val theta: Angle) +data class PixelCoordinates(override val x: Double, override val y: Double, val phi: Angle, val theta: Angle) : Point2D diff --git a/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt b/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt index 5877ccef7..ef6b7db4e 100644 --- a/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt +++ b/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt @@ -2,10 +2,10 @@ import io.kotest.core.annotation.EnabledIf import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.shouldBe -import nebulosa.fits.Fits import nebulosa.fits.Header -import nebulosa.math.AngleFormatter -import nebulosa.math.format +import nebulosa.fits.fits +import nebulosa.math.formatHMS +import nebulosa.math.formatSignedDMS import nebulosa.test.NonGitHubOnlyCondition import nebulosa.wcs.WCS import kotlin.random.Random @@ -37,11 +37,11 @@ class LibWCSTest : StringSpec() { val bottomRight = it.pixToSky(width.toDouble(), height.toDouble()) val center = it.pixToSky(width / 2.0, height / 2.0) - println("top left: ${topLeft.rightAscension.format(AngleFormatter.HMS)} ${topLeft.declination.format(AngleFormatter.SIGNED_DMS)}") - println("top right: ${topRight.rightAscension.format(AngleFormatter.HMS)} ${topRight.declination.format(AngleFormatter.SIGNED_DMS)}") - println("bottom left: ${bottomLeft.rightAscension.format(AngleFormatter.HMS)} ${bottomLeft.declination.format(AngleFormatter.SIGNED_DMS)}") - println("bottom right: ${bottomRight.rightAscension.format(AngleFormatter.HMS)} ${bottomRight.declination.format(AngleFormatter.SIGNED_DMS)}") - println("center: ${center.rightAscension.format(AngleFormatter.HMS)} ${center.declination.format(AngleFormatter.SIGNED_DMS)}") + println("top left: ${topLeft.rightAscension.formatHMS()} ${topLeft.declination.formatSignedDMS()}") + println("top right: ${topRight.rightAscension.formatHMS()} ${topRight.declination.formatSignedDMS()}") + println("bottom left: ${bottomLeft.rightAscension.formatHMS()} ${bottomLeft.declination.formatSignedDMS()}") + println("bottom right: ${bottomRight.rightAscension.formatHMS()} ${bottomRight.declination.formatSignedDMS()}") + println("center: ${center.rightAscension.formatHMS()} ${center.declination.formatSignedDMS()}") for ((x0, y0) in data) { val (rightAscension, declination) = it.pixToSky(x0.toDouble(), y0.toDouble()) @@ -53,7 +53,7 @@ class LibWCSTest : StringSpec() { } private fun readHeaderFromFits(name: String): Header { - return Fits("src/test/resources/$name.fits").use { it.readHdu()!!.header } + return "src/test/resources/$name.fits".fits().use { it.first!!.header } } companion object { diff --git a/settings.gradle.kts b/settings.gradle.kts index d26534d3e..11ad21ad8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,7 @@ buildCache { dependencyResolutionManagement { versionCatalogs { create("libs") { - library("okio", "com.squareup.okio:okio:3.7.0") + library("okio", "com.squareup.okio:okio:3.8.0") library("okhttp", "com.squareup.okhttp3:okhttp:4.12.0") library("okhttp-logging", "com.squareup.okhttp3:logging-interceptor:4.12.0") library("jackson-core", "com.fasterxml.jackson.core:jackson-databind:2.16.1") @@ -21,16 +21,16 @@ dependencyResolutionManagement { library("retrofit", "com.squareup.retrofit2:retrofit:2.9.0") library("retrofit-jackson", "com.squareup.retrofit2:converter-jackson:2.9.0") library("rx", "io.reactivex.rxjava3:rxjava:3.1.8") - library("logback", "ch.qos.logback:logback-classic:1.4.14") + library("logback", "ch.qos.logback:logback-classic:1.5.1") library("eventbus", "org.greenrobot:eventbus-java:3.3.1") - library("netty-transport", "io.netty:netty-transport:4.1.106.Final") - library("netty-codec", "io.netty:netty-codec:4.1.106.Final") + library("netty-transport", "io.netty:netty-transport:4.1.107.Final") + library("netty-codec", "io.netty:netty-codec:4.1.107.Final") library("xml", "com.fasterxml:aalto-xml:1.3.2") library("csv", "de.siegmar:fastcsv:3.0.0") library("apache-lang3", "org.apache.commons:commons-lang3:3.14.0") - library("apache-codec", "commons-codec:commons-codec:1.16.0") + library("apache-codec", "commons-codec:commons-codec:1.16.1") library("apache-collections", "org.apache.commons:commons-collections4:4.4") - library("oshi", "com.github.oshi:oshi-core:6.4.11") + 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") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.8.0") @@ -47,6 +47,7 @@ include(":nebulosa-adql") include(":nebulosa-alignment") include(":nebulosa-alpaca-api") include(":nebulosa-alpaca-discovery-protocol") +include(":nebulosa-alpaca-indi") include(":nebulosa-astap") include(":nebulosa-astrometrynet") include(":nebulosa-astrometrynet-jna")