diff --git a/api/schemas/objectbox.json b/api/schemas/objectbox.json index 811401385..0f7413c13 100644 --- a/api/schemas/objectbox.json +++ b/api/schemas/objectbox.json @@ -4,77 +4,77 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "1:4508028933515523414", - "lastPropertyId": "13:5569629325911720184", + "id": "1:3544801173480775772", + "lastPropertyId": "13:3755368355153819967", "name": "CalibrationFrameEntity", "properties": [ { - "id": "1:279471804400581871", + "id": "1:6440158350156700816", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:9048727858630632737", + "id": "2:7830549305901803879", "name": "type", - "indexId": "1:3018423918314968566", + "indexId": "1:3705837194399110688", "type": 5, "flags": 8 }, { - "id": "3:5712791023807889534", - "name": "name", - "indexId": "2:8432810603549739468", + "id": "3:8490362500884478696", + "name": "group", + "indexId": "2:2460719507268221169", "type": 9, "flags": 2048 }, { - "id": "4:3434117744352502900", + "id": "4:169758157435742191", "name": "filter", "type": 9 }, { - "id": "5:1871034143652415809", + "id": "5:5772177826523179837", "name": "exposureTime", "type": 6 }, { - "id": "6:8846123268014704509", + "id": "6:979735190507089416", "name": "temperature", "type": 8 }, { - "id": "7:8561154143050278063", + "id": "7:1567591787936780727", "name": "width", "type": 5 }, { - "id": "8:6920579444153489022", + "id": "8:804894592407875320", "name": "height", "type": 5 }, { - "id": "9:4300769060778976734", + "id": "9:7150567366206966047", "name": "binX", "type": 5 }, { - "id": "10:4693474237106002327", + "id": "10:6904147472104067341", "name": "binY", "type": 5 }, { - "id": "11:8369728096653684761", + "id": "11:5805422636156073861", "name": "gain", "type": 8 }, { - "id": "12:617052828938607363", + "id": "12:3861144650886065321", "name": "path", "type": 9 }, { - "id": "13:5569629325911720184", + "id": "13:3755368355153819967", "name": "enabled", "type": 1 } @@ -82,25 +82,25 @@ "relations": [] }, { - "id": "2:4800249862026080527", - "lastPropertyId": "3:211299529025119304", + "id": "2:5695036645028998704", + "lastPropertyId": "3:5935807626551879093", "name": "PreferenceEntity", "properties": [ { - "id": "1:3593540058272630983", + "id": "1:1241938942467328378", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:2699303611424729430", + "id": "2:5066364999797986961", "name": "key", - "indexId": "3:2030544424571300028", + "indexId": "3:361394127200064680", "type": 9, "flags": 34848 }, { - "id": "3:211299529025119304", + "id": "3:5935807626551879093", "name": "value", "type": 9 } @@ -108,28 +108,28 @@ "relations": [] }, { - "id": "3:9190695617085753667", - "lastPropertyId": "4:411434182698925224", + "id": "3:13725857459345728", + "lastPropertyId": "4:8575761112465612996", "name": "SatelliteEntity", "properties": [ { - "id": "1:7748265871438465999", + "id": "1:7008444193321057279", "name": "id", "type": 6, "flags": 129 }, { - "id": "2:2980713713220488130", + "id": "2:7254931361809919912", "name": "name", "type": 9 }, { - "id": "3:8036745814034214740", + "id": "3:7655077553453802998", "name": "tle", "type": 9 }, { - "id": "4:411434182698925224", + "id": "4:8575761112465612996", "name": "groups", "type": 30 } @@ -137,68 +137,68 @@ "relations": [] }, { - "id": "4:6299583728620001761", - "lastPropertyId": "12:4179508964623201115", + "id": "4:2355261488865870711", + "lastPropertyId": "12:8881688937650635468", "name": "SimbadEntity", "properties": [ { - "id": "1:7284883107181783588", + "id": "1:8754753767317947963", "name": "id", "type": 6, "flags": 129 }, { - "id": "2:1059978401562504177", + "id": "2:875189598014282513", "name": "name", "type": 9 }, { - "id": "3:2238737597611607433", + "id": "3:1840539013499888018", "name": "type", "type": 5 }, { - "id": "4:6034348124979703831", + "id": "4:8380920369067256416", "name": "rightAscensionJ2000", "type": 8 }, { - "id": "5:6603670815168137185", + "id": "5:4114744755808135895", "name": "declinationJ2000", "type": 8 }, { - "id": "6:4798847469480514750", + "id": "6:5877086147655445788", "name": "magnitude", "type": 8 }, { - "id": "7:4280564484498302769", + "id": "7:4614518058111040649", "name": "pmRA", "type": 8 }, { - "id": "8:1070997648386390650", + "id": "8:5619165542749552220", "name": "pmDEC", "type": 8 }, { - "id": "9:7408560810497672822", + "id": "9:8196290885692683478", "name": "parallax", "type": 8 }, { - "id": "10:7464931444484734827", + "id": "10:2681231197677728845", "name": "radialVelocity", "type": 8 }, { - "id": "11:531497562996887037", + "id": "11:2414643968839286765", "name": "redshift", "type": 8 }, { - "id": "12:4179508964623201115", + "id": "12:8881688937650635468", "name": "constellation", "type": 5 } @@ -206,8 +206,8 @@ "relations": [] } ], - "lastEntityId": "4:6299583728620001761", - "lastIndexId": "3:2030544424571300028", + "lastEntityId": "4:2355261488865870711", + "lastIndexId": "3:361394127200064680", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, diff --git a/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt b/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt index 5d13a502a..1cdfcf562 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/MinorPlanet.kt @@ -11,7 +11,7 @@ data class MinorPlanet( @JvmField val neo: Boolean = false, @JvmField val orbitType: String = "", @JvmField val parameters: List = emptyList(), - @JvmField val searchItems: List = emptyList(), + @JvmField val list: List = emptyList(), ) { data class OrbitalPhysicalParameter( @@ -60,8 +60,8 @@ data class MinorPlanet( body.body!!.pha, body.body!!.neo, body.body?.type?.name ?: "", items, ) } else if (body.list != null) { - val searchItems = body.list!!.map { SearchItem(it.name, it.pdes) } - return MinorPlanet(searchItems = searchItems) + val list = body.list!!.map { SearchItem(it.name, it.pdes) } + return MinorPlanet(list = list) } else { return EMPTY } diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/Subscriber.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/Subscriber.kt index f49f4c467..d9bfee611 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/Subscriber.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/Subscriber.kt @@ -2,7 +2,6 @@ package nebulosa.api.beans.annotations import org.springframework.context.annotation.Lazy -@Retention @Lazy(false) @Target(AnnotationTarget.CLASS) annotation class Subscriber diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/angle/AngleParam.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/angle/AngleParam.kt index 147d173a6..f89ae63fd 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/angle/AngleParam.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/angle/AngleParam.kt @@ -1,6 +1,5 @@ package nebulosa.api.beans.converters.angle -@Retention @Target(AnnotationTarget.VALUE_PARAMETER) annotation class AngleParam( val name: String = "", diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParam.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParam.kt index 0d7e2970b..e02e8eebf 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParam.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParam.kt @@ -1,7 +1,6 @@ package nebulosa.api.beans.converters.device @Target(AnnotationTarget.VALUE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) annotation class DeviceOrEntityParam( val name: String = "", val defaultValue: String = "" diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/location/LocationParam.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/location/LocationParam.kt index e2c1a1bf5..311e3db3e 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/location/LocationParam.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/location/LocationParam.kt @@ -1,5 +1,4 @@ package nebulosa.api.beans.converters.location -@Retention @Target(AnnotationTarget.VALUE_PARAMETER) annotation class LocationParam diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/time/DateAndTimeParam.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/time/DateAndTimeParam.kt index cdcc85c38..e41fad45d 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/time/DateAndTimeParam.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/time/DateAndTimeParam.kt @@ -1,10 +1,9 @@ package nebulosa.api.beans.converters.time -@Retention @Target(AnnotationTarget.VALUE_PARAMETER) annotation class DateAndTimeParam( val datePattern: String = "yyyy-MM-dd", - val timePattern: String = "HH:mm", + val timePattern: String = "HH:mm:ss", val noSeconds: Boolean = true, val nullable: Boolean = false, ) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt index 199bf8db7..d69ec5960 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt @@ -1,7 +1,6 @@ package nebulosa.api.calibration import jakarta.validation.Valid -import jakarta.validation.constraints.NotBlank import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* import java.nio.file.Path @@ -16,26 +15,24 @@ class CalibrationFrameController( @GetMapping fun groups() = calibrationFrameService.groups() - @GetMapping("{name}") - fun groupedCalibrationFrames(@PathVariable name: String): List { - var id = 0 - val groupedFrames = calibrationFrameService.groupedCalibrationFrames(name) - return groupedFrames.map { CalibrationFrameGroup(++id, name, it.key, it.value) } + @GetMapping("{group}") + fun frames(@PathVariable group: String): List { + return calibrationFrameService.frames(group).sorted() } - @PutMapping("{name}") - fun upload(@PathVariable name: String, @RequestParam path: Path): List { - return calibrationFrameService.upload(name, path) + @PutMapping("{group}") + fun upload(@PathVariable group: String, @RequestParam path: Path): List { + return calibrationFrameService.upload(group, path) } - @PatchMapping("{frame}") - fun edit( - frame: CalibrationFrameEntity, - @Valid @NotBlank @RequestParam name: String, @RequestParam enabled: Boolean, - ) = calibrationFrameService.edit(frame, name, enabled) + @PostMapping + fun update(@RequestBody @Valid body: CalibrationFrameEntity): CalibrationFrameEntity { + require(body.id > 0L) { "invalid frame id" } + return calibrationFrameService.edit(body) + } - @DeleteMapping("{frame}") - fun delete(frame: CalibrationFrameEntity) { - calibrationFrameService.delete(frame) + @DeleteMapping("{id}") + fun delete(@PathVariable id: Long) { + calibrationFrameService.delete(id) } } diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt index 1b15e657e..4e684e1b4 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt @@ -7,6 +7,7 @@ import io.objectbox.annotation.Index import nebulosa.api.beans.converters.database.FrameTypePropertyConverter import nebulosa.api.beans.converters.database.PathPropertyConverter import nebulosa.api.database.BoxEntity +import nebulosa.fits.INVALID_TEMPERATURE import nebulosa.indi.device.camera.FrameType import java.nio.file.Path @@ -14,10 +15,10 @@ import java.nio.file.Path data class CalibrationFrameEntity( @Id override var id: Long = 0L, @JvmField @Index @Convert(converter = FrameTypePropertyConverter::class, dbType = Int::class) var type: FrameType = FrameType.LIGHT, - @JvmField @Index var name: String = "", + @JvmField @Index var group: String = "", @JvmField var filter: String? = null, @JvmField var exposureTime: Long = 0L, - @JvmField var temperature: Double = 0.0, + @JvmField var temperature: Double = INVALID_TEMPERATURE, @JvmField var width: Int = 0, @JvmField var height: Int = 0, @JvmField var binX: Int = 0, @@ -25,4 +26,28 @@ data class CalibrationFrameEntity( @JvmField var gain: Double = 0.0, @JvmField @Convert(converter = PathPropertyConverter::class, dbType = String::class) var path: Path? = null, @JvmField var enabled: Boolean = true, -) : BoxEntity +) : BoxEntity, Comparable { + + override fun compareTo(other: CalibrationFrameEntity): Int { + return if (type.ordinal > other.type.ordinal) 1 + else if (type.ordinal < other.type.ordinal) -1 + else if (exposureTime > other.exposureTime) 1 + else if (exposureTime < other.exposureTime) -1 + else if (width > other.width) 1 + else if (width < other.width) -1 + else if (height > other.height) 1 + else if (height < other.height) -1 + else if (binX > other.binX) 1 + else if (binX < other.binX) -1 + else if (binY > other.binY) 1 + else if (binY < other.binY) -1 + else if (gain > other.gain) 1 + else if (gain < other.gain) -1 + else if (temperature > other.temperature) 1 + else if (temperature < other.temperature) -1 + else if (filter != null && other.filter != null) filter!!.compareTo(other.filter!!) + else if (filter == null) -1 + else if (other.filter == null) 1 + else 0 + } +} diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt deleted file mode 100644 index df469a2cd..000000000 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.calibration - -data class CalibrationFrameGroup( - @JvmField val id: Int, - @JvmField val name: String, - @JvmField val key: CalibrationGroupKey, - @JvmField val frames: List, -) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt index 056d8ac54..e91a4c454 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt @@ -11,24 +11,24 @@ import org.springframework.stereotype.Component class CalibrationFrameRepository(@Qualifier("calibrationFrameBox") override val box: Box) : BoxRepository() { - fun groups() = box.all.map { it.name }.distinct() + fun groups() = box.all.map { it.group }.distinct() - fun findAll(name: String): List { - return box.query(CalibrationFrameEntity_.name equal name) + fun findAll(group: String): List { + return box.query(CalibrationFrameEntity_.group equal group) .build().use { it.find() } } @Synchronized - fun delete(name: String, path: String) { - val condition = and(CalibrationFrameEntity_.name equal name, CalibrationFrameEntity_.path equal path) + fun delete(group: String, path: String) { + val condition = and(CalibrationFrameEntity_.group equal group, CalibrationFrameEntity_.path equal path) return box.query(condition).build().use { it.remove() } } - fun darkFrames(name: String, width: Int, height: Int, bin: Int, exposureTime: Long, gain: Double): List { + fun darkFrames(group: String, width: Int, height: Int, bin: Int, exposureTime: Long, gain: Double): List { val condition = and( CalibrationFrameEntity_.type equal FrameType.DARK.ordinal, CalibrationFrameEntity_.enabled.isTrue, - CalibrationFrameEntity_.name equal name, + CalibrationFrameEntity_.group equal group, CalibrationFrameEntity_.width equal width, CalibrationFrameEntity_.height equal height, CalibrationFrameEntity_.binX equal bin, @@ -40,11 +40,11 @@ class CalibrationFrameRepository(@Qualifier("calibrationFrameBox") override val return box.query(condition).build().use { it.find() } } - fun biasFrames(name: String, width: Int, height: Int, bin: Int, gain: Double): List { + fun biasFrames(group: String, width: Int, height: Int, bin: Int, gain: Double): List { val condition = and( CalibrationFrameEntity_.type equal FrameType.BIAS.ordinal, CalibrationFrameEntity_.enabled.isTrue, - CalibrationFrameEntity_.name equal name, + CalibrationFrameEntity_.group equal group, CalibrationFrameEntity_.width equal width, CalibrationFrameEntity_.height equal height, CalibrationFrameEntity_.binX equal bin, @@ -55,11 +55,11 @@ class CalibrationFrameRepository(@Qualifier("calibrationFrameBox") override val return box.query(condition).build().use { it.find() } } - fun flatFrames(name: String, filter: String?, width: Int, height: Int, bin: Int): List { + fun flatFrames(group: String, filter: String?, width: Int, height: Int, bin: Int): List { val condition = and( CalibrationFrameEntity_.type equal FrameType.FLAT.ordinal, CalibrationFrameEntity_.enabled.isTrue, - CalibrationFrameEntity_.name equal name, + CalibrationFrameEntity_.group equal group, CalibrationFrameEntity_.width equal width, CalibrationFrameEntity_.height equal height, CalibrationFrameEntity_.binX equal bin, diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index 5939b03d3..c5e25be84 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -28,11 +28,11 @@ class CalibrationFrameService( private val calibrationFrameRepository: CalibrationFrameRepository, ) : CalibrationFrameProvider { - fun calibrate(name: String, image: Image, createNew: Boolean = false): Image { + fun calibrate(group: String, image: Image, createNew: Boolean = false): Image { return synchronized(image) { - val darkFrame = findBestDarkFrames(name, image).firstOrNull() - val biasFrame = if (darkFrame == null) findBestBiasFrames(name, image).firstOrNull() else null - val flatFrame = findBestFlatFrames(name, image).firstOrNull() + val darkFrame = findBestDarkFrames(group, image).firstOrNull() + val biasFrame = if (darkFrame == null) findBestBiasFrames(group, image).firstOrNull() else null + val flatFrame = findBestFlatFrames(group, image).firstOrNull() val darkImage = darkFrame?.path?.fits()?.use(Image::open) val biasImage = biasFrame?.path?.fits()?.use(Image::open) @@ -92,27 +92,28 @@ class CalibrationFrameService( } } - fun groups() = calibrationFrameRepository.groups() + fun groups(): List { + return calibrationFrameRepository.groups() + } - fun groupedCalibrationFrames(name: String): Map> { - val frames = calibrationFrameRepository.findAll(name) - return frames.groupBy(CalibrationGroupKey::from) + fun frames(group: String): List { + return calibrationFrameRepository.findAll(group) } - fun upload(name: String, path: Path): List { + fun upload(group: String, path: Path): List { val files = if (path.isRegularFile()) listOf(path) else if (path.isDirectory()) path.listDirectoryEntries("*.{fits,fit,xisf}").filter { it.isRegularFile() } else return emptyList() - return upload(name, files) + return upload(group, files) } @Synchronized - fun upload(name: String, files: List): List { + fun upload(group: String, files: List): List { val frames = ArrayList(files.size) for (file in files) { - calibrationFrameRepository.delete(name, "$file") + calibrationFrameRepository.delete(group, "$file") try { val image = if (file.isFits()) file.fits() @@ -125,12 +126,12 @@ class CalibrationFrameService( val frameType = header.frameType?.takeIf { it != FrameType.LIGHT } ?: return@use val exposureTime = if (frameType == FrameType.DARK) header.exposureTimeInMicroseconds else 0L - val temperature = if (frameType == FrameType.DARK) header.temperature else 999.0 + val temperature = if (frameType == FrameType.DARK) header.temperature else INVALID_TEMPERATURE val gain = if (frameType != FrameType.FLAT) header.gain else 0.0 val filter = if (frameType == FrameType.FLAT) header.filter else null val frame = CalibrationFrameEntity( - 0L, frameType, name, filter, + 0L, frameType, group, filter, exposureTime, temperature, header.width, header.height, header.binX, header.binY, gain, file, @@ -147,23 +148,21 @@ class CalibrationFrameService( return frames } - fun edit(frame: CalibrationFrameEntity, name: String, enabled: Boolean): CalibrationFrameEntity { - frame.name = name - frame.enabled = enabled + fun edit(frame: CalibrationFrameEntity): CalibrationFrameEntity { return calibrationFrameRepository.save(frame) } - fun delete(frame: CalibrationFrameEntity) { - calibrationFrameRepository.delete(frame) + fun delete(id: Long) { + calibrationFrameRepository.delete(id) } override fun findBestDarkFrames( - name: String, temperature: Double, width: Int, height: Int, + group: String, temperature: Double, width: Int, height: Int, binX: Int, binY: Int, exposureTimeInMicroseconds: Long, gain: Double, ): List { val frames = calibrationFrameRepository - .darkFrames(name, width, height, binX, exposureTimeInMicroseconds, gain) + .darkFrames(group, width, height, binX, exposureTimeInMicroseconds, gain) if (frames.isEmpty()) return emptyList() @@ -175,46 +174,45 @@ class CalibrationFrameService( return groupedFrames.firstEntry().value } - fun findBestDarkFrames(name: String, image: Image): List { + fun findBestDarkFrames(group: String, image: Image): List { val header = image.header val temperature = header.temperature val binX = header.binX val exposureTime = header.exposureTimeInMicroseconds - return findBestDarkFrames(name, temperature, image.width, image.height, binX, binX, exposureTime, header.gain) + return findBestDarkFrames(group, temperature, image.width, image.height, binX, binX, exposureTime, header.gain) } override fun findBestFlatFrames( - name: String, width: Int, height: Int, + group: String, width: Int, height: Int, binX: Int, binY: Int, filter: String? ): List { // TODO: Generate master from matched frames. (Subtract the master bias frame from each flat frame) return calibrationFrameRepository - .flatFrames(name, filter, width, height, binX) + .flatFrames(group, filter, width, height, binX) } - fun findBestFlatFrames(name: String, image: Image): List { + fun findBestFlatFrames(group: String, image: Image): List { val header = image.header val filter = header.filter val binX = header.binX - return findBestFlatFrames(name, image.width, image.height, binX, binX, filter) + return findBestFlatFrames(group, image.width, image.height, binX, binX, filter) } override fun findBestBiasFrames( - name: String, width: Int, height: Int, + group: String, width: Int, height: Int, binX: Int, binY: Int, gain: Double, ): List { // TODO: Generate master from matched frames. - return calibrationFrameRepository - .biasFrames(name, width, height, binX, gain) + return calibrationFrameRepository.biasFrames(group, width, height, binX, gain) } - fun findBestBiasFrames(name: String, image: Image): List { + fun findBestBiasFrames(group: String, image: Image): List { val header = image.header val binX = header.binX - return findBestBiasFrames(name, image.width, image.height, binX, binX, image.header.gain) + return findBestBiasFrames(group, image.width, image.height, binX, binX, image.header.gain) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt deleted file mode 100644 index 5f63971b4..000000000 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nebulosa.api.calibration - -import nebulosa.indi.device.camera.FrameType -import kotlin.math.roundToInt - -data class CalibrationGroupKey( - @JvmField val type: FrameType, - @JvmField val filter: String?, - @JvmField val width: Int, - @JvmField val height: Int, - @JvmField val binX: Int, - @JvmField val binY: Int, - @JvmField val exposureTime: Long, - @JvmField val temperature: Int, - @JvmField val gain: Double, -) { - - companion object { - - @JvmStatic - fun from(frame: CalibrationFrameEntity) = CalibrationGroupKey( - frame.type, frame.filter?.ifBlank { null }, - frame.width, frame.height, - frame.binX, frame.binY, frame.exposureTime, - frame.temperature.roundToInt(), frame.gain, - ) - } -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt index 97769f311..57384a107 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt @@ -13,6 +13,7 @@ class CameraSerializer(private val capturesPath: Path) : StdSerializer(C override fun serialize(value: Camera, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("type", value.type.name) gen.writeStringField("sender", value.sender.id) gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) @@ -78,6 +79,7 @@ class CameraSerializer(private val capturesPath: Path) : StdSerializer(C private fun JsonGenerator.writeMainOrGuideHead(camera: Camera, fieldName: String) { writeObjectFieldStart(fieldName) + writeStringField("type", camera.type.name) writeStringField("id", camera.id) writeStringField("name", camera.name) writeStringField("sender", camera.sender.id) diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt index ab76e2024..9aeb365e3 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserSerializer.kt @@ -11,6 +11,7 @@ class FocuserSerializer : StdSerializer(Focuser::class.java) { override fun serialize(value: Focuser, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("type", value.type.name) gen.writeStringField("sender", value.sender.id) gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) diff --git a/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt b/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt index 5ed33f323..0c2b9eb10 100644 --- a/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt +++ b/api/src/main/kotlin/nebulosa/api/framing/FramingService.kt @@ -1,22 +1,32 @@ package nebulosa.api.framing +import com.fasterxml.jackson.databind.ObjectMapper import nebulosa.fits.fits import nebulosa.hips2fits.FormatOutputType import nebulosa.hips2fits.Hips2FitsService +import nebulosa.hips2fits.HipsSurvey import nebulosa.image.Image import nebulosa.io.transferAndCloseOutput import nebulosa.log.loggerFor import nebulosa.math.Angle import nebulosa.platesolver.PlateSolution +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.Resource import org.springframework.stereotype.Service import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.outputStream @Service -class FramingService(private val hips2FitsService: Hips2FitsService) { +class FramingService( + private val hips2FitsService: Hips2FitsService, + private val objectMapper: ObjectMapper, +) { - val availableHipsSurveys by lazy { hips2FitsService.availableSurveys().execute().body()!!.sorted() } + @Value("classpath:HIPS_SURVEYS.json") + private lateinit var hipsSurveysResource: Resource + + val availableHipsSurveys by lazy { hipsSurveysResource.inputStream.use { objectMapper.readValue(it, Array::class.java) }.sorted() } @Synchronized fun frame( diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputSerializer.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputSerializer.kt index 1d9c8d878..0e8232898 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputSerializer.kt @@ -11,6 +11,7 @@ class GuideOutputSerializer : StdSerializer(GuideOutput::class.java override fun serialize(value: GuideOutput, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("type", value.type.name) gen.writeStringField("sender", value.sender.id) gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt index ed2639a42..f0a54f1d4 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt @@ -56,11 +56,6 @@ class GuidingController(private val guidingService: GuidingService) { guidingService.settle(body) } - @GetMapping("settle") - fun settle(): SettleInfo { - return guidingService.settle() - } - @PutMapping("dither") fun dither( @RequestParam amount: Double, diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt index 4b5c38ba7..422f31d64 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt @@ -88,10 +88,6 @@ class GuidingService( preferenceService.putJSON("GUIDER.SETTLE_INFO", settle) } - fun settle(): SettleInfo { - return SettleInfo.from(guider) - } - fun dither(amount: Double, raOnly: Boolean = false) { if (phd2Client.isOpen) { guider.dither(amount, raOnly) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt b/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt index 5ec2eda2b..e899eaf11 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/SettleInfo.kt @@ -1,6 +1,5 @@ package nebulosa.api.guiding -import nebulosa.guiding.Guider import org.hibernate.validator.constraints.Range data class SettleInfo( @@ -12,9 +11,5 @@ data class SettleInfo( companion object { @JvmStatic val EMPTY = SettleInfo() - - @JvmStatic - fun from(guider: Guider) = - SettleInfo(guider.settleAmount, guider.settleTime.toSeconds(), guider.settleTimeout.toSeconds()) } } diff --git a/api/src/main/kotlin/nebulosa/api/image/AnnotateImageRequest.kt b/api/src/main/kotlin/nebulosa/api/image/AnnotateImageRequest.kt new file mode 100644 index 000000000..cc56de4e9 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/image/AnnotateImageRequest.kt @@ -0,0 +1,9 @@ +package nebulosa.api.image + +data class AnnotateImageRequest( + @JvmField val starsAndDSOs: Boolean = true, + @JvmField val minorPlanets: Boolean = false, + @JvmField val minorPlanetMagLimit: Double = 12.0, + @JvmField val includeMinorPlanetsWithoutMagnitude: Boolean = false, + @JvmField val useSimbad: Boolean = false, +) diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt index 6c6d29312..33d886df0 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt @@ -38,17 +38,12 @@ class ImageController( imageService.saveImageAs(path, save, camera) } - @GetMapping("annotations") + @PutMapping("annotations") fun annotationsOfImage( @RequestParam path: Path, - @RequestParam(required = false, defaultValue = "true") starsAndDSOs: Boolean, - @RequestParam(required = false, defaultValue = "false") minorPlanets: Boolean, - @RequestParam(required = false, defaultValue = "12.0") minorPlanetMagLimit: Double, - @RequestParam(required = false, defaultValue = "false") includeMinorPlanetsWithoutMagnitude: Boolean, - @RequestParam(required = false, defaultValue = "false") useSimbad: Boolean, + @RequestBody request: AnnotateImageRequest, @LocationParam location: Location? = null, - ) = imageService - .annotations(path, starsAndDSOs, minorPlanets, minorPlanetMagLimit, includeMinorPlanetsWithoutMagnitude, useSimbad, location) + ) = imageService.annotations(path, request, location) @GetMapping("coordinate-interpolation") fun coordinateInterpolation(@RequestParam path: Path): CoordinateInterpolation? { diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt b/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt index 9c41c8d78..757f7174d 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageInfo.kt @@ -14,9 +14,7 @@ data class ImageInfo( @JvmField val width: Int, @JvmField val height: Int, @JvmField val mono: Boolean, - @JvmField val stretchShadow: Float = 0.0f, - @JvmField val stretchHighlight: Float = 1.0f, - @JvmField val stretchMidtone: Float = 0.5f, + @JvmField val stretch: ImageTransformation.Stretch, @field:JsonSerialize(using = RightAscensionSerializer::class) @JvmField val rightAscension: Double? = null, @field:JsonSerialize(using = DeclinationSerializer::class) @JvmField val declination: Double? = null, @JvmField val solved: ImageSolved? = null, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index 69a022f33..ee4dc029f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -43,6 +43,7 @@ import java.util.* import java.util.concurrent.CompletableFuture import javax.imageio.ImageIO import kotlin.io.path.outputStream +import kotlin.math.roundToInt @Service class ImageService( @@ -65,7 +66,7 @@ class ImageService( private data class TransformedImage( @JvmField val image: Image, @JvmField val statistics: Statistics.Data? = null, - @JvmField val strectchParams: ScreenTransformFunction.Parameters? = null, + @JvmField val stretchParameters: ScreenTransformFunction.Parameters? = null, @JvmField val instrument: Camera? = null, ) @@ -85,12 +86,16 @@ class ImageService( output: HttpServletResponse, ) { val (image, calibration) = imageBucket.open(path, transformation.debayer, force = transformation.force) - val (transformedImage, statistics, stretchParams, instrument) = image!!.transform(true, transformation, ImageOperation.OPEN, camera) + val (transformedImage, statistics, stretchParameters, instrument) = image!!.transform(true, transformation, ImageOperation.OPEN, camera) val info = ImageInfo( path, transformedImage.width, transformedImage.height, transformedImage.mono, - stretchParams!!.shadow, stretchParams.highlight, stretchParams.midtone, + transformation.stretch.copy( + shadow = (stretchParameters!!.shadow * 65536f).roundToInt(), + highlight = (stretchParameters.highlight * 65536f).roundToInt(), + midtone = (stretchParameters.midtone * 65536f).roundToInt(), + ), transformedImage.header.rightAscension.takeIf { it.isFinite() }, transformedImage.header.declination.takeIf { it.isFinite() }, calibration?.let(::ImageSolved), @@ -98,10 +103,12 @@ class ImageService( transformedImage.header.bitpix, instrument, statistics, ) + val format = if (transformation.useJPEG) "jpeg" else "png" + output.addHeader(IMAGE_INFO_HEADER, objectMapper.writeValueAsString(info)) - output.contentType = "image/png" + output.contentType = "image/$format" - ImageIO.write(transformedImage, "PNG", output.outputStream) + ImageIO.write(transformedImage, format, output.outputStream) } private fun Image.transform( @@ -112,7 +119,7 @@ class ImageService( val (autoStretch, shadow, highlight, midtone) = transformation.stretch val scnrEnabled = transformation.scnr.channel != null - val manualStretch = shadow != 0f || highlight != 1f || midtone != 0.5f + val manualStretch = shadow != 0 || highlight != 65536 || midtone != 32768 val shouldBeTransformed = enabled && (autoStretch || manualStretch || transformation.mirrorHorizontal || transformation.mirrorVertical || transformation.invert @@ -166,13 +173,7 @@ class ImageService( } @Synchronized - fun annotations( - path: Path, - starsAndDSOs: Boolean, minorPlanets: Boolean, - minorPlanetMagLimit: Double = 12.0, includeMinorPlanetsWithoutMagnitude: Boolean = false, - useSimbad: Boolean = false, - location: Location? = null, - ): List { + fun annotations(path: Path, request: AnnotateImageRequest, location: Location? = null): List { val (image, calibration) = imageBucket.open(path) if (image == null || calibration.isNullOrEmpty() || !calibration.solved) { @@ -182,7 +183,7 @@ class ImageService( val wcs = try { WCS(calibration) } catch (e: WCSException) { - LOG.error("unable to generate annotations for image. path={}", path) + LOG.error("unable to generate annotations for image. path={}", path, e) return emptyList() } @@ -191,7 +192,7 @@ class ImageService( val dateTime = image.header.observationDate ?: LocalDateTime.now() - if (minorPlanets) { + if (request.minorPlanets) { threadPoolTaskExecutor.submitCompletable { val latitude = image.header.latitude ?: location?.latitude?.deg ?: 0.0 val longitude = image.header.longitude ?: location?.longitude?.deg ?: 0.0 @@ -204,7 +205,7 @@ class ImageService( val identifiedBody = smallBodyDatabaseService.identify( dateTime, latitude, longitude, 0.0, calibration.rightAscension, calibration.declination, calibration.radius, - minorPlanetMagLimit, !includeMinorPlanetsWithoutMagnitude, + request.minorPlanetMagLimit, !request.includeMinorPlanetsWithoutMagnitude, ).execute().body() ?: return@submitCompletable val radiusInSeconds = calibration.radius.toArcsec @@ -230,15 +231,15 @@ class ImageService( .also(tasks::add) } - if (starsAndDSOs) { + if (request.starsAndDSOs) { threadPoolTaskExecutor.submitCompletable { - LOG.info("finding star/DSO annotations. dateTime={}, useSimbad={}, calibration={}", dateTime, useSimbad, calibration) + LOG.info("finding star/DSO annotations. dateTime={}, useSimbad={}, calibration={}", dateTime, request.useSimbad, calibration) val rightAscension = calibration.rightAscension val declination = calibration.declination val radius = calibration.radius - val catalog = if (useSimbad) { + val catalog = if (request.useSimbad) { simbadService.search(SimbadSearch.Builder().region(rightAscension, declination, radius).build()) } else { simbadEntityRepository.search(null, null, rightAscension, declination, radius) @@ -308,7 +309,7 @@ class ImageService( val wcs = try { WCS(calibration) } catch (e: WCSException) { - LOG.error("unable to generate annotations for image. path={}", path) + LOG.error("unable to generate annotations for image. path={}", path, e) return null } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt b/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt index 42455df07..ed20ba5c6 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageTransformation.kt @@ -12,6 +12,7 @@ data class ImageTransformation( @JvmField val mirrorVertical: Boolean = false, @JvmField val invert: Boolean = false, @JvmField val scnr: SCNR = SCNR.EMPTY, + @JvmField val useJPEG: Boolean = false, ) { data class SCNR( @@ -28,9 +29,9 @@ data class ImageTransformation( data class Stretch( @JvmField val auto: Boolean = false, - @JvmField val shadow: Float = 0f, - @JvmField val highlight: Float = 0.5f, - @JvmField val midtone: Float = 1f, + @JvmField val shadow: Int = 0, + @JvmField val highlight: Int = 32768, + @JvmField val midtone: Int = 65536, ) { companion object { diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index da3b115e4..0ee2d8a3c 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -189,15 +189,15 @@ class MountController( @PutMapping("{mount}/remote-control/start") fun remoteControlStart( mount: Mount, - @RequestParam type: MountRemoteControlType, + @RequestParam protocol: MountRemoteControlProtocol, @RequestParam(required = false, defaultValue = "0.0.0.0") host: String, @RequestParam(required = false, defaultValue = "10001") @Valid @Positive port: Int, ) { - mountService.remoteControlStart(mount, type, host, port) + mountService.remoteControlStart(mount, protocol, host, port) } @PutMapping("{mount}/remote-control/stop") - fun remoteControlStart(mount: Mount, @RequestParam type: MountRemoteControlType) { + fun remoteControlStart(mount: Mount, @RequestParam type: MountRemoteControlProtocol) { mountService.remoteControlStop(mount, type) } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControl.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControl.kt index 15118bcb1..377afc9e0 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControl.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControl.kt @@ -18,7 +18,7 @@ import java.io.Closeable import java.time.OffsetDateTime data class MountRemoteControl( - @JvmField val type: MountRemoteControlType, + @JvmField val protocol: MountRemoteControlProtocol, @field:JsonIgnore @JvmField val server: NettyServer, @JvmField val mount: Mount, ) : StellariumMountHandler, LX200MountHandler, DeviceEventHandler, Closeable { @@ -28,8 +28,8 @@ data class MountRemoteControl( @JsonIgnore private val deviceProvider = mount.sender as? INDIDeviceProvider init { - if (server is StellariumProtocolServer) { - deviceProvider?.registerDeviceEventHandler(this) + if (server is StellariumProtocolServer && deviceProvider != null) { + deviceProvider.registerDeviceEventHandler(this) server.attachMountHandler(this) } else if (server is LX200ProtocolServer) { server.attachMountHandler(this) diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControlType.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControlProtocol.kt similarity index 59% rename from api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControlType.kt rename to api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControlProtocol.kt index 9641863eb..a49a51581 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControlType.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountRemoteControlProtocol.kt @@ -1,6 +1,6 @@ package nebulosa.api.mounts -enum class MountRemoteControlType { +enum class MountRemoteControlProtocol { STELLARIUM, LX200, } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt index 9fecfbc99..49a2b6dd1 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountSerializer.kt @@ -16,6 +16,7 @@ class MountSerializer : StdSerializer(Mount::class.java) { override fun serialize(value: Mount, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("type", value.type.name) gen.writeStringField("sender", value.sender.id) gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index 36d8b6069..876e6509e 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -288,19 +288,19 @@ class MountService( } } - fun remoteControlStart(mount: Mount, type: MountRemoteControlType, host: String, port: Int) { - check(remoteControls.none { it.mount === mount && it.type == type }) { "$type ${mount.name} Remote Control is already running" } + fun remoteControlStart(mount: Mount, protocol: MountRemoteControlProtocol, host: String, port: Int) { + check(remoteControls.none { it.mount === mount && it.protocol == protocol }) { "$protocol ${mount.name} Remote Control is already running" } - val server = if (type == MountRemoteControlType.STELLARIUM) StellariumProtocolServer(host, port) + val server = if (protocol == MountRemoteControlProtocol.STELLARIUM) StellariumProtocolServer(host, port) else LX200ProtocolServer(host, port) server.run() - remoteControls.add(MountRemoteControl(type, server, mount)) + remoteControls.add(MountRemoteControl(protocol, server, mount)) } - fun remoteControlStop(mount: Mount, type: MountRemoteControlType) { - val remoteControl = remoteControls.find { it.mount === mount && it.type == type } ?: return + fun remoteControlStop(mount: Mount, type: MountRemoteControlProtocol) { + val remoteControl = remoteControls.find { it.mount === mount && it.protocol == type } ?: return remoteControl.use(remoteControls::remove) } diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt index 2d4ed2456..5e44131bf 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt @@ -1,8 +1,6 @@ package nebulosa.api.platesolver import jakarta.validation.Valid -import nebulosa.api.beans.converters.angle.AngleParam -import nebulosa.math.Angle import org.springframework.web.bind.annotation.* import java.nio.file.Path @@ -13,17 +11,13 @@ class PlateSolverController( ) { @PutMapping("start") - fun startSolver( + fun start( @RequestParam path: Path, @RequestBody @Valid solver: PlateSolverRequest, - @RequestParam(required = false, defaultValue = "true") blind: Boolean, - @AngleParam(required = false, isHours = true, defaultValue = "0.0") centerRA: Angle, - @AngleParam(required = false, defaultValue = "0.0") centerDEC: Angle, - @AngleParam(required = false, defaultValue = "4.0") radius: Angle, - ) = plateSolverService.solveImage(solver, path, centerRA, centerDEC, if (blind) 0.0 else radius) + ) = plateSolverService.solveImage(solver, path) @PutMapping("stop") - fun stopSolver() { + fun stop() { plateSolverService.stopSolver() } } diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt index 7dba84bbe..af3219921 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt @@ -1,9 +1,14 @@ package nebulosa.api.platesolver +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import nebulosa.api.beans.converters.angle.DeclinationDeserializer +import nebulosa.api.beans.converters.angle.DegreesDeserializer +import nebulosa.api.beans.converters.angle.RightAscensionDeserializer import nebulosa.astap.platesolver.AstapPlateSolver import nebulosa.astrometrynet.nova.NovaAstrometryNetService import nebulosa.astrometrynet.platesolver.LocalAstrometryNetPlateSolver import nebulosa.astrometrynet.platesolver.NovaAstrometryNetPlateSolver +import nebulosa.math.Angle import nebulosa.pixinsight.platesolver.PixInsightPlateSolver import nebulosa.pixinsight.script.startPixInsight import nebulosa.siril.platesolver.SirilPlateSolver @@ -26,6 +31,10 @@ data class PlateSolverRequest( @field:DurationMin(seconds = 0) @field:DurationMax(minutes = 5) @field:DurationUnit(ChronoUnit.SECONDS) @JvmField val timeout: Duration = Duration.ZERO, @JvmField val slot: Int = 1, + @JvmField val blind: Boolean = true, + @JsonDeserialize(using = RightAscensionDeserializer::class) @JvmField val centerRA: Angle = 0.0, + @JsonDeserialize(using = DeclinationDeserializer::class) @JvmField val centerDEC: Angle = 0.0, + @JsonDeserialize(using = DegreesDeserializer::class) @JvmField val radius: Angle = if (blind) 0.0 else 4.0, ) { fun get(httpClient: OkHttpClient? = null) = with(this) { diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt index 890f40922..dfbe20d30 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverService.kt @@ -3,7 +3,6 @@ package nebulosa.api.platesolver import nebulosa.api.image.ImageBucket import nebulosa.api.image.ImageSolved import nebulosa.common.concurrency.cancel.CancellationToken -import nebulosa.math.Angle import okhttp3.OkHttpClient import org.springframework.stereotype.Service import java.nio.file.Path @@ -17,22 +16,18 @@ class PlateSolverService( private val cancellationToken = AtomicReference() - fun solveImage( - options: PlateSolverRequest, path: Path, - centerRA: Angle, centerDEC: Angle, radius: Angle, - ): ImageSolved { - val calibration = solve(options, path, centerRA, centerDEC, radius) + fun solveImage(request: PlateSolverRequest, path: Path): ImageSolved { + val calibration = solve(request, path) imageBucket.put(path, calibration) return ImageSolved(calibration) } @Synchronized - fun solve( - options: PlateSolverRequest, path: Path, - centerRA: Angle = 0.0, centerDEC: Angle = 0.0, radius: Angle = 0.0, - ) = CancellationToken().use { + fun solve(request: PlateSolverRequest, path: Path) = CancellationToken().use { cancellationToken.set(it) - options.get(httpClient).solve(path, null, centerRA, centerDEC, radius, options.downsampleFactor, options.timeout, it) + val solver = request.get(httpClient) + val radius = if (request.blind) 0.0 else request.radius + solver.solve(path, null, request.centerRA, request.centerDEC, radius, request.downsampleFactor, request.timeout, it) } fun stopSolver() { diff --git a/api/src/main/kotlin/nebulosa/api/rotators/RotatorSerializer.kt b/api/src/main/kotlin/nebulosa/api/rotators/RotatorSerializer.kt index 851b48561..fc654c7bb 100644 --- a/api/src/main/kotlin/nebulosa/api/rotators/RotatorSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/rotators/RotatorSerializer.kt @@ -11,6 +11,7 @@ class RotatorSerializer : StdSerializer(Rotator::class.java) { override fun serialize(value: Rotator, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("type", value.type.name) gen.writeStringField("sender", value.sender.id) gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceCaptureMode.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceCaptureMode.kt deleted file mode 100644 index d58b34657..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceCaptureMode.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.sequencer - -enum class SequenceCaptureMode { - INTERLEAVED, - - /** - * Processes each sequence entry in full before advancing to the next sequence entry. - */ - FULLY, -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerCaptureMode.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerCaptureMode.kt new file mode 100644 index 000000000..f865aae4a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerCaptureMode.kt @@ -0,0 +1,10 @@ +package nebulosa.api.sequencer + +enum class SequencerCaptureMode { + INTERLEAVED, + + /** + * Processes each sequence in full before advancing to the next sequence. + */ + FULLY, +} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt index 4ffc3620f..d8d0478dd 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt @@ -18,7 +18,7 @@ class SequencerController( fun start( camera: Camera, mount: Mount?, wheel: FilterWheel?, focuser: Focuser?, rotator: Rotator?, - @RequestBody @Valid body: SequencePlanRequest, + @RequestBody @Valid body: SequencerPlanRequest, ) = sequencerService.start(camera, body, mount, wheel, focuser, rotator) @PutMapping("{camera}/stop") diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt index e19e973dd..efd19c02c 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt @@ -54,7 +54,7 @@ class SequencerExecutor( } fun execute( - camera: Camera, request: SequencePlanRequest, + camera: Camera, request: SequencerPlanRequest, mount: Mount? = null, wheel: FilterWheel? = null, focuser: Focuser? = null, rotator: Rotator? = null, ) { check(camera.connected) { "${camera.name} Camera is not connected" } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencePlanRequest.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerPlanRequest.kt similarity index 84% rename from api/src/main/kotlin/nebulosa/api/sequencer/SequencePlanRequest.kt rename to api/src/main/kotlin/nebulosa/api/sequencer/SequencerPlanRequest.kt index 6aa50874a..3313353e8 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencePlanRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerPlanRequest.kt @@ -13,12 +13,12 @@ import java.nio.file.Path import java.time.Duration import java.time.temporal.ChronoUnit -data class SequencePlanRequest( +data class SequencerPlanRequest( @JvmField @field:DurationUnit(ChronoUnit.SECONDS) @field:DurationMin(seconds = 0) @field:DurationMax(minutes = 60) val initialDelay: Duration = Duration.ZERO, - @JvmField val captureMode: SequenceCaptureMode = SequenceCaptureMode.INTERLEAVED, + @JvmField val captureMode: SequencerCaptureMode = SequencerCaptureMode.INTERLEAVED, @JvmField val autoSubFolderMode: AutoSubFolderMode = AutoSubFolderMode.OFF, @JvmField val savePath: Path? = null, - @JvmField @field:NotEmpty val entries: List = emptyList(), + @JvmField @field:NotEmpty val sequences: List = emptyList(), @JvmField @field:Valid val dither: DitherAfterExposureRequest = DitherAfterExposureRequest.DISABLED, @JvmField @field:Valid val autoFocus: AutoFocusAfterConditions = AutoFocusAfterConditions.DISABLED, @JvmField val namingFormat: CameraCaptureNamingFormat = CameraCaptureNamingFormat.DEFAULT, diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt index b3b154d9f..1672a18d7 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt @@ -18,7 +18,7 @@ class SequencerService( @Synchronized fun start( - camera: Camera, request: SequencePlanRequest, + camera: Camera, request: SequencerPlanRequest, mount: Mount?, wheel: FilterWheel?, focuser: Focuser?, rotator: Rotator?, ) { val savePath = request.savePath diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt index 90c20fa8d..31f1d2c37 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt @@ -35,7 +35,7 @@ import java.util.concurrent.atomic.AtomicReference data class SequencerTask( @JvmField val camera: Camera, - @JvmField val plan: SequencePlanRequest, + @JvmField val plan: SequencerPlanRequest, @JvmField val guider: Guider? = null, @JvmField val mount: Mount? = null, @JvmField val wheel: FilterWheel? = null, @@ -45,7 +45,7 @@ data class SequencerTask( private val calibrationFrameProvider: CalibrationFrameProvider? = null, ) : AbstractTask(), Consumer, CameraEventAware, WheelEventAware, PauseListener { - private val usedEntries = plan.entries.filter { it.enabled } + private val sequences = plan.sequences.filter { it.enabled } private val initialDelayTask = DelayTask(plan.initialDelay) @@ -63,7 +63,7 @@ data class SequencerTask( @Volatile private var progress = 0.0 init { - require(usedEntries.isNotEmpty()) { "no entries found" } + require(sequences.isNotEmpty()) { "no entries found" } initialDelayTask.subscribe(this) tasks.add(initialDelayTask) @@ -75,12 +75,12 @@ data class SequencerTask( namingFormat = plan.namingFormat, ) - if (plan.captureMode == SequenceCaptureMode.FULLY || usedEntries.size == 1) { - for (i in usedEntries.indices) { - val request = mapRequest(usedEntries[i]) + if (plan.captureMode == SequencerCaptureMode.FULLY || sequences.size == 1) { + for (i in sequences.indices) { + val request = mapRequest(sequences[i]) // ID. - tasks.add(SequencerIdTask(plan.entries.indexOfFirst { it === usedEntries[i] } + 1)) + tasks.add(SequencerIdTask(plan.sequences.indexOfFirst { it === sequences[i] } + 1)) // FILTER WHEEL. request.wheelMoveTask()?.also(tasks::add) @@ -94,23 +94,22 @@ data class SequencerTask( cameraCaptureTask.subscribe(this) estimatedCaptureTime += cameraCaptureTask.estimatedCaptureTime - tasks.add(SequenceCaptureModeCameraCaptureTask(cameraCaptureTask, SequenceCaptureMode.FULLY, i)) + tasks.add(SequenceCaptureModeCameraCaptureTask(cameraCaptureTask, SequencerCaptureMode.FULLY, i)) } } else { - val sequenceIdTasks = usedEntries.map { req -> SequencerIdTask(plan.entries.indexOfFirst { it === req } + 1) } - val requests = usedEntries.map { mapRequest(it) } - val cameraCaptureTasks = requests - .mapIndexed { i, req -> - val task = CameraCaptureTask( - camera, req, guider, - i > 0, executor, calibrationFrameProvider, - mount, wheel, focuser, rotator - ) - - SequenceCaptureModeCameraCaptureTask(task, SequenceCaptureMode.INTERLEAVED, i) - } + val sequenceIdTasks = sequences.map { req -> SequencerIdTask(plan.sequences.indexOfFirst { it === req } + 1) } + val requests = sequences.map { mapRequest(it) } + val cameraCaptureTasks = requests.mapIndexed { i, req -> + val task = CameraCaptureTask( + camera, req, guider, + i > 0, executor, calibrationFrameProvider, + mount, wheel, focuser, rotator + ) + + SequenceCaptureModeCameraCaptureTask(task, SequencerCaptureMode.INTERLEAVED, i) + } val wheelMoveTasks = requests.map { it.wheelMoveTask() } - val count = IntArray(requests.size) { usedEntries[it].exposureAmount } + val count = IntArray(requests.size) { sequences[it].exposureAmount } for ((cameraCaptureTask) in cameraCaptureTasks) { cameraCaptureTask.subscribe(this) @@ -118,14 +117,14 @@ data class SequencerTask( } while (count.sum() > 0) { - for (i in usedEntries.indices) { + for (i in sequences.indices) { if (count[i] > 0) { tasks.add(sequenceIdTasks[i]) wheelMoveTasks[i]?.also(tasks::add) val task = cameraCaptureTasks[i] - if (count[i] == usedEntries[i].exposureAmount) { + if (count[i] == sequences[i].exposureAmount) { tasks.add(InitializeCameraCaptureTask(task.task)) } @@ -273,12 +272,12 @@ data class SequencerTask( private data class SequenceCaptureModeCameraCaptureTask( @JvmField val task: CameraCaptureTask, - @JvmField val mode: SequenceCaptureMode, + @JvmField val mode: SequencerCaptureMode, @JvmField val index: Int, ) : Task { override fun execute(cancellationToken: CancellationToken) { - if (mode == SequenceCaptureMode.FULLY) { + if (mode == SequencerCaptureMode.FULLY) { task.initialize(cancellationToken) task.executeInLoop(cancellationToken) task.finalize(cancellationToken) diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt index 698abf566..cbadc3fc4 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelSerializer.kt @@ -11,6 +11,7 @@ class WheelSerializer : StdSerializer(FilterWheel::class.java) { override fun serialize(value: FilterWheel, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField("type", value.type.name) gen.writeStringField("sender", value.sender.id) gen.writeStringField("id", value.id) gen.writeStringField("name", value.name) diff --git a/api/src/main/resources/HIPS_SURVEYS.json b/api/src/main/resources/HIPS_SURVEYS.json new file mode 100644 index 000000000..c9334cace --- /dev/null +++ b/api/src/main/resources/HIPS_SURVEYS.json @@ -0,0 +1,118 @@ +[ +{ "ID":"CDS/P/2MASS/H", "hips_doi":"10.26093/cds/aladin/2thy-66", "creator_did":"ivo://CDS/P/2MASS/H", "hips_initial_ra":"266.40499479", "hips_initial_dec":"-28.936173970", "hips_initial_fov":"58.63230142835039", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_skyval_method":"SKYVAL", "hips_skyval_value":"-0.5 100.0 -3146.3114318847656 13218.762664794922", "hips_overlay":"mean", "hips_hierarchy":"median", "hips_creator":"Oberto A. (CDS)", "hips_copyright":"CNRS/Unistra", "hips_version":"1.4", "hips_release_date":"2021-02-23T18:05Z", "hips_frame":"equatorial", "hips_order":"9", "hips_order_min":"0", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"-0.5 100", "moc_access_url":"http://alasky.u-strasbg.fr/2MASS/H/Moc.fits", "hips_status":"public master clonableOnce", "obs_title":"2MASS H (1.66um)", "obs_collection":"The Two Micron All Sky Survey - H band (2MASS H)", "obs_description":"2MASS has uniformly scanned the entire sky in three near-infrared bands to detect and characterize point sources brighter than about 1 mJy in each band, with signal-to-noise ratio (SNR) greater than 10, using a pixel size of 2.0\". This has achieved an 80,000-fold improvement in sensitivity relative to earlier surveys. 2MASS used two highly-automated 1.3-m telescopes, one at Mt. Hopkins, AZ, and one at CTIO, Chile. Each telescope was equipped with a three-channel camera, each channel consisting of a 256x256 array of HgCdTe detectors, capable of observing the sky simultaneously at J (1.25 microns), H (1.65 microns), and Ks (2.17 microns). The University of Massachusetts (UMass) was responsible for the overall management of the project, and for developing the infrared cameras and on-site computing systems at both facilities. The Infrared Processing and Analysis Center (IPAC) is responsible for all data processing through the Production Pipeline, and construction and distribution of the data products. Funding is provided primarily by NASA and the NSF", "obs_copyright_url":"http://www.ipac.caltech.edu/2mass/", "obs_ack":"University of Massachusetts & IPAC/Caltech", "bib_reference":"2006AJ....131.1163S", "bib_reference_url":"http://simbad.u-strasbg.fr/simbad/sim-ref?bibcode=2006AJ....131.1163S", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "em_min":"1.525E-6", "em_max":"1.798E-6", "hips_data_range":"-3146 13219", "prov_progenitor":"IPAC/NASA", "client_category":"Image/Infrared/2MASS", "hips_builder":"Aladin/HipsGen v11.023", "hips_pixel_scale":"2.236E-4", "s_pixel_scale":"2.777E-4", "moc_sky_fraction":"1", "hips_estsize":"4631264878", "hipsgen_date":"2020-05-29T23:00Z", "hipsgen_params":"in=2MASSh out=Hips-H creator_did=ivo://CDS-test/P/2MASS/H -f \"hips_pixel_cut=-0.5 100 log\" skyval=SKYVAL \"fitskeys=ORDATE SCANNO SCANDIR\" maxthread=64 hips_frame=equatorial TILES PNG DETAILS", "hips_creation_date":"2013-05-06T20:36Z", "hips_service_url":"https://alasky.cds.unistra.fr/2MASS/H", "hips_progenitor_url":"https://alasky.cds.unistra.fr/2MASS/H/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/2MASS/H", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/2MASS/H", "hips_status_2":"public mirror unclonable", "hips_tile_format_2":"jpeg", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"266.40499479", "obs_initial_dec":"-28.936173970", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288569682"}, +{ "ID":"CDS/P/2MASS/J", "hips_doi":"10.26093/cds/aladin/3ntd-6fa", "creator_did":"ivo://CDS/P/2MASS/J", "hips_initial_ra":"266.40499479", "hips_initial_dec":"-28.936173970", "hips_initial_fov":"58.63230142835039", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_skyval_method":"SKYVAL", "hips_skyval_value":"-0.5 100.0 -357.66149139404297 1421.6846084594727", "hips_overlay":"mean", "hips_hierarchy":[ "median", "mean"], "hips_creator":"Oberto A. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"2MASS J (1.23um)", "hips_builder":[ "Aladin/HipsGen v11.023", "Aladin/HipsGen v11.023"], "hips_version":"1.4", "hips_creation_date":"2014-02-11T11:28Z", "hips_release_date":"2021-02-24T06:06Z", "hips_frame":"equatorial", "hips_order":"9", "hips_order_min":"0", "hips_tile_width":"512", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/2MASS/J/Moc.fits", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"-0.5 40", "hips_data_range":"-357.7 1422", "obs_collection":"The Two Micron All Sky Survey - J band (2MASS J)", "obs_description":"2MASS has uniformly scanned the entire sky in three near-infrared bands to detect and characterize point sources brighter than about 1 mJy in each band, with signal-to-noise ratio (SNR) greater than 10, using a pixel size of 2.0\". This has achieved an 80,000-fold improvement in sensitivity relative to earlier surveys. 2MASS used two highly-automated 1.3-m telescopes, one at Mt. Hopkins, AZ, and one at CTIO, Chile. Each telescope was equipped with a three-channel camera, each channel consisting of a 256x256 array of HgCdTe detectors, capable of observing the sky simultaneously at J (1.25 microns), H (1.65 microns), and Ks (2.17 microns). The University of Massachusetts (UMass) was responsible for the overall management of the project, and for developing the infrared cameras and on-site computing systems at both facilities. The Infrared Processing and Analysis Center (IPAC) is responsible for all data processing through the Production Pipeline, and construction and distribution of the data products. Funding is provided primarily by NASA and the NSF", "obs_copyright_url":"http://www.ipac.caltech.edu/2mass/", "obs_ack":"University of Massachusetts & IPAC/Caltech", "bib_reference":"2006AJ....131.1163S", "bib_reference_url":"http://simbad.u-strasbg.fr/simbad/sim-ref?bibcode=2006AJ....131.1163S", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "em_min":"1.147E-6", "em_max":"1.323E-6", "prov_progenitor":"IPAC/NASA", "client_category":"Image/Infrared/2MASS", "hips_pixel_scale":"2.236E-4", "s_pixel_scale":"2.777E-4", "moc_sky_fraction":"1", "hips_estsize":"4593684487", "hipsgen_date":[ "2020-06-16T12:47Z", "2020-06-22T09:53Z"], "hipsgen_params":"in=2MASSj out=Hips-J creator_did=ivo://CDS-test/P/2MASS/J -f \"hips_pixel_cut=-0.5 40 log\" fading=true skyval=SKYVAL maxthread=64 hips_frame=equatorial JPEG", "hips_service_url":"https://alasky.cds.unistra.fr/2MASS/J", "hips_progenitor_url":"https://alasky.cds.unistra.fr/2MASS/J/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/2MASS/J", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/2MASS/J", "hips_status_2":"public mirror unclonable", "hips_tile_format_2":"jpeg", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"266.40499479", "obs_initial_dec":"-28.936173970", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288568394"}, +{ "ID":"CDS/P/2MASS/K", "hips_doi":"10.26093/cds/aladin/2ea3-abw", "creator_did":"ivo://CDS/P/2MASS/K", "hips_initial_ra":"266.40499479", "hips_initial_dec":"-28.936173970", "hips_initial_fov":"58.63230142835039", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_skyval_method":"SKYVAL", "hips_skyval_value":"-0.5 100.0 -2660.7283935546875 9046.068969726562", "hips_overlay":"mean", "hips_hierarchy":[ "median", "mean"], "hips_creator":"Oberto A. (CDS)", "hips_copyright":"CNRS/Unistra", "hips_version":"1.4", "hips_release_date":"2021-02-23T01:29Z", "hips_frame":"equatorial", "hips_order":"9", "hips_order_min":"0", "hips_tile_width":"512", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/2MASS/K/Moc.fits", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"-0.5 40", "hips_data_range":"-2661 9046", "obs_title":"2MASS K (2.16um)", "obs_collection":"The Two Micron All Sky Survey - K band (2MASS K)", "obs_description":"2MASS has uniformly scanned the entire sky in three near-infrared bands to detect and characterize point sources brighter than about 1 mJy in each band, with signal-to-noise ratio (SNR) greater than 10, using a pixel size of 2.0\". This has achieved an 80,000-fold improvement in sensitivity relative to earlier surveys. 2MASS used two highly-automated 1.3-m telescopes, one at Mt. Hopkins, AZ, and one at CTIO, Chile. Each telescope was equipped with a three-channel camera, each channel consisting of a 256x256 array of HgCdTe detectors, capable of observing the sky simultaneously at J (1.25 microns), H (1.65 microns), and Ks (2.17 microns). The University of Massachusetts (UMass) was responsible for the overall management of the project, and for developing the infrared cameras and on-site computing systems at both facilities. The Infrared Processing and Analysis Center (IPAC) is responsible for all data processing through the Production Pipeline, and construction and distribution of the data products. Funding is provided primarily by NASA and the NSF", "obs_copyright_url":"http://www.ipac.caltech.edu/2mass/", "obs_ack":"University of Massachusetts & IPAC/Caltech", "bib_reference":"2006AJ....131.1163S", "bib_reference_url":"http://simbad.u-strasbg.fr/simbad/sim-ref?bibcode=2006AJ....131.1163S", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "em_min":"2.015E-6", "em_max":"2.303E-6", "prov_progenitor":"IPAC/NASA", "client_category":"Image/Infrared/2MASS", "hips_builder":"Aladin/HipsGen v11.023", "hips_pixel_scale":"2.236E-4", "s_pixel_scale":"2.777E-4", "moc_sky_fraction":"1", "hips_estsize":"4706425658", "hips_creation_date":"2013-01-14T09:45Z", "hipsgen_date":"2020-08-04T10:56Z", "hipsgen_params":"cache=CACHE-TODEL-K cacheRemoveOnExit=true in=2MASSk out=Hips-K3 creator_did=ivo://CDS-test/P/2MASS/K \"hips_pixel_cut=-0.5 40 log\" maxthread=64 -f hips_frame=equatorial skyval=SKYVAL JPEG", "hips_service_url":"https://alasky.cds.unistra.fr/2MASS/K", "hips_progenitor_url":"https://alasky.cds.unistra.fr/2MASS/K/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/2MASS/K", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/2MASS/K", "hips_status_2":"public mirror unclonable", "hips_tile_format_2":"jpeg", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"266.40499479", "obs_initial_dec":"-28.936173970", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288570974"}, +{ "ID":"CDS/P/2MASS/color", "hips_doi":"10.26093/cds/aladin/bzc8-nw", "creator_did":"ivo://CDS/P/2MASS/color", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"266.40499479", "hips_initial_dec":"-28.936173970", "obs_collection":"The Two Micron All Sky Survey - J-H-K bands (2MASS color)", "obs_title":"2MASS color J (1.23um), H (1.66um), K (2.16um)", "obs_description":"2MASS has uniformly scanned the entire sky in three near-infrared bands to detect and characterize point sources brighter than about 1 mJy in each band, with signal-to-noise ratio (SNR) greater than 10, using a pixel size of 2.0\". This has achieved an 80,000-fold improvement in sensitivity relative to earlier surveys. 2MASS used two highly-automated 1.3-m telescopes, one at Mt. Hopkins, AZ, and one at CTIO, Chile. Each telescope was equipped with a three-channel camera, each channel consisting of a 256x256 array of HgCdTe detectors, capable of observing the sky simultaneously at J (1.25 microns), H (1.65 microns), and Ks (2.17 microns). The University of Massachusetts (UMass) was responsible for the overall management of the project, and for developing the infrared cameras and on-site computing systems at both facilities. The Infrared Processing and Analysis Center (IPAC) is responsible for all data processing through the Production Pipeline, and construction and distribution of the data products. Funding is provided primarily by NASA and the NSF", "obs_copyright_url":"http://www.ipac.caltech.edu/2mass/", "hips_creator":"Oberto A. (CDS)", "client_application":[ "AladinLite", "AladinDesktop"], "client_sort_key":"04-001-00", "prov_progenitor":"IPAC/NASA", "client_category":"Image/Infrared/2MASS", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "em_min":"1.147E-6", "em_max":"2.303E-6", "hips_builder":"Aladin/HipsGen v11.023", "hips_version":"1.4", "hips_release_date":"2021-02-24T03:22Z", "hips_frame":"equatorial", "hips_order":"9", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"University of Massachusetts & IPAC/Caltech", "bib_reference":"2006AJ....131.1163S", "bib_reference_url":"http://cdsbib.u-strasbg.fr/cgi-bin/cdsbib?2006AJ....131.1163S", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "hips_pixel_scale":"2.236E-4", "dataproduct_type":"image", "hips_rgb_red":"2MASS K [-0.5 20.0 40.0 Log]", "hips_rgb_green":"2MASSh [1.0 20.0 100.0 Log]", "hips_rgb_blue":"2MASS J [-0.5 20.0 40.0 Log]", "moc_sky_fraction":"1", "hips_estsize":"75160788", "hipsgen_date":"2020-10-01T09:28Z", "hipsgen_params":"inRed=Hips-K2 inBlue=Hips-J2 inGreen=Hips-H out=Hips-Color5 creator_did=ivo://CDS-test/P/2MASS/Color skyval=SKYVAL \"fitskeys=ORDATE SCANNO SCANDIR\" maxthread=64 hips_frame=equatorial \"cmRed=-0.5 20 40 log\" \"cmBlue=-0.5 20 40 log\" \"cmGreen=1 20 100 log\" color=jpeg RGB", "hips_creation_date":"2012-02-24T12:43Z", "hips_tile_format":"jpeg", "hipsgen_date_1":"2020-10-12T12:49Z", "hipsgen_params_1":"inRed=Hips-K2 inBlue=Hips-J2 inGreen=Hips-H out=Hips-Color5 creator_did=ivo://CDS-test/P/2MASS/Color skyval=SKYVAL \"fitskeys=ORDATE SCANNO SCANDIR\" maxthread=64 hips_frame=equatorial \"cmRed=-0.5 20 40 log\" \"cmBlue=-0.5 20 40 log\" \"cmGreen=1 20 100 log\" color=jpeg RGB", "hips_hierarchy":"median", "dataproduct_subtype":"color", "hips_service_url":"https://alasky.cds.unistra.fr/2MASS/Color", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/2MASS/Color", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://casda.csiro.au/hips/2MASS/Color", "hips_status_2":"public mirror unclonable", "hips_service_url_3":"https://irsa.ipac.caltech.edu/data/hips/CDS/2MASS/Color", "hips_status_3":"public mirror unclonable", "hips_service_url_4":"https://healpix.ias.u-psud.fr/CDS_P_2MASS_color", "hips_status_4":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"266.40499479", "obs_initial_dec":"-28.936173970", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288896092"}, +{ "ID":"CDS/P/AKARI/FIS/Color", "hips_doi":"10.26093/cds/aladin/3rag-15t", "creator_did":"ivo://CDS/P/AKARI/FIS/Color", "obs_collection":"AKARI FIS Color", "obs_title":"AKARI FIS Color WideL (140um), WideS (90um), N60 (65um)", "obs_description":"AKARI (Previously known as ASTRO-F or IRIS - InfraRed Imaging Surveyor) is the second space mission for infrared astronomy in Japan. AKARI was developed by the members of JAXA/ISAS and collaborators. IRAS (Infrared Astronomical Satellite, launched in 1983 by the United Kingdom, the United States, and the Netherlands) carried out the first all-sky survey at infrared wavelengths and made a huge impact on astronomy. The AKARI mission was an ambitious plan to make an all-sky survey with much better sensitivity, spatial resolution and wider wavelength coverage than those of IRAS. All-sky survey obtained by the Far-Infrared Surveyor (FIS) onboard the AKARI satellite, at 65um (Color), 90 um (WIDE-S), 140um (WIDE-L),and 160um (N160). See http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/Doi_AKARI_FIR_AllSkySurvey.pdf.", "obs_ack":"University of Tokyo, ISAS/JAXA, Tohoku University, University of Tsukuba, RAL, and Open University", "obs_copyright":"ISAS/JAXA", "obs_copyright_url":"http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/", "client_application":[ "AladinLite", "AladinLite", "AladinDesktop"], "client_category":"Image/Infrared/AKARI-FIS", "client_sort_key":"04-05-00", "hips_release_date":"2019-05-05T04:59Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"png jpeg", "dataproduct_type":"image", "hips_rgb_red":"WideLHiPS [0.0 250.0 500.0 Log]", "hips_rgb_green":"WideSHiPS [-1.5 91.75 185.0 Log]", "hips_rgb_blue":"N60HiPS [-1.5 69.25 140.0 Log]", "moc_access_url":"http://alasky.u-strasbg.fr/AKARI-FIS/ColorLSN60/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"ISAS/JAXA", "bib_reference":"2015PASJ...67...50D", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2015PASJ...67...50D", "t_min":"53863", "t_max":"54338", "obs_regime":"Infrared", "em_min":"6.5E-5", "em_max":"0.00014", "hips_creation_date":"2015-01-09T14:17Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"110.0", "hips_initial_ra":"83.5553478", "hips_initial_dec":"9.0998893", "hips_order_min":"0", "dataproduct_subtype":"color", "moc_sky_fraction":"1", "hips_estsize":"367003", "hipsgen_date":"2019-05-05T04:59Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/AKARI-FIS/ColorLSN60 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AKARI-FIS/ColorLSN60", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AKARI-FIS/ColorLSN60", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://healpix.ias.u-psud.fr/CDS_P_AKARI_FIS_Color", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"5", "obs_initial_ra":"83.5553478", "obs_initial_dec":"9.0998893", "obs_initial_fov":"1.8322594196359496", "TIMESTAMP":"1721288716997"}, +{ "ID":"CDS/P/AKARI/FIS/N160", "hips_doi":"10.26093/cds/aladin/23kb-b0r", "creator_did":"ivo://CDS/P/AKARI/FIS/N160", "obs_collection":"AKARI FIS N160", "obs_title":"AKARI FIS N160 (160um)", "obs_description":"AKARI (Previously known as ASTRO-F or IRIS - InfraRed Imaging Surveyor) is the second space mission for infrared astronomy in Japan. AKARI was developed by the members of JAXA/ISAS and collaborators. IRAS (Infrared Astronomical Satellite, launched in 1983 by the United Kingdom, the United States, and the Netherlands) carried out the first all-sky survey at infrared wavelengths and made a huge impact on astronomy. The AKARI mission was an ambitious plan to make an all-sky survey with much better sensitivity, spatial resolution and wider wavelength coverage than those of IRAS. All-sky survey obtained by the Far-Infrared Surveyor (FIS) onboard the AKARI satellite, at 65um (Color), 90 um (WIDE-S), 140um (WIDE-L),and 160um (N160). See http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/Doi_AKARI_FIR_AllSkySurvey.pdf.", "obs_ack":"University of Tokyo, ISAS/JAXA, Tohoku University, University of Tsukuba, RAL and Open University", "obs_copyright":"ISAS/JAXA", "obs_copyright_url":"http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/", "client_category":"Image/Infrared/AKARI-FIS", "client_sort_key":"04-05-04", "hips_release_date":"2019-05-05T05:01Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"0 500", "hips_data_range":"-125.7 304.6", "moc_access_url":"http://alasky.u-strasbg.fr/AKARI-FIS/N160/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AKARI-FIS/N160/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"ISAS/JAXA", "bib_reference":"2015PASJ...67...50D", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2015PASJ...67...50D", "t_min":"53863", "t_max":"54338", "obs_regime":"Infrared", "em_min":"0.00016", "em_max":"0.00016", "hips_creation_date":"2015-01-08T14:34Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"110.0", "hips_initial_ra":"83.5553478", "hips_initial_dec":"9.0998893", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"17870688", "hipsgen_date":"2019-05-05T05:01Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/AKARI-FIS/N160 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AKARI-FIS/N160", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AKARI-FIS/N160", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"41", "moc_time_range":"1", "moc_order":"5", "obs_initial_ra":"83.5553478", "obs_initial_dec":"9.0998893", "obs_initial_fov":"1.8322594196359496", "TIMESTAMP":"1721288413290"}, +{ "ID":"CDS/P/AKARI/FIS/N60", "hips_doi":"10.26093/cds/aladin/2a4f-2bn", "creator_did":"ivo://CDS/P/AKARI/FIS/N60", "obs_collection":"AKARI FIS N60", "obs_title":"AKARI FIS N60 (65um)", "obs_description":"AKARI (Previously known as ASTRO-F or IRIS - InfraRed Imaging Surveyor) is the second space mission for infrared astronomy in Japan. AKARI was developed by the members of JAXA/ISAS and collaborators. IRAS (Infrared Astronomical Satellite, launched in 1983 by the United Kingdom, the United States, and the Netherlands) carried out the first all-sky survey at infrared wavelengths and made a huge impact on astronomy. The AKARI mission was an ambitious plan to make an all-sky survey with much better sensitivity, spatial resolution and wider wavelength coverage than those of IRAS. All-sky survey obtained by the Far-Infrared Surveyor (FIS) onboard the AKARI satellite, at 65um (Color), 90 um (WIDE-S), 140um (WIDE-L),and 160um (N160). See http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/Doi_AKARI_FIR_AllSkySurvey.pdf.", "obs_ack":"University of Tokyo, ISAS/JAXA, Tohoku University, University of Tsukuba, RAL and Open University", "obs_copyright":"ISAS/JAXA", "obs_copyright_url":"http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/", "client_category":"Image/Infrared/AKARI-FIS", "client_sort_key":"04-05-01", "hips_release_date":"2019-05-05T05:03Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-1.5 140", "hips_data_range":"-104.6 251", "moc_access_url":"http://alasky.u-strasbg.fr/AKARI-FIS/N60/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AKARI-FIS/N60/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"ISAS/JAXA", "bib_reference":"2015PASJ...67...50D", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2015PASJ...67...50D", "t_min":"53863", "t_max":"54338", "obs_regime":"Infrared", "em_min":"6.5E-5", "em_max":"6.5E-5", "hips_creation_date":"2015-01-08T16:04Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"110.0", "hips_initial_ra":"83.5553478", "hips_initial_dec":"9.0998893", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"17870688", "hipsgen_date":"2019-05-05T05:03Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/AKARI-FIS/N60 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AKARI-FIS/N60", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AKARI-FIS/N60", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"41", "moc_time_range":"1", "moc_order":"5", "obs_initial_ra":"83.5553478", "obs_initial_dec":"9.0998893", "obs_initial_fov":"1.8322594196359496", "TIMESTAMP":"1721288413358"}, +{ "ID":"CDS/P/AKARI/FIS/WideL", "hips_doi":"10.26093/cds/aladin/1tkx-knx", "creator_did":"ivo://CDS/P/AKARI/FIS/WideL", "obs_collection":"AKARI FIS WideL", "obs_title":"AKARI FIS WideL (140um)", "obs_description":"AKARI (Previously known as ASTRO-F or IRIS - InfraRed Imaging Surveyor) is the second space mission for infrared astronomy in Japan. AKARI was developed by the members of JAXA/ISAS and collaborators. IRAS (Infrared Astronomical Satellite, launched in 1983 by the United Kingdom, the United States, and the Netherlands) carried out the first all-sky survey at infrared wavelengths and made a huge impact on astronomy. The AKARI mission was an ambitious plan to make an all-sky survey with much better sensitivity, spatial resolution and wider wavelength coverage than those of IRAS. All-sky survey obtained by the Far-Infrared Surveyor (FIS) onboard the AKARI satellite, at 65um (Color), 90 um (WIDE-S), 140um (WIDE-L),and 160um (N160). See http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/Doi_AKARI_FIR_AllSkySurvey.pdf.", "obs_ack":"University of Tokyo, ISAS/JAXA, Tohoku University, University of Tsukuba, RAL and Open University", "obs_copyright":"ISAS/JAXA", "obs_copyright_url":"http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/", "client_category":"Image/Infrared/AKARI-FIS", "client_sort_key":"04-05-03", "hips_release_date":"2019-05-05T05:05Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"0 500", "hips_data_range":"-38.17 154", "moc_access_url":"http://alasky.u-strasbg.fr/AKARI-FIS/WideL/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AKARI-FIS/WideL/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"ISAS/JAXA", "bib_reference":"2015PASJ...67...50D", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2015PASJ...67...50D", "t_min":"53863", "t_max":"54338", "obs_regime":"Infrared", "em_min":"0.00014", "em_max":"0.00014", "hips_creation_date":"2015-01-08T15:53Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"110.0", "hips_initial_ra":"83.5553478", "hips_initial_dec":"9.0998893", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"17870688", "hipsgen_date":"2019-05-05T05:05Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/AKARI-FIS/WideL UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AKARI-FIS/WideL", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AKARI-FIS/WideL", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"41", "moc_time_range":"1", "moc_order":"5", "obs_initial_ra":"83.5553478", "obs_initial_dec":"9.0998893", "obs_initial_fov":"1.8322594196359496", "TIMESTAMP":"1721288413410"}, +{ "ID":"CDS/P/AKARI/FIS/WideS", "hips_doi":"10.26093/cds/aladin/3w3d-gt8", "creator_did":"ivo://CDS/P/AKARI/FIS/WideS", "obs_collection":"AKARI FIS WideS", "obs_title":"AKARI FIS WideS (90um)", "obs_description":"AKARI (Previously known as ASTRO-F or IRIS - InfraRed Imaging Surveyor) is the second space mission for infrared astronomy in Japan. AKARI was developed by the members of JAXA/ISAS and collaborators. IRAS (Infrared Astronomical Satellite, launched in 1983 by the United Kingdom, the United States, and the Netherlands) carried out the first all-sky survey at infrared wavelengths and made a huge impact on astronomy. The AKARI mission was an ambitious plan to make an all-sky survey with much better sensitivity, spatial resolution and wider wavelength coverage than those of IRAS. All-sky survey obtained by the Far-Infrared Surveyor (FIS) onboard the AKARI satellite, at 65um (Color), 90 um (WIDE-S), 140um (WIDE-L),and 160um (N160). See http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/Doi_AKARI_FIR_AllSkySurvey.pdf.", "obs_ack":"University of Tokyo, ISAS/JAXA, Tohoku University, University of Tsukuba, RAL and Open University", "obs_copyright":"ISAS/JAXA", "obs_copyright_url":"http://www.ir.isas.jaxa.jp/AKARI/Archive/Images/FIS_AllSkyMap/", "client_category":"Image/Infrared/AKARI-FIS", "client_sort_key":"04-05-02", "hips_release_date":"2019-05-05T05:07Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-1.5 140", "hips_data_range":"-12.06 38.42", "moc_access_url":"http://alasky.u-strasbg.fr/AKARI-FIS/WideS/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AKARI-FIS/WideS/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"ISAS/JAXA", "bib_reference":"2015PASJ...67...50D", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2015PASJ...67...50D", "t_min":"53863", "t_max":"54338", "obs_regime":"Infrared", "em_min":"9e-5", "em_max":"9e-5", "hips_creation_date":"2015-01-08T15:45Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"110.0", "hips_initial_ra":"83.5553478", "hips_initial_dec":"9.0998893", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"17870688", "hipsgen_date":"2019-05-05T05:07Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/AKARI-FIS/WideS UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AKARI-FIS/WideS", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AKARI-FIS/WideS", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"41", "moc_time_range":"1", "moc_order":"5", "obs_initial_ra":"83.5553478", "obs_initial_dec":"9.0998893", "obs_initial_fov":"1.8322594196359496", "TIMESTAMP":"1721288413462"}, +{ "ID":"CDS/P/CO", "hips_doi":"10.26093/cds/aladin/1zd9-hss", "creator_did":"ivo://CDS/P/CO", "obs_collection":"CO composite survey", "obs_title":"CO composite survey", "obs_description":"This survey contains data from the composite CO map constructed by Dame, Hartmann and Thaddeus (2001) from 37 individual surveys of the Galaxy in the CO (1-0) line. Due to the composite nature of the map and processing used to render a uniform S/N appearance, the user is cautioned that angular resolution and sensitivity vary across the map. Survey data are limited to Galactic latitudes |b|<32 deg., with roughly half of that area containing observations.To create this file, the velocity-integrated brightness temperature map, W(CO), was obtained from the CfA Millimeter Wave Group website and then interpolated onto a HEALPix grid with Nside=512.", "obs_copyright":"Composite map by Dame et al (2001,ApJ,547,792), - HEALPixed by LAMBDA", "obs_copyright_url":"http://lambda.gsfc.nasa.gov/product/foreground/fg_WCO_get.cfm", "client_category":"Image/Gas-lines/CO", "client_sort_key":"06-08-01", "hips_creation_date":"2011-02-14T12:00Z", "hips_release_date":"2019-05-05T06:00Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_width":"64", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/CO/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.\"", "prov_progenitor":"HEASARC/LAMBDA", "bib_reference":"2001ApJ...547..792D", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2001ApJ...547..792D", "t_min":"44239", "t_max":"51544", "obs_regime":"Radio", "em_min":"2.604173540653e-3", "em_max":"2.609386874402e-3", "hips_pixel_scale":"0.01431", "hips_initial_fov":"158.63230142835039", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:00Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/CO UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/CO", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/CO", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414282"}, +{ "ID":"CDS/P/CO-Dame-2022", "hips_initial_fov":"360.0", "hips_initial_ra":"266.415009", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/CO-Dame-2022", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"Dame and Thaddeus 2022 Velocity Integrated CO Map", "obs_collection":"Dame CO maps", "obs_description":"Data published by Dame & Thaddeus (2022) significantly extends the Galactic plane CO survey of Dame, Hartmann & Thaddeus (2001) with complementary coverage of the entire northern sky (? > -17�). The coverage extension was carried out with the same telescope as was used for the plane survey, the CfA 1.2 m, and perfectly meshes with its irregular boundaries in latitude. The merged survey is released by the authors in the form of CO line spectral data cubes. To create the LAMBDA map, the moment-masked data cube for the combined survey was integrated over +/- 36 km/sec, the full range over which significant emission is detected (see Figure 8 of Dame & Thaddeus 2022; the combined survey does not include the high-velocity observations available from the Dame, Hartmann & Thaddeus 2001 data). The velocity integrated brightness temperature map was then interpolated from the original rectilinear projection onto pixel centers appropriate for HEALPix Nside=256. The LAMBDA map is in units of K-km/sec, and a mask is provided indicating those portions of the sky that are unobserved.", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.\"", "prov_progenitor":"HEASARC/LAMBDA", "bib_reference":"2022ApJS..262....5D", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2022ApJS..262....5D", "obs_copyright":"Composite map by Dame & Thaddeus (2022 ApJS, 262, 5) - HEALPixed by LAMBDA", "obs_copyright_url":"https://lambda.gsfc.nasa.gov/product/foreground/fg_wco_dt2022_get.html", "client_category":"Image/Gas-lines/CO", "obs_regime":"Radio", "em_min":"2.604173540653e-3", "em_max":"2.609386874402e-3", "hips_builder":"Aladin/HipsGen v12.119", "hips_version":"1.4", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"32", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-1 210", "hips_data_range":"-382.9 1137", "hips_pixel_scale":"0.01431", "dataproduct_type":"image", "hipsgen_date":"2024-07-17T09:58Z", "hipsgen_params":"in=lambda_Wco_DT2022.fits out=./hips id=CDS/P/CO-Dame-2022 MAPTILES", "hips_release_date":"2024-07-17T10:09Z", "hips_creation_date":"2024-07-17T09:58Z", "moc_sky_fraction":"1", "hipsgen_date_1":"2024-07-17T10:00Z", "hipsgen_params_1":"in=lambda_Wco_DT2022.fits out=./hips id=CDS/P/CO-Dame-2022 blank=0 MAPTILES", "hipsgen_date_2":"2024-07-17T10:09Z", "hipsgen_params_2":"in=lambda_Wco_DT2022.fits out=./hips id=CDS/P/CO-Dame-2022 blank=0 \"pixelCut=-1 210 log\" PNG", "hips_service_url":"https://alasky.cds.unistra.fr/CO-maps/CDS_P_CO-Dame-2022", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/CO-maps/CDS_P_CO-Dame-2022", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"3", "obs_initial_ra":"266.415009", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288498778"}, +{ "ID":"CDS/P/DM/flux-Bp/I/345/gaia2", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/DM/flux-Bp/I/345/gaia2", "hips_creator":"Boch T. (CDS)", "obs_title":"Bp flux map for table I/345/gaia2 (Gaia DR2)", "obs_regime":"Optical", "em_min":"3.28045e-07", "em_max":"6.71903e-07", "prov_did":"ivo://CDS/I/345/gaia2", "client_category":"Ancillary/GaiaDR2", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T08:57Z", "hips_frame":"equatorial", "hips_order":"4", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0 100000", "hips_data_range":"-2.521E8 7.565E8", "hips_pixel_scale":"0.007157", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"4486027", "hipsgen_date":"2018-04-17T07:37Z", "hipsgen_params":"in=density-maps/param-weighted/gaia-DR2-Bp-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-Bp -creator_did=ivo://CDS/P/DM/flux-Bp/I/345/out \"-hips_pixel_cut=0 200000 sqrt\" -method=mean MAPTILES JPEG", "hips_creation_date":"2018-04-17T07:37Z", "hipsgen_date_1":"2018-04-17T07:38Z", "hipsgen_params_1":"in=density-maps/param-weighted/gaia-DR2-Bp-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-Bp -creator_did=ivo://CDS/P/DM/flux-Bp/I/345/out \"-hips_pixel_cut=0 200000 sqrt\" -method=mean MAPTILES JPEG", "hipsgen_date_2":"2018-04-17T08:02Z", "hipsgen_params_2":"in=density-maps/param-weighted/gaia-DR2-Bp-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-Bp -creator_did=ivo://CDS/P/DM/flux-Bp/I/345/out \"-hips_pixel_cut=0 100000 sqrt\" -method=mean MAPTILES JPEG", "hipsgen_date_3":"2018-04-17T08:03Z", "hipsgen_params_3":"in=density-maps/param-weighted/gaia-DR2-Bp-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-Bp -creator_did=ivo://CDS/P/DM/flux-Bp/I/345/out \"-hips_pixel_cut=0 100000 sqrt\" -method=mean MAPTILES JPEG", "hipsgen_date_4":"2019-05-21T08:57Z", "hipsgen_params_4":"out=/asd-volumes/sc1-asd-volume6/ancillary/GaiaDR2/Bp-flux-map UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR2/Bp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR2/Bp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"9", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288478430"}, +{ "ID":"CDS/P/DM/flux-Bp/I/350/gaiaedr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/DM/flux-Bp/I/350/gaiaedr3", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"Bp flux map for table I/350/gaiaedr3 (Gaia EDR3)", "obs_collection":"Gaia", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://dx.doi.org/10.5270/esa-1ugzkg7", "obs_regime":"Optical", "em_min":"3.29283e-7", "em_max":"6.73811e-7", "data_bunit":"count.s-1.sr-1", "client_category":"Ancillary/GaiaEDR3", "prov_did":"ivo://CDS/I/350/gaiaedr3", "hips_builder":"Aladin/HipsGen v11.025", "hips_version":"1.4", "hips_release_date":"2020-11-27T12:53Z", "hips_frame":"equatorial", "hips_order":"7", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_creation_date":"2019-05-21T08:57Z", "hips_pixel_scale":"8.946E-4", "hips_estsize":"287105287", "hipsgen_date":"2020-11-26T10:02Z", "hipsgen_params":"out=hips-flux/save/bp-flux TREE", "hipsgen_date_1":"2020-11-26T10:52Z", "hipsgen_params_1":"out=hips-flux/bp-flux TREE", "hipsgen_date_2":"2020-11-26T10:56Z", "hipsgen_params_2":"out=hips-flux/bp-flux TREE", "hipsgen_date_3":"2020-11-26T11:01Z", "hipsgen_params_3":"out=hips-flux/bp-flux TREE", "hipsgen_date_4":"2020-11-26T11:17Z", "hipsgen_params_4":"out=hips-flux/bp-flux TREE", "hipsgen_date_5":"2020-11-26T11:21Z", "hipsgen_params_5":"out=hips-flux/bp-flux TREE", "hipsgen_date_6":"2020-11-26T11:22Z", "hipsgen_params_6":"out=hips-flux/bp-flux TREE", "hipsgen_date_7":"2020-11-26T12:56Z", "hipsgen_params_7":"out=hips-flux/bp-flux TREE", "hipsgen_date_8":"2020-11-27T07:42Z", "hipsgen_params_8":"out=hips-flux/bp-flux TREE", "hips_pixel_cut":"0 2.0E7", "hipsgen_date_9":"2020-11-27T12:53Z", "hipsgen_params_9":"out=hips-flux/bp-flux \"pixelCut=0 2e7 log\" JPEG", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaEDR3/Bp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaEDR3/Bp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288487630"}, +{ "ID":"CDS/P/DM/flux-Bp/I/355/gaiadr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/DM/flux-Bp/I/355/gaiadr3", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"Bp flux map for table I/355/gaiadr3 (Gaia DR3)", "obs_collection":"Gaia", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://doi.org/10.5270/esa-qa4lep3", "obs_regime":"Optical", "em_min":"3.29283e-7", "em_max":"6.73811e-7", "data_bunit":"count.s-1.sr-1", "client_category":"Ancillary/GaiaDR3", "prov_did":"ivo://CDS/I/355/gaiadr3", "hips_builder":"Aladin/HipsGen v11.071", "hips_version":"1.4", "hips_release_date":"2022-06-16T09:03Z", "hips_frame":"equatorial", "hips_order":"7", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_creation_date":"2022-06-12T22:59Z", "hips_pixel_scale":"8.946E-4", "hips_pixel_cut":"0 2.0E7", "hips_estsize":"287105287", "hipsgen_date":"2022-06-14T14:17Z", "hipsgen_params":"\"hips_pixel_cut=0 2e7 asinh\" out=bp-flux \"hips_pixel_cut=0 20000000 asinh\" JPEG", "hipsgen_date_1":"2022-06-15T06:54Z", "hipsgen_params_1":"\"hips_pixel_cut=0 2e7 asinh\" out=bp-flux \"hips_pixel_cut=0 20000000 asinh\" method=MEAN JPEG -f", "hipsgen_date_2":"2022-06-16T06:43Z", "hipsgen_params_2":"\"hips_pixel_cut=0 2e7 asinh\" out=bp-flux \"hips_pixel_cut=0 20000000 asinh\" method=MEAN JPEG -f", "hipsgen_date_3":"2022-06-16T07:05Z", "hipsgen_params_3":"\"hips_pixel_cut=0 2e7 asinh\" out=bp-flux \"hips_pixel_cut=0 20000000 asinh\" method=MEAN JPEG -f", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR3/Bp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR3/Bp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288487962"}, +{ "ID":"CDS/P/DM/flux-G/I/345/gaia2", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/DM/flux-G/I/345/gaia2", "hips_creator":"Boch T. (CDS)", "obs_title":"G flux map for table I/345/gaia2 (Gaia DR2)", "obs_regime":"Optical", "em_min":"3.30660e-07", "em_max":"10.45065e-07", "prov_did":"ivo://CDS/I/345/gaia2", "client_category":"Ancillary/GaiaDR2", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T08:57Z", "hips_frame":"equatorial", "hips_order":"4", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0 200000", "hips_data_range":"-3.760E8 1.128E9", "hips_pixel_scale":"0.007157", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"4486027", "hipsgen_date":"2018-04-17T07:05Z", "hipsgen_params":"in=density-maps/param-weighted/gaia-DR2-G-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-G -creator_did=ivo://CDS/P/DM/flux-G/I/345/out \"-hips_pixel_cut=0 200000 sqrt\" -method=mean MAPTILES JPEG", "hips_creation_date":"2018-04-17T07:05Z", "hipsgen_date_1":"2018-04-17T07:08Z", "hipsgen_params_1":"in=density-maps/param-weighted/gaia-DR2-G-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-G -creator_did=ivo://CDS/P/DM/flux-G/I/345/out \"-hips_pixel_cut=0 200000 sqrt\" -method=mean MAPTILES JPEG", "hipsgen_date_2":"2019-05-21T08:57Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/ancillary/GaiaDR2/G-flux-map UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR2/G-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR2/G-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"9", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288478570"}, +{ "ID":"CDS/P/DM/flux-G/I/350/gaiaedr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/DM/flux-G/I/350/gaiaedr3", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"G flux map for table I/350/gaiaedr3 (Gaia EDR3)", "obs_collection":"Gaia", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://dx.doi.org/10.5270/esa-1ugzkg7", "obs_regime":"Optical", "em_min":"3.29402e-7", "em_max":"10.30196e-7", "data_bunit":"count.s-1.sr-1", "client_category":"Ancillary/GaiaEDR3", "prov_did":"ivo://CDS/I/350/gaiaedr3", "hips_builder":"Aladin/HipsGen v11.025", "hips_version":"1.4", "hips_release_date":"2020-11-27T15:11Z", "hips_frame":"equatorial", "hips_order":"7", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_creation_date":"2019-05-21T08:57Z", "hips_pixel_scale":"8.946E-4", "hips_estsize":"287105287", "hipsgen_date":"2020-11-26T10:02Z", "hipsgen_params":"out=hips-flux/save/bp-flux TREE", "hipsgen_date_1":"2020-11-26T10:52Z", "hipsgen_params_1":"out=hips-flux/bp-flux TREE", "hipsgen_date_2":"2020-11-26T10:56Z", "hipsgen_params_2":"out=hips-flux/bp-flux TREE", "hipsgen_date_3":"2020-11-26T11:01Z", "hipsgen_params_3":"out=hips-flux/bp-flux TREE", "hipsgen_date_4":"2020-11-26T11:17Z", "hipsgen_params_4":"out=hips-flux/bp-flux TREE", "hipsgen_date_5":"2020-11-26T11:21Z", "hipsgen_params_5":"out=hips-flux/bp-flux TREE", "hipsgen_date_6":"2020-11-26T11:22Z", "hipsgen_params_6":"out=hips-flux/bp-flux TREE", "hipsgen_date_7":"2020-11-26T12:56Z", "hipsgen_params_7":"out=hips-flux/bp-flux TREE", "hipsgen_date_8":"2020-11-26T15:09Z", "hipsgen_params_8":"out=hips-flux/g-flux TREE", "hipsgen_date_9":"2020-11-27T10:18Z", "hipsgen_params_9":"out=hips-flux/g-flux TREE", "hips_pixel_cut":"0 2.0E7", "hipsgen_date_10":"2020-11-27T15:11Z", "hipsgen_params_10":"out=hips-flux/g-flux \"pixelCut=0 2e7 log\" JPEG", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaEDR3/G-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaEDR3/G-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288487742"}, +{ "ID":"CDS/P/DM/flux-G/I/355/gaiadr3", "creator_did":"ivo://CDS/P/DM/flux-G/I/355/gaiadr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"G flux map for table I/355/gaiadr3 (Gaia DR3)", "obs_collection":"Gaia", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://doi.org/10.5270/esa-qa4lep3", "obs_regime":"Optical", "em_min":"3.29402e-7", "em_max":"10.30196e-7", "data_bunit":"count.s-1.sr-1", "client_category":"Ancillary/GaiaDR3", "prov_did":"ivo://CDS/I/355/gaiadr3", "hips_builder":"Aladin/HipsGen v11.071", "hips_version":"1.4", "hips_release_date":"2022-06-16T14:31Z", "hips_frame":"equatorial", "hips_order":"7", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "hips_pixel_scale":"8.946E-4", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"287105287", "hipsgen_date":"2022-06-12T22:59Z", "hipsgen_params":"out=hips-flux/bp-flux TREE", "hips_creation_date":"2022-06-12T22:59Z", "hipsgen_date_1":"2022-06-13T01:55Z", "hipsgen_params_1":"out=hips-flux/g-flux TREE", "hipsgen_date_2":"2022-06-16T12:26Z", "hipsgen_params_2":"\"hips_pixel_cut=0 2e7 asinh\" out=g-flux \"hips_pixel_cut=0 20000000 asinh\" method=MEAN JPEG -f", "hips_pixel_cut":"0 2.0E7", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR3/G-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR3/G-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288488014"}, +{ "ID":"CDS/P/DM/flux-Rp/I/345/gaia2", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/DM/flux-Rp/I/345/gaia2", "hips_creator":"Boch T. (CDS)", "obs_title":"Rp flux map for table I/345/gaia2 (Gaia DR2)", "obs_regime":"Optical", "em_min":"6.25497e-07", "em_max":"10.60579e-07", "prov_did":"ivo://CDS/I/345/gaia2", "client_category":"Ancillary/GaiaDR2", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T08:57Z", "hips_frame":"equatorial", "hips_order":"4", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0 100000", "hips_data_range":"-1.383E8 4.150E8", "hips_pixel_scale":"0.007157", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"4486027", "hipsgen_date":"2018-04-17T08:17Z", "hipsgen_params":"in=density-maps/param-weighted/gaia-DR2-Rp-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-Rp -creator_did=ivo://CDS/P/DM/flux-Rp/I/345/out \"-hips_pixel_cut=0 100000 sqrt\" -method=mean MAPTILES JPEG", "hips_creation_date":"2018-04-17T08:17Z", "hipsgen_date_1":"2018-04-17T08:18Z", "hipsgen_params_1":"in=density-maps/param-weighted/gaia-DR2-Rp-map.hpx out=density-maps/param-weighted/hips-gaia-DR2-flux-Rp -creator_did=ivo://CDS/P/DM/flux-Rp/I/345/out \"-hips_pixel_cut=0 100000 sqrt\" -method=mean MAPTILES JPEG", "hipsgen_date_2":"2019-05-21T08:57Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/ancillary/GaiaDR2/Rp-flux-map UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR2/Rp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR2/Rp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"9", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288478642"}, +{ "ID":"CDS/P/DM/flux-Rp/I/350/gaiaedr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/DM/flux-Rp/I/350/gaiaedr3", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"Rp flux map for table I/350/gaiaedr3 (Gaia EDR3)", "obs_collection":"Gaia", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://dx.doi.org/10.5270/esa-1ugzkg7", "obs_regime":"Optical", "em_min":"6.19605e-7", "em_max":"10.42296e-7", "data_bunit":"count.s-1.sr-1", "client_category":"Ancillary/GaiaEDR3", "prov_did":"ivo://CDS/I/350/gaiaedr3", "hips_builder":"Aladin/HipsGen v11.025", "hips_version":"1.4", "hips_release_date":"2020-11-27T14:11Z", "hips_frame":"equatorial", "hips_order":"7", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_creation_date":"2019-05-21T08:57Z", "hips_pixel_scale":"8.946E-4", "hips_estsize":"287105287", "hipsgen_date":"2020-11-26T10:02Z", "hipsgen_params":"out=hips-flux/save/bp-flux TREE", "hipsgen_date_1":"2020-11-26T10:52Z", "hipsgen_params_1":"out=hips-flux/bp-flux TREE", "hipsgen_date_2":"2020-11-26T10:56Z", "hipsgen_params_2":"out=hips-flux/bp-flux TREE", "hipsgen_date_3":"2020-11-26T11:01Z", "hipsgen_params_3":"out=hips-flux/bp-flux TREE", "hipsgen_date_4":"2020-11-26T11:17Z", "hipsgen_params_4":"out=hips-flux/bp-flux TREE", "hipsgen_date_5":"2020-11-26T11:21Z", "hipsgen_params_5":"out=hips-flux/bp-flux TREE", "hipsgen_date_6":"2020-11-26T11:22Z", "hipsgen_params_6":"out=hips-flux/bp-flux TREE", "hipsgen_date_7":"2020-11-26T12:56Z", "hipsgen_params_7":"out=hips-flux/bp-flux TREE", "hipsgen_date_8":"2020-11-26T14:07Z", "hipsgen_params_8":"out=hips-flux/rp-flux TREE", "hipsgen_date_9":"2020-11-26T14:17Z", "hipsgen_params_9":"out=hips-flux/rp-flux mode=keeptile TREE", "hipsgen_date_10":"2020-11-26T14:45Z", "hipsgen_params_10":"out=hips-flux/rp-flux TREE", "hipsgen_date_11":"2020-11-27T08:52Z", "hipsgen_params_11":"out=hips-flux/rp-flux TREE", "hips_pixel_cut":"0 2.0E7", "hipsgen_date_12":"2020-11-27T14:11Z", "hipsgen_params_12":"out=hips-flux/rp-flux \"pixelCut=0 2e7 log\" JPEG", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaEDR3/Rp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaEDR3/Rp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288487794"}, +{ "ID":"CDS/P/DM/flux-Rp/I/355/gaiadr3", "creator_did":"ivo://CDS/P/DM/flux-Rp/I/355/gaiadr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"Rp flux map for table I/355/gaiadr3 (Gaia DR3)", "obs_collection":"Gaia", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://doi.org/10.5270/esa-qa4lep3", "obs_regime":"Optical", "em_min":"6.19605e-7", "em_max":"10.42296e-7", "data_bunit":"count.s-1.sr-1", "client_category":"Ancillary/GaiaDR3", "prov_did":"ivo://CDS/I/355/gaiadr3", "hips_builder":"Aladin/HipsGen v11.071", "hips_version":"1.4", "hips_release_date":"2022-06-16T12:25Z", "hips_frame":"galactic", "hips_order":"7", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"-32", "hips_pixel_scale":"8.946E-4", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"287105287", "hipsgen_date":"2022-06-12T22:59Z", "hipsgen_params":"out=hips-flux/bp-flux TREE", "hips_creation_date":"2022-06-12T22:59Z", "hipsgen_date_1":"2022-06-13T00:27Z", "hipsgen_params_1":"out=hips-flux/rp-flux TREE", "hipsgen_date_2":"2022-06-16T09:08Z", "hipsgen_params_2":"\"hips_pixel_cut=0 2e7 asinh\" out=rp-flux \"hips_pixel_cut=0 20000000 asinh\" method=MEAN JPEG -f", "hips_pixel_cut":"0 2.0E7", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR3/Rp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR3/Rp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288488070"}, +{ "ID":"CDS/P/DM/flux-color-Rp-G-Bp/I/345/gaia2", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/DM/flux-color-Rp-G-Bp/I/345/gaia2", "hips_creator":"Boch T. (CDS)", "obs_title":"Color flux map for I/345/gaia2 (Gaia DR2)", "obs_regime":"Optical", "prov_did":"ivo://CDS/I/345/gaia2", "client_category":"Ancillary/GaiaDR2", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T08:57Z", "hips_frame":"equatorial", "hips_order":"4", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg", "hips_pixel_scale":"0.007157", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"73407", "hips_creation_date":"2018-04-17T07:37Z", "hips_pixel_cut":"0 100000", "hips_data_range":"-2.521E8 7.565E8", "hipsgen_date":"2018-04-19T12:45Z", "hipsgen_params":"out=density-maps/param-weighted/test -order=3 allsky", "hipsgen_date_1":"2018-04-19T12:46Z", "hipsgen_params_1":"out=density-maps/param-weighted/test color=jpeg -order=3 allsky", "hips_order_min":"0", "dataproduct_subtype":"color", "hipsgen_date_2":"2019-05-21T08:57Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/ancillary/GaiaDR2/color-Rp-G-Bp-flux-map UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR2/color-Rp-G-Bp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR2/color-Rp-G-Bp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"9", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288478498"}, +{ "ID":"CDS/P/DM/flux-color-Rp-G-Bp/I/350/gaiaedr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/DM/flux-color-Rp-G-Bp/I/350/gaiaedr3", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"Color flux map for I/350/gaiaedr3 (Gaia EDR3)", "obs_collection":"Gaia", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://dx.doi.org/10.5270/esa-1ugzkg7", "obs_regime":"Optical", "em_min":"3.29283e-7", "em_max":"10.42296e-7", "client_category":"Ancillary/GaiaEDR3", "prov_did":"ivo://CDS/I/350/gaiaedr3", "hips_version":"1.4", "hips_frame":"equatorial", "hips_order":"7", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_creation_date":"2020-11-26T16:37Z", "hips_release_date":"2020-11-27T12:53Z", "hips_order_min":"0", "dataproduct_subtype":"color", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaEDR3/color-Rp-G-Bp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaEDR3/color-Rp-G-Bp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288487682"}, +{ "ID":"CDS/P/DM/flux-color-Rp-G-Bp/I/355/gaiadr3", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/DM/flux-color-Rp-G-Bp/I/355/gaiadr3", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"Color flux map for I/355/gaiadr3 (Gaia DR3)", "obs_collection":"Gaia", "obs_description":"This color flux map has been created from Gaia DR3 individual sources data (fluxes in Rp=red, G=green, Bp=blue filters)", "obs_copyright":"ESA/Gaia mission/DPAC", "obs_copyright_url":"https://doi.org/10.5270/esa-qa4lep3", "obs_regime":"Optical", "em_min":"3.29283e-7", "em_max":"10.42296e-7", "client_category":"Ancillary/GaiaDR3", "prov_did":"ivo://CDS/I/355/gaiadr3", "hips_version":"1.4", "hips_frame":"equatorial", "hips_order":"7", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_creation_date":"2022-06-12T16:37Z", "hips_release_date":"2022-06-16T09:03Z", "hips_order_min":"0", "dataproduct_subtype":"color", "hips_service_url":"https://alasky.cds.unistra.fr/ancillary/GaiaDR3/color-Rp-G-Bp-flux-map", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/ancillary/GaiaDR3/color-Rp-G-Bp-flux-map", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288487906"}, +{ "ID":"CDS/P/DSS2/NIR", "hips_doi":"10.26093/cds/aladin/3hh0-7dk", "creator_did":"ivo://CDS/P/DSS2/NIR", "obs_collection":"DSS2 NIR (XI+IS)", "obs_title":"DSS2 NIR (XI+IS)", "obs_description":"The Catalogs and Surveys Group of the Space Telescope Science Institute has digitized the photographic Sky survey plates from the Palomar and UK Schmidt telescopes to produce the \"Digitized Sky Survey\"(DSS). Each plate covers 6.5 x 6.5 degrees of the sky and have been digitized using a modified PDS microdensitometer. The DSS NIT HiPS is a combination of DSS2-XI and DSS2-IS. DSS2-XI north is the digitalization of the POSS-II N (1987-2002 - filter: IV-N +RG9) from Caltech, DSS2-IS south is the digitalization of the SERC-IS (1990-2002 - filter: IV-N +RG175). The all-sky HEALPix resampling has been done by the CDS with the help of CADC.", "obs_ack":"The Digitized Sky Surveys were produced at the Space Telescope Science Institute under U.S. Government grant NAG W-2166. The images of these surveys are based on photographic data obtained using the Oschin Schmidt Telescope on Palomar Mountain and the UK Schmidt Telescope. The plates were processed into the present compressed digital form with the permission of these institutions. The National Geographic Society - Palomar Observatory Sky Atlas (POSS-I) was made by the California Institute of Technology with grants from the National Geographic Society. The Second Palomar Observatory Sky Survey (POSS-II) was made by the California Institute of Technology with funds from the National Science Foundation, the National Geographic Society, the Sloan Foundation, the Samuel Oschin Foundation, and the Eastman Kodak Corporation. The Oschin Schmidt Telescope is operated by the California Institute of Technology and Palomar Observatory. The UK Schmidt Telescope was operated by the Royal Observatory Edinburgh, with funding from the UK Science and Engineering Research Council (later the UK Particle Physics and Astronomy Research Council), until 1988 June, and thereafter by the Anglo-Australian Observatory. The blue plates of the southern Sky Atlas and its Equatorial Extension (together known as the SERC-J), as well as the Equatorial Red (ER), and the Second Epoch [red] Survey (SES) were all taken with the UK Schmidt. Supplemental funding for sky-survey work at the ST ScI is provided by the European Southern Observatory.", "prov_progenitor":"STScI", "bib_reference":"1996ASPC..101...88L", "bib_reference_url":"http://cdsads.u-strasbg.fr/abs/1996ASPC..101...88L", "obs_copyright":"Digitized Sky Survey - STScI/NASA, Healpixed by CDS", "obs_copyright_url":"http://archive.stsci.edu/dss/copyright.html", "client_category":"Image/Optical/DSS", "client_sort_key":"03-01-03a", "t_min":"46796", "t_max":"52620", "em_min":"7E-7", "em_max":"9.5E-7", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-20T15:03Z", "hips_creator":"Fernique P. (CDS)", "hips_frame":"equatorial", "hips_order":"9", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_hierarchy":"mean", "hips_pixel_scale":"2.236E-4", "dataproduct_type":"image", "moc_sky_fraction":"0.9955", "hips_creation_date":"2015-09-08T12:14Z", "hips_tile_format":"jpeg fits", "hips_pixel_bitpix":"16", "data_pixel_bitpix":"16", "hips_pixel_cut":"1249 10940", "hips_data_range":"-11612 34834", "hips_initial_ra":"200.0641577", "hips_initial_dec":"-62.0757716", "hips_initial_fov":"30.0", "s_pixel_scale":"2.798E-4", "obs_regime":"Optical", "hips_copyright":"CNRS/Unistra", "hips_order_min":"0", "hips_estsize":"2328304001", "hipsgen_date":"2019-05-20T15:03Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume6/DSS/DSS2-NIR UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/DSS/DSS2-NIR", "hips_progenitor_url":"https://alasky.cds.unistra.fr/DSS/DSS2-NIR/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/DSS/DSS2-NIR", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"41", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"200.0641577", "obs_initial_dec":"-62.0757716", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288455126"}, +{ "ID":"CDS/P/DSS2/blue", "hips_doi":"10.26093/cds/aladin/2szd-bms", "creator_did":"ivo://CDS/P/DSS2/blue", "obs_collection":"DSS2 Blue (XJ+S)", "obs_title":"DSS2 Blue (XJ+S)", "obs_description":"The Catalogs and Surveys Group of the Space Telescope Science Institute has digitized the photographic Sky survey plates from the Palomar and UK Schmidt telescopes to produce the \"Digitized Sky Survey\"(DSS). Each plate covers 6.5 x 6.5 degrees of the sky and have been\ndigitized using a modified PDS microdensitometer. The DSS blue HiPS is a combination of DSS2-XJ and DSS2-S. DSS2-XJ north is the digitalization of the POSS-II J (1987-1998 - 0.491um) from Caltech,DSS2-S south is the digitalization of the SERC-J (1975-1987 - 0.468um) and SERC-EJ (1979-1988 - 0.468um) from ROE.The all-sky HEALPix resampling has been done by the CDS with the help of CADC.", "obs_copyright":"Digitized Sky Survey - STScI/NASA, Healpixed by CDS", "obs_copyright_url":"http://archive.stsci.edu/dss/copyright.html", "client_category":"Image/Optical/DSS", "client_sort_key":"03-01-03", "hips_release_date":"2019-06-17T07:58Z", "hips_builder":"Aladin/HipsGen v10.125", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"9", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"4286 19959", "hips_data_range":"-4736 29668", "client_application":[ "AladinLite", "AladinDesktop"], "moc_access_url":"http://alasky.u-strasbg.fr/DSS/DSS2-blue-XJ-S/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/DSS/DSS2-blue-XJ-S/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"The Digitized Sky Surveys were produced at the Space Telescope Science Institute under U.S. Government grant NAG W-2166. The images of these surveys are based on photographic data obtained using the Oschin Schmidt Telescope on Palomar Mountain and the UK Schmidt Telescope. The plates were processed into the present compressed digital form with the permission of these institutions. The National Geographic Society - Palomar Observatory Sky Atlas (POSS-I) was made by the California Institute of Technology with grants from the National Geographic Society. The Second Palomar Observatory Sky Survey (POSS-II) was made by the California Institute of Technology with funds from the National Science Foundation, the National Geographic Society, the Sloan Foundation, the Samuel Oschin Foundation, and the Eastman Kodak Corporation. The Oschin Schmidt Telescope is operated by the California Institute of Technology and Palomar Observatory. The UK Schmidt Telescope was operated by the Royal Observatory Edinburgh, with funding from the UK Science and Engineering Research Council (later the UK Particle Physics and Astronomy Research Council), until 1988 June, and thereafter by the Anglo-Australian Observatory. The blue plates of the southern Sky Atlas and its Equatorial Extension (together known as the SERC-J), as well as the Equatorial Red (ER), and the Second Epoch [red] Survey (SES) were all taken with the UK Schmidt. Supplemental funding for sky-survey work at the ST ScI is provided by the European Southern Observatory.", "prov_progenitor":"STScI", "bib_reference":"1996ASPC..101...88L", "bib_reference_url":"http://cdsads.u-strasbg.fr/abs/1996ASPC..101...88L", "t_min":"42413", "t_max":"50814", "obs_regime":"Optical", "em_min":"4.68e-7", "em_max":"4.91e-7", "hips_creation_date":"2015-02-07T11:42Z", "hips_pixel_scale":"2.236E-4", "hips_initial_fov":"23.0", "hips_initial_ra":"271.0198457", "hips_initial_dec":"-24.3603897", "hips_order_min":"0", "hips_pixel_bitpix":"16", "moc_sky_fraction":"0.9972", "hips_estsize":"2331148605", "hipsgen_date":"2019-06-17T07:58Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume6/DSS/DSS2-blue-XJ-S UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/DSS/DSS2-blue-XJ-S", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/DSS/DSS2-blue-XJ-S", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"41", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"271.0198457", "obs_initial_dec":"-24.3603897", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288414390"}, +{ "ID":"CDS/P/DSS2/color", "hips_doi":"10.26093/cds/aladin/ht9n-7r", "creator_did":"ivo://CDS/P/DSS2/color", "obs_collection":"DSS colored", "obs_title":"DSS colored", "obs_description":"Color composition generated by CDS. This HiPS survey is based on 2 others HiPS surveys, respectively DSS2-red and DSS2-blue HiPS, both of them directly generated from original scanned plates downloaded from STScI site. The red component has been built from POSS-II F, AAO-SES,SR and SERC-ER plates. The blue component has been build from POSS-II J and SERC-J,EJ. The green component is based on the mean of other components. Three missing plates from red survey (253, 260, 359) has been replaced by pixels from the DSSColor STScI jpeg survey. The 11 missing blue plates (mainly in galactic plane) have not been replaced (only red component).", "obs_copyright":"Digitized Sky Survey - STScI/NASA, Colored & Healpixed by CDS", "obs_copyright_url":"http://archive.stsci.edu/dss/copyright.html", "client_category":"Image/Optical/DSS", "client_sort_key":"03-00", "hips_builder":"Aladin/HipsGen v10.123", "hips_creation_date":"2010-05-01T19:05Z", "hips_release_date":"2019-05-07T10:55Z", "hips_creator":"Oberto A. (CDS) , Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"9", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg", "dataproduct_type":"image", "client_application":[ "AladinLite", "AladinDesktop"], "moc_access_url":"http://alasky.u-strasbg.fr/DSS/DSSColor/Moc.fits", "hips_status":"public master clonableOnce", "hips_rgb_red":"DSS2Merged [1488.0 8488.8125 14666.0 Linear]", "hips_rgb_blue":"DSS2-blue-XJ-S [4286.0 12122.5 19959.0 Linear]", "hips_hierarchy":"median", "hips_pixel_scale":"2.236E-4", "hips_initial_ra":"085.30251", "hips_initial_dec":"-02.25468", "hips_initial_fov":"2", "moc_sky_fraction":"1", "hips_copyright":"CNRS/Unistra", "obs_ack":"The Digitized Sky Surveys were produced at the Space Telescope Science Institute under U.S. Government grant NAG W-2166. The images of these surveys are based on photographic data obtained using the Oschin Schmidt Telescope on Palomar Mountain and the UK Schmidt Telescope. The plates were processed into the present compressed digital form with the permission of these institutions. The National Geographic Society - Palomar Observatory Sky Atlas (POSS-I) was made by the California Institute of Technology with grants from the National Geographic Society. The Second Palomar Observatory Sky Survey (POSS-II) was made by the California Institute of Technology with funds from the National Science Foundation, the National Geographic Society, the Sloan Foundation, the Samuel Oschin Foundation, and the Eastman Kodak Corporation. The Oschin Schmidt Telescope is operated by the California Institute of Technology and Palomar Observatory. The UK Schmidt Telescope was operated by the Royal Observatory Edinburgh, with funding from the UK Science and Engineering Research Council (later the UK Particle Physics and Astronomy Research Council), until 1988 June, and thereafter by the Anglo-Australian Observatory. The blue plates of the southern Sky Atlas and its Equatorial Extension (together known as the SERC-J), as well as the Equatorial Red (ER), and the Second Epoch [red] Survey (SES) were all taken with the UK Schmidt. Supplemental funding for sky-survey work at the ST ScI is provided by the European Southern Observatory.", "prov_progenitor":"STScI", "bib_reference":"1996ASPC..101...88L", "bib_reference_url":"http://cdsads.u-strasbg.fr/abs/1996ASPC..101...88L", "t_min":"42413", "t_max":"51179", "obs_regime":"Optical", "em_min":"4e-7", "em_max":"6e-7", "hips_order_min":"0", "dataproduct_subtype":"color", "hips_estsize":"37580398", "hipsgen_date":"2019-05-07T10:55Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume8/DSS/DSSColor UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/DSS/DSSColor", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/DSS/DSSColor", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://casda.csiro.au/hips/DSS2/color", "hips_status_2":"public mirror unclonable", "hips_service_url_3":"https://irsa.ipac.caltech.edu/data/hips/CDS/DSS2/color", "hips_status_3":"public mirror unclonable", "hips_service_url_4":"https://healpix.ias.u-psud.fr/CDS_P_DSS2_color", "hips_status_4":"public mirror unclonable", "hips_service_url_5":"http://skies.esac.esa.int/DSSColor", "hips_status_5":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"085.30251", "obs_initial_dec":"-02.25468", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288896984"}, +{ "ID":"CDS/P/DSS2/red", "hips_doi":"10.26093/cds/aladin/wenz-vg", "creator_did":"ivo://CDS/P/DSS2/red", "obs_collection":"DSS2 Red (F+R)", "obs_title":"DSS2 Red (F+R)", "obs_description":"The Catalogs and Surveys Group of the Space Telescope Science Institute has digitized the photographic Sky survey plates from the Palomar and UK Schmidt telescopes to produce the \"Digitized Sky Survey\" (DSS). Each plate covers 6.5 x 6.5 degrees of the sky and have been digitized using a modified PDS microdensitometer.\nDSS2F north is the digitalization of the POSS2/UKSTU Red survey (0.658um)\nDSS2R south is the digitalization of the AAO Red survey (0.64um)\nThe all-sky HEALPix resampling has been done by the CDS", "obs_copyright":"Digitized Sky Survey - STScI/NASA, Healpixed by CDS", "obs_copyright_url":"http://archive.stsci.edu/dss/copyright.html", "client_category":"Image/Optical/DSS", "client_sort_key":"03-01-02", "hips_creation_date":"2012-07-13T14:03Z", "hips_release_date":"2019-05-07T10:59Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"9", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"1000 10000", "dataproduct_type":"image", "client_application":[ "AladinLite", "AladinDesktop"], "moc_access_url":"http://alasky.u-strasbg.fr/DSS/DSS2Merged/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"The Digitized Sky Surveys were produced at the Space Telescope Science Institute under U.S. Government grant NAG W-2166. The images of these surveys are based on photographic data obtained using the Oschin Schmidt Telescope on Palomar Mountain and the UK Schmidt Telescope. The plates were processed into the present compressed digital form with the permission of these institutions. The National Geographic Society - Palomar Observatory Sky Atlas (POSS-I) was made by the California Institute of Technology with grants from the National Geographic Society. The Second Palomar Observatory Sky Survey (POSS-II) was made by the California Institute of Technology with funds from the National Science Foundation, the National Geographic Society, the Sloan Foundation, the Samuel Oschin Foundation, and the Eastman Kodak Corporation. The Oschin Schmidt Telescope is operated by the California Institute of Technology and Palomar Observatory. The UK Schmidt Telescope was operated by the Royal Observatory Edinburgh, with funding from the UK Science and Engineering Research Council (later the UK Particle Physics and Astronomy Research Council), until 1988 June, and thereafter by the Anglo-Australian Observatory. The blue plates of the southern Sky Atlas and its Equatorial Extension (together known as the SERC-J), as well as the Equatorial Red (ER), and the Second Epoch [red] Survey (SES) were all taken with the UK Schmidt. Supplemental funding for sky-survey work at the ST ScI is provided by the European Southern Observatory.", "prov_progenitor":"STScI", "bib_reference":"1996ASPC..101...88L", "bib_reference_url":"http://cdsads.u-strasbg.fr/abs/1996ASPC..101...88L", "t_min":"45700", "t_max":"51179", "obs_regime":"Optical", "em_min":"6.4e-7", "em_max":"6.58e-7", "hips_pixel_scale":"2.236E-4", "hips_initial_fov":"33.0", "hips_initial_ra":"82.9368880", "hips_initial_dec":"-3.3581890", "hips_order_min":"0", "hips_pixel_bitpix":"16", "moc_sky_fraction":"1", "hips_estsize":"2301246266", "hipsgen_date":"2019-05-07T10:59Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume8/DSS/DSS2Merged UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/DSS/DSS2Merged", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/DSS/DSS2Merged", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/DSS2/red", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"82.9368880", "obs_initial_dec":"-3.3581890", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288897892"}, +{ "ID":"CDS/P/EGRET/Dif/100-150", "hips_doi":"10.26093/cds/aladin/3k8t-1gp", "creator_did":"ivo://CDS/P/EGRET/Dif/100-150", "obs_collection":"Diffuse Gamma-ray EGRET maps - 100-150MeV", "obs_title":"EGRET Dif 100-150MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-04", "hips_release_date":"2019-05-05T06:00Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_100-150/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"8.2656e-15", "em_max":"1.2398e-14", "hips_creation_date":"2014-06-05T11:13Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:00Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_100-150 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_100-150", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_100-150", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414574"}, +{ "ID":"CDS/P/EGRET/Dif/1000-2000", "hips_doi":"10.26093/cds/aladin/3535-cdg", "creator_did":"ivo://CDS/P/EGRET/Dif/1000-2000", "obs_collection":"Diffuse Gamma-ray EGRET maps - 1000-2000MeV", "obs_title":"EGRET Dif 1000-2000MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-08", "hips_release_date":"2019-05-05T06:00Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_1000-2000/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"6.1992e-16", "em_max":"1.2398e-15", "hips_creation_date":"2014-06-05T11:18Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:00Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_1000-2000 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_1000-2000", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_1000-2000", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414630"}, +{ "ID":"CDS/P/EGRET/Dif/150-300", "hips_doi":"10.26093/cds/aladin/34b7-dwe", "creator_did":"ivo://CDS/P/EGRET/Dif/150-300", "obs_collection":"Diffuse Gamma-ray EGRET maps - 150-300MeV", "obs_title":"EGRET Dif 150-300MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-05", "hips_release_date":"2019-05-05T06:01Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_150-300/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"4.1328e-15", "em_max":"8.2656e-15", "hips_creation_date":"2014-06-05T11:14Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:01Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_150-300 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_150-300", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_150-300", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414682"}, +{ "ID":"CDS/P/EGRET/Dif/2000-4000", "hips_doi":"10.26093/cds/aladin/3bd9-mn9", "creator_did":"ivo://CDS/P/EGRET/Dif/2000-4000", "obs_collection":"Diffuse Gamma-ray EGRET maps - 2000-4000MeV", "obs_title":"EGRET Dif 2000-4000MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-09", "hips_release_date":"2019-05-05T06:01Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_2000-4000/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"3.0996e-16", "em_max":"6.1992e-16", "hips_creation_date":"2014-06-05T11:19Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:01Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_2000-4000 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_2000-4000", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_2000-4000", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414734"}, +{ "ID":"CDS/P/EGRET/Dif/30-50", "hips_doi":"10.26093/cds/aladin/2nbf-ypm", "creator_did":"ivo://CDS/P/EGRET/Dif/30-50", "obs_collection":"Diffuse Gamma-ray EGRET maps - 30-50MeV", "obs_title":"EGRET Dif 30-50MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-01", "hips_release_date":"2019-05-05T06:01Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_30-50/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"2.4797e-14", "em_max":"4.1328e-14", "hips_creation_date":"2014-06-05T11:10Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:01Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_30-50 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_30-50", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_30-50", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414794"}, +{ "ID":"CDS/P/EGRET/Dif/300-500", "hips_doi":"10.26093/cds/aladin/5prj-x6", "creator_did":"ivo://CDS/P/EGRET/Dif/300-500", "obs_collection":"Diffuse Gamma-ray EGRET maps - 300-500MeV", "obs_title":"EGRET Dif 300-500MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-06", "hips_release_date":"2019-05-05T06:01Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_300-500/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"2.4797e-15", "em_max":"4.1328e-15", "hips_creation_date":"2014-06-05T11:16Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:01Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_300-500 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_300-500", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_300-500", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414854"}, +{ "ID":"CDS/P/EGRET/Dif/4000-10000", "hips_doi":"10.26093/cds/aladin/2ffk-vw8", "creator_did":"ivo://CDS/P/EGRET/Dif/4000-10000", "obs_collection":"Diffuse Gamma-ray EGRET maps - 4000-10000MeV", "obs_title":"EGRET Dif 4000-10000MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-10", "hips_release_date":"2019-05-05T06:02Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_4000-10000/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"1.2398e-16", "em_max":"3.0996e-16", "hips_creation_date":"2014-06-05T11:19Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:02Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_4000-10000 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_4000-10000", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_4000-10000", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414922"}, +{ "ID":"CDS/P/EGRET/Dif/50-70", "hips_doi":"10.26093/cds/aladin/bw2v-fp", "creator_did":"ivo://CDS/P/EGRET/Dif/50-70", "obs_collection":"Diffuse Gamma-ray EGRET maps - 50-70MeV", "obs_title":"EGRET Dif 50-70MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-02", "hips_release_date":"2019-05-05T06:02Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_50-70/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"1.7712e-14", "em_max":"2.4797e-14", "hips_creation_date":"2014-06-05T11:09Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:02Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_50-70 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_50-70", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_50-70", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288414978"}, +{ "ID":"CDS/P/EGRET/Dif/500-1000", "hips_doi":"10.26093/cds/aladin/216g-av7", "creator_did":"ivo://CDS/P/EGRET/Dif/500-1000", "obs_collection":"Diffuse Gamma-ray EGRET maps - 500-1000MeV", "obs_title":"EGRET Dif 500-1000MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-07", "hips_release_date":"2019-05-05T06:02Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_500-1000/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"1.2398e-15", "em_max":"2.4797e-15", "hips_creation_date":"2014-06-05T11:17Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:02Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_500-1000 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_500-1000", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_500-1000", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415034"}, +{ "ID":"CDS/P/EGRET/Dif/70-100", "hips_doi":"10.26093/cds/aladin/3mnf-56w", "creator_did":"ivo://CDS/P/EGRET/Dif/70-100", "obs_collection":"Diffuse Gamma-ray EGRET maps - 70-100MeV", "obs_title":"EGRET Dif 70-100MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright_url":"ftp://legacy.gsfc.nasa.gov/compton/data/egret/diffuse_maps/README.fitsmaps.html", "client_category":"Image/Gamma-ray/EGRET/Diffuse", "client_sort_key":"00-02-01-03", "hips_release_date":"2019-05-05T06:03Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"1.731E-6 6.218E-5", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-dif/EGRET_dif_70-100/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "obs_copyright":"Compton Gamma Ray Observatory (CGRO)", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"1.2398e-14", "em_max":"1.7712e-14", "hips_creation_date":"2014-06-05T11:12Z", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:03Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-dif/EGRET_dif_70-100 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_70-100", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-dif/EGRET_dif_70-100", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415090"}, +{ "ID":"CDS/P/EGRET/inf100", "hips_doi":"10.26093/cds/aladin/35cp-apx", "creator_did":"ivo://CDS/P/EGRET/inf100", "obs_collection":"Gamma-ray EGRET maps - inf 100MeV", "obs_title":"EGRET inf 100MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright":[ "Distributed by SkyView/HEASARC - HEALPixed by CDS", "Compton Gamma Ray Observatory (CGRO)"], "client_category":"Image/Gamma-ray/EGRET", "client_sort_key":"00-02-00a", "hips_release_date":"2019-05-05T06:03Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 3.588E-4", "hips_data_range":"-2.037E-4 6.112E-4", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-inf100/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"1.2398e-14", "em_max":"4.1328e-14", "hips_creation_date":"2014-06-05T16:54Z", "hips_tile_width":"512", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:03Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-inf100 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-inf100", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-inf100", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415146"}, +{ "ID":"CDS/P/EGRET/sup100", "hips_doi":"10.26093/cds/aladin/5c99-fg", "creator_did":"ivo://CDS/P/EGRET/sup100", "obs_collection":"Gamma-ray EGRET maps - sup 100MeV", "obs_title":"EGRET sup 100MeV", "obs_description":"This data presents all-sky maps of diffuse gamma radiation in energy ranges between 100 MeV to 150 MeV, based on data collected by the EGRET instrument on the Compton Gamma Ray Observatory. EGRET detected gamma rays in the energy range from 30 MeV to over 30 GeV, with an energy resolution of 20-25% over most of that range. The instrument is described in Hughes et al. (1980), Kanbach (1988, 1989), Thompson et. al (1993) and Esposito et al. (1998). The work described here started with standard EGRET all-sky maps (ftp://cossc.gsfc.nasa.gov/compton/data/egret/high_level/combined_data) of photon counts, instrument exposure, and gamma-ray intensity, binned in 0.5 degree pixels, in both Galactic and equatorial coordinates. The energy ranges in MeV are: (narrow ranges) 30-50, 50-70, 70-100, 100-150, 150-300, 300-500, 500-1000, 1000-2000, 2000-4000, 4000-10000; (broader ranges) 30-100, 100-300, 300-1000; (integral ranges) >100, >300, >1000.", "bib_reference":"2005ApJ...621..291C", "obs_copyright":[ "Distributed by SkyView/HEASARC - HEALPixed by CDS", "Compton Gamma Ray Observatory (CGRO)"], "client_category":"Image/Gamma-ray/EGRET", "client_sort_key":"00-02-00b", "hips_release_date":"2019-05-05T06:04Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 1.508E-4", "hips_data_range":"-8.375E-5 2.512E-4", "moc_access_url":"http://alasky.u-strasbg.fr/EGRET/EGRET-sup100/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2005ApJ...621..291C", "t_min":"48361", "t_max":"48943", "obs_regime":"Gamma-ray", "em_min":"1.2398e-16", "em_max":"1.2398e-14", "hips_creation_date":"2014-06-05T17:00Z", "hips_tile_width":"512", "hips_hierarchy":"mean", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:04Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/EGRET/EGRET-sup100 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/EGRET/EGRET-sup100", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/EGRET/EGRET-sup100", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415202"}, +{ "ID":"CDS/P/Fermi/3", "hips_doi":"10.26093/cds/aladin/3q2w-3mk", "creator_did":"ivo://CDS/P/Fermi/3", "obs_collection":"Fermi3 300-1000MeV", "obs_title":"Fermi 300-1000MeV HEALPix survey", "obs_description":"Launched on June 11, 2008, the Fermi Gamma-ray Space Telescope observes the cosmos using the highest-energy form of light. This survey sums all data observed by the Fermi mission up to week 396. This version of the Fermi survey are intensity maps where the summed counts maps are divided by the exposure for each pixel. We anticipate using the HEASARC's Hera capabilities to update this survey on a roughly quarterly basis. Data is broken into 5 energy bands : 30-100 MeV Band 1, 100-300 MeV Band 2, 300-1000 MeV Band 3, 1-3 GeV Band 4 , 3-300 GeV Band 5. The SkyView data are based upon a Cartesian projection of the counts divided by the exposure maps. In the Cartesian projection pixels near the pole have a much smaller area than pixels on the equator, so these pixels have smaller integrated flux. When creating large scale images in other projections users may wish to make sure to compensate for this effect the flux conserving clip-resampling option.", "obs_copyright":"Distributed by SkyView/HEASARC - HEALPixed by CDS", "client_category":"Image/Gamma-ray", "client_sort_key":"00-01-04", "hips_creation_date":"2013-06-28T08:03Z", "hips_release_date":"2019-05-05T06:04Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/Fermi/300-1000MeV/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference":"2009ApJ...697.1071A", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2009ApJ...697.1071A", "obs_copyright_url":"http://skyview.gsfc.nasa.gov/current/cgi/survey.pl", "t_min":"54628", "t_max":"56291", "obs_regime":"Gamma-ray", "em_min":"1.2398e-15", "em_max":"4.1328e-15", "hips_tile_width":"512", "hips_pixel_scale":"0.01431", "hips_initial_fov":"150.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:04Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/Fermi/300-1000MeV UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/Fermi/300-1000MeV", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/Fermi/300-1000MeV", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415254"}, +{ "ID":"CDS/P/Fermi/4", "hips_doi":"10.26093/cds/aladin/1r4n-6sg", "creator_did":"ivo://CDS/P/Fermi/4", "obs_collection":"Fermi4 1-3GeV", "obs_title":"Fermi 1-3GeV HEALPix survey.", "obs_description":"Launched on June 11, 2008, the Fermi Gamma-ray Space Telescope observes the cosmos using the highest-energy form of light. This survey sums all data observed by the Fermi mission up to week 396. This version of the Fermi survey are intensity maps where the summed counts maps are divided by the exposure for each pixel. We anticipate using the HEASARC's Hera capabilities to update this survey on a roughly quarterly basis. Data is broken into 5 energy bands : 30-100 MeV Band 1, 100-300 MeV Band 2, 300-1000 MeV Band 3, 1-3 GeV Band 4 , 3-300 GeV Band 5. The SkyView data are based upon a Cartesian projection of the counts divided by the exposure maps. In the Cartesian projection pixels near the pole have a much smaller area than pixels on the equator, so these pixels have smaller integrated flux. When creating large scale images in other projections users may wish to make sure to compensate for this effect the flux conserving clip-resampling option.", "obs_copyright":"Distributed by SkyView/HEASARC - HEALPixed by CDS", "client_category":"Image/Gamma-ray", "client_sort_key":"00-01-03", "hips_creation_date":"2013-06-28T08:28Z", "hips_release_date":"2019-05-05T06:05Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/Fermi/1-3GeV/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference":"2009ApJ...697.1071A", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2009ApJ...697.1071A", "obs_copyright_url":"http://skyview.gsfc.nasa.gov/current/cgi/survey.pl", "t_min":"54628", "t_max":"56291", "obs_regime":"Gamma-ray", "em_min":"4.1328e-16", "em_max":"1.2398e-15", "hips_tile_width":"512", "hips_pixel_scale":"0.01431", "hips_initial_fov":"150.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:05Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/Fermi/1-3GeV UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/Fermi/1-3GeV", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/Fermi/1-3GeV", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415310"}, +{ "ID":"CDS/P/Fermi/5", "hips_doi":"10.26093/cds/aladin/3mva-x6", "creator_did":"ivo://CDS/P/Fermi/5", "obs_collection":"Fermi5 3-300GeV", "obs_title":"Fermi 3-300GeV HEALPix survey", "obs_description":"Launched on June 11, 2008, the Fermi Gamma-ray Space Telescope observes the cosmos using the highest-energy form of light. This survey sums all data observed by the Fermi mission up to week 396. This version of the Fermi survey are intensity maps where the summed counts maps are divided by the exposure for each pixel. We anticipate using the HEASARC's Hera capabilities to update this survey on a roughly quarterly basis. Data is broken into 5 energy bands : 30-100 MeV Band 1, 100-300 MeV Band 2, 300-1000 MeV Band 3, 1-3 GeV Band 4 , 3-300 GeV Band 5. The SkyView data are based upon a Cartesian projection of the counts divided by the exposure maps. In the Cartesian projection pixels near the pole have a much smaller area than pixels on the equator, so these pixels have smaller integrated flux. When creating large scale images in other projections users may wish to make sure to compensate for this effect the flux conserving clip-resampling option.", "obs_copyright":"Distributed by SkyView/HEASARC - HEALPixed by CDS", "client_category":"Image/Gamma-ray", "client_sort_key":"00-01-02", "hips_creation_date":"2013-06-28T09:09Z", "hips_release_date":"2019-05-05T06:05Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/Fermi/3-300GeV/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference":"2009ApJ...697.1071A", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2009ApJ...697.1071A", "obs_copyright_url":"http://skyview.gsfc.nasa.gov/current/cgi/survey.pl", "t_min":"54628", "t_max":"56291", "obs_regime":"Gamma-ray", "em_min":"4.1328e-18", "em_max":"4.1328e-16", "hips_tile_width":"512", "hips_pixel_scale":"0.01431", "hips_initial_fov":"150.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:05Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/Fermi/3-300GeV UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/Fermi/3-300GeV", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/Fermi/3-300GeV", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415366"}, +{ "ID":"CDS/P/Fermi/color", "hips_doi":"10.26093/cds/aladin/276y-1xd", "creator_did":"ivo://CDS/P/Fermi/color", "obs_collection":"Fermi color", "obs_title":"Fermi Color HEALPix survey", "obs_description":"Launched on June 11, 2008, the Fermi Gamma-ray Space Telescope observes the cosmos using the highest-energy form of light. This survey sums all data observed by the Fermi mission up to week 396. This version of the Fermi survey are intensity maps where the summed counts maps are divided by the exposure for each pixel. We anticipate using the HEASARC's Hera capabilities to update this survey on a roughly quarterly basis. Data is broken into 5 energy bands : 30-100 MeV Band 1, 100-300 MeV Band 2, 300-1000 MeV Band 3, 1-3 GeV Band 4 , 3-300 GeV Band 5. The SkyView data are based upon a Cartesian projection of the counts divided by the exposure maps. In the Cartesian projection pixels near the pole have a much smaller area than pixels on the equator, so these pixels have smaller integrated flux. When creating large scale images in other projections users may wish to make sure to compensate for this effect the flux conserving clip-resampling option.", "obs_copyright":"Distributed by SkyView/HEASARC - HEALPixed by CDS", "client_category":"Image/Gamma-ray", "client_sort_key":"00-01-01", "hips_creation_date":"2013-06-28T11:09Z", "hips_release_date":"2019-05-05T06:06Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"equatorial", "hips_tile_format":"jpeg", "dataproduct_type":"image", "hips_rgb_red":"300-1000MeVALLSKY~1 [0.0 10.0 20.0 Sqrt]", "hips_rgb_green":"1-3GeVALLSKY~1 [0.0 5.0 10.0 Sqrt]", "hips_rgb_blue":"3-300GeVALLSKY [0.0 2.0 4.0 Sqrt]", "client_application":[ "AladinLite", "AladinDesktop"], "moc_access_url":"http://alasky.u-strasbg.fr/Fermi/Color/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"This research has made use of data, software and/or web tools obtained from the High Energy Astrophysics Science Archive Research Center (HEASARC), a service of the Astrophysics Science Division at NASA/GSFC and of the Smithsonian Astrophysical Observatory's High Energy Astrophysics Division", "prov_progenitor":"NASA/HEASARC", "bib_reference":"2009ApJ...697.1071A", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2009ApJ...697.1071A", "obs_copyright_url":"http://skyview.gsfc.nasa.gov/current/cgi/survey.pl", "t_min":"54628", "t_max":"56291", "obs_regime":"Gamma-ray", "em_min":"4.1328e-18", "em_max":"4.1328e-15", "hips_tile_width":"512", "hips_pixel_scale":"0.01431", "hips_initial_fov":"150.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "dataproduct_subtype":"color", "moc_sky_fraction":"1", "hips_estsize":"9182", "hipsgen_date":"2019-05-05T06:06Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/Fermi/Color UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/Fermi/Color", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/Fermi/Color", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://healpix.ias.u-psud.fr/CDS_P_Fermi_color", "hips_status_2":"public mirror unclonable", "hips_service_url_3":"http://skies.esac.esa.int/FermiColor", "hips_status_3":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288727333"}, +{ "ID":"CDS/P/Finkbeiner", "hips_doi":"10.26093/cds/aladin/3a4t-avb", "creator_did":"ivo://CDS/P/Finkbeiner", "obs_collection":"Finkbeiner Halpha", "obs_title":"Finkbeiner Halpha composite survey", "obs_description":"D. Finkbeiner has assembled a full sky Halpha map using data from several surveys: the Wisconsin H-Alpha Mapper (WHAM), the Virginia Tech Spectral-Line Survey (VTSS), and the Southern H-Alpha Sky Survey Atlas (SHASSA). The composite map can be used to provide limits on free-free foreground emission.", "obs_copyright":"Composite map by Douglas Finkbeiner (2004).", "client_category":"Image/Gas-lines/Halpha", "client_sort_key":"06-01", "hips_creation_date":"2010-12-14T01:07Z", "hips_release_date":"2019-05-05T06:07Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_width":"128", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"-10 800", "dataproduct_type":"image", "client_application":[ "AladinLite", "AladinDesktop"], "moc_access_url":"http://alasky.u-strasbg.fr/FinkbeinerHalpha/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"All of these data products are available to the public on the World Wide Web", "prov_progenitor":"The data can be found as an on line material in the reference 2003ApJS..146..407F", "bib_reference":"2003ApJS..146..407F", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2003ApJS..146..407F", "t_min":"50753", "t_max":"51818", "obs_regime":"Optical", "em_min":"48E-8", "em_max":"73E-8", "hips_pixel_scale":"0.01431", "hips_initial_fov":"150.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:07Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/FinkbeinerHalpha UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/FinkbeinerHalpha", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/FinkbeinerHalpha", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"http://skies.esac.esa.int/FinkbeinerHa", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288732541"}, +{ "ID":"CDS/P/GalaxyCounts/2MPZ/0001-001", "creator_did":"ivo://CDS/P/GalaxyCounts/2MPZ/0001-001", "client_category":"Ancillary/GalaxyCounts/2MPZ", "obs_collection":"LIGO/Virgo probability maps (0.001 < z < 0.01, 40 to 80 Mpc)", "obs_title":"LIGO (0.001 < z < 0.01, 40 to 80 Mpc)", "obs_description":"The initial discovery of LIGO on 14 September 2015 was the in-spiral merger and ring-down of the black hole binary at a distance of about 500 Mpc or a redshift of about 0.1. The search for electromagnetic counterparts for the in-spiral of binary black holes is impeded by poor initial source localizations and a lack of a compelling model for the counterpart; therefore, rapid electromagnetic follow-up is required to understand the astrophysical context of these sources. Because astrophysical sources of gravitational radiation are likely to reside in galaxies, it would make sense to search rst in regions where the LIGO-Virgo probability is large and where the density of galaxies is large as well. Under the Bayesian prior assumption that the probability of a gravitational-wave event from a given region of space is proportional to the density of galaxies within the probed volume, one can calculate an improved localization of the position of the source simply by multiplying the LIGO-Virgo skymap by the density of galaxies in the range of redshifts. We propose using the 2-MASS Photometric Redshift Galaxy Catalogue for this purpose and demonstrate that using it can dramatically reduce the search region for electromagnetic counterparts.", "obs_ack":"The software and galaxy maps used in this paper is available at http://ubc-astrophysics.github.io . We used the VizieR Service, the NASA ADS service, the Super-COSMOS Science Archive, the NASA/IPAC Infrared Science Archive, the HEALPy libraries and arXiv.org.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"UBC-Astrophysics", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2016MNRAS.462.1085A/abstract", "obs_copyright":"UBC-Astrophysics", "obs_copyright_url":"http://copyright.ubc.ca/guidelines-and-resources/faq/", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T06:36Z", "hips_frame":"equatorial", "hips_order":"3", "hips_tile_width":"32", "hips_master_url":"http://alasky.unistra.fr/pub/arxiv.1602.07710v1/HIPS_0001_001/", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-3.735E-7 0.1841", "hips_data_range":"-0.1577 0.4732", "hips_pixel_scale":"0.229", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"6566", "hipsgen_date":"2016-10-21T14:47Z", "hipsgen_params":"in=2MPZ.gz_0.001_0.01_smoothed.fits out=HIPS_0001_001 ivorn=ivo://CDS/P/LIGO/0001 \"Publisher=M.Buga [CDS]\"", "hips_creation_date":"2016-10-21T14:47Z", "hipsgen_date_1":"2016-10-21T14:47Z", "hipsgen_params_1":"in=2MPZ.gz_0.001_0.01_smoothed.fits out=HIPS_0001_001 ivorn=ivo://CDS/P/LIGO/0001 \"Publisher=M.Buga [CDS]\"", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "bib_reference":"2016MNRAS.462.1085A", "hips_order_min":"0", "hipsgen_date_2":"2019-05-21T06:36Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/pub/arxiv.1602.07710v1/HIPS_0001_001 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_0001_001", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_0001_001", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288464706"}, +{ "ID":"CDS/P/GalaxyCounts/2MPZ/001-002", "creator_did":"ivo://CDS/P/GalaxyCounts/2MPZ/001-002", "client_category":"Ancillary/GalaxyCounts/2MPZ", "obs_collection":"LIGO/Virgo probability maps (0.01 < z < 0.02, 40 to 80 Mpc)", "obs_title":"LIGO (0.01 < z < 0.02, 40 to 80 Mpc)", "obs_description":"The initial discovery of LIGO on 14 September 2015 was the in-spiral merger and ring-down of the black hole binary at a distance of about 500 Mpc or a redshift of about 0.1. The search for electromagnetic counterparts for the in-spiral of binary black holes is impeded by poor initial source localizations and a lack of a compelling model for the counterpart; therefore, rapid electromagnetic follow-up is required to understand the astrophysical context of these sources. Because astrophysical sources of gravitational radiation are likely to reside in galaxies, it would make sense to search rst in regions where the LIGO-Virgo probability is large and where the density of galaxies is large as well. Under the Bayesian prior assumption that the probability of a gravitational-wave event from a given region of space is proportional to the density of galaxies within the probed volume, one can calculate an improved localization of the position of the source simply by multiplying the LIGO-Virgo skymap by the density of galaxies in the range of redshifts. We propose using the 2-MASS Photometric Redshift Galaxy Catalogue for this purpose and demonstrate that using it can dramatically reduce the search region for electromagnetic counterparts.", "obs_ack":"The software and galaxy maps used in this paper is available at http://ubc-astrophysics.github.io . We used the VizieR Service, the NASA ADS service, the Super-COSMOS Science Archive, the NASA/IPAC Infrared Science Archive, the HEALPy libraries and arXiv.org.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"UBC-Astrophysics", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2016MNRAS.462.1085A/abstract", "obs_copyright":"UBC-Astrophysics", "obs_copyright_url":"http://copyright.ubc.ca/guidelines-and-resources/faq/", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T06:36Z", "hips_frame":"equatorial", "hips_order":"3", "hips_tile_width":"32", "hips_master_url":"http://alasky.unistra.fr/pub/arxiv.1602.07710v1/HIPS_001_002/", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-8.864E-7 0.2865", "hips_data_range":"-0.2589 0.7766", "hips_pixel_scale":"0.229", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"6566", "hipsgen_date":"2016-10-21T14:49Z", "hipsgen_params":"in=2MPZ.gz_0.01_0.02_smoothed.fits out=HIPS_001_002 ivorn=ivo://CDS/P/LIGO/001 \"Publisher=M.Buga [CDS]\"", "hips_creation_date":"2016-10-21T14:49Z", "hipsgen_date_1":"2016-10-21T14:49Z", "hipsgen_params_1":"in=2MPZ.gz_0.01_0.02_smoothed.fits out=HIPS_001_002 ivorn=ivo://CDS/P/LIGO/001 \"Publisher=M.Buga [CDS]\"", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "bib_reference":"2016MNRAS.462.1085A", "hips_order_min":"0", "hipsgen_date_2":"2019-05-21T06:36Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/pub/arxiv.1602.07710v1/HIPS_001_002 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_001_002", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_001_002", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288464782"}, +{ "ID":"CDS/P/GalaxyCounts/2MPZ/002-003", "creator_did":"ivo://CDS/P/GalaxyCounts/2MPZ/002-003", "client_category":"Ancillary/GalaxyCounts/2MPZ", "obs_collection":"LIGO/Virgo probability maps (0.02 < z < 0.03, 40 to 80 Mpc)", "obs_title":"LIGO (0.02 < z < 0.03, 40 to 80 Mpc)", "obs_description":"The initial discovery of LIGO on 14 September 2015 was the in-spiral merger and ring-down of the black hole binary at a distance of about 500 Mpc or a redshift of about 0.1. The search for electromagnetic counterparts for the in-spiral of binary black holes is impeded by poor initial source localizations and a lack of a compelling model for the counterpart; therefore, rapid electromagnetic follow-up is required to understand the astrophysical context of these sources. Because astrophysical sources of gravitational radiation are likely to reside in galaxies, it would make sense to search rst in regions where the LIGO-Virgo probability is large and where the density of galaxies is large as well. Under the Bayesian prior assumption that the probability of a gravitational-wave event from a given region of space is proportional to the density of galaxies within the probed volume, one can calculate an improved localization of the position of the source simply by multiplying the LIGO-Virgo skymap by the density of galaxies in the range of redshifts. We propose using the 2-MASS Photometric Redshift Galaxy Catalogue for this purpose and demonstrate that using it can dramatically reduce the search region for electromagnetic counterparts.", "obs_ack":"The software and galaxy maps used in this paper is available at http://ubc-astrophysics.github.io . We used the VizieR Service, the NASA ADS service, the Super-COSMOS Science Archive, the NASA/IPAC Infrared Science Archive, the HEALPy libraries and arXiv.org.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"UBC-Astrophysics", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2016MNRAS.462.1085A/abstract", "obs_copyright":"UBC-Astrophysics", "obs_copyright_url":"http://copyright.ubc.ca/guidelines-and-resources/faq/", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T06:36Z", "hips_frame":"equatorial", "hips_order":"3", "hips_tile_width":"32", "hips_master_url":"http://alasky.unistra.fr/pub/arxiv.1602.07710v1/HIPS_002_003/", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-8.387E-7 0.3839", "hips_data_range":"-0.371 1.113", "hips_pixel_scale":"0.229", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"6566", "hipsgen_date":"2016-10-21T14:49Z", "hipsgen_params":"in=2MPZ.gz_0.02_0.03_smoothed.fits out=HIPS_002_003 ivorn=ivo://CDS/P/LIGO/002 \"Publisher=M.Buga [CDS]\"", "hips_creation_date":"2016-10-21T14:49Z", "hipsgen_date_1":"2016-10-21T14:49Z", "hipsgen_params_1":"in=2MPZ.gz_0.02_0.03_smoothed.fits out=HIPS_002_003 ivorn=ivo://CDS/P/LIGO/002 \"Publisher=M.Buga [CDS]\"", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "bib_reference":"2016MNRAS.462.1085A", "hips_order_min":"0", "hipsgen_date_2":"2019-05-21T06:36Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/pub/arxiv.1602.07710v1/HIPS_002_003 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_002_003", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_002_003", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288464846"}, +{ "ID":"CDS/P/GalaxyCounts/2MPZ/003-004", "creator_did":"ivo://CDS/P/GalaxyCounts/2MPZ/003-004", "client_category":"Ancillary/GalaxyCounts/2MPZ", "obs_collection":"LIGO/Virgo probability maps (0.03 < z < 0.04, 40 to 80 Mpc)", "obs_title":"LIGO (0.03 < z < 0.04, 40 to 80 Mpc)", "obs_description":"The initial discovery of LIGO on 14 September 2015 was the in-spiral merger and ring-down of the black hole binary at a distance of about 500 Mpc or a redshift of about 0.1. The search for electromagnetic counterparts for the in-spiral of binary black holes is impeded by poor initial source localizations and a lack of a compelling model for the counterpart; therefore, rapid electromagnetic follow-up is required to understand the astrophysical context of these sources. Because astrophysical sources of gravitational radiation are likely to reside in galaxies, it would make sense to search rst in regions where the LIGO-Virgo probability is large and where the density of galaxies is large as well. Under the Bayesian prior assumption that the probability of a gravitational-wave event from a given region of space is proportional to the density of galaxies within the probed volume, one can calculate an improved localization of the position of the source simply by multiplying the LIGO-Virgo skymap by the density of galaxies in the range of redshifts. We propose using the 2-MASS Photometric Redshift Galaxy Catalogue for this purpose and demonstrate that using it can dramatically reduce the search region for electromagnetic counterparts.", "obs_ack":"The software and galaxy maps used in this paper is available at http://ubc-astrophysics.github.io . We used the VizieR Service, the NASA ADS service, the Super-COSMOS Science Archive, the NASA/IPAC Infrared Science Archive, the HEALPy libraries and arXiv.org.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"UBC-Astrophysics", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2016MNRAS.462.1085A/abstract", "obs_copyright":"UBC-Astrophysics", "obs_copyright_url":"http://copyright.ubc.ca/guidelines-and-resources/faq/", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T06:37Z", "hips_frame":"equatorial", "hips_order":"3", "hips_tile_width":"32", "hips_master_url":"http://alasky.unistra.fr/pub/arxiv.1602.07710v1/HIPS_003_004/", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-1.356E-6 0.4006", "hips_data_range":"-0.3117 0.935", "hips_pixel_scale":"0.229", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"6566", "hipsgen_date":"2016-10-21T14:50Z", "hipsgen_params":"in=2MPZ.gz_0.03_0.04_smoothed.fits out=HIPS_003_004 ivorn=ivo://CDS/P/LIGO/003 \"Publisher=M.Buga [CDS]\"", "hips_creation_date":"2016-10-21T14:50Z", "hipsgen_date_1":"2016-10-21T14:50Z", "hipsgen_params_1":"in=2MPZ.gz_0.03_0.04_smoothed.fits out=HIPS_003_004 ivorn=ivo://CDS/P/LIGO/003 \"Publisher=M.Buga [CDS]\"", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "bib_reference":"2016MNRAS.462.1085A", "hips_order_min":"0", "hipsgen_date_2":"2019-05-21T06:37Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/pub/arxiv.1602.07710v1/HIPS_003_004 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_003_004", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_003_004", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288464898"}, +{ "ID":"CDS/P/GalaxyCounts/2MPZ/004-005", "creator_did":"ivo://CDS/P/GalaxyCounts/2MPZ/004-005", "client_category":"Ancillary/GalaxyCounts/2MPZ", "obs_collection":"LIGO/Virgo probability maps (0.04 < z < 0.05, 40 to 80 Mpc)", "obs_title":"LIGO (0.04 < z < 0.05, 40 to 80 Mpc)", "obs_description":"The initial discovery of LIGO on 14 September 2015 was the in-spiral merger and ring-down of the black hole binary at a distance of about 500 Mpc or a redshift of about 0.1. The search for electromagnetic counterparts for the in-spiral of binary black holes is impeded by poor initial source localizations and a lack of a compelling model for the counterpart; therefore, rapid electromagnetic follow-up is required to understand the astrophysical context of these sources. Because astrophysical sources of gravitational radiation are likely to reside in galaxies, it would make sense to search rst in regions where the LIGO-Virgo probability is large and where the density of galaxies is large as well. Under the Bayesian prior assumption that the probability of a gravitational-wave event from a given region of space is proportional to the density of galaxies within the probed volume, one can calculate an improved localization of the position of the source simply by multiplying the LIGO-Virgo skymap by the density of galaxies in the range of redshifts. We propose using the 2-MASS Photometric Redshift Galaxy Catalogue for this purpose and demonstrate that using it can dramatically reduce the search region for electromagnetic counterparts.", "obs_ack":"The software and galaxy maps used in this paper is available at http://ubc-astrophysics.github.io . We used the VizieR Service, the NASA ADS service, the Super-COSMOS Science Archive, the NASA/IPAC Infrared Science Archive, the HEALPy libraries and arXiv.org.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"UBC-Astrophysics", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2016MNRAS.462.1085A/abstract", "obs_copyright":"UBC-Astrophysics", "obs_copyright_url":"http://copyright.ubc.ca/guidelines-and-resources/faq/", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T06:37Z", "hips_frame":"equatorial", "hips_order":"3", "hips_tile_width":"32", "hips_master_url":"http://alasky.unistra.fr/pub/arxiv.1602.07710v1/HIPS_004_005/", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-7.349E-7 0.4508", "hips_data_range":"-0.3217 0.965", "hips_pixel_scale":"0.229", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"6566", "hipsgen_date":"2016-10-21T14:50Z", "hipsgen_params":"in=2MPZ.gz_0.04_0.05_smoothed.fits out=HIPS_004_005 ivorn=ivo://CDS/P/LIGO/004 \"Publisher=M.Buga [CDS]\"", "hips_creation_date":"2016-10-21T14:50Z", "hipsgen_date_1":"2016-10-21T14:50Z", "hipsgen_params_1":"in=2MPZ.gz_0.04_0.05_smoothed.fits out=HIPS_004_005 ivorn=ivo://CDS/P/LIGO/004 \"Publisher=M.Buga [CDS]\"", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "bib_reference":"2016MNRAS.462.1085A", "hips_order_min":"0", "hipsgen_date_2":"2019-05-21T06:37Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/pub/arxiv.1602.07710v1/HIPS_004_005 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_004_005", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_004_005", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288464954"}, +{ "ID":"CDS/P/GalaxyCounts/2MPZ/005-007", "creator_did":"ivo://CDS/P/GalaxyCounts/2MPZ/005-007", "client_category":"Ancillary/GalaxyCounts/2MPZ", "obs_collection":"LIGO/Virgo probability maps (0.05 < z < 0.07, 40 to 80 Mpc)", "obs_title":"LIGO (0.05 < z < 0.07, 40 to 80 Mpc)", "obs_description":"The initial discovery of LIGO on 14 September 2015 was the in-spiral merger and ring-down of the black hole binary at a distance of about 500 Mpc or a redshift of about 0.1. The search for electromagnetic counterparts for the in-spiral of binary black holes is impeded by poor initial source localizations and a lack of a compelling model for the counterpart; therefore, rapid electromagnetic follow-up is required to understand the astrophysical context of these sources. Because astrophysical sources of gravitational radiation are likely to reside in galaxies, it would make sense to search rst in regions where the LIGO-Virgo probability is large and where the density of galaxies is large as well. Under the Bayesian prior assumption that the probability of a gravitational-wave event from a given region of space is proportional to the density of galaxies within the probed volume, one can calculate an improved localization of the position of the source simply by multiplying the LIGO-Virgo skymap by the density of galaxies in the range of redshifts. We propose using the 2-MASS Photometric Redshift Galaxy Catalogue for this purpose and demonstrate that using it can dramatically reduce the search region for electromagnetic counterparts.", "obs_ack":"The software and galaxy maps used in this paper is available at http://ubc-astrophysics.github.io . We used the VizieR Service, the NASA ADS service, the Super-COSMOS Science Archive, the NASA/IPAC Infrared Science Archive, the HEALPy libraries and arXiv.org.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"UBC-Astrophysics", "bib_reference_url":"https://ui.adsabs.harvard.edu/ads/2016MNRAS.462.1085A/abstract", "obs_copyright":"UBC-Astrophysics", "obs_copyright_url":"http://copyright.ubc.ca/guidelines-and-resources/faq/", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T06:37Z", "hips_frame":"equatorial", "hips_order":"3", "hips_tile_width":"32", "hips_master_url":"http://alasky.unistra.fr/pub/arxiv.1602.07710v1/HIPS_005_007/", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-8.968E-7 0.6313", "hips_data_range":"-0.4141 1.242", "hips_pixel_scale":"0.229", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"6566", "hipsgen_date":"2016-10-21T14:51Z", "hipsgen_params":"in=2MPZ.gz_0.05_0.07_smoothed.fits out=HIPS_005_007 ivorn=ivo://CDS/P/LIGO/005 \"Publisher=M.Buga [CDS]\"", "hips_creation_date":"2016-10-21T14:51Z", "hipsgen_date_1":"2016-10-21T14:51Z", "hipsgen_params_1":"in=2MPZ.gz_0.05_0.07_smoothed.fits out=HIPS_005_007 ivorn=ivo://CDS/P/LIGO/005 \"Publisher=M.Buga [CDS]\"", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "bib_reference":"2016MNRAS.462.1085A", "hips_order_min":"0", "hipsgen_date_2":"2019-05-21T06:37Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/pub/arxiv.1602.07710v1/HIPS_005_007 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_005_007", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_005_007", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288465030"}, +{ "ID":"CDS/P/GalaxyCounts/2MPZ/007-01", "creator_did":"ivo://CDS/P/GalaxyCounts/2MPZ/007-01", "client_category":"Ancillary/GalaxyCounts/2MPZ", "obs_collection":"LIGO/Virgo probability maps (0.07 < z < 0.1, 40 to 80 Mpc)", "obs_title":"LIGO (0.07 < z < 0.1, 40 to 80 Mpc)", "obs_description":"The initial discovery of LIGO on 14 September 2015 was the in-spiral merger and ring-down of the black hole binary at a distance of about 500 Mpc or a redshift of about 0.1. The search for electromagnetic counterparts for the in-spiral of binary black holes is impeded by poor initial source localizations and a lack of a compelling model for the counterpart; therefore, rapid electromagnetic follow-up is required to understand the astrophysical context of these sources. Because astrophysical sources of gravitational radiation are likely to reside in galaxies, it would make sense to search rst in regions where the LIGO-Virgo probability is large and where the density of galaxies is large as well. Under the Bayesian prior assumption that the probability of a gravitational-wave event from a given region of space is proportional to the density of galaxies within the probed volume, one can calculate an improved localization of the position of the source simply by multiplying the LIGO-Virgo skymap by the density of galaxies in the range of redshifts. We propose using the 2-MASS Photometric Redshift Galaxy Catalogue for this purpose and demonstrate that using it can dramatically reduce the search region for electromagnetic counterparts.", "obs_ack":"The software and galaxy maps used in this paper is available at http://ubc-astrophysics.github.io . We used the VizieR Service, the NASA ADS service, the Super-COSMOS Science Archive, the NASA/IPAC Infrared Science Archive, the HEALPy libraries and arXiv.org.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"UBC-Astrophysics", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2016MNRAS.462.1085A/abstract", "obs_copyright":"UBC-Astrophysics", "obs_copyright_url":"http://copyright.ubc.ca/guidelines-and-resources/faq/", "t_min":"50600", "t_max":"51941", "obs_regime":"Infrared", "hips_builder":"Aladin/HipsGen v10.125", "hips_version":"1.4", "hips_release_date":"2019-05-21T06:37Z", "hips_frame":"equatorial", "hips_order":"3", "hips_tile_width":"32", "hips_master_url":"http://alasky.unistra.fr/pub/arxiv.1602.07710v1/HIPS_007_01/", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-1.468E-6 0.688", "hips_data_range":"-0.4203 1.261", "hips_pixel_scale":"0.229", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"6566", "hipsgen_date":"2016-10-21T14:51Z", "hipsgen_params":"in=2MPZ.gz_0.07_0.1_smoothed.fits out=HIPS_007_01 ivorn=ivo://CDS/P/LIGO/007 \"Publisher=M.Buga [CDS]\"", "hips_creation_date":"2016-10-21T14:51Z", "hipsgen_date_1":"2016-10-21T14:51Z", "hipsgen_params_1":"in=2MPZ.gz_0.07_0.1_smoothed.fits out=HIPS_007_01 ivorn=ivo://CDS/P/LIGO/007 \"Publisher=M.Buga [CDS]\"", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "bib_reference":"2016MNRAS.462.1085A", "hips_order_min":"0", "hipsgen_date_2":"2019-05-21T06:37Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume6/pub/arxiv.1602.07710v1/HIPS_007_01 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_007_01", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/pub/arxiv.1602.07710v1/HIPS_007_01", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288465098"}, +{ "ID":"CDS/P/HI", "creator_did":"ivo://CDS/P/HI", "obs_collection":"HI", "obs_title":"HI composite survey", "obs_description":"Composite all-sky map of neutral hydrogen column density (N_HI), formed from the Leiden/Dwingeloo HI survey data (Hartmann & Burton 1997) and the composite N_HI map of Dickey and Lockman (1990). The two datasets are not matched in sensitivity or resolution: note that discontinuities exist in the constructed composite map. A pixel mask is provided to indicate which dataset was used for each location on the sky. Hartmann & Burton provide a velocity integrated (-450 km/s < V_lsr < +400 km/s) HI brightness temperature map in Galactic coordinates, sampled every 0.5 degrees. This entire data set was converted to N_HI by multiplying by their factor of 1.8224e18 K km s-1 cm-2 and then interpolated to pixel centers appropriate for HEALPix Nside=512. Since the Leiden/Dwingeloo survey does not have sky coverage for declinations < -30 deg., the lower resolution Dickey & Lockman map was also interpolated to HEALPix and used to fill in the coverage gap. The Dickey & Lockman N_HI map is itself a composite of several surveys which had been merged and averaged onto 1 deg. bins in Galactic coordinates. Their map includes emission between -250 km/s < V_lsr < 250 km/s (excluding the LMC and SMC). The Leiden/Dwingeloo Survey data were obtained from the CDS. The Dickey & Lockman map was obtained from NCSA ADIL. This original HEALPix file is distributed and maintained by LAMBDA.", "obs_copyright":"Composite HI map by LAMBDA", "obs_copyright_url":"http://lambda.gsfc.nasa.gov/product/foreground/fg_HI_get.cfm", "client_category":"Image/Gas-lines/HI", "client_sort_key":"06-05", "hips_creation_date":"2011-02-14T12:00Z", "hips_release_date":"2019-05-05T06:29Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_width":"64", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/HI/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.", "prov_progenitor":"HEASARC/LAMBDA", "bib_reference":"1990ARA&A..28..215D", "bib_reference_url":"http://adsabs.harvard.edu/abs/1990ARA%26A..28..215D", "t_min":"44239", "t_max":"47892", "obs_regime":"Radio", "em_min":"0.21", "em_max":"0.21", "hips_pixel_scale":"0.01431", "hips_initial_fov":"120.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1112337", "hipsgen_date":"2019-05-05T06:29Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/HI UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/HI", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/HI", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288415898"}, +{ "ID":"CDS/P/HI4PI/NHI", "creator_did":"ivo://CDS/P/HI4PI/NHI", "client_category":"Image/Gas-lines/HI", "obs_collection":"HI4PI NHI", "obs_title":"HI4PI NHI survey (full-sky HI column density distribution)", "obs_description":"The HI4PI data release comprises 21-cm neutral atomic hydrogen data of the Milky Way (-600km/s0deg; -470km/s=10.040 AladinDesktop>=11.125", "moc_type":"smoc", "moc_order":"11", "obs_initial_ra":"282.3154012", "obs_initial_dec":"-6.75", "obs_initial_fov":"0.028629053431811713", "TIMESTAMP":"1721288493318"}, +{ "ID":"CDS/P/Mellinger/color", "creator_did":"ivo://CDS/P/Mellinger/color", "obs_collection":"Mellinger color", "obs_title":"Mellinger color optical survey", "obs_description":"Using a portable low-cost CCD camera system, 70 fields (each covering 40deg x 27deg) were imaged over a time span of 22 months from dark-sky locations in South Africa, Texas, and Michigan. The fields were photometrically calibrated against standard catalog stars. Using sky background data from the Pioneer 10 and 11 space probes, gradients resulting from artificial light pollution, airglow, and zodiacal light were eliminated, while the large-scale galactic and extragalactic background resulting from unresolved sources was preserved. The 648 megapixel image is a valuable educational tool, being able to fully utilize the resolution and dynamic range of modern full-dome planetarium projection systems.", "prov_progenitor":"Axel Mellinger", "bib_reference":"2009PASP..121.1180M", "bib_reference_url":"http://adsabs.harvard.edu/cgi-bin/nph-bib_query?db_key=AST&bibcode=2009PASP..121.1180M", "obs_copyright":"Copyright 2000-2017 Axel Mellinger. All rights reserved.", "obs_copyright_url":"http://www.milkywaysky.com/", "client_category":"Image/Optical", "client_sort_key":"03-03", "hips_creation_date":"2010-07-12T00:00Z", "hips_release_date":"2019-05-05T06:40Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"4", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"jpeg", "dataproduct_type":"image", "client_application":[ "AladinLite", "AladinDesktop"], "moc_access_url":"http://alasky.u-strasbg.fr/MellingerRGB/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "t_min":"54374", "t_max":"55044", "obs_regime":"Optical", "em_min":"4e-7", "em_max":"8e-7", "hips_pixel_scale":"0.007157", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "dataproduct_subtype":"color", "moc_sky_fraction":"1", "hips_estsize":"36707", "hipsgen_date":"2019-05-05T06:40Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/MellingerRGB UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/MellingerRGB", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/MellingerRGB", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://healpix.ias.u-psud.fr/CDS_P_Mellinger_color", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"4", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"3.6645188392718993", "TIMESTAMP":"1721288719181"}, +{ "ID":"CDS/P/NEOWISER/Color", "creator_did":"ivo://CDS/P/NEOWISER/Color", "obs_collection":"NEOWISER W2-W1", "obs_title":"NEOWISER color Red (W2) , Blue (W1)", "obs_description":"The NEOWISE project is the asteroid-hunting portion of the Wide-field Infrared Survey Explorer (WISE) mission. Funded by NASA's Planetary Science Division, NEOWISE harvests measurements of asteroids and comets from the WISE images and provides a rich archive for searching WISE data for solar system objects. Here we update our full-depth coadds by folding in the most recently published year of W1/W2 exposures released by NEOWISER. These new single-frame data were acquired between 2015 December 13 and 2016 December 13, and became public in 2017 June. In the present work, we simply re-ran the latest unWISE coaddition code (Meisner et al. 2017a) on inputs including this additional year of publicly available NEOWISER frames. The resulting set of full-depth coadds uniformly incorporates all publicly available W1 and W2 exposures, with observation dates ranging from 2010 January 7 to 2016 December 13. The inputs consisted of ~ 10.5 million frames per band, totaling ~ 140 terabytes of single-exposure pixel data.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_ack":"This publication also makes use of data products from NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology, funded by the Planetary Science Division of the National Aeronautics and Space Administration", "prov_progenitor":"IPAC/NASA", "bib_reference":"2014ApJ...792...30M", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2014ApJ...792...30M/abstract", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE/NEOWISER", "t_min":"55203", "t_max":"57735", "obs_regime":"Infrared", "em_min":"2.754e-6", "em_max":"5.3413e-6", "hips_builder":"Aladin/HipsGen v10.123", "hips_initial_fov":"20.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "hips_version":"1.4", "hips_release_date":"2019-05-05T08:29Z", "hips_frame":"equatorial", "hips_order":"8", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_pixel_scale":"4.473E-4", "dataproduct_type":"image", "hips_rgb_red":"NEOWISER W2 [0.0 NaN 190.0 Linear]", "hips_rgb_blue":"NEOWISER W1 [0.0 NaN 190.0 Linear]", "moc_sky_fraction":"1", "hips_estsize":"14092654", "hipsgen_date":"2018-03-04T12:58Z", "hipsgen_params":"inRed=/var/www/NEOWISER/W2/ inBlue=/var/www/NEOWISER/W1/ out=W1W2Color2 creator_did=CDS/P/NEOWISER/Color \"cmRed=0 190\" \"cmBlue=0 190\" method=FIRST color=png RGB verbose=4", "hips_creation_date":"2018-03-04T12:58Z", "hips_hierarchy":"first", "hips_tile_format":"png", "hipsgen_date_1":"2018-03-05T05:57Z", "hipsgen_params_1":"inRed=/var/www/NEOWISER/W2/ inBlue=/var/www/NEOWISER/W1/ out=W1W2Color2 creator_did=CDS/P/NEOWISER/Color \"cmRed=0 190\" \"cmBlue=0 190\" method=FIRST color=png RGB verbose=4", "hips_order_min":"0", "hipsgen_date_2":"2019-05-05T08:23Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume10/NEOWISER/W1W2 UPDATE", "dataproduct_subtype":"color", "hipsgen_date_3":"2019-05-05T08:29Z", "hipsgen_params_3":"out=/asd-volumes/sc1-asd-volume10/NEOWISER/W1W2 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/NEOWISER/W1W2", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/NEOWISER/W1W2", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://healpix.ias.u-psud.fr/CDS_P_NEOWISER_Color", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288719557"}, +{ "ID":"CDS/P/NEOWISER/W1", "creator_did":"ivo://CDS/P/NEOWISER/W1", "obs_collection":"NEOWISER W1 (3.4um)", "obs_title":"NEOWISER W1", "obs_description":"The NEOWISE project is the asteroid-hunting portion of the Wide-field Infrared Survey Explorer (WISE) mission. Funded by NASA's Planetary Science Division, NEOWISE harvests measurements of asteroids and comets from the WISE images and provides a rich archive for searching WISE data for solar system objects. Here we update our full-depth coadds by folding in the most recently published year of W1/W2 exposures released by NEOWISER. These new single-frame data were acquired between 2015 December 13 and 2016 December 13, and became public in 2017 June. In the present work, we simply re-ran the latest unWISE coaddition code (Meisner et al. 2017a) on inputs including this additional year of publicly available NEOWISER frames. The resulting set of full-depth coadds uniformly incorporates all publicly available W1 and W2 exposures, with observation dates ranging from 2010 January 7 to 2016 December 13. The inputs consisted of ~ 10.5 million frames per band, totaling ~ 140 terabytes of single-exposure pixel data.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_ack":"This publication also makes use of data products from NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology, funded by the Planetary Science Division of the National Aeronautics and Space Administration", "prov_progenitor":"IPAC/NASA", "bib_reference":"2014ApJ...792...30M", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2014ApJ...792...30M/abstract", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE/NEOWISER", "t_min":"55203", "t_max":"57735", "obs_regime":"Infrared", "em_min":"2.754e-6", "em_max":"3.8723e-6", "hips_builder":"Aladin/HipsGen v10.123", "hips_version":"1.4", "hips_release_date":"2019-05-05T08:11Z", "hips_frame":"equatorial", "hips_order":"8", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"0 230", "hips_data_range":"-890.6 2675", "hips_pixel_scale":"4.473E-4", "s_pixel_scale":"7.638E-4", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"1139026029", "hipsgen_date":"2018-01-29T12:31Z", "hips_initial_fov":"20.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_skyval_method":"TRUE", "hips_skyval_value":"0.774662435054779 31.438140829062462 -890.6055234372616 2674.915220052004", "hips_overlay":"mean", "hips_hierarchy":"first", "hipsgen_params":"in=w1_std_u out=/data1/buga/NEOWISER/NEOWISER/NEOWISER_W1 skyval=true maxRatio=0 maxthread=40 method=FIRST creator_did=CDS/C/NEOWISER/W1 verbose=4 INDEX TILES PNG DETAILS", "hips_creation_date":"2018-01-29T12:31Z", "hipsgen_date_1":"2018-01-29T20:37Z", "hipsgen_params_1":"in=w1_std_u out=/data1/buga/NEOWISER/NEOWISER/NEOWISER_W1 skyval=true maxRatio=0 maxthread=40 method=FIRST creator_did=CDS/C/NEOWISER/W1 verbose=4 INDEX TILES PNG DETAILS", "hipsgen_date_2":"2018-01-30T21:24Z", "hipsgen_params_2":"in=w1_std_u out=/data1/buga/NEOWISER/NEOWISER/NEOWISER_W1 skyval=true maxRatio=0 maxthread=40 \"pixelCut=0 230\" method=FIRST creator_did=CDS/C/NEOWISER/W1 verbose=4 JPEG", "hips_order_min":"0", "hipsgen_date_3":"2019-05-05T08:11Z", "hipsgen_params_3":"out=/asd-volumes/sc1-asd-volume10/NEOWISER/W1 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/NEOWISER/W1", "hips_progenitor_url":"https://alasky.cds.unistra.fr/NEOWISER/W1/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/NEOWISER/W1", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288475558"}, +{ "ID":"CDS/P/NEOWISER/W2", "creator_did":"ivo://CDS/P/NEOWISER/W2", "obs_collection":"NEOWISER W2 (4.6um)", "obs_title":"NEOWISER W2", "obs_description":"The NEOWISE project is the asteroid-hunting portion of the Wide-field Infrared Survey Explorer (WISE) mission. Funded by NASA's Planetary Science Division, NEOWISE harvests measurements of asteroids and comets from the WISE images and provides a rich archive for searching WISE data for solar system objects. Here we update our full-depth coadds by folding in the most recently published year of W1/W2 exposures released by NEOWISER. These new single-frame data were acquired between 2015 December 13 and 2016 December 13, and became public in 2017 June. In the present work, we simply re-ran the latest unWISE coaddition code (Meisner et al. 2017a) on inputs including this additional year of publicly available NEOWISER frames. The resulting set of full-depth coadds uniformly incorporates all publicly available W1 and W2 exposures, with observation dates ranging from 2010 January 7 to 2016 December 13. The inputs consisted of a ~ 10.5 million frames per band, totaling a ~ 140 terabytes of single-exposure pixel data.", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_ack":"This publication also makes use of data products from NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology, funded by the Planetary Science Division of the National Aeronautics and Space Administration", "prov_progenitor":"IPAC/NASA", "bib_reference":"2014ApJ...792...30M", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2014ApJ...792...30M/abstract", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE/NEOWISER", "t_min":"55203", "t_max":"57735", "obs_regime":"Infrared", "em_min":"3.9633e-6", "em_max":"5.3413e-6", "hips_builder":"Aladin/HipsGen v10.123", "hips_version":"1.4", "hips_release_date":"2019-05-05T08:18Z", "hips_frame":"equatorial", "hips_order":"8", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"0 230", "hips_data_range":"-4234 12717", "hips_pixel_scale":"4.473E-4", "s_pixel_scale":"7.638E-4", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"1139026029", "hipsgen_date":"2018-02-08T13:07Z", "hips_initial_fov":"20.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_skyval_method":"TRUE", "hips_skyval_value":"0.0 230.0 -4234.388455033302 12716.9615162611", "hips_overlay":"mean", "hips_hierarchy":"first", "hipsgen_params":"cache=Temp/ cachesize=500000 in=w2_std_u/ out=/data1/buga/NEOWISER/NEOWISER/NEOWISER_W2 skyval=true maxRatio=0 maxthread=40 \"pixelCut=0 230\" method=FIRST creator_did=CDS/C/NEOWISER/W2 verbose=4 INDEX TILES", "hips_creation_date":"2018-02-08T13:07Z", "hipsgen_date_1":"2018-02-09T00:59Z", "hipsgen_params_1":"cache=Temp/ cachesize=500000 in=w2_std_u/ out=/data1/buga/NEOWISER/NEOWISER/NEOWISER_W2 skyval=true maxRatio=0 maxthread=40 \"pixelCut=0 230\" method=FIRST creator_did=CDS/C/NEOWISER/W2 verbose=4 JPEG DETAILS", "hips_order_min":"0", "hipsgen_date_2":"2019-05-05T08:18Z", "hipsgen_params_2":"out=/asd-volumes/sc1-asd-volume10/NEOWISER/W2 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/NEOWISER/W2", "hips_progenitor_url":"https://alasky.cds.unistra.fr/NEOWISER/W2/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/NEOWISER/W2", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"9", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.11451621372724685", "TIMESTAMP":"1721288475614"}, +{ "ID":"CDS/P/PLANCK/R2/CMB", "creator_did":"ivo://CDS/P/PLANCK/R2/CMB", "obs_collection":"PLANCK R2 CMB", "obs_title":"PLANCK Maps of the CMB fluctuations.", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2", "client_sort_key":"05-04-03", "hips_release_date":"2019-05-05T06:48Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-3.057E-4 3.607E-4", "hips_data_range":"-0.002454 0.002741", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/COM_CMB_IQU-smica-field-Int_2048_R2.00/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"3E-4", "em_max":"1.11E-2", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-06T13:52Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:48Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/COM_CMB_IQU-smica-field-Int_2048_R2.00 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/COM_CMB_IQU-smica-field-Int_2048_R2.00", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/COM_CMB_IQU-smica-field-Int_2048_R2.00", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288417526"}, +{ "ID":"CDS/P/PLANCK/R2/HFI/color", "creator_did":"ivo://CDS/P/PLANCK/R2/HFI/color", "obs_collection":"PLANCK R2 HFI color", "obs_title":"PLANCK R2 HFI color composition 353-545-857 GHz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/HFI", "client_sort_key":"05-04-xR2-02-00", "hips_release_date":"2019-05-05T06:49Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"jpeg", "dataproduct_type":"image", "hips_rgb_red":"HFI_SkyMap_353_2048_R2.00 [1.368E-4 0.0111184 0.0221 Linear]", "hips_rgb_green":"HFI_SkyMap_545_2048_R2.00 [-0.5 12.305 25.11 Linear]", "hips_rgb_blue":"HFI_SkyMap_857_2048_R2.00 [-2.0 52.8 107.6 Linear]", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/HFI_Color_353_545_857/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"3.49E-4", "em_max":"8.49E-4", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"9182", "hips_creation_date":"2015-02-06T13:38Z", "hips_order_min":"0", "dataproduct_subtype":"color", "hipsgen_date":"2019-05-05T06:49Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/HFI_Color_353_545_857 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/HFI_Color_353_545_857", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/HFI_Color_353_545_857", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/PLANCK/R2/HFI/color", "hips_status_2":"public mirror unclonable", "hips_service_url_3":"https://healpix.ias.u-psud.fr/CDS_P_PLANCK_R2_HFI_color", "hips_status_3":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288719873"}, +{ "ID":"CDS/P/PLANCK/R2/HFI100", "creator_did":"ivo://CDS/P/PLANCK/R2/HFI100", "obs_collection":"PLANCK R2 HFI100", "obs_title":"PLANCK R2 nominal frequency HFI map 100Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/HFI", "client_sort_key":"05-04-xR2-01-08", "hips_release_date":"2019-05-05T06:49Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-2.681E-4 0.00487", "hips_data_range":"-0.05606 0.1659", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/HFI_SkyMap_100_2048_R2.00/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"2.99E-3", "em_max":"2.99E-3", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T16:46Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:49Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/HFI_SkyMap_100_2048_R2.00 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_100_2048_R2.00", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_100_2048_R2.00", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288417690"}, +{ "ID":"CDS/P/PLANCK/R2/HFI143", "creator_did":"ivo://CDS/P/PLANCK/R2/HFI143", "obs_collection":"PLANCK R2 HFI143", "obs_title":"PLANCK R2 nominal frequency HFI map 143Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/HFI", "client_sort_key":"05-04-xR2-01-07", "hips_release_date":"2019-05-05T06:49Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-2.370E-4 0.005372", "hips_data_range":"-0.061 0.1813", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/HFI_SkyMap_143_2048_R2.00/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"2.09E-3", "em_max":"2.09E-3", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T16:49Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:49Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/HFI_SkyMap_143_2048_R2.00 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_143_2048_R2.00", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_143_2048_R2.00", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288417750"}, +{ "ID":"CDS/P/PLANCK/R2/HFI217", "creator_did":"ivo://CDS/P/PLANCK/R2/HFI217", "obs_collection":"PLANCK R2 HFI217", "obs_title":"PLANCK R2 nominal frequency HFI map 217Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/HFI", "client_sort_key":"05-04-xR2-01-06", "hips_release_date":"2019-05-05T06:49Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-1.571E-4 0.019", "hips_data_range":"-0.1518 0.4534", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/HFI_SkyMap_217_2048_R2.00/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"1.38E-3", "em_max":"1.38E-3", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T16:54Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:49Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/HFI_SkyMap_217_2048_R2.00 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_217_2048_R2.00", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_217_2048_R2.00", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288417802"}, +{ "ID":"CDS/P/PLANCK/R2/HFI353", "creator_did":"ivo://CDS/P/PLANCK/R2/HFI353", "obs_collection":"PLANCK R2 HFI353", "obs_title":"PLANCK R2 nominal frequency HFI map 353Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/HFI", "client_sort_key":"05-04-xR2-01-05", "hips_release_date":"2019-05-05T06:50Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"2.591E-4 0.1375", "hips_data_range":"-0.8131 2.439", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/HFI_SkyMap_353_2048_R2.00/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"8.49E-4", "em_max":"8.49E-4", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T17:37Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:50Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/HFI_SkyMap_353_2048_R2.00 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_353_2048_R2.00", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_353_2048_R2.00", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288417854"}, +{ "ID":"CDS/P/PLANCK/R2/HFI545", "creator_did":"ivo://CDS/P/PLANCK/R2/HFI545", "obs_collection":"PLANCK R2 HFI545", "obs_title":"PLANCK R2 nominal frequency HFI map 545Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/HFI", "client_sort_key":"05-04-xR2-01-04", "hips_release_date":"2019-05-05T06:50Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"0.2338 141.2", "hips_data_range":"-884.2 2654", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/HFI_SkyMap_545_2048_R2.00/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"5.50E-4", "em_max":"5.50E-4", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T17:41Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:50Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/HFI_SkyMap_545_2048_R2.00 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_545_2048_R2.00", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_545_2048_R2.00", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288417906"}, +{ "ID":"CDS/P/PLANCK/R2/HFI857", "creator_did":"ivo://CDS/P/PLANCK/R2/HFI857", "obs_collection":"PLANCK R2 HFI857", "obs_title":"PLANCK R2 nominal frequency HFI map 857Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/HFI", "client_sort_key":"05-04-xR2-01-03", "hips_release_date":"2019-05-05T06:50Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"0.4857 467.3", "hips_data_range":"-3826 11480", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/HFI_SkyMap_857_2048_R2.00/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"3.49E-4", "em_max":"3.49E-4", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T17:52Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:50Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/HFI_SkyMap_857_2048_R2.00 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_857_2048_R2.00", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/HFI_SkyMap_857_2048_R2.00", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288417966"}, +{ "ID":"CDS/P/PLANCK/R2/LFI/color", "creator_did":"ivo://CDS/P/PLANCK/R2/LFI/color", "obs_collection":"PLANCK R2 LFI color", "obs_title":"PLANCK R2 LFI color composition 30-44-70 GHz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/LFI", "client_sort_key":"05-04-xR2-02-00", "hips_release_date":"2019-05-05T06:51Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"jpeg", "dataproduct_type":"image", "hips_rgb_red":"LFI_SkyMap_030_1024_R2.01 [-3.6E-4 0.0011315 0.002623 Linear]", "hips_rgb_green":"LFI_SkyMap_044_1024_R2.01 [-5.916E-4 0.0020472000000000003 0.004686 Sqrt]", "hips_rgb_blue":"LFI_SkyMap_070_1024_R2.01 [-7.446E-4 0.0015612000000000002 0.003867 Sqrt]", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/LFI_Color_30_44_70/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"4.28E-3", "em_max":"9.99E-3", "hips_tile_width":"128", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"9182", "hips_creation_date":"2015-02-06T12:09Z", "hips_order_min":"0", "dataproduct_subtype":"color", "hipsgen_date":"2019-05-05T06:51Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/LFI_Color_30_44_70 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/LFI_Color_30_44_70", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/LFI_Color_30_44_70", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/PLANCK/R2/LFI/color", "hips_status_2":"public mirror unclonable", "hips_service_url_3":"https://healpix.ias.u-psud.fr/CDS_P_PLANCK_R2_LFI_color", "hips_status_3":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"3", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"7.3290376785437985", "TIMESTAMP":"1721288902748"}, +{ "ID":"CDS/P/PLANCK/R2/LFI030", "creator_did":"ivo://CDS/P/PLANCK/R2/LFI030", "obs_collection":"PLANCK R2 LFI030", "obs_title":"PLANCK R2 nominal frequency LFI map 30 Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/LFI", "client_sort_key":"05-04-xR2-02-03", "hips_release_date":"2019-05-05T06:51Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-2.004E-4 0.05758", "hips_data_range":"-0.1084 0.3236", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/LFI_SkyMap_030_1024_R2.01/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"9.99E-3", "em_max":"9.99E-3", "hips_tile_width":"128", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T16:40Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:51Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/LFI_SkyMap_030_1024_R2.01 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/LFI_SkyMap_030_1024_R2.01", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/LFI_SkyMap_030_1024_R2.01", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288418074"}, +{ "ID":"CDS/P/PLANCK/R2/LFI044", "creator_did":"ivo://CDS/P/PLANCK/R2/LFI044", "obs_collection":"PLANCK R2 LFI044", "obs_title":"PLANCK R2 nominal frequency LFI map 44 Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/LFI", "client_sort_key":"05-04-xR2-02-02", "hips_release_date":"2019-05-05T06:51Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-2.356E-4 0.0233", "hips_data_range":"-0.05806 0.1724", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/LFI_SkyMap_044_1024_R2.01/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"6.81E-3", "em_max":"6.81E-3", "hips_tile_width":"128", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T16:42Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:51Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/LFI_SkyMap_044_1024_R2.01 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/LFI_SkyMap_044_1024_R2.01", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/LFI_SkyMap_044_1024_R2.01", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288418126"}, +{ "ID":"CDS/P/PLANCK/R2/LFI070", "creator_did":"ivo://CDS/P/PLANCK/R2/LFI070", "obs_collection":"PLANCK R2 LFI070", "obs_title":"PLANCK R2 nominal frequency LFI map 70Ghz", "obs_description":"The Planck mission will collect and characterise radiation from the Cosmic Microwave Background (CMB) using sensitive radio receivers operating at extremely low temperatures. These receivers will determine the black body equivalent temperature of the background radiation and will be capable of distinguishing temperature variations of about one microkelvin. These measurements will be used to produce the best ever maps of anisotropies in the CMB radiation field.", "obs_copyright_url":"http://pla.esac.esa.int/pla", "prov_progenitor":"ESA", "client_category":"Deprecated/HiPS/CDS/Radio/PLANCK/R2/LFI", "client_sort_key":"05-04-xR2-02-01", "hips_release_date":"2019-05-05T06:52Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png fits", "dataproduct_type":"image", "hips_pixel_cut":"-3.952E-4 0.004807", "hips_data_range":"-0.05934 0.1745", "moc_access_url":"http://alasky.u-strasbg.fr/PLANCK/R2/LFI_SkyMap_070_2048_R2.01/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"300.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "obs_copyright":"Planck Legacy Archive", "t_min":"55056", "t_max":"56507", "obs_regime":"Radio", "em_min":"4.28E-3", "em_max":"4.28E-3", "hips_tile_width":"256", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1116925", "hips_creation_date":"2015-02-05T16:29Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T06:52Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/PLANCK/R2/LFI_SkyMap_070_2048_R2.01 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R2/LFI_SkyMap_070_2048_R2.01", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R2/LFI_SkyMap_070_2048_R2.01", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288418178"}, +{ "ID":"CDS/P/PLANCK/R3/CMB", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/CMB", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK Map of the CMB fluctuations", "obs_collection":"PLANCK R3 CMB", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.0003498161703617", "em_max":"0.009993081933333", "client_category":"Image/Radio/PLANCK/R3", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-11-04T16:44Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"256", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-3.037E-4 3.660E-4", "hips_data_range":"-0.00648 0.004229", "hips_pixel_scale":"0.02863", "dataproduct_type":"image", "hipsgen_date":"2022-11-04T16:44Z", "hipsgen_params":"in=COM_CMB_IQU-smica_2048_R3.00_full.fits out=CMB-smica-R3 creator_did=CDS/P/PLANCK/R3/CMB", "hips_creation_date":"2022-11-04T16:44Z", "hips_estsize":"363479", "hips_nb_tiles":"2042", "hips_check_code":"png:3762770760 fits:269935101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/CMB-smica-R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/CMB-smica-R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288492618"}, +{ "ID":"CDS/P/PLANCK/R3/HFI/color", "hips_initial_fov":"140.0", "hips_initial_ra":"99.6310748", "hips_initial_dec":"+2.5518726", "creator_did":"ivo://CDS/P/PLANCK/R3/HFI/color", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 HFI color composition 353-545-857 GHz", "obs_collection":"PLANCK R3 HFI color composition 353-545-857 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.0003498161703617", "em_max":"0.0008492704192635", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-11-07T13:34Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"128", "hips_status":"public master clonableOnce", "hips_pixel_scale":"0.05726", "dataproduct_type":"image", "hips_rgb_red":"PLANCK R3 frequency HFI map 353 GHz [1.368E-4 0.0111184 0.0221 Linear]", "hips_rgb_green":"PLANCK R3 frequency HFI map 545 GHz [-0.5 12.305 25.11 Linear]", "hips_rgb_blue":"PLANCK R3 frequency HFI map 857 GHz [-2.0 52.8 107.6 Linear]", "hipsgen_date":"2022-11-07T13:34Z", "hipsgen_params":"inRed=HFI_SkyMap_353_R3 inGreen=HFI_SkyMap_545_R3 inBlue=HFI_SkyMap_857_R3 out=couleurHFI/ creator_did=CDS/P/PLANCK/R3/HFI/color RGB \"cmRed=1.368E-4 0.0111184 0.0221 Linear\" \"cmGreen=-0.5 12.305 25.11 Linear\" \"cmBlue=-2.0 52.8 107.6 Linear\" hips_tile_width=128 RGB", "hips_creation_date":"2022-11-07T13:34Z", "hips_hierarchy":"median", "hips_tile_format":"png", "dataproduct_subtype":"color", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/HFI_Color_353_545_857", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/HFI_Color_353_545_857", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"99.6310748", "obs_initial_dec":"+2.5518726", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288492674"}, +{ "ID":"CDS/P/PLANCK/R3/HFI100", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/HFI100", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency HFI map 100 GHz", "obs_collection":"PLANCK R3 HFI 100 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.00299792458", "em_max":"0.00299792458", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T09:55Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"256", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-2.676E-4 0.004868", "hips_data_range":"-0.05603 0.1658", "hips_pixel_scale":"0.02863", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T09:55Z", "hipsgen_params":"in=../Frequency-maps_Single-frequency/HFI_SkyMap_100_2048_R3.01_full.fits out=HiPS_HFI_SkyMap_100/ creator_did=CDS/P/PLANCK/R3/HFI100", "hips_creation_date":"2022-10-26T09:55Z", "hips_estsize":"334736", "hips_nb_tiles":"2042", "hips_check_code":"png:2495301502 fits:269935101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_100_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_100_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288492786"}, +{ "ID":"CDS/P/PLANCK/R3/HFI143", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/HFI143", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency HFI map 143 GHz", "obs_collection":"PLANCK R3 HFI 143 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.002096450755245", "em_max":"0.002096450755245", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T10:09Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"256", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-2.371E-4 0.005359", "hips_data_range":"-0.06099 0.1813", "hips_pixel_scale":"0.02863", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T10:09Z", "hipsgen_params":"in=../Frequency-maps_Single-frequency/HFI_SkyMap_143_2048_R3.01_full.fits out=HiPS_HFI_SkyMap_143/ creator_did=CDS/P/PLANCK/R3/HFI143", "hips_creation_date":"2022-10-26T10:09Z", "hips_estsize":"324689", "hips_nb_tiles":"2042", "hips_check_code":"png:3377468219 fits:269935101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_143_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_143_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288492842"}, +{ "ID":"CDS/P/PLANCK/R3/HFI217", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/HFI217", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency HFI map 217 GHz", "obs_collection":"PLANCK R3 HFI 217 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.001381532064516", "em_max":"0.001381532064516", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T10:12Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"256", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-1.540E-4 0.01882", "hips_data_range":"-0.1515 0.4527", "hips_pixel_scale":"0.02863", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T10:12Z", "hipsgen_params":"in=../../Frequency-maps_Single-frequency/HFI_SkyMap_217_2048_R3.01_full.fits out=HiPS_HFI_SkyMap_217/ creator_did=CDS/P/PLANCK/R3/HFI217", "hips_creation_date":"2022-10-26T10:12Z", "hips_estsize":"310664", "hips_nb_tiles":"2042", "hips_check_code":"png:3589847422 fits:269935101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_217_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_217_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288492894"}, +{ "ID":"CDS/P/PLANCK/R3/HFI353", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/HFI353", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency HFI map 353 GHz", "obs_collection":"PLANCK R3 HFI 353 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.0008492704192635", "em_max":"0.0008492704192635", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T10:14Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"256", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"9.754E-5 0.1335", "hips_data_range":"-0.8379 2.513", "hips_pixel_scale":"0.02863", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T10:14Z", "hipsgen_params":"in=../../Frequency-maps_Single-frequency/HFI_SkyMap_353-psb_2048_R3.01_full.fits out=HiPS_HFI_SkyMap_353/ creator_did=CDS/P/PLANCK/R3/HFI353", "hips_creation_date":"2022-10-26T10:14Z", "hips_estsize":"303717", "hips_nb_tiles":"2042", "hips_check_code":"png:1815710823 fits:269935101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_353_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_353_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288492954"}, +{ "ID":"CDS/P/PLANCK/R3/HFI545", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/HFI545", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency HFI map 545 GHz", "obs_collection":"PLANCK R3 HFI 545 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.0005500779045872", "em_max":"0.0005500779045872", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T10:16Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"256", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0.2317 142.6", "hips_data_range":"-890.5 2672", "hips_pixel_scale":"0.02863", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T10:16Z", "hipsgen_params":"in=../../Frequency-maps_Single-frequency/HFI_SkyMap_545_2048_R3.01_full.fits out=HiPS_HFI_SkyMap_545/ creator_did=CDS/P/PLANCK/R3/HFI545", "hips_creation_date":"2022-10-26T10:16Z", "hips_estsize":"293347", "hips_nb_tiles":"2042", "hips_check_code":"png:1830480926 fits:269935101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_545_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_545_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288493006"}, +{ "ID":"CDS/P/PLANCK/R3/HFI857", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/HFI857", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency HFI map 857 GHz", "obs_collection":"PLANCK R3 HFI 857 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.0003498161703617", "em_max":"0.0003498161703617", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T10:18Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"256", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0.4803 469.8", "hips_data_range":"-3834 11505", "hips_pixel_scale":"0.02863", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T10:17Z", "hipsgen_params":"in=../../Frequency-maps_Single-frequency/HFI_SkyMap_857_2048_R3.01_full.fits out=HiPS_HFI_SkyMap_857/ creator_did=CDS/P/PLANCK/R3/HFI857", "hips_creation_date":"2022-10-26T10:17Z", "hips_estsize":"291476", "hips_nb_tiles":"2042", "hips_check_code":"png:3351337634 fits:269935101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_857_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/HFI_SkyMap_857_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288493062"}, +{ "ID":"CDS/P/PLANCK/R3/LFI/color", "hips_initial_fov":"0.4580648549089874", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/LFI/color", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 LFI Color composition 30-44-70 GHz", "obs_collection":"PLANCK R3 LFI Color composition 30-44-70 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.0042827494", "em_max":"0.009993081933333", "client_category":"Image/Radio/PLANCK/R3/HFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-11-07T13:38Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"128", "hips_status":"public master clonableOnce", "hips_pixel_scale":"0.05726", "dataproduct_type":"image", "hips_rgb_red":"PLANCK R3 frequency LFI map 30 GHz [-3.6E-4 0.0011315 0.002623 Linear]", "hips_rgb_green":"PLANCK R3 frequency LFI map 44 GHz [-5.916E-4 0.0020472000000000003 0.004686 Sqrt]", "hips_rgb_blue":"PLANCK R3 frequency LFI map 70 GHz [-7.446E-4 0.0015612000000000002 0.003867 Sqrt]", "hipsgen_date":"2022-11-07T13:38Z", "hipsgen_params":"inRed=LFI_SkyMap_30_R3 inGreen=LFI_SkyMap_44_R3 inBlue=LFI_SkyMap_70_R3 out=couleurLFI/ creator_did=CDS/P/PLANCK/R3/LFI/color RGB \"cmRed=-3.6E-4 0.0011315 0.002623 Linear\" \"cmGreen=-5.916E-4 0.0020472000000000003 0.004686 Sqrt\" \"cmBlue=-7.446E-4 0.0015612000000000002 0.003867 Sqrt\" hips_tile_width=128 RGB", "hips_creation_date":"2022-11-07T13:38Z", "hips_hierarchy":"median", "hips_tile_format":"png", "dataproduct_subtype":"color", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/LFI_Color_30_44_70", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/LFI_Color_30_44_70", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288492730"}, +{ "ID":"CDS/P/PLANCK/R3/LFI30", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/LFI30", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency LFI map 30 GHz", "obs_collection":"PLANCK R3 LFI 30 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.009993081933333", "em_max":"0.009993081933333", "client_category":"Image/Radio/PLANCK/R3/LFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T13:18Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"128", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-1.856E-4 0.05743", "hips_data_range":"-0.1081 0.3228", "hips_pixel_scale":"0.05726", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T13:17Z", "hipsgen_params":"in=../../Frequency-maps_Single-frequency/LFI_SkyMap_030-BPassCorrected_1024_R3.00_full.fits out=HiPS_LFI_SkyMap_30/ creator_did=CDS/P/PLANCK/R3/LFI30", "hips_creation_date":"2022-10-26T13:17Z", "hips_estsize":"86960", "hips_nb_tiles":"2042", "hips_check_code":"png:2686887806 fits:4278383101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/LFI_SkyMap_30_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/LFI_SkyMap_30_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288493118"}, +{ "ID":"CDS/P/PLANCK/R3/LFI44", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/LFI44", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency LFI map 44 GHz", "obs_collection":"PLANCK R3 LFI 44 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.006813464954545", "em_max":"0.006813464954545", "client_category":"Image/Radio/PLANCK/R3/LFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T13:18Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"128", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-2.378E-4 0.02326", "hips_data_range":"-0.05799 0.1721", "hips_pixel_scale":"0.05726", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T13:18Z", "hipsgen_params":"in=../../Frequency-maps_Single-frequency/LFI_SkyMap_044-BPassCorrected_1024_R3.00_full.fits out=HiPS_LFI_SkyMap_44/ creator_did=CDS/P/PLANCK/R3/LFI44", "hips_creation_date":"2022-10-26T13:18Z", "hips_estsize":"91012", "hips_nb_tiles":"2042", "hips_check_code":"png:1098360562 fits:4278383101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/LFI_SkyMap_44_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/LFI_SkyMap_44_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288493190"}, +{ "ID":"CDS/P/PLANCK/R3/LFI70", "hips_initial_fov":"58.63230142835039", "hips_initial_ra":"0", "hips_initial_dec":"+0", "creator_did":"ivo://CDS/P/PLANCK/R3/LFI70", "hips_creator":"Buga M. (CDS)", "hips_copyright":"CNRS/Unistra", "obs_title":"PLANCK R3 frequency LFI map 70 GHz", "obs_collection":"PLANCK R3 LFI 70 GHz", "obs_description":"Planck is ESA's mission to observe the first light in the Universe. Planck was launched on 14 May 2009, and the minimum requirement for success was for the spacecraft to complete two whole surveys of the sky. In the end, Planck worked perfectly for 30 months, about twice the span originally required, and completed five full-sky surveys with both instruments. Able to work at slightly higher temperatures than HFI, the Low Frequency Instrument (LFI) continued to survey the sky for a large part of 2013, providing even more data to improve the Planck final results. Planck was turned off on 23 October 2013. The high-quality data the mission has produced will continue to be scientifically explored in the years to come.", "obs_ack":"ESA and the Planck Collaboration", "prov_progenitor":"ESA", "bib_reference":"2020A&A...641A...1P", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2020A%26A...641A...1P/abstract", "obs_copyright":"EUROPEAN SPACE AGENCY. ALL RIGHTS RESERVED", "obs_copyright_url":"https://www.cosmos.esa.int/web/planck", "t_min":"55054.9166667", "t_max":"56587.9166667", "obs_regime":"Radio", "em_min":"0.0042827494", "em_max":"0.0042827494", "client_category":"Image/Radio/PLANCK/R3/LFI", "hips_builder":"Aladin/HipsGen v12.013", "hips_version":"1.4", "hips_release_date":"2022-10-26T13:19Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"128", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-2.741E-4 0.00991", "hips_data_range":"-0.07587 0.2254", "hips_pixel_scale":"0.05726", "dataproduct_type":"image", "hipsgen_date":"2022-10-26T13:19Z", "hipsgen_params":"in=../../Frequency-maps_Single-frequency/LFI_SkyMap_070-BPassCorrected_1024_R3.00_full.fits out=HiPS_LFI_SkyMap_70/ creator_did=CDS/P/PLANCK/R3/LFI70", "hips_creation_date":"2022-10-26T13:19Z", "hips_estsize":"94756", "hips_nb_tiles":"2042", "hips_check_code":"png:150953168 fits:4278383101", "hips_service_url":"https://alasky.cds.unistra.fr/PLANCK/R3/LFI_SkyMap_70_R3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/PLANCK/R3/LFI_SkyMap_70_R3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_sky_fraction":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288493258"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/I/11GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/I/11GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 11GHz Intensity", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.02727272727", "em_max":"0.02727272727", "client_category":"Image/Radio/QUIJOTE/DR1/Intensity", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-10T09:14Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-1.281 30", "hips_data_range":"-322.8 961.2", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-10T08:56Z", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_11GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_11GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288494714"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/I/13GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/I/13GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 13GHz Intensity", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.02307692307", "em_max":"0.02307692307", "client_category":"Image/Radio/QUIJOTE/DR1/Intensity", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-10T09:19Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-1.281 30", "hips_data_range":"-231.6 686.8", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-10T07:57Z", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_13GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_13GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288494778"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/I/17GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/I/17GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 17GHz Intensity", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.01764705882", "em_max":"0.01764705882", "client_category":"Image/Radio/QUIJOTE/DR1/Intensity", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-10T09:50Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-3.177 50", "hips_data_range":"-129.7 368.7", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-10T07:57Z", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_17GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_17GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288494834"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/I/19GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/I/19GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 19GHz Intensity", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.01578947368", "em_max":"0.01578947368", "client_category":"Image/Radio/QUIJOTE/DR1/Intensity", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-10T09:53Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"-3.041 61.26", "hips_data_range":"-100.9 282.6", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-10T07:57Z", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_19GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_I_19GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288494890"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/P/11GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/P/11GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 11GHz Polarization", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.02727272727", "em_max":"0.02727272727", "client_category":"Image/Radio/QUIJOTE/DR1/Polarization", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-14T08:45Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0.01065 2.5", "hips_data_range":"-10.54 31.62", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-10T08:56Z", "hips_rgb_red":"TFIELD1 [-0.1923 14.36 70.49 Linear]", "hips_rgb_green":"TFIELD1 [-0.1389 67.11 317.5 Linear]", "hips_rgb_blue":"TFIELD1 [-0.4765 37.97 185.9 Linear]", "hips_hierarchy":"median", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_11GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_11GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288494958"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/P/13GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/P/13GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 13GHz Polarization", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.02307692307", "em_max":"0.02307692307", "client_category":"Image/Radio/QUIJOTE/DR1/Polarization", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-14T09:26Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0.01065 2.5", "hips_data_range":"-8.084 24.25", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-14T09:23Z", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_13GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_13GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288495038"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/P/17GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/P/17GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 17GHz Polarization", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.01764705882", "em_max":"0.01764705882", "client_category":"Image/Radio/QUIJOTE/DR1/Polarization", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-14T09:09Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0.003608 1.692", "hips_data_range":"-4.453 13.36", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-14T09:06Z", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_17GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_17GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288495090"}, +{ "ID":"CDS/P/QUIJOTE/DR1/MFI/P/19GHz", "hips_initial_fov":"360.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "creator_did":"ivo://CDS/P/QUIJOTE/DR1/MFI/P/19GHz", "hips_creator":"QUIJOTE collaboration", "hips_copyright":"CNRS/Unistra", "obs_title":"QUIJOTE MFI DR1 19GHz Polarization", "obs_collection":"QUIJOTE MFI DR1", "obs_description":"The QUIJOTE (Q-U-I JOint TEnerife) CMB Experiment is a scientific collaboration between the Instituto de Astrofisica de Canarias (Tenerife, Spain), the Instituto de Fisica de Cantabria (Santander, Spain), the Departamento de Ingenieria de COMunicaciones (Santander, Spain), the Jodrell Bank Observatory (Manchester, UK), the Cavendish Laboratory (Cambridge, UK), and the IDOM company (Spain). It started operations in November 2012, and it consists in two telescopes and three instruments dedicated to measure the polarization of the microwave sky in the frequency range between 10 GHz and 40GHz, and at angular scales of one degree. We present QUIJOTE intensity and polarization maps in four frequency bands centred around 11, 13, 17 and 19 GHz, and covering approximately 30 000 deg2, including most of the Northern sky region. These maps result from 9 000 hours of observations taken between May 2013 and June 2018 with the first QUIJOTE instrument (MFI), and have angular resolutions of around one degree, and sensitivities in polarization within the range 35-40 microkelvin per 1-degree beam, being a factor 2-4 worse in intensity.", "obs_ack":"Please acknowledge the use of the QUIJOTE MFI wide survey data products by: citing the main QUIJOTE MFI wide survey paper ( Rubino-Martin et al. 2023 ), and if using derived products, the relevant associated paper(s); and adding an acknowledgment statement: \"Some of the presented results are based on observations obtained with the QUIJOTE experiment ( http://research.iac.es/proyecto/quijote )\".", "prov_progenitor":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "bib_reference":"2023MNRAS.519.3383R", "bib_reference_url":"https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.3383R", "obs_copyright_url":"https://research.iac.es/proyecto/quijote/pages/en/data/mfi-wide-survey.php", "obs_regime":"Radio", "em_min":"0.01578947368", "em_max":"0.01578947368", "client_category":"Image/Radio/QUIJOTE/DR1/Polarization", "hips_builder":"Aladin/HipsGen v11.024", "hips_version":"1.4", "hips_release_date":"2023-01-14T09:34Z", "hips_frame":"galactic", "hips_order":"3", "hips_order_min":"0", "hips_tile_width":"64", "hips_status":"public master clonableOnce", "hips_tile_format":"png fits", "hips_pixel_bitpix":"-32", "hips_pixel_cut":"0.003608 1.692", "hips_data_range":"-3.556 10.67", "hips_pixel_scale":"1.832", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"8", "hips_creation_date":"2023-01-14T09:32Z", "hips_service_url":"https://alasky.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_19GHz", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/QUIJOTE/DR1/MFI/CDS_P_QUIJOTE_DR1_MFI_P_19GHz", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"7", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288495146"}, +{ "ID":"CDS/P/RASS", "creator_did":"ivo://CDS/P/RASS", "obs_collection":"RASS", "obs_title":"ROSAT X-Ray All-Sky Survey", "obs_description":"The ROSAT All-Sky X-ray Survey was obtained during 1990/1991 using the ROSAT Position Sensitive Proportional Counter (PSPC) in combination with the ROSAT X-ray Telescope (XRT).", "obs_copyright":"Distributed by MPE - HEALPixed by CDS", "obs_copyright_url":"http://www.mpe.mpg.de/xray/home.php", "client_category":"Image/X/ROSAT", "client_sort_key":"01-02", "hips_creation_date":"2014-03-29T13:46Z", "hips_release_date":"2019-05-05T06:52Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"4", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/RASS/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"100.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"NASA/HEASARC", "bib_reference":"1999A&A...349..389V", "bib_reference_url":"http://simbad.u-strasbg.fr/simbad/sim-ref?bibcode=1999A%26A...349..389V&simbo=on", "t_min":"48058", "t_max":"48602", "obs_regime":"X-ray", "em_min":"5.1660e-10", "em_max":"1.2398e-8", "hips_pixel_scale":"0.007157", "moc_sky_fraction":"1", "hips_estsize":"2247318", "hips_order_min":"0", "hips_pixel_bitpix":"16", "hipsgen_date":"2019-05-05T06:52Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/RASS UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/RASS", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/RASS", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"4", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"3.6645188392718993", "TIMESTAMP":"1721288418230"}, +{ "ID":"CDS/P/SHASSA/DU", "creator_did":"ivo://CDS/P/SHASSA/DU", "obs_collection":"SHASSA DU", "obs_title":"SHASSA DU - Continuum", "obs_description":"The Southern H-Alpha Sky Survey Atlas is the product of a wide-angle digital imaging survey of the H-alpha emission from the warm ionized interstellar gas of our Galaxy. This atlas covers the southern hemisphere sky (declinations less than +15 degrees). The observations were taken with a robotic camera operating at Cerro Tololo Inter-American Observatory (CTIO) in Chile.", "obs_copyright":"By courtesy of Swarthmore College Incorporated", "obs_copyrigh_url":"http://amundsen.astro.swarthmore.edu/SHASSA/index.html", "client_category":"Image/Gas-lines/Halpha", "client_sort_key":"06-02-02", "hips_creation_date":"2011-02-01T12:00Z", "hips_release_date":"2019-05-05T07:17Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"4", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"-20 8000", "moc_access_url":"http://alasky.u-strasbg.fr/SHASSA-DU/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"1.0921117184376416E-7", "hips_initial_ra":"45.0", "hips_initial_dec":"7.114779961355992E-8", "hips_copyright":"CNRS/Unistra", "obs_ack":"the Southern H-Alpha Sky Survey Atlas (SHASSA), which is supported by the National Science Foundation", "prov_progenitor":"SHASSA", "bib_reference":"2001PASP..113.1326G", "bib_reference_url":"http://simbad.u-strasbg.fr/simbad/sim-ref?bibcode=2001PASP..113.1326G&simbo=on", "obs_copyright_url":"http://amundsen.astro.swarthmore.edu/SHASSA/ack.html", "t_min":"50753", "t_max":"51847", "obs_regime":"Optical", "em_min":"6.56e-7", "em_max":"6.56e-7", "hips_pixel_scale":"0.007157", "moc_sky_fraction":"1", "hips_estsize":"4504376", "hips_order_min":"0", "hips_pixel_bitpix":"32", "hipsgen_date":"2019-05-05T07:17Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/SHASSA-v2/SHASSA-DU UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/SHASSA-DU", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/SHASSA-DU", "hips_status_1":"public mirror clonableOnce", "TIMESTAMP":"1721288418890"}, +{ "ID":"CDS/P/SHASSA/FL", "creator_did":"ivo://CDS/P/SHASSA/FL", "obs_collection":"SHASSA FL", "obs_title":"SHASSA FL - Continuum subtract.", "obs_description":"The Southern H-Alpha Sky Survey Atlas is the product of a wide-angle digital imaging survey of the H-alpha emission from the warm ionized interstellar gas of our Galaxy. This atlas covers the southern hemisphere sky (declinations less than +15 degrees). The observations were taken with a robotic camera operating at Cerro Tololo Inter-American Observatory (CTIO) in Chile.", "obs_copyright":"By courtesy of Swarthmore College Incorporated", "client_category":"Image/Gas-lines/Halpha", "client_sort_key":"06-02-03", "hips_creation_date":"2011-02-01T12:00Z", "hips_release_date":"2019-05-05T07:17Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"4", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"-200 5000", "moc_access_url":"http://alasky.u-strasbg.fr/SHASSA-FL/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"1.0921117184376416E-7", "hips_initial_ra":"45.0", "hips_initial_dec":"7.114779961355992E-8", "hips_copyright":"By courtesy of Swarthmore College Incorporated", "obs_ack":"the Southern H-Alpha Sky Survey Atlas (SHASSA), which is supported by the National Science Foundation", "prov_progenitor":"SHASSA", "bib_reference":"2001PASP..113.1326G", "bib_reference_url":"http://simbad.u-strasbg.fr/simbad/sim-ref?bibcode=2001PASP..113.1326G&simbo=on", "obs_copyright_url":"http://amundsen.astro.swarthmore.edu/SHASSA/ack.html", "t_min":"50753", "t_max":"51847", "obs_regime":"Optical", "em_min":"6.56e-7", "em_max":"6.56e-7", "hips_pixel_scale":"0.007157", "moc_sky_fraction":"1", "hips_estsize":"4504376", "hips_order_min":"0", "hips_pixel_bitpix":"32", "hipsgen_date":"2019-05-05T07:17Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/SHASSA-v2/SHASSA-FL UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/SHASSA-FL", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/SHASSA-FL", "hips_status_1":"public mirror clonableOnce", "TIMESTAMP":"1721288418946"}, +{ "ID":"CDS/P/SHASSA/H", "creator_did":"ivo://CDS/P/SHASSA/H", "obs_collection":"SHASSA H", "obs_title":"SHASSA H - H-alpha emission", "obs_description":"The Southern H-Alpha Sky Survey Atlas is the product of a wide-angle digital imaging survey of the H-alpha emission from the warm ionized interstellar gas of our Galaxy. This atlas covers the southern hemisphere sky (declinations less than +15 degrees). The observations were taken with a robotic camera operating at Cerro Tololo Inter-American Observatory (CTIO) in Chile.", "obs_copyright":"By courtesy of Swarthmore College Incorporated", "obs_copyright_url":[ "http://amundsen.astro.swarthmore.edu/SHASSA/index.html", "http://amundsen.astro.swarthmore.edu/SHASSA/ack.html"], "client_category":"Image/Gas-lines/Halpha", "client_sort_key":"06-02-01", "hips_creation_date":"2010-12-13T12:00Z", "hips_release_date":"2019-05-05T07:18Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"4", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"-10 20000", "moc_access_url":"http://alasky.u-strasbg.fr/SHASSA-H3/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"1.0921117184376416E-7", "hips_initial_ra":"45.0", "hips_initial_dec":"7.114779961355992E-8", "hips_copyright":"CNRS/Unistra", "obs_ack":"the Southern H-Alpha Sky Survey Atlas (SHASSA), which is supported by the National Science Foundation", "prov_progenitor":"SHASSA", "bib_reference":"2001PASP..113.1326G", "bib_reference_url":"http://simbad.u-strasbg.fr/simbad/sim-ref?bibcode=2001PASP..113.1326G&simbo=on", "t_min":"50753", "t_max":"51847", "obs_regime":"Optical", "em_min":"6.56e-7", "em_max":"6.56e-7", "hips_pixel_scale":"0.007157", "moc_sky_fraction":"1", "hips_estsize":"4504376", "hips_order_min":"0", "hips_pixel_bitpix":"32", "hipsgen_date":"2019-05-05T07:18Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/SHASSA-v2/SHASSA-H3 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/SHASSA-H3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/SHASSA-H3", "hips_status_1":"public mirror clonableOnce", "TIMESTAMP":"1721288419002"}, +{ "ID":"CDS/P/SPITZER/MIPS1", "creator_did":"ivo://CDS/P/SPITZER/MIPS1", "obs_collection":"SPITZER MIPS1", "obs_title":"MIPS1 survey in Healpix", "obs_description":"Composite map from Spitzer Legacy Programs MIPSGAL: A 24 and 70 Micron Survey of the Inner Galactic Disk with MIPS (Carey S.) C2D: From Molecular Cores to Planet-Forming Disks (Evans N.) Taurus 2: Finishing the Spitzer Map of the Taurus Molecular Clouds (Padgett D.) SAGE: Spitzer Survey of the Large Magellanic Cloud: Surveying the Agents of a Galaxy's Evolution (Meixner M.) SAGE-SMC: Surveying the Agents of Galaxy Evolution in the Tidally- Disrupted, Low-Metallicity Small Magellanic Cloud (Gordon K.) SINGS: The Spitzer Infrared Nearby Galaxies Survey - Physics of the Star-Forming ISM and Galaxy Evolution (Kennicutt R.)", "obs_copyright":"Spitzer mission - JPL/NASA", "client_category":"Image/Infrared/Spitzer", "client_sort_key":"04-03-05", "hips_release_date":"2023-04-18T09:42Z", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"8", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.unistra.fr/MIPS1/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"JPL/NASA", "bib_reference":[ "2009PASP..121...76C", "2003PASP..115..965E", "2006ApJ...645.1283P", "2006AJ....132.2268M", "2011AJ....142..102G", "2003PASP..115..928K"], "bib_reference_url":[ "http://adsabs.harvard.edu/abs/2009PASP..121...76C", "http://adsabs.harvard.edu/abs/2003PASP..115..965E", "http://adsabs.harvard.edu/abs/2006ApJ...645.1283P", "http://adsabs.harvard.edu/abs/2006AJ....132.2268M", "http://adsabs.harvard.edu/abs/2011AJ....142..102G", "http://adsabs.harvard.edu/abs/2003PASP..115..928K"], "obs_copyright_url":"https://www.jpl.nasa.gov/copyrights.php", "t_min":"52876", "t_max":"55195", "obs_regime":"Infrared", "em_min":"1.98889e-05", "em_max":"3.09383e-05", "hips_builder":"Aladin/HipsGen v12.044", "hips_creation_date":"2011-07-04T15:11Z", "hips_pixel_bitpix":"-32", "hips_pixel_scale":"0.229", "moc_sky_fraction":"1", "hips_pixel_cut":"0 55", "hipsgen_date":"2017-03-27T11:44Z", "hipsgen_params":"out=MIPS1 \"-pixelCut=0 55\" UPDATE", "hips_initial_fov":"0.2290324274544937", "hips_initial_ra":"45.0", "hips_initial_dec":"0.14920792779581243", "hips_order_min":"0", "hipsgen_date_1":"2019-05-05T07:21Z", "hipsgen_params_1":"out=/asd-volumes/sc1-asd-volume10/MIPS1 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/Spitzer/MIPS1", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/Spitzer/MIPS1", "hips_status_1":"public mirror clonableOnce", "TIMESTAMP":"1721288419546"}, +{ "ID":"CDS/P/WISE/W1", "creator_did":"ivo://CDS/P/WISE/W1", "obs_collection":"WISE W1", "obs_title":"WISE W1 (3.4um)", "obs_description":"Wide-field Infrared Survey Explore (WISE) is a MIDEX (medium class Explorer) mission funded by NASA. The WISE short-wavelength channels employ 4.2 and 5.4um cutoff HgCdTe arrays fabricated by Teledyne Imaging Sensors with 1024x1024 pixels each 18 um square. WISE W1 (3.4um) from raw Atlas Images (not background matched nor zodi-corrected). Resampled in Healpix by Frank Masci (IPAC). The spatial resolution is limited to 12 arcsec.", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE/Low", "client_sort_key":"04-003-XX-01", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_status":"public master clonableOnce", "obs_ack":"This publication makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, funded by the National Aeronautics and Space Administration", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55210", "t_max":"55530", "em_min":"2.754e-6", "em_max":"3.8723e-6", "hips_builder":"Aladin/HipsGen v10.123", "hips_release_date":"2019-05-07T11:10Z", "hips_pixel_bitpix":"-32", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "hips_copyright":"CNRS/Unistra", "obs_regime":"Infrared", "moc_sky_fraction":"1", "hips_estsize":"17797289", "hips_creation_date":"2012-04-05T13:30Z", "hips_order_min":"0", "hipsgen_date":"2019-05-07T11:10Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume8/WISE/W1 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WISE/W1", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WISE/W1", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420430"}, +{ "ID":"CDS/P/WISE/W2", "creator_did":"ivo://CDS/P/WISE/W2", "obs_collection":"WISE W2", "obs_title":"WISE W2 (4.6um)", "obs_description":"Wide-field Infrared Survey Explore (WISE) is a MIDEX (medium class Explorer) mission funded by NASA. The WISE short-wavelength channels employ 4.2 and 5.4um cutoff HgCdTe arrays fabricated by Teledyne Imaging Sensors with 1024x1024 pixels each 18 um square. WISE W2 (4.6um) from raw Atlas Images (not background matched nor zodi-corrected).Resampled in Healpix by Frank Masci (IPAC). The spatial resolution is limited to 12 arcsec.", "obs_copyright":"University of Massachusetts & IPAC/Caltech", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE/Low", "client_sort_key":"04-003-XX-02", "hips_release_date":"2019-05-07T11:12Z", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_status":"public master clonableOnce", "obs_ack":"This publication makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, funded by the National Aeronautics and Space Administration", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55210", "t_max":"55530", "em_min":"3.9633e-06", "em_max":"5.3413e-06", "hips_builder":"Aladin/HipsGen v10.123", "hips_creation_date":"2012-04-05T15:29Z", "hips_pixel_bitpix":"-32", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "hips_copyright":"CNRS/Unistra", "obs_regime":"Infrared", "moc_sky_fraction":"1", "hips_estsize":"17797289", "hips_order_min":"0", "hipsgen_date":"2019-05-07T11:12Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume8/WISE/W2 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WISE/W2", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WISE/W2", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420482"}, +{ "ID":"CDS/P/WISE/W3", "creator_did":"ivo://CDS/P/WISE/W3", "obs_collection":"WISE W3", "obs_title":"WISE W3 (12um)", "obs_description":"Wide-field Infrared Survey Explore (WISE) is a MIDEX (medium class Explorer) mission funded by NASA. The WISE short-wavelength channels employ 4.2 and 5.4um cutoff HgCdTe arrays fabricated by Teledyne Imaging Sensors with 1024x1024 pixels each 18 um square. WISE W3 (12um) from raw Atlas Images (not background matched nor zodi-corrected). Resampled in Healpix by Frank Masci (IPAC). The spatial resolution is limited to 12 arcsec.", "obs_copyright":"WISE acknowledgment", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE/Low", "client_sort_key":"04-003-XX-03", "hips_release_date":"2019-05-07T11:14Z", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_status":"public master clonableOnce", "obs_ack":"This publication makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, funded by the National Aeronautics and Space Administration", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55210", "t_max":"55530", "em_min":"7.443e-6", "em_max":"1.72613e-5", "hips_builder":"Aladin/HipsGen v10.123", "hips_creation_date":"2012-04-05T16:29Z", "hips_pixel_bitpix":"-32", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "hips_copyright":"CNRS/Unistra", "obs_regime":"Infrared", "moc_sky_fraction":"1", "hips_estsize":"17797289", "hips_order_min":"0", "hipsgen_date":"2019-05-07T11:14Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume8/WISE/W3 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WISE/W3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WISE/W3", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420534"}, +{ "ID":"CDS/P/WISE/W4", "creator_did":"ivo://CDS/P/WISE/W4", "obs_collection":"WISE W4", "obs_title":"WISE W4 (22um)", "obs_description":"Wide-field Infrared Survey Explore (WISE) is a MIDEX (medium class Explorer) mission funded by NASA. The WISE short-wavelength channels employ 4.2 and 5.4um cutoff HgCdTe arrays fabricated by Teledyne Imaging Sensors with 1024x1024 pixels each 18 um square. WISE W1 (3.4um) from raw Atlas Images (not background matched nor zodi-corrected). Resampled in Healpix by Frank Masci (IPAC). The spatial resolution is limited to 12 arcsec.", "obs_copyright":"WISE acknowledgment", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE/Low", "client_sort_key":"04-003-XX-04", "hips_release_date":"2019-05-07T11:15Z", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"5", "hips_frame":"galactic", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_status":"public master clonableOnce", "obs_ack":"This publication makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, funded by the National Aeronautics and Space Administration", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55210", "t_max":"55530", "em_min":"1.952e-5", "em_max":"2.79107e-5", "hips_builder":"Aladin/HipsGen v10.123", "hips_creation_date":"2012-04-10T06:02Z", "hips_pixel_bitpix":"-32", "hips_hierarchy":"mean", "hips_pixel_scale":"0.003579", "hips_initial_fov":"130.0", "hips_initial_ra":"266.4150089", "hips_initial_dec":"-29.0061110", "hips_copyright":"CNRS/Unistra", "obs_regime":"Infrared", "moc_sky_fraction":"1", "hips_estsize":"17797289", "hips_order_min":"0", "hipsgen_date":"2019-05-07T11:15Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume8/WISE/W4 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WISE/W4", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WISE/W4", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4150089", "obs_initial_dec":"-29.0061110", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420590"}, +{ "ID":"CDS/P/WISE/WSSA/12um", "creator_did":"ivo://CDS/P/WISE/WSSA/12um", "obs_collection":"WISE WSSA 12um", "obs_title":"Diffuse dust 12um WSSA (Meisner & Finkbeiner 2013)", "obs_description":"Diffuse Galactic dust emission at 12um from the processing of the Wide-field Infrared Survey Explorer (WISE) data set by Meisner & Finkbeiner (2013). The 430 WISE Sky Survey Atlas (WSSA) tiles were resampled in Healpix by Thomas Boch (CDS). The spatial resolution is limited to 15 arcsec.", "obs_copyright":"Meisner & Finkbeiner (2013)", "obs_copyright_url":"http://faun.rc.fas.harvard.edu/ameisner/wssa/", "client_category":"Image/Infrared/WISE/WSSA", "client_sort_key":"04-003-01-01", "hips_release_date":"2019-05-05T07:49Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"CDS", "hips_version":"1.4", "hips_order":"7", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/WSSA/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"1.0921117184376416E-7", "hips_initial_ra":"0", "hips_initial_dec":"+0", "hips_copyright":"CNRS/Unistra", "obs_ack":"see 2014ApJ...781....5M", "prov_progenitor":"Meisner & Finkbeiner (2013)", "bib_reference":"2014ApJ...781....5M", "bib_reference_url":"https://ui.adsabs.harvard.edu/?#abs/2014ApJ...781....5M", "t_min":"55210", "t_max":"55530", "obs_regime":"Infrared", "em_min":"1.2e-5", "em_max":"1.2e-5", "hips_pixel_scale":"8.946E-4", "moc_sky_fraction":"1", "hips_estsize":"284756513", "hips_creation_date":"2014-04-17T22:08Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T07:49Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/WSSA UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WSSA", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WSSA", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"7", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.4580648549089874", "TIMESTAMP":"1721288420658"}, +{ "ID":"CDS/P/WMAP/K/9yr", "creator_did":"ivo://CDS/P/WMAP/K/9yr", "obs_collection":"WMAP K 9yr", "obs_title":"WMAP K - 9yr", "obs_description":"The WMAP (Wilkinson Microwave Anisotropy Probe) mission is designed to determine the geometry, content, and evolution of the universe.The K-band is centered at 13 mm (23 GHz), its beam size is 0.88 deg (square-root of the beam solid angle).", "obs_copyright_url":"http://lambda.gsfc.nasa.gov/product/map/dr5/maps_band_r9_i_9yr_get.cfm", "prov_progenitor":"HEASARC/LAMBDA", "client_category":"Image/Radio/WMAP", "client_sort_key":"05-03-05", "hips_release_date":"2019-05-05T07:50Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 10", "hips_data_range":"-2 200", "moc_access_url":"http://alasky.u-strasbg.fr/WMAP9yr/WMAPK9yr/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"0.2290324274544937", "hips_initial_ra":"0", "hips_initial_dec":"+0", "hips_copyright":"CNRS/Unistra", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.\"", "bib_reference":"2013ApJS..208...20B", "bib_reference_url":"http://adsabs.harvard.edu/abs/2013ApJS..208...20B", "obs_copyright":"HEASARC/LAMBDA", "t_min":"52131", "t_max":"55427", "obs_regime":"Radio", "em_min":"1.17e-2", "em_max":"1.57e-2", "hips_tile_width":"64", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1126099", "hips_creation_date":"2014-11-25T18:10Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T07:50Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/WMAP9yr/WMAPK9yr UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WMAP9yr/WMAPK9yr", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WMAP9yr/WMAPK9yr", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420714"}, +{ "ID":"CDS/P/WMAP/Ka/9yr", "creator_did":"ivo://CDS/P/WMAP/Ka/9yr", "obs_collection":"WMAP Ka 9yr", "obs_title":"WMAP Ka - 9yr", "obs_description":"The WMAP (Wilkinson Microwave Anisotropy Probe) mission is designed to determine the geometry, content, and evolution of the universe.The Ka-band is centered at 9.1 mm (33 GHz), its beam size is 0.66 deg (square-root of the beam solid angle).", "obs_copyright_url":"http://lambda.gsfc.nasa.gov/product/map/dr5/maps_band_r9_i_9yr_get.cfm", "prov_progenitor":"HEASARC/LAMBDA", "client_category":"Image/Radio/WMAP", "client_sort_key":"05-03-04", "hips_release_date":"2019-05-05T07:52Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 10", "hips_data_range":"-2 200", "moc_access_url":"http://alasky.u-strasbg.fr/WMAP9yr/WMAPKa9yr/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"0.2290324274544937", "hips_initial_ra":"0", "hips_initial_dec":"+0", "hips_copyright":"CNRS/Unistra", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.\"", "bib_reference":"2013ApJS..208...20B", "bib_reference_url":"http://adsabs.harvard.edu/abs/2013ApJS..208...20B", "obs_copyright":"HEASARC/LAMBDA", "t_min":"52131", "t_max":"55427", "obs_regime":"Radio", "em_min":"7.94e-3", "em_max":"1.04e-2", "hips_tile_width":"64", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1126099", "hips_creation_date":"2014-11-25T18:12Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T07:52Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/WMAP9yr/WMAPKa9yr UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WMAP9yr/WMAPKa9yr", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WMAP9yr/WMAPKa9yr", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420798"}, +{ "ID":"CDS/P/WMAP/Q/9yr", "creator_did":"ivo://CDS/P/WMAP/Q/9yr", "obs_collection":"WMAP Q 9yr", "obs_title":"WMAP Q - 9yr", "obs_description":"The WMAP (Wilkinson Microwave Anisotropy Probe) mission is designed to determine the geometry, content, and evolution of the universe.The Q-band is centered at 7.3 mm (41 GHz), its beam size is 0.51 deg (square-root of the beam solid angle).", "obs_copyright_url":"http://lambda.gsfc.nasa.gov/product/map/dr5/maps_band_r9_i_9yr_get.cfm", "prov_progenitor":"HEASARC/LAMBDA", "client_category":"Image/Radio/WMAP", "client_sort_key":"05-03-03", "hips_release_date":"2019-05-05T07:52Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 10", "hips_data_range":"-2 200", "moc_access_url":"http://alasky.u-strasbg.fr/WMAP9yr/WMAPQ9yr/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"0.2290324274544937", "hips_initial_ra":"0", "hips_initial_dec":"+0", "hips_copyright":"CNRS/Unistra", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.\"", "bib_reference":"2013ApJS..208...20B", "bib_reference_url":"http://adsabs.harvard.edu/abs/2013ApJS..208...20B", "obs_copyright":"HEASARC/LAMBDA", "t_min":"52131", "t_max":"55427", "obs_regime":"Radio", "em_min":"6.43e-3", "em_max":"8.61e-3", "hips_tile_width":"64", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1126099", "hips_creation_date":"2014-11-25T18:14Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T07:52Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/WMAP9yr/WMAPQ9yr UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WMAP9yr/WMAPQ9yr", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WMAP9yr/WMAPQ9yr", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420854"}, +{ "ID":"CDS/P/WMAP/V/9yr", "creator_did":"ivo://CDS/P/WMAP/V/9yr", "obs_collection":"WMAP V 9yr", "obs_title":"WMAP V - 9yr", "obs_description":"The WMAP (Wilkinson Microwave Anisotropy Probe) mission is designed to determine the geometry, content, and evolution of the universe.The V-band is centered at 4.9 mm (61 GHz), its beam size is 0.35 deg (square-root of the beam solid angle).", "obs_copyright_url":"http://lambda.gsfc.nasa.gov/product/map/dr5/maps_band_r9_i_9yr_get.cfm", "prov_progenitor":"HEASARC/LAMBDA", "client_category":"Image/Radio/WMAP", "client_sort_key":"05-03-02", "hips_release_date":"2019-05-05T07:53Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "moc_access_url":"http://alasky.u-strasbg.fr/WMAP9yr/WMAPV9yr/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"0.2290324274544937", "hips_initial_ra":"0", "hips_initial_dec":"+0", "hips_copyright":"CNRS/Unistra", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.\"", "bib_reference":"2013ApJS..208...20B", "bib_reference_url":"http://adsabs.harvard.edu/abs/2013ApJS..208...20B", "obs_copyright":"HEASARC/LAMBDA", "t_min":"52131", "t_max":"55427", "obs_regime":"Radio", "em_min":"4.31e-3", "em_max":"5.73e-3", "hips_frame":"galactic", "hips_tile_width":"64", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1126099", "hips_creation_date":"2014-11-25T18:15Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T07:53Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/WMAP9yr/WMAPV9yr UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WMAP9yr/WMAPV9yr", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WMAP9yr/WMAPV9yr", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420906"}, +{ "ID":"CDS/P/WMAP/W/9yr", "creator_did":"ivo://CDS/P/WMAP/W/9yr", "obs_collection":"WMAP W 9yr", "obs_title":"WMAP W - 9yr", "obs_description":"The WMAP (Wilkinson Microwave Anisotropy Probe) mission is designed to determine the geometry, content, and evolution of the universe.The W-band is centered at 3.2 mm (94 GHz), its beam size is 0.22 deg (square-root of the beam solid angle).", "obs_copyright_url":"http://lambda.gsfc.nasa.gov/product/map/dr5/maps_band_r9_i_9yr_get.cfm", "prov_progenitor":"HEASARC/LAMBDA", "client_category":"Image/Radio/WMAP", "client_sort_key":"05-03-01", "hips_release_date":"2019-05-05T07:53Z", "hips_builder":"Aladin/HipsGen v10.123", "hips_creator":"Fernique P. (CDS)", "hips_version":"1.4", "hips_order":"3", "hips_frame":"galactic", "hips_tile_format":"png jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 10", "hips_data_range":"-2 200", "moc_access_url":"http://alasky.u-strasbg.fr/WMAP9yr/WMAPW9yr/Moc.fits", "hips_status":"public master clonableOnce", "hips_initial_fov":"0.2290324274544937", "hips_initial_ra":"0", "hips_initial_dec":"+0", "hips_copyright":"CNRS/Unistra", "obs_ack":"We acknowledge the use of the Legacy Archive for Microwave Background Data Analysis (LAMBDA), part of the High Energy Astrophysics Science Archive Center (HEASARC). HEASARC/LAMBDA is a service of the Astrophysics Science Division at the NASA Goddard Space Flight Center.\"", "bib_reference":"2013ApJS..208...20B", "bib_reference_url":"http://adsabs.harvard.edu/abs/2013ApJS..208...20B", "obs_copyright":"HEASARC/LAMBDA", "t_min":"52131", "t_max":"55427", "obs_regime":"Radio", "em_min":"2.78e-3", "em_max":"3.71e-3", "hips_tile_width":"64", "hips_pixel_scale":"0.01431", "moc_sky_fraction":"1", "hips_estsize":"1126099", "hips_creation_date":"2014-11-25T18:16Z", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "hipsgen_date":"2019-05-05T07:53Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume10/WMAP9yr/WMAPW9yr UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/WMAP9yr/WMAPW9yr", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/WMAP9yr/WMAPW9yr", "hips_status_1":"public mirror clonableOnce", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"0", "obs_initial_dec":"+0", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288420958"}, +{ "ID":"CDS/P/allWISE/W1", "hips_doi":"10.26093/cds/aladin/32ax-d9f", "creator_did":"ivo://CDS/P/allWISE/W1", "obs_collection":"The Wide-field Infrared Survey Explorer - W1 band (allWISE W1)", "obs_title":"AllWISE W1 (3.4um) from raw Atlas Images", "obs_description":"NASA's Wide-field Infrared Survey Explorer (WISE; Wright et al.2010) mapped the sky at 3.4, 4.6, 12, and 22 um (W1, W2, W3, W4) in 2010 with an angular resolution of 6.1\", 6.4\", 6.5\", & 12.0\" in the four bands. WISE achieved 5 sigma point source sensitivities better than 0.08, 0.11, 1 and 6 mJy in unconfused regions on the ecliptic in the four bands. Sensitivity improves toward the ecliptic poles due to denser coverage and lower zodiacal background.The All-Sky Release includes all data taken during the WISE full cryogenic mission phase, 7 January 2010 to 6 August 2010, that were processed with improved calibrations and reduction algorithms.", "obs_ack":"This Progressive Survey distribution makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, and NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology.WISE and NEOWISE are funded by the National Aeronautics and Space Administration.", "obs_copyright":"IPAC/NASA", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/", "client_category":"Image/Infrared/WISE", "client_sort_key":"04-003-01", "hips_creation_date":"2014-04-15T08:59Z", "hips_release_date":"2019-05-20T08:06Z", "hips_builder":"Aladin/HipsGen v10.125", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"8", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 400", "hips_data_range":"4.296 2345", "moc_access_url":"http://alasky.u-strasbg.fr/AllWISE/W1/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AllWISE/W1/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55378", "t_max":"55414", "obs_regime":"Infrared", "em_min":"2.754e-6", "em_max":"3.8723e-6", "hips_hierarchy":"mean", "hips_pixel_scale":"4.473E-4", "hips_initial_fov":"140", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1148421127", "hipsgen_date":"2019-05-20T08:06Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume6/AllWISE/W1 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AllWISE/W1", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AllWISE/W1", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/AllWISE/W1", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288573526"}, +{ "ID":"CDS/P/allWISE/W2", "hips_doi":"10.26093/cds/aladin/1etf-s4n", "creator_did":"ivo://CDS/P/allWISE/W2", "obs_collection":"The Wide-field Infrared Survey Explorer - W2 band (allWISE W2)", "obs_title":"AllWISE W2 (4.6um) from raw Atlas Images", "obs_description":"NASA's Wide-field Infrared Survey Explorer (WISE; Wright et al.2010) mapped the sky at 3.4, 4.6, 12, and 22 um (W1, W2, W3, W4) in 2010 with an angular resolution of 6.1\", 6.4\", 6.5\", & 12.0\" in the four bands. WISE achieved 5 sigma point source sensitivities better than 0.08, 0.11, 1 and 6 mJy in unconfused regions on the ecliptic in the four bands. Sensitivity improves toward the ecliptic poles due to denser coverage and lower zodiacal background.The All-Sky Release includes all data taken during the WISE full cryogenic mission phase, 7 January 2010 to 6 August 2010, that were processed with improved calibrations and reduction algorithms.", "obs_ack":"This Progressive Survey distribution makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, and NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology.WISE and NEOWISE are funded by the National Aeronautics and Space Administration.", "obs_copyright":"IPAC/NASA", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allwise/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE", "client_sort_key":"04-003-02", "hips_release_date":"2019-05-20T08:16Z", "hips_builder":"Aladin/HipsGen v10.125", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"8", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"0 100", "hips_data_range":"7.619 143.8", "moc_access_url":"http://alasky.u-strasbg.fr/AllWISE/W2/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AllWISE/W2/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55378", "t_max":"55414", "obs_regime":"Infrared", "em_min":"3.9633e-6", "em_max":"5.3413e-6", "hips_creation_date":"2014-04-15T09:19Z", "hips_hierarchy":"mean", "hips_pixel_scale":"4.473E-4", "hips_initial_fov":"140", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1148421127", "hipsgen_date":"2019-05-20T08:16Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume6/AllWISE/W2 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AllWISE/W2", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AllWISE/W2", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/AllWISE/W2", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288574806"}, +{ "ID":"CDS/P/allWISE/W3", "hips_doi":"10.26093/cds/aladin/na1n-03", "creator_did":"ivo://CDS/P/allWISE/W3", "obs_collection":"The Wide-field Infrared Survey Explorer - W3 band (allWISE W3)", "obs_title":"AllWISE W3 (12um) from raw Atlas Images", "obs_description":"NASA's Wide-field Infrared Survey Explorer (WISE; Wright et al.2010) mapped the sky at 3.4, 4.6, 12, and 22 um (W1, W2, W3, W4) in 2010 with an angular resolution of 6.1\", 6.4\", 6.5\", & 12.0\" in the four bands. WISE achieved 5 sigma point source sensitivities better than 0.08, 0.11, 1 and 6 mJy in unconfused regions on the ecliptic in the four bands. Sensitivity improves toward the ecliptic poles due to denser coverage and lower zodiacal background.The All-Sky Release includes all data taken during the WISE full cryogenic mission phase, 7 January 2010 to 6 August 2010, that were processed with improved calibrations and reduction algorithms.", "obs_ack":"This Progressive Survey distribution makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, and NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology.WISE and NEOWISE are funded by the National Aeronautics and Space Administration.", "obs_copyright":"IPAC/NASA", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allwise/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE", "client_sort_key":"04-003-03", "hips_release_date":"2019-05-20T08:23Z", "hips_builder":"Aladin/HipsGen v10.125", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"8", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"260 1000", "hips_data_range":"269.6 477.3", "moc_access_url":"http://alasky.u-strasbg.fr/AllWISE/W3/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AllWISE/W3/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55378", "t_max":"55414", "obs_regime":"Infrared", "em_min":"7.443e-6", "em_max":"1.72613e-5", "hips_creation_date":"2014-04-15T09:25Z", "hips_hierarchy":"mean", "hips_pixel_scale":"4.473E-4", "hips_initial_fov":"140", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"0.9999", "hips_estsize":"1148421127", "hipsgen_date":"2019-05-20T08:23Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume6/AllWISE/W3 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AllWISE/W3", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AllWISE/W3", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/AllWISE/W3", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288576082"}, +{ "ID":"CDS/P/allWISE/W4", "hips_doi":"10.26093/cds/aladin/2tc8-gd0", "creator_did":"ivo://CDS/P/allWISE/W4", "obs_collection":"The Wide-field Infrared Survey Explorer - W4 band (allWISE W4)", "obs_title":"AllWISE W4 (22um) from raw Atlas Images", "obs_description":"NASA's Wide-field Infrared Survey Explorer (WISE; Wright et al.2010) mapped the sky at 3.4, 4.6, 12, and 22 um (W1, W2, W3, W4) in 2010 with an angular resolution of 6.1\", 6.4\", 6.5\", & 12.0\" in the four bands. WISE achieved 5 sigma point source sensitivities better than 0.08, 0.11, 1 and 6 mJy in unconfused regions on the ecliptic in the four bands. Sensitivity improves toward the ecliptic poles due to denser coverage and lower zodiacal background.The All-Sky Release includes all data taken during the WISE full cryogenic mission phase, 7 January 2010 to 6 August 2010, that were processed with improved calibrations and reduction algorithms.", "obs_ack":"This Progressive Survey distribution makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, and NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology.WISE and NEOWISE are funded by the National Aeronautics and Space Administration.", "obs_copyright":"IPAC/NASA", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allwise/expsup/sec1_6b.html", "client_category":"Image/Infrared/WISE", "client_sort_key":"04-003-04", "hips_release_date":"2019-05-20T08:28Z", "hips_builder":"Aladin/HipsGen v10.125", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"8", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg fits", "dataproduct_type":"image", "hips_pixel_cut":"100 200", "hips_data_range":"102.5 103.1", "moc_access_url":"http://alasky.u-strasbg.fr/AllWISE/W4/Moc.fits", "hips_progenitor_url":"https://alasky.cds.unistra.fr/AllWISE/W4/HpxFinder", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"IPAC/NASA", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55378", "t_max":"55414", "obs_regime":"Infrared", "em_min":"1.952e-5", "em_max":"2.79107e-5", "hips_creation_date":"2014-04-15T09:29Z", "hips_hierarchy":"mean", "hips_pixel_scale":"4.473E-4", "hips_initial_fov":"140", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "hips_order_min":"0", "hips_pixel_bitpix":"-32", "moc_sky_fraction":"1", "hips_estsize":"1148421127", "hipsgen_date":"2019-05-20T08:28Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume6/AllWISE/W4 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AllWISE/W4", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AllWISE/W4", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/AllWISE/W4", "hips_status_2":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288577374"}, +{ "ID":"CDS/P/allWISE/color", "hips_doi":"10.26093/cds/aladin/35rf-zj", "creator_did":"ivo://CDS/P/allWISE/color", "obs_collection":"The Wide-field Infrared Survey Explorer - W4-W2-W1 bands (allWISE color RGB-W4-W2-W1)", "obs_title":"AllWISE color Red (W4) , Green (W2) , Blue (W1) from raw Atlas Images", "obs_description":"NASA's Wide-field Infrared Survey Explorer (WISE; Wright et al. 2010) mapped the sky at 3.4, 4.6, 12, and 22 um (W1, W2, W3, W4) in 2010 with an angular resolution of 6.1\", 6.4\", 6.5\", & 12.0\" in the four bands. WISE achieved 5\\u03c3 point source sensitivities better than 0.08, 0.11, 1 and 6 mJy in unconfused regions on the ecliptic in the four bands. Sensitivity improves toward the ecliptic poles due to denser coverage and lower zodiacal background. The All-Sky Release includes all data taken during the WISE full cryogenic mission phase, 7 January 2010 to 6 August 2010, that were processed with improved calibrations and reduction algorithms.", "obs_ack":"This Progressive Survey distribution makes use of data products from the Wide-field Infrared Survey Explorer, which is a joint project of the University of California, Los Angeles, and the Jet Propulsion Laboratory/California Institute of Technology, and NEOWISE, which is a project of the Jet Propulsion Laboratory/California Institute of Technology. WISE and NEOWISE are funded by the National Aeronautics and Space Administration.", "obs_copyright":"IPAC/NASA", "obs_copyright_url":"http://wise2.ipac.caltech.edu/docs/release/allsky/", "client_application":[ "AladinLite", "AladinDesktop"], "client_category":"Image/Infrared/WISE", "client_sort_key":"04-003-00", "hips_creation_date":"2014-04-15T08:59Z", "hips_release_date":"2019-05-20T08:30Z", "hips_builder":"Aladin/HipsGen v10.125", "hips_creator":"Boch T. (CDS)", "hips_version":"1.4", "hips_order":"8", "hips_frame":"equatorial", "hips_tile_width":"512", "hips_tile_format":"jpeg", "dataproduct_type":"image", "hips_pixel_cut":"0 3", "hips_rgb_red":"w4 [102.0 151.0 200.0 Log]", "hips_rgb_green":"w2 [0.0 80.0 160.0 Log]", "hips_rgb_blue":"w1 [0.0 200.0 400.0 Log]", "moc_access_url":"http://alasky.u-strasbg.fr/AllWISE/RGB-W4-W2-W1/Moc.fits", "hips_status":"public master clonableOnce", "hips_copyright":"CNRS/Unistra", "prov_progenitor":"IPAC/NASA - healpixed by CDS", "bib_reference":"2010AJ....140.1868W", "bib_reference_url":"http://adsabs.harvard.edu/abs/2010AJ....140.1868W", "t_min":"55378", "t_max":"55414", "obs_regime":"Infrared", "em_min":"2.754e-6", "em_max":"2.79107e-5", "hips_hierarchy":"mean", "hips_pixel_scale":"4.473E-4", "hips_initial_fov":"12.0", "hips_initial_ra":"161.1024001", "hips_initial_dec":"-59.6638730", "hips_order_min":"0", "dataproduct_subtype":"color", "moc_sky_fraction":"1", "hips_estsize":"18790203", "hipsgen_date":"2019-05-20T08:30Z", "hipsgen_params":"out=/asd-volumes/sc1-asd-volume6/AllWISE/RGB-W4-W2-W1 UPDATE", "hips_service_url":"https://alasky.cds.unistra.fr/AllWISE/RGB-W4-W2-W1", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/AllWISE/RGB-W4-W2-W1", "hips_status_1":"public mirror clonableOnce", "hips_service_url_2":"https://irsa.ipac.caltech.edu/data/hips/CDS/AllWISE/RGB-W4-W2-W1", "hips_status_2":"public mirror unclonable", "hips_service_url_3":"https://healpix.ias.u-psud.fr/CDS_P_allWISE_color", "hips_status_3":"public mirror unclonable", "hips_service_url_4":"http://skies.esac.esa.int/AllWISEColor", "hips_status_4":"public mirror unclonable", "moc_type":"stmoc", "moc_time_order":"25", "moc_time_range":"1", "moc_order":"8", "obs_initial_ra":"161.1024001", "obs_initial_dec":"-59.6638730", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288899888"}, +{ "ID":"CDS/P/unWISE/W1", "hips_initial_fov":"60.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/unWISE/W1", "client_category":"Image/Infrared/WISE/unWISE", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_overlay":"mean", "hips_hierarchy":"median", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"unWISE W1 (3.4um)", "obs_collection":"unWISE", "obs_description":"unWISE: unofficial, unblurred coadds of the WISE imaging, that combine nine years of Wide-field Infrared Survey Explorer (WISE) and NEOWISE exposures, resulting in the deepest ever 3-5 micrometer full-sky maps.", "prov_progenitor":"https://portal.nersc.gov/project/cosmo/data/unwise/neo8/unwise-coadds/", "bib_reference":"2022RNAAS...6..188M", "obs_copyright":"IPAC/NASA - D. Lang for the reprocessing", "obs_regime":"Infrared", "em_min":"2.754e-6", "em_max":"3.8723e-6", "hips_builder":"Aladin/HipsGen v12.019", "hips_version":"1.4", "hips_release_date":"2022-12-13T14:16Z", "hips_frame":"equatorial", "hips_order":"8", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"-5 9400", "hips_data_range":"-36096 108229", "hips_pixel_scale":"0.229", "s_pixel_scale":"7.638E-4", "dataproduct_type":"image", "hipsgen_date":"2022-12-12T09:52Z", "hipsgen_params":"in=org-data/W1 out=hips/W1 creator_did=CDS/P/unWISE/W1 hips_frame=equatorial TILES", "hips_creation_date":"2019-12-17T12:05Z", "hipsgen_date_1":"2022-12-12T13:23Z", "hipsgen_params_1":"out=hips/W1 creator_did=CDS/P/unWISE/W1 hips_frame=equatorial \"hips_pixel_cut=-5 9400 log\" JPEG", "hips_estsize":"1079386335", "hips_nb_tiles":"1048573", "hips_check_code":"jpeg:0 fits:4098166269", "hipsgen_date_2":"2022-12-13T07:56Z", "hipsgen_params_2":"out=hips/W1 CHECKCODE", "hipsgen_date_3":"2022-12-13T14:16Z", "hipsgen_params_3":"out=hips/W1 CHECKCODE -f", "hips_service_url":"https://alasky.cds.unistra.fr/unWISE/W1", "hips_progenitor_url":"https://alasky.cds.unistra.fr/unWISE/W1/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/unWISE/W1", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_sky_fraction":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288485942"}, +{ "ID":"CDS/P/unWISE/W2", "hips_initial_fov":"60.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/unWISE/W2", "client_category":"Image/Infrared/WISE/unWISE", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_overlay":"mean", "hips_hierarchy":"median", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"unWISE W2 (4.6um)", "obs_collection":"unWISE", "obs_description":"unWISE: unofficial, unblurred coadds of the WISE imaging, that combine nine years of Wide-field Infrared Survey Explorer (WISE) and NEOWISE exposures, resulting in the deepest ever 3-5 micrometer full-sky maps.", "prov_progenitor":"https://portal.nersc.gov/project/cosmo/data/unwise/neo8/unwise-coadds/", "bib_reference":"2022RNAAS...6..188M", "obs_copyright":"IPAC/NASA - D. Lang for the reprocessing", "obs_regime":"Infrared", "em_min":"3.9633e-6", "em_max":"5.3413e-6", "hips_builder":"Aladin/HipsGen v12.001", "hips_version":"1.4", "hips_release_date":"2022-12-13T08:52Z", "hips_frame":"equatorial", "hips_order":"8", "hips_order_min":"0", "hips_tile_width":"512", "hips_status":"public master clonableOnce", "hips_tile_format":"jpeg fits", "hips_pixel_cut":"-5 9400", "hips_data_range":"-82669 247939", "hips_pixel_scale":"0.229", "s_pixel_scale":"7.638E-4", "dataproduct_type":"image", "hipsgen_date":"2022-12-08T17:36Z", "hipsgen_params":"in=org-data/W2 out=hips/W2 creator_did=CDS/P/unWISE/W2/neo8 hips_frame=equatorial TILES", "hips_creation_date":"2019-12-17T12:05Z", "hipsgen_date_1":"2022-12-09T12:33Z", "hipsgen_params_1":"out=hips/W2 creator_did=CDS/P/unWISE/W2/neo8 hips_frame=equatorial \"hips_pixel_cut=-5 9400 log\" JPEG", "hips_estsize":"1079386335", "hips_nb_tiles":"1048573", "hips_check_code":"jpeg:0 fits:4098166269", "hipsgen_date_2":"2022-12-13T08:52Z", "hipsgen_params_2":"out=hips/W2 CHECKCODE", "hips_service_url":"https://alasky.cds.unistra.fr/unWISE/W2", "hips_progenitor_url":"https://alasky.cds.unistra.fr/unWISE/W2/HpxFinder", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/unWISE/W2", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_sky_fraction":"1", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288485994"}, +{ "ID":"CDS/P/unWISE/color-W2-W1W2-W1", "hips_initial_fov":"60.0", "hips_initial_ra":"266.4168166", "hips_initial_dec":"-29.0078249", "creator_did":"ivo://CDS/P/unWISE/color-W2-W1W2-W1", "client_category":"Image/Infrared/WISE/unWISE", "hips_pixel_bitpix":"-32", "data_pixel_bitpix":"-32", "hips_sampling":"bilinear", "hips_overlay":"mean", "hips_hierarchy":"median", "hips_creator":"Boch T. (CDS)", "hips_copyright":"CNRS/Universite de Strasbourg", "obs_title":"unWISE color, from W2 and W1 bands", "obs_collection":"unWISE", "obs_description":"unWISE: unofficial, unblurred coadds of the WISE imaging, that combine nine years of Wide-field Infrared Survey Explorer (WISE) and NEOWISE exposures, resulting in the deepest ever 3-5 micrometer full-sky maps.", "prov_progenitor":"http://unwise.me/", "bib_reference":"2022RNAAS...6..188M", "obs_copyright":"IPAC/NASA - D. Lang for the reprocessing", "obs_regime":"Infrared", "em_min":"2.754e-6", "em_max":"5.3413e-6", "hips_builder":"Aladin/HipsGen v11.025", "hips_version":"1.4", "hips_release_date":"2022-12-12T11:02Z", "hips_frame":"equatorial", "hips_order":"8", "hips_tile_width":"512", "hips_tile_format":"jpeg", "hips_status":"public master clonableOnce", "hips_pixel_scale":"4.473E-4", "dataproduct_type":"image", "moc_sky_fraction":"1", "hips_estsize":"18790203", "hips_creation_date":"2019-12-18T13:22Z", "hips_order_min":"0", "dataproduct_subtype":"color", "hips_service_url":"https://alasky.cds.unistra.fr/unWISE/color-W2-W1W2-W1", "hips_service_url_1":"https://alaskybis.cds.unistra.fr/unWISE/color-W2-W1W2-W1", "hips_status_1":"public mirror clonableOnce", "moc_type":"smoc", "moc_order":"8", "obs_initial_ra":"266.4168166", "obs_initial_dec":"-29.0078249", "obs_initial_fov":"0.2290324274544937", "TIMESTAMP":"1721288486050"} +] diff --git a/desktop/.prettierignore b/desktop/.prettierignore new file mode 100644 index 000000000..cff9d990c --- /dev/null +++ b/desktop/.prettierignore @@ -0,0 +1,6 @@ +**/node_modules +**/dist +**/release +**/*.svg + +package-lock.json diff --git a/desktop/.vscode/extensions.json b/desktop/.vscode/extensions.json index 3537a1c79..4ed06f5d0 100644 --- a/desktop/.vscode/extensions.json +++ b/desktop/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "angular.ng-template"] + "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "angular.ng-template", "editorconfig.editorconfig"] } diff --git a/desktop/app/local.storage.ts b/desktop/app/local.storage.ts deleted file mode 100644 index a57ec7d76..000000000 --- a/desktop/app/local.storage.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as fs from 'fs' -import { dirname } from 'path' -import type { Undefinable } from '../src/shared/utils/types' - -export class LocalStorage> { - private readonly data = Object.create(null) as T - - constructor(private readonly path: string) { - try { - console.info(`loading config file at ${path}`) - - const parsedData = JSON.parse(fs.readFileSync(path, 'utf8')) as unknown - - if (typeof parsedData === 'object' && !Array.isArray(parsedData) && parsedData) { - Object.assign(this.data, parsedData) - } - } catch (e) { - console.error(e) - - this.ensureDirectory() - - fs.writeFileSync(path, '{}', { mode: 0o666 }) - } - } - - get(key: K): Undefinable { - return this.data[key] - } - - set(key: K, value: T[K]) { - this.data[key] = value - } - - has(key: K | string) { - return key in this.data - } - - save() { - try { - this.ensureDirectory() - fs.writeFileSync(this.path, JSON.stringify(this.data)) - } catch (e) { - console.error(e) - } - } - - private ensureDirectory(): void { - const dir = dirname(this.path) - - if (!fs.existsSync(dir)) { - // Ensure the directory exists as it could have been deleted in the meantime. - fs.mkdirSync(dir, { recursive: true }) - } - } -} diff --git a/desktop/app/main.ts b/desktop/app/main.ts index 38222a771..4f3c7b2ad 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -2,11 +2,10 @@ import { Menu, app, ipcMain } from 'electron' import * as fs from 'fs' import type { ChildProcessWithoutNullStreams } from 'node:child_process' import { spawn } from 'node:child_process' -import { join, resolve } from 'path' +import { join } from 'path' import { WebSocket } from 'ws' -import type { InternalEventType, JsonFile, StoredWindowData } from '../src/shared/types/app.types' +import type { InternalEventType, JsonFile } from '../src/shared/types/app.types' import { ArgumentParser } from './argument.parser' -import { LocalStorage } from './local.storage' import { WindowManager } from './window.manager' Object.assign(global, { WebSocket }) @@ -24,10 +23,8 @@ if (parsedArgs.apiMode) { app.disableHardwareAcceleration() } -const configPath = resolve(app.getPath('userData'), 'config.json') -const storage = new LocalStorage(configPath) const appIcon = join(__dirname, parsedArgs.serve ? `../src/assets/icons/nebulosa.png` : `assets/icons/nebulosa.png`) -const windowManager = new WindowManager(parsedArgs, storage, appIcon) +const windowManager = new WindowManager(parsedArgs, appIcon) let apiProcess: ChildProcessWithoutNullStreams | null process.on('beforeExit', () => { @@ -72,7 +69,7 @@ async function startApp() { if (text) { const regex = /server is started at port: (\d+)/i - const match = text.match(regex) + const match = regex.exec(text) if (match) { const port = parseInt(match[1]) diff --git a/desktop/app/package-lock.json b/desktop/app/package-lock.json index 5fa867a85..11a11b8bf 100644 --- a/desktop/app/package-lock.json +++ b/desktop/app/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@stomp/stompjs": "7.0.0", + "electron-store": "8.2.0", "ws": "8.18.0" } }, @@ -18,6 +19,279 @@ "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==" }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-store": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.2.0.tgz", + "integrity": "sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==", + "dependencies": { + "conf": "^10.2.0", + "type-fest": "^2.17.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", diff --git a/desktop/app/package.json b/desktop/app/package.json index 7ff59d7c7..dae08a583 100644 --- a/desktop/app/package.json +++ b/desktop/app/package.json @@ -12,6 +12,7 @@ "private": true, "dependencies": { "@stomp/stompjs": "7.0.0", + "electron-store": "8.2.0", "ws": "8.18.0" } } diff --git a/desktop/app/window.manager.ts b/desktop/app/window.manager.ts index 418fd8df5..8e7770918 100644 --- a/desktop/app/window.manager.ts +++ b/desktop/app/window.manager.ts @@ -1,12 +1,20 @@ import { Client } from '@stomp/stompjs' +import type { Point, Size } from 'electron' import { BrowserWindow, Notification, dialog, screen, shell } from 'electron' +import Store from 'electron-store' import type { ChildProcessWithoutNullStreams } from 'node:child_process' import { join } from 'path' import type { MessageEvent } from '../src/shared/types/api.types' -import type { CloseWindow, ConfirmationEvent, FullscreenWindow, NotificationEvent, OpenDirectory, OpenFile, OpenWindow, ResizeWindow, StoredWindowData, WindowCommand } from '../src/shared/types/app.types' +import type { CloseWindow, ConfirmationEvent, FullscreenWindow, NotificationEvent, OpenDirectory, OpenFile, OpenWindow, ResizeWindow, WindowCommand } from '../src/shared/types/app.types' import type { Nullable } from '../src/shared/utils/types' import type { ParsedArgument } from './argument.parser' -import type { LocalStorage } from './local.storage' + +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style +export interface WindowInfo { + [key: `window.${string}`]: (Size & Point) | undefined +} + +const store = new Store({ name: 'nebulosa' }) export class ApplicationWindow { constructor( @@ -77,7 +85,6 @@ export class WindowManager { constructor( public readonly args: ParsedArgument, - public readonly storage: LocalStorage, defaultAppIcon: string = 'nebulosa.png', ) { this.appIcon = join(__dirname, args.serve ? `../src/assets/icons/${defaultAppIcon}` : `assets/icons/${defaultAppIcon}`) @@ -90,7 +97,6 @@ export class WindowManager { if (appWindow) { if (open.data) { - console.info('window data changed. id=%s, data=%s', open.id, open.data) appWindow.browserWindow.webContents.send('DATA.CHANGED', open.data) } @@ -108,12 +114,12 @@ export class WindowManager { const computedHeight = preference.height ? Math.trunc(this.computeHeight(preference.height, computedWidth)) : 416 const screenSize = screen.getPrimaryDisplay().workAreaSize - const storedData = this.storage.get(`window.${open.id}`) + const data = store.get(`window.${open.id}`) const resizable = preference.resizable - const width = resizable ? Math.max(minWidth, Math.min(storedData?.width ?? computedWidth, screenSize.width)) : computedWidth - const height = resizable ? Math.max(minHeight, Math.min(storedData?.height ?? computedHeight, screenSize.height)) : computedHeight - const x = Math.max(0, Math.min(storedData?.x ?? 0, screenSize.width - width)) - const y = Math.max(0, Math.min(storedData?.y ?? 0, screenSize.height - height)) + const width = resizable ? Math.max(minWidth, Math.min(data?.width ?? computedWidth, screenSize.width)) : computedWidth + const height = resizable ? Math.max(minHeight, Math.min(data?.height ?? computedHeight, screenSize.height)) : computedHeight + const x = Math.max(0, Math.min(data?.x ?? 0, screenSize.width - width)) + const y = Math.max(0, Math.min(data?.y ?? 0, screenSize.height - height)) const browserWindow = new BrowserWindow({ title: 'Nebulosa', @@ -129,6 +135,7 @@ export class WindowManager { resizable: this.args.serve || resizable, autoHideMenuBar: true, icon: preference.icon ? join(__dirname, this.args.serve ? `../src/assets/icons/${preference.icon}.png` : `assets/icons/${preference.icon}.png`) : this.appIcon, + show: false, webPreferences: { nodeIntegration: true, allowRunningInsecureContent: this.args.serve, @@ -139,9 +146,13 @@ export class WindowManager { }, }) - if (!storedData) { - browserWindow.center() - } + browserWindow.on('ready-to-show', () => { + browserWindow.show() + + if (!data) { + browserWindow.center() + } + }) if (this.args.serve) { await browserWindow.loadURL(`http://localhost:4200/${open.path}?data=${encodedData}`) @@ -193,8 +204,7 @@ export class WindowManager { saveWindowData(window: ApplicationWindow) { const [x, y] = window.browserWindow.getPosition() const [width, height] = window.browserWindow.getSize() - this.storage.set(`window.${window.data.id}`, { x, y, width, height }) - this.storage.save() + store.set(`window.${window.data.id}`, { x, y, width, height }) } async createMainWindow(apiProcess?: ChildProcessWithoutNullStreams, port: number = this.port, host: string = this.host) { @@ -254,9 +264,12 @@ export class WindowManager { const url = new URL(join('file:', __dirname, 'assets', 'images', 'splash.png')) + browserWindow.on('ready-to-show', () => { + browserWindow.show() + browserWindow.center() + }) + await browserWindow.loadURL(url.href) - browserWindow.show() - browserWindow.center() return browserWindow } else { @@ -284,8 +297,12 @@ export class WindowManager { return undefined } + findWindowWith(command: WindowCommand, sender: Electron.WebContents) { + return this.findWindow(command.windowId) ?? this.findWindow(sender.id) + } + async handleFileOpen(event: Electron.IpcMainInvokeEvent, command: OpenFile) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) if (window) { const properties: Electron.OpenDialogOptions['properties'] = ['openFile'] @@ -307,7 +324,7 @@ export class WindowManager { } async handleFileSave(event: Electron.IpcMainInvokeEvent, command: OpenFile) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) if (window) { const ret = await dialog.showSaveDialog(window.browserWindow, { @@ -323,7 +340,7 @@ export class WindowManager { } async handleDirectoryOpen(event: Electron.IpcMainInvokeEvent, command: OpenDirectory) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) if (window) { const ret = await dialog.showOpenDialog(window.browserWindow, { @@ -339,7 +356,7 @@ export class WindowManager { async handleWindowOpen(event: Electron.IpcMainInvokeEvent, command: OpenWindow) { if (command.preference.modal) { - const parentWindow = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const parentWindow = this.findWindowWith(command, event.sender) const appWindow = await this.createWindow(command, parentWindow?.browserWindow) return new Promise((resolve) => { @@ -359,7 +376,7 @@ export class WindowManager { } handleWindowClose(event: Electron.IpcMainInvokeEvent, command: CloseWindow) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) if (window) { window.resolver?.(command.data) @@ -372,7 +389,7 @@ export class WindowManager { } handleWindowResize(event: Electron.IpcMainInvokeEvent, command: ResizeWindow) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) if (window && !window.data.preference.resizable && window.data.preference.autoResizable !== false) { const [width] = window.browserWindow.getSize() @@ -391,30 +408,30 @@ export class WindowManager { } handleWindowMinimize(event: Electron.IpcMainInvokeEvent, command: WindowCommand) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) window?.browserWindow.minimize() return !!window && window.browserWindow.isMinimized() } handleWindowMaximize(event: Electron.IpcMainInvokeEvent, command: WindowCommand) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) return !!window && window.toggleMaximize() } handleWindowPin(event: Electron.IpcMainInvokeEvent, command: WindowCommand) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) window?.browserWindow.setAlwaysOnTop(true) return !!window && window.browserWindow.isAlwaysOnTop() } handleWindowUnpin(event: Electron.IpcMainInvokeEvent, command: WindowCommand) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) window?.browserWindow.setAlwaysOnTop(false) return !!window && window.browserWindow.isAlwaysOnTop() } handleWindowFullscreen(event: Electron.IpcMainInvokeEvent, command: FullscreenWindow) { - const window = this.findWindow(command.windowId) ?? this.findWindow(event.sender.id) + const window = this.findWindowWith(command, event.sender) if (window) { if (command.enabled) window.browserWindow.setFullScreen(true) diff --git a/desktop/camera.png b/desktop/camera.png index 8cff67580..0d9668ac1 100644 Binary files a/desktop/camera.png and b/desktop/camera.png differ diff --git a/desktop/eslint.config.mjs b/desktop/eslint.config.mjs index 6176d33fc..900b0beff 100644 --- a/desktop/eslint.config.mjs +++ b/desktop/eslint.config.mjs @@ -2,6 +2,9 @@ import eslint from '@eslint/js' import tseslint from 'typescript-eslint' export default tseslint.config( + { + ignores: ['**/*.mjs'], + }, { files: ['**/*.ts'], ...eslint.configs.recommended, @@ -33,6 +36,7 @@ export default tseslint.config( rules: { 'no-unused-vars': 'off', 'no-loss-of-precision': 'off', + 'no-extra-semi': 'warn', '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-loss-of-precision': 'off', '@typescript-eslint/restrict-template-expressions': 'off', @@ -54,6 +58,13 @@ export default tseslint.config( ignorePrimitives: true, }, ], + '@typescript-eslint/no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + }, + ], }, }, ) diff --git a/desktop/focuser.png b/desktop/focuser.png index dde69a2d4..262acf026 100644 Binary files a/desktop/focuser.png and b/desktop/focuser.png differ diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 181e67c23..118e713cc 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -24,7 +24,6 @@ "chartjs-plugin-zoom": "2.0.1", "hotkeys-js": "3.13.7", "leaflet": "1.9.4", - "moment": "2.30.1", "ngx-moveable": "0.50.0", "nuid": "2.0.1-2", "panzoom": "9.4.3", @@ -14515,15 +14514,6 @@ "node": ">=10" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/moveable": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/moveable/-/moveable-0.53.0.tgz", diff --git a/desktop/package.json b/desktop/package.json index 3855fa538..789d2b5f3 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,5 +1,6 @@ { "name": "nebulosa", + "codename": "Ceres", "version": "0.1.0", "description": "The complete integrated solution for all of your astronomical imaging needs.", "author": { @@ -11,11 +12,12 @@ "main": "app/main.js", "private": true, "scripts": { - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps && npm run scripts", "ng": "ng", + "scripts": "node --no-warnings scripts/nebulosa.mjs", "start": "npm-run-all -p electron:serve ng:serve", "ng:serve": "ng serve -c web --hmr", - "build": "npm run electron:serve-tsc && ng build --base-href ./", + "build": "npm run scripts && npm run electron:serve-tsc && ng build --base-href ./", "build:dev": "npm run build -- -c dev", "build:prod": "npm run build -- -c production", "web:build": "npm run build -- -c web-production", @@ -30,8 +32,7 @@ "lint": "npx eslint .", "prettier:ts": "npx prettier '**/*.ts' --write", "prettier:html": "npx prettier '**/*.html' --write", - "prettier:scss": "npx prettier '**/*.scss' --write", - "prettier:json": "npx prettier '**/*.json' --write" + "prettier:scss": "npx prettier '**/*.scss' --write" }, "dependencies": { "@angular/animations": "18.1.3", @@ -48,7 +49,6 @@ "chartjs-plugin-zoom": "2.0.1", "hotkeys-js": "3.13.7", "leaflet": "1.9.4", - "moment": "2.30.1", "ngx-moveable": "0.50.0", "nuid": "2.0.1-2", "panzoom": "9.4.3", @@ -81,9 +81,6 @@ "typescript-eslint": "8.0.0", "wait-on": "7.2.0" }, - "overrides": { - "axios": "1.6.2" - }, "engines": { "node": ">= 20.11.1" }, diff --git a/desktop/scripts/nebulosa.mjs b/desktop/scripts/nebulosa.mjs new file mode 100644 index 000000000..b8951e41b --- /dev/null +++ b/desktop/scripts/nebulosa.mjs @@ -0,0 +1,47 @@ +import { execSync } from 'child_process' +import fs from 'fs' +import mainPackageJson from '../app/package.json' with { type: 'json' } +import rendererPackageJson from '../package.json' with { type: 'json' } + +const dependencies = [] + +if (rendererPackageJson.dependencies) { + for (const [name, version] of Object.entries(rendererPackageJson.dependencies).filter((e) => !e[1].includes('#'))) { + dependencies.push({ name, version }) + } +} + +if (mainPackageJson.dependencies) { + for (const [name, version] of Object.entries(mainPackageJson.dependencies).filter((e) => !e[1].includes('#'))) { + dependencies.push({ name, version }) + } +} + +if (rendererPackageJson.devDependencies) { + for (const [name, version] of Object.entries(rendererPackageJson.devDependencies).filter((e) => !e[1].includes('#'))) { + dependencies.push({ name, version }) + } +} + +if (mainPackageJson.devDependencies) { + for (const [name, version] of Object.entries(mainPackageJson.devDependencies).filter((e) => !e[1].includes('#'))) { + dependencies.push({ name, version }) + } +} + +dependencies.sort((a, b) => a.name.localeCompare(b.name)) + +const data = { + name: rendererPackageJson.name, + codename: rendererPackageJson.codename, + version: rendererPackageJson.version, + description: rendererPackageJson.description, + author: rendererPackageJson.author, + build: { + commit: execSync('git rev-parse HEAD').toString().trim(), + date: new Date().toISOString(), + }, + dependencies, +} + +fs.writeFileSync('src/assets/data/nebulosa.json', JSON.stringify(data)) diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index 940303b31..7c59df1fe 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -17,13 +17,16 @@ + [value]="version" />

+

+ {{ description }} +

© 2022-2024 Tiago Melo -

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

+

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

+ + +
+ COMMIT: {{ commit }} + DATE: {{ date }} +
+ + +
+ @for (dep of dependencies; track $index) { + + {{ dep.name }} ({{ dep.version }}) + + }
diff --git a/desktop/src/app/about/about.component.ts b/desktop/src/app/about/about.component.ts index 81d6dd66a..27d76b644 100644 --- a/desktop/src/app/about/about.component.ts +++ b/desktop/src/app/about/about.component.ts @@ -1,4 +1,6 @@ import { Component } from '@angular/core' +import nebulosa from '../../assets/data/nebulosa.json' +import { DependencyItem, FLAT_ICON_URL, IconItem } from '../../shared/types/about.types' import { AppComponent } from '../app.component' @Component({ @@ -6,7 +8,55 @@ import { AppComponent } from '../app.component' templateUrl: './about.component.html', }) export class AboutComponent { + protected readonly codename = nebulosa.codename + protected readonly version = nebulosa.version + protected readonly description = nebulosa.description + protected readonly commit = nebulosa.build.commit + protected readonly date = nebulosa.build.date + protected readonly icons: IconItem[] = [] + protected readonly dependencies: DependencyItem[] = [] + constructor(app: AppComponent) { app.title = 'About' + + this.mapDependencies() + + this.icons.push({ link: `${FLAT_ICON_URL}/information_9195785`, name: 'Information', author: 'Anggara - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/sky_3982229`, name: 'Sky', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/target_3207593`, name: 'Target', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/camera-lens_5708327`, name: 'Camera', author: 'juicy_fish - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/telescope_4011463`, name: 'Telescope', author: 'Smashicons - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/observatory_2256076`, name: 'Observatory', author: 'Nikita Golubev - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/focus_3801224`, name: 'Focus', author: 'FetchLab - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/switch_404449`, name: 'Switch', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/image_4371206`, name: 'Image', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/image-processing_6062419`, name: 'Image processing', author: 'juicy_fish - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/star_740882`, name: 'Star', author: 'Vectors Market - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/rotate_3303063`, name: 'Rotate', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/rgb-print_7664547`, name: 'Color wheel', author: 'BomSymbols - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/cogwheel_3953226`, name: 'Settings', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/contrast_439842`, name: 'Sun', author: 'DinosoftLabs - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/full-moon_9689786`, name: 'Moon', author: 'vectorsmarket15 - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/jupiter_1086078`, name: 'Planet', author: 'monkik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/asteroid_1086068`, name: 'Asteroid', author: 'monkik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/satellite_1086093`, name: 'Satellite', author: 'monkik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/witch-hat_5606276`, name: 'Witch hat', author: 'Luvdat - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/picture_2659360`, name: 'Picture', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/calculator_7182540`, name: 'Calculator', author: 'Iconic Panda - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/target_10542035`, name: 'Target', author: 'Arkinasi - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/stack_3342239`, name: 'Stack', author: 'Pixel perfect - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/blackhole_6704410`, name: 'Blackhole', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/calibration_2364169`, name: 'Calibration', author: 'Freepik - Flaticon' }) + } + + private mapDependencies() { + for (const { name, version } of nebulosa.dependencies) { + this.dependencies.push(this.mapDependency(name, version)) + } + } + + private mapDependency(name: string, version: string): DependencyItem { + const link = `https://www.npmjs.com/package/${name}` + return { name, version, link } } } diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 0fb4e41bc..3be7dfd3c 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -8,7 +8,7 @@ [(device)]="camera" (deviceChange)="cameraChanged()" /> @@ -41,15 +41,15 @@ [info]="status" />
+ value="RA: {{ tppaResult.rightAscension }}" + [severity]="tppaResult.failed ? 'danger' : 'info'" /> + value="DEC: {{ tppaResult.declination }}" + [severity]="tppaResult.failed ? 'danger' : 'info'" />
@@ -67,7 +67,7 @@ optionLabel="label" optionValue="value" [autoDisplayFirst]="false" - (ngModelChange)="plateSolverChanged()" /> + (ngModelChange)="savePreference()" /> @@ -80,7 +80,7 @@ [min]="1" [max]="60" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber /> @@ -100,8 +100,8 @@
Azimuth - {{ tppaAzimuthError }} - {{ tppaAzimuthErrorDirection }} + {{ tppaResult.azimuthError }} + {{ tppaResult.azimuthErrorDirection }}
Altitude - {{ tppaAltitudeError }} - {{ tppaAltitudeErrorDirection }} + {{ tppaResult.altitudeError }} + {{ tppaResult.altitudeErrorDirection }}
Total - {{ tppaTotalError }} + {{ tppaResult.totalError }}
@@ -155,7 +155,7 @@ [text]="true" /> } @else if (!running) { + (ngModelChange)="savePreference()" + spinnableNumber />
- - - - +
{ - if (event.device.id === this.camera.id) { + electronService.on('CAMERA.UPDATED', (event) => { + if (event.device.id === this.camera?.id) { ngZone.run(() => { - Object.assign(this.camera, event.device) + if (this.camera) { + Object.assign(this.camera, event.device) + } }) } }) - electron.on('CAMERA.ATTACHED', (event) => { + electronService.on('CAMERA.ATTACHED', (event) => { ngZone.run(() => { this.cameras.push(event.device) this.cameras.sort(deviceComparator) }) }) - electron.on('CAMERA.DETACHED', (event) => { + electronService.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.camera = this.cameras[0] } this.cameras.splice(index, 1) @@ -115,28 +91,30 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { }) }) - electron.on('MOUNT.UPDATED', (event) => { - if (event.device.id === this.mount.id) { + electronService.on('MOUNT.UPDATED', (event) => { + if (event.device.id === this.mount?.id) { ngZone.run(() => { - Object.assign(this.mount, event.device) + if (this.mount) { + Object.assign(this.mount, event.device) + } }) } }) - electron.on('MOUNT.ATTACHED', (event) => { + electronService.on('MOUNT.ATTACHED', (event) => { ngZone.run(() => { this.mounts.push(event.device) this.mounts.sort(deviceComparator) }) }) - electron.on('MOUNT.DETACHED', (event) => { + electronService.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.mount = this.mounts[0] } this.mounts.splice(index, 1) @@ -144,28 +122,30 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { }) }) - electron.on('GUIDE_OUTPUT.UPDATED', (event) => { - if (event.device.id === this.guideOutput.id) { + electronService.on('GUIDE_OUTPUT.UPDATED', (event) => { + if (event.device.id === this.guideOutput?.id) { ngZone.run(() => { - Object.assign(this.guideOutput, event.device) + if (this.guideOutput) { + Object.assign(this.guideOutput, event.device) + } }) } }) - electron.on('GUIDE_OUTPUT.ATTACHED', (event) => { + electronService.on('GUIDE_OUTPUT.ATTACHED', (event) => { ngZone.run(() => { this.guideOutputs.push(event.device) this.guideOutputs.sort(deviceComparator) }) }) - electron.on('GUIDE_OUTPUT.DETACHED', (event) => { + electronService.on('GUIDE_OUTPUT.DETACHED', (event) => { ngZone.run(() => { const index = this.guideOutputs.findIndex((e) => e.id === event.device.id) if (index >= 0) { if (this.guideOutputs[index] === this.guideOutput) { - Object.assign(this.guideOutput, this.guideOutputs[0] ?? EMPTY_GUIDE_OUTPUT) + this.guideOutput = this.guideOutputs[0] } this.guideOutputs.splice(index, 1) @@ -173,30 +153,29 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { }) }) - electron.on('TPPA.ELAPSED', (event) => { - if (event.camera.id === this.camera.id) { + electronService.on('TPPA.ELAPSED', (event) => { + if (event.camera.id === this.camera?.id) { ngZone.run(() => { this.status = event.state this.running = event.state !== 'FINISHED' - this.pausingOrPaused = event.state === 'PAUSING' || event.state === 'PAUSED' if (event.state === 'COMPUTED') { - this.tppaFailed = false - this.tppaRightAscension = event.rightAscension - this.tppaDeclination = event.declination - this.tppaAzimuthError = event.azimuthError - this.tppaAltitudeError = event.altitudeError - this.tppaAzimuthErrorDirection = event.azimuthErrorDirection - this.tppaAltitudeErrorDirection = event.altitudeErrorDirection - this.tppaTotalError = event.totalError + this.tppaResult.failed = false + this.tppaResult.rightAscension = event.rightAscension + this.tppaResult.declination = event.declination + this.tppaResult.azimuthError = event.azimuthError + this.tppaResult.altitudeError = event.altitudeError + this.tppaResult.azimuthErrorDirection = event.azimuthErrorDirection + this.tppaResult.altitudeErrorDirection = event.altitudeErrorDirection + this.tppaResult.totalError = event.totalError } else if (event.state === 'FINISHED') { this.cameraExposure.reset() } else if (event.state === 'SOLVED' || event.state === 'SLEWED') { - this.tppaFailed = false - this.tppaRightAscension = event.rightAscension - this.tppaDeclination = event.declination + this.tppaResult.failed = false + this.tppaResult.rightAscension = event.rightAscension + this.tppaResult.declination = event.declination } else if (event.state === 'FAILED') { - this.tppaFailed = true + this.tppaResult.failed = true } if (event.capture && event.capture.state !== 'CAPTURE_FINISHED') { @@ -206,16 +185,16 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { } }) - electron.on('DARV.ELAPSED', (event) => { - if (event.camera.id === this.camera.id) { + electronService.on('DARV.ELAPSED', (event) => { + if (event.camera.id === this.camera?.id) { ngZone.run(() => { this.status = event.state this.running = this.cameraExposure.handleCameraCaptureEvent(event.capture) if (event.state === 'FORWARD' || event.state === 'BACKWARD') { - this.darvDirection = event.direction + this.darvResult.direction = event.direction } else { - this.darvDirection = undefined + this.darvResult.direction = undefined } }) } @@ -225,7 +204,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { } async ngAfterViewInit() { - this.pinger.register(this, 30000) + this.ticker.register(this, 30000) this.cameras = (await this.api.cameras()).sort(deviceComparator) this.mounts = (await this.api.mounts()).sort(deviceComparator) @@ -234,21 +213,21 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) void this.darvStop() void this.tppaStop() } - async ping() { - if (this.camera.id) await this.api.cameraListen(this.camera) - if (this.mount.id) await this.api.mountListen(this.mount) - if (this.guideOutput.id) await this.api.guideOutputListen(this.guideOutput) + async tick() { + if (this.camera?.id) await this.api.cameraListen(this.camera) + if (this.mount?.id) await this.api.mountListen(this.mount) + if (this.guideOutput?.id) await this.api.guideOutputListen(this.guideOutput) } - async cameraChanged() { - if (this.camera.id) { - await this.ping() + protected async cameraChanged() { + if (this.camera?.id) { + await this.tick() const camera = await this.api.camera(this.camera.id) Object.assign(this.camera, camera) @@ -256,9 +235,9 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { } } - async mountChanged() { - if (this.mount.id) { - await this.ping() + protected async mountChanged() { + if (this.mount?.id) { + await this.tick() const mount = await this.api.mount(this.mount.id) Object.assign(this.mount, mount) @@ -267,126 +246,94 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { } } - async guideOutputChanged() { - if (this.guideOutput.id) { - await this.ping() + protected async guideOutputChanged() { + if (this.guideOutput?.id) { + await this.tick() const guideOutput = await this.api.guideOutput(this.guideOutput.id) Object.assign(this.guideOutput, guideOutput) } } - async showCameraDialog() { - if (this.camera.id) { + protected async showCameraDialog() { + if (this.camera?.id) { if (this.tab === 0) { - if (await CameraComponent.showAsDialog(this.browserWindow, 'TPPA', this.camera, this.tppaRequest.capture)) { + if (await CameraComponent.showAsDialog(this.browserWindowService, '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)) { + if (await CameraComponent.showAsDialog(this.browserWindowService, 'DARV', this.camera, this.darvRequest.capture)) { this.savePreference() } } } } - plateSolverChanged() { - this.tppaRequest.plateSolver = this.preference.plateSolverRequest(this.tppaRequest.plateSolver.type).get() - this.savePreference() - } - - initialPauseChanged() { - this.darvRequest.capture.exposureDelay = this.darvRequest.initialPause - this.savePreference() - } + protected async darvStart(direction: GuideDirection = 'EAST') { + if (this.camera?.id && this.guideOutput?.id) { + this.method = 'DARV' + this.darvRequest.direction = direction + this.darvRequest.reversed = this.preference.darvHemisphere === 'SOUTHERN' + Object.assign(this.tppaRequest.plateSolver, this.preferenceService.settings.get().plateSolver[this.tppaRequest.plateSolver.type]) - driftForChanged() { - this.darvRequest.capture.exposureTime = this.darvRequest.exposureTime * 1000000 - this.savePreference() + await this.openCameraImage() + await this.api.darvStart(this.camera, this.guideOutput, this.darvRequest) + } } - async darvStart(direction: GuideDirection = 'EAST') { - 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() - await this.api.darvStart(this.camera, this.guideOutput, this.darvRequest) + protected async darvStop() { + if (this.camera?.id) { + await this.api.darvStop(this.camera) + } } - darvStop() { - return this.api.darvStop(this.camera) - } + protected async tppaStart() { + if (this.camera?.id && this.mount?.id) { + this.method = 'TPPA' - async tppaStart() { - this.alignmentMethod = 'TPPA' - await this.openCameraImage() - await this.api.tppaStart(this.camera, this.mount, this.tppaRequest) + await this.openCameraImage() + await this.api.tppaStart(this.camera, this.mount, this.tppaRequest) + } } - tppaPause() { - return this.api.tppaPause(this.camera) + protected async tppaPause() { + if (this.camera?.id) { + await this.api.tppaPause(this.camera) + } } - tppaUnpause() { - return this.api.tppaUnpause(this.camera) + protected async tppaUnpause() { + if (this.camera?.id) { + await this.api.tppaUnpause(this.camera) + } } - tppaStop() { - return this.api.tppaStop(this.camera) + protected async tppaStop() { + if (this.camera?.id) { + await this.api.tppaStop(this.camera) + } } - openCameraImage() { - return this.browserWindow.openCameraImage(this.camera, 'ALIGNMENT') + protected async openCameraImage() { + if (this.camera?.id) { + await this.browserWindowService.openCameraImage(this.camera, 'ALIGNMENT') + } } private loadPreference() { - const preference = this.preference.alignmentPreference.get() - - this.tppaRequest.startFromCurrentPosition = preference.tppaStartFromCurrentPosition - this.tppaRequest.stepDirection = preference.tppaStepDirection - this.tppaRequest.compensateRefraction = preference.tppaCompensateRefraction - this.tppaRequest.stopTrackingWhenDone = preference.tppaStopTrackingWhenDone - this.tppaRequest.stepDuration = preference.tppaStepDuration - this.tppaRequest.plateSolver.type = preference.tppaPlateSolverType - this.darvRequest.initialPause = preference.darvInitialPause - this.darvRequest.exposureTime = preference.darvExposureTime - this.darvHemisphere = preference.darvHemisphere - - if (this.camera.id) { - const cameraPreference = this.preference.cameraPreference(this.camera).get() - Object.assign(this.tppaRequest.capture, this.preference.cameraStartCaptureForTPPA(this.camera).get(cameraPreference)) - Object.assign(this.darvRequest.capture, this.preference.cameraStartCaptureForDARV(this.camera).get(cameraPreference)) + Object.assign(this.preference, this.preferenceService.alignment.get()) + this.tppaRequest = this.preference.tppaRequest + this.darvRequest = this.preference.darvRequest + if (this.camera?.id) { if (this.camera.connected) { updateCameraStartCaptureFromCamera(this.tppaRequest.capture, this.camera) updateCameraStartCaptureFromCamera(this.darvRequest.capture, this.camera) } } - - this.plateSolverChanged() } - savePreference() { - this.preference.cameraStartCaptureForTPPA(this.camera).set(this.tppaRequest.capture) - this.preference.cameraStartCaptureForDARV(this.camera).set(this.darvRequest.capture) - - const preference: AlignmentPreference = { - tppaStartFromCurrentPosition: this.tppaRequest.startFromCurrentPosition, - tppaStepDirection: this.tppaRequest.stepDirection, - tppaCompensateRefraction: this.tppaRequest.compensateRefraction, - tppaStopTrackingWhenDone: this.tppaRequest.stopTrackingWhenDone, - tppaStepDuration: this.tppaRequest.stepDuration, - tppaPlateSolverType: this.tppaRequest.plateSolver.type, - darvInitialPause: this.darvRequest.initialPause, - darvExposureTime: this.darvRequest.exposureTime, - darvHemisphere: this.darvHemisphere, - } - - this.preference.alignmentPreference.set(preference) + protected savePreference() { + this.preferenceService.alignment.set(this.preference) } } diff --git a/desktop/src/app/app.component.html b/desktop/src/app/app.component.html index 404b64eb1..263704236 100644 --- a/desktop/src/app/app.component.html +++ b/desktop/src/app/app.component.html @@ -7,15 +7,12 @@
{{ title }}
{{ subTitle }}
- - - boolean | Promise private readonly resizeObserver?: ResizeObserver @@ -30,14 +31,14 @@ export class AppComponent implements OnDestroy { constructor( private readonly windowTitle: Title, - private readonly electron: ElectronService, - confirmation: ConfirmationService, + private readonly electronService: ElectronService, + confirmationService: ConfirmationService, ngZone: NgZone, hostElementRef: ElementRef, ) { console.info('APP_CONFIG', APP_CONFIG) - if (electron.isElectron) { + if (electronService.isElectron) { console.info('Run in electron', window.preference) } else { console.info('Run in browser', window.preference) @@ -48,7 +49,7 @@ export class AppComponent implements OnDestroy { const height = entries[0].target.clientHeight if (height) { - void this.electron.resizeWindow(height) + void this.electronService.resizeWindow(height) } }) @@ -57,34 +58,39 @@ export class AppComponent implements OnDestroy { this.resizeObserver = undefined } - electron.on('CONFIRMATION', (event) => { - if (confirmation.has(event.idempotencyKey)) { + electronService.on('CONFIRMATION', (event) => { + if (confirmationService.has(event.idempotencyKey)) { void ngZone.run(() => { - return confirmation.processConfirmationEvent(event) + return confirmationService.processConfirmationEvent(event) }) } }) } + @HostListener('window:unload') ngOnDestroy() { this.resizeObserver?.disconnect() } pin() { this.pinned = !this.pinned - if (this.pinned) return this.electron.pinWindow() - else return this.electron.unpinWindow() + if (this.pinned) return this.electronService.pinWindow() + else return this.electronService.unpinWindow() } minimize() { - return this.electron.minimizeWindow() + return this.electronService.minimizeWindow() } maximize() { - return this.electron.maximizeWindow() + return this.electronService.maximizeWindow() } - close(data?: unknown) { - return this.electron.closeWindow(data) + async close(data?: unknown, force: boolean = false) { + if (!this.beforeClose || (await this.beforeClose()) || force) { + return await this.electronService.closeWindow(data) + } else { + return undefined + } } } diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index cb7b10fff..d816d784b 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -49,6 +49,7 @@ import { DeviceChooserComponent } from '../shared/components/device-chooser/devi import { DeviceListMenuComponent } from '../shared/components/device-list-menu/device-list-menu.component' import { DialogMenuComponent } from '../shared/components/dialog-menu/dialog-menu.component' import { HistogramComponent } from '../shared/components/histogram/histogram.component' +import { LocationComponent } from '../shared/components/location/location.dialog' import { MapComponent } from '../shared/components/map/map.component' import { MenuBarComponent } from '../shared/components/menu-bar/menu-bar.component' import { MenuItemComponent } from '../shared/components/menu-item/menu-item.component' @@ -56,9 +57,8 @@ import { MoonComponent } from '../shared/components/moon/moon.component' import { PathChooserComponent } from '../shared/components/path-chooser/path-chooser.component' import { SlideMenuComponent } from '../shared/components/slide-menu/slide-menu.component' import { ConfirmDialog } from '../shared/dialogs/confirm/confirm.dialog' -import { LocationDialog } from '../shared/dialogs/location/location.dialog' -import { ScrollableNumberDirective } from '../shared/directives/input-number-scrollable' import { NoDropdownDirective } from '../shared/directives/no-dropdown.directive' +import { SpinnableNumberDirective } from '../shared/directives/spinnable-number.directive' import { StopPropagationDirective } from '../shared/directives/stop-propagation.directive' import { ConfirmationInterceptor } from '../shared/interceptors/confirmation.interceptor' import { IdempotencyKeyInterceptor } from '../shared/interceptors/idempotency-key.interceptor' @@ -69,6 +69,7 @@ import { EnumDropdownPipe } from '../shared/pipes/enum-dropdown.pipe' import { EnumPipe } from '../shared/pipes/enum.pipe' import { EnvPipe } from '../shared/pipes/env.pipe' import { ExposureTimePipe } from '../shared/pipes/exposureTime.pipe' +import { PathPipe } from '../shared/pipes/path.pipe' import { SkyObjectPipe } from '../shared/pipes/skyObject.pipe' import { WinPipe } from '../shared/pipes/win.pipe' import { AboutComponent } from './about/about.component' @@ -81,12 +82,14 @@ 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 { ExposureTimeComponent } from './camera/exposure-time.component' import { FilterWheelComponent } from './filterwheel/filterwheel.component' import { FlatWizardComponent } from './flat-wizard/flat-wizard.component' import { FocuserComponent } from './focuser/focuser.component' import { FramingComponent } from './framing/framing.component' import { GuiderComponent } from './guider/guider.component' import { HomeComponent } from './home/home.component' +import { CrossHairComponent } from './image/crosshair.component' import { ImageComponent } from './image/image.component' import { INDIComponent } from './indi/indi.component' import { INDIPropertyComponent } from './indi/property/indi-property.component' @@ -110,6 +113,7 @@ import { StackerComponent } from './stacker/stacker.component' CameraInfoComponent, CameraExposureComponent, ConfirmDialog, + CrossHairComponent, DeviceChooserComponent, DeviceListMenuComponent, DialogMenuComponent, @@ -117,6 +121,7 @@ import { StackerComponent } from './stacker/stacker.component' EnumPipe, EnumDropdownPipe, EnvPipe, + ExposureTimeComponent, ExposureTimePipe, FilterWheelComponent, FlatWizardComponent, @@ -127,10 +132,10 @@ import { StackerComponent } from './stacker/stacker.component' HistogramComponent, HomeComponent, ImageComponent, - ScrollableNumberDirective, + SpinnableNumberDirective, INDIComponent, INDIPropertyComponent, - LocationDialog, + LocationComponent, MapComponent, MenuBarComponent, MenuItemComponent, @@ -138,6 +143,7 @@ import { StackerComponent } from './stacker/stacker.component' MountComponent, NoDropdownDirective, PathChooserComponent, + PathPipe, RotatorComponent, SequencerComponent, SettingsComponent, diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index ab0142c87..840e598a0 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -17,12 +17,14 @@
+ [class.invisible]="!sun.image" + style="height: 225px"> + [src]="sun.image" + style="width: 223px" /> SDO/HMI @@ -43,12 +45,12 @@
+ style="height: 225px"> + [width]="200" + [height]="200" + [illuminationRatio]="moon.position.illuminated / 100" + [waning]="moon.position.leading" />
- +
@@ -109,13 +111,13 @@
@@ -168,8 +170,8 @@ [step]="1" [showButtons]="true" inputStyleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="closeApproachDays" - scrollableNumber /> + [(ngModel)]="minorPlanet.closeApproach.days" + spinnableNumber /> @@ -181,13 +183,13 @@ [showButtons]="true" inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" - [(ngModel)]="closeApproachDistance" - scrollableNumber /> + [(ngModel)]="minorPlanet.closeApproach.lunarDistance" + spinnableNumber />
@@ -263,7 +265,7 @@ + [(ngModel)]="skyObject.search.filter.text" />
@@ -278,7 +280,7 @@ tooltipPosition="bottom" />
+ [(ngModel)]="satellite.search.filter.text" />
@@ -376,7 +378,7 @@ tooltipPosition="bottom" />
+ [value]="position.rightAscensionJ2000" />
@@ -463,7 +465,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.declinationJ2000" /> + [value]="position.declinationJ2000" />
@@ -473,7 +475,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.rightAscension" /> + [value]="position.rightAscension" />
@@ -483,7 +485,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.declination" /> + [value]="position.declination" />
@@ -493,7 +495,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.azimuth" /> + [value]="position.azimuth" />
@@ -503,7 +505,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.altitude" /> + [value]="position.altitude" />
@@ -513,7 +515,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.magnitude < 30 ? bodyPosition.magnitude.toFixed(2) : '-'" /> + [value]="position.magnitude < 30 ? position.magnitude.toFixed(2) : '-'" />
@@ -523,7 +525,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.constellation" /> + [value]="position.constellation" />
@@ -533,8 +535,8 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.distance.toFixed(3)" /> - + [value]="position.distance.toFixed(3)" /> +
@@ -543,7 +545,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.illuminated.toFixed(3)" /> + [value]="position.illuminated.toFixed(3)" />
@@ -553,7 +555,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="bodyPosition.elongation.toFixed(3)" /> + [value]="position.elongation.toFixed(3)" />
@@ -573,7 +575,7 @@
- {{ name ?? '' }} + {{ body.name }}
@@ -643,12 +645,12 @@
-
+
+ [value]="skyObject.search.filter.rightAscension" />
@@ -679,9 +679,9 @@ + [value]="skyObject.search.filter.declination" />
@@ -696,16 +696,17 @@ [showButtons]="true" inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" - [(ngModel)]="skyObjectFilter.radius" - scrollableNumber /> + [(ngModel)]="skyObject.search.filter.radius" + spinnableNumber />
+ @let constellations = ['ALL'].concat('CONSTELLATION' | dropdownOptions);
+ @let skyObjectTypes = ['ALL'].concat('SKY_OBJECT_TYPE' | dropdownOptions); + [(ngModel)]="skyObject.search.filter.magnitude[0]" + spinnableNumber />
@@ -756,8 +758,8 @@ [showButtons]="true" inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" - [(ngModel)]="skyObjectFilter.magnitude[1]" - scrollableNumber /> + [(ngModel)]="skyObject.search.filter.magnitude[1]" + spinnableNumber />
@@ -769,26 +771,28 @@ icon="mdi mdi-filter" label="Filter" size="small" - (onClick)="filterSkyObject()" /> + (onClick)="searchSkyObject()" />
- + @let groups = 'SATELLITE_GROUP_TYPE' | dropdownOptions; + + @for (group of groups; track $index) { - + }
@@ -799,46 +803,27 @@ icon="mdi mdi-restore" label="Reset" size="small" - (onClick)="resetSatelliteFilter()" /> + (onClick)="resetSatelliteSearchGroups()" /> - -
- - -
-
-
- - + (onClick)="searchSatellite()" />
-
+
-
+
- Enable + Date Time + [(ngModel)]="dateTimeAndLocation.manual" + (ngModelChange)="manualDateTimeChanged()" />
-
+
-
- Hour + -
-
- Minute + spinnableNumber /> + + + -
+ spinnableNumber /> + +
+ + + +
@@ -899,5 +892,6 @@ + [header]="body.name" /> + diff --git a/desktop/src/app/atlas/atlas.component.scss b/desktop/src/app/atlas/atlas.component.scss index d1370a819..e332e3151 100644 --- a/desktop/src/app/atlas/atlas.component.scss +++ b/desktop/src/app/atlas/atlas.component.scss @@ -1,58 +1,56 @@ -:host { +neb-atlas { display: flex; flex-direction: column; height: 100vh; - ::ng-deep { - .p-tabview { - p-table.planet .p-datatable-wrapper { - height: 229px; - } + .p-tabview { + p-table.planet .p-datatable-wrapper { + height: 229px; + } - p-table.minorPlanet .p-datatable-wrapper { - height: 154px; - } + p-table.minorPlanet .p-datatable-wrapper { + height: 154px; + } - p-table.skyObject .p-datatable-wrapper, - p-table.satellite .p-datatable-wrapper { - height: 143px; - } + p-table.skyObject .p-datatable-wrapper, + p-table.satellite .p-datatable-wrapper { + height: 143px; + } - .p-tabview-nav li.main { - width: calc(100% / 6) !important; + .p-tabview-nav li.main { + width: calc(100% / 6) !important; - .p-tabview-nav-link { - display: flex; - justify-content: center; - padding: 0px; - height: 100%; + .p-tabview-nav-link { + display: flex; + justify-content: center; + padding: 0px; + height: 100%; - img { - height: 20px; - } + img { + height: 20px; } } } + } - .info.p-tabview { - padding-left: 0.21rem; - padding-right: 0.21rem; - } + .info.p-tabview { + padding-left: 0.21rem; + padding-right: 0.21rem; + } - .p-tabview .p-tabview-left-icon { - margin-right: 0px; - } + .p-tabview .p-tabview-left-icon { + margin-right: 0px; } -} -.p-input-icon-left span.pi:first-of-type { - position: absolute; - top: 50%; - margin-top: -0.5rem; - left: 0.75rem; - z-index: 1; -} + .p-input-icon-left span.pi:first-of-type { + position: absolute; + top: 50%; + margin-top: -0.5rem; + left: 0.75rem; + z-index: 1; + } -.p-input-icon-right p-checkbox { - margin-top: -0.745rem; + .p-input-icon-right p-checkbox { + margin-top: -0.745rem; + } } diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 697e5cc48..dbcf90086 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -1,145 +1,65 @@ -import { AfterContentInit, AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { AfterContentInit, AfterViewInit, Component, HostListener, NgZone, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { Chart, ChartData, ChartOptions } from 'chart.js' import zoomPlugin from 'chartjs-plugin-zoom' -import moment from 'moment' import { UIChart } from 'primeng/chart' import { ListboxChangeEvent } from 'primeng/listbox' import { OverlayPanel } from 'primeng/overlaypanel' -import { Subscription, timer } from 'rxjs' +import { timer } from 'rxjs' import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' import { ONE_DECIMAL_PLACE_FORMATTER, TWO_DIGITS_FORMATTER } from '../../shared/constants' -import { SkyObjectPipe } from '../../shared/pipes/skyObject.pipe' +import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { PrimeService } from '../../shared/services/prime.service' +import { extractDate, extractTime } from '../../shared/types/angular.types' import { - CONSTELLATIONS, - CloseApproach, - Constellation, - DeepSkyObject, - EMPTY_BODY_POSITION, - EMPTY_SEARCH_FILTER, + AltitudeDataPoint, + BodyTabType, + BodyTag, + DEFAULT_BODY_TAB_REFRESH, + DEFAULT_DATE_TIME_AND_LOCATION, + DEFAULT_LOCATION, + DEFAULT_MINOR_PLANET, + DEFAULT_MOON, + DEFAULT_PLANET, + DEFAULT_SATELLITE, + DEFAULT_SKY_ATLAS_PREFERENCE, + DEFAULT_SKY_OBJECT, + DEFAULT_SUN, Location, - MinorPlanet, - MinorPlanetSearchItem, - PlanetTableItem, + MinorPlanetListItem, SATELLITE_GROUPS, - Satellite, - SatelliteGroupType, - SettingsDialog, SkyAtlasInput, - SkyAtlasTab, + resetSatelliteSearchGroup, + searchFilterWithDefault, } from '../../shared/types/atlas.types' import { Mount } from '../../shared/types/mount.types' import { AppComponent } from '../app.component' -Chart.register(zoomPlugin) - @Component({ selector: 'neb-atlas', templateUrl: './atlas.component.html', styleUrls: ['./atlas.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { - refreshingPosition = false - refreshingChart = false - tab = SkyAtlasTab.SUN - - get refreshing() { - return this.refreshingPosition || this.refreshingChart - } - - readonly bodyPosition = structuredClone(EMPTY_BODY_POSITION) - moonIlluminated = 1 - moonWaning = false - - useManualDateTime = false - dateTime = new Date() - dateTimeHour = this.dateTime.getHours() - dateTimeMinute = this.dateTime.getMinutes() - - planet?: PlanetTableItem - readonly planets: PlanetTableItem[] = [ - { name: 'Mercury', type: 'Planet', code: '199' }, - { name: 'Venus', type: 'Planet', code: '299' }, - { name: 'Mars', type: 'Planet', code: '499' }, - { name: 'Jupiter', type: 'Planet', code: '599' }, - { name: 'Saturn', type: 'Planet', code: '699' }, - { name: 'Uranus', type: 'Planet', code: '799' }, - { name: 'Neptune', type: 'Planet', code: '899' }, - { name: 'Pluto', type: 'Dwarf Planet', code: '999' }, - { name: 'Phobos', type: `Mars' Satellite`, code: '401' }, - { name: 'Deimos', type: `Mars' Satellite`, code: '402' }, - { name: 'Io', type: `Jupiter's Satellite`, code: '501' }, - { name: 'Europa', type: `Jupiter's Satellite`, code: '402' }, - { name: 'Ganymede', type: `Jupiter's Satellite`, code: '403' }, - { name: 'Callisto', type: `Jupiter's Satellite`, code: '504' }, - { name: 'Mimas', type: `Saturn's Satellite`, code: '601' }, - { name: 'Enceladus', type: `Saturn's Satellite`, code: '602' }, - { name: 'Tethys', type: `Saturn's Satellite`, code: '603' }, - { name: 'Dione', type: `Saturn's Satellite`, code: '604' }, - { name: 'Rhea', type: `Saturn's Satellite`, code: '605' }, - { name: 'Titan', type: `Saturn's Satellite`, code: '606' }, - { name: 'Hyperion', type: `Saturn's Satellite`, code: '607' }, - { name: 'Iapetus', type: `Saturn's Satellite`, code: '608' }, - { name: 'Ariel', type: `Uranus' Satellite`, code: '701' }, - { name: 'Umbriel', type: `Uranus' Satellite`, code: '702' }, - { name: 'Titania', type: `Uranus' Satellite`, code: '703' }, - { name: 'Oberon', type: `Uranus' Satellite`, code: '704' }, - { name: 'Miranda', type: `Uranus' Satellite`, code: '705' }, - { name: 'Triton', type: `Neptune's Satellite`, code: '801' }, - { name: 'Charon', type: `Pluto's Satellite`, code: '901' }, - { name: '1 Ceres', type: 'Dwarf Planet', code: '1;' }, - { name: '90377 Sedna', type: 'Dwarf Planet', code: '90377;' }, - { name: '136199 Eris', type: 'Dwarf Planet', code: '136199;' }, - { name: '2 Pallas', type: 'Asteroid', code: '2;' }, - { name: '3 Juno', type: 'Asteroid', code: '3;' }, - { name: '4 Vesta', type: 'Asteroid', code: '4;' }, - ] - - minorPlanetTab = 0 - minorPlanet?: MinorPlanet - minorPlanetSearchText = '' - minorPlanetChoiceItems: { name: string; pdes: string }[] = [] - showMinorPlanetChoiceDialog = false - closeApproach?: CloseApproach - closeApproaches: CloseApproach[] = [] - closeApproachDays = 7 - closeApproachDistance = 10 - - skyObject?: DeepSkyObject - skyObjectItems: DeepSkyObject[] = [] - skyObjectSearchText = '' - readonly skyObjectFilter = structuredClone(EMPTY_SEARCH_FILTER) - showSkyObjectFilter = false - readonly constellationOptions: (Constellation | 'ALL')[] = ['ALL', ...CONSTELLATIONS] - - satellite?: Satellite - satelliteItems: Satellite[] = [] - satelliteSearchText = '' - showSatelliteFilterDialog = false - readonly satelliteSearchGroup = new Map() - - name? = 'Sun' - tags: { title: string; severity: 'success' | 'info' | 'warning' | 'danger' }[] = [] - - @ViewChild('imageOfSun') - private readonly imageOfSun!: ElementRef - - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent - - @ViewChild('calendarPanel') - private readonly calendarPanel!: OverlayPanel - - @ViewChild('chart') - private readonly chart!: UIChart - - readonly altitudeData: ChartData = { + protected readonly sun = structuredClone(DEFAULT_SUN) + protected readonly moon = structuredClone(DEFAULT_MOON) + protected readonly planet = structuredClone(DEFAULT_PLANET) + protected readonly minorPlanet = structuredClone(DEFAULT_MINOR_PLANET) + protected readonly skyObject = structuredClone(DEFAULT_SKY_OBJECT) + protected readonly satellite = structuredClone(DEFAULT_SATELLITE) + protected readonly preference = structuredClone(DEFAULT_SKY_ATLAS_PREFERENCE) + protected readonly refresh = structuredClone(DEFAULT_BODY_TAB_REFRESH) + protected readonly dateTimeAndLocation = structuredClone(DEFAULT_DATE_TIME_AND_LOCATION) + + protected tab = BodyTabType.SUN + protected locations: Location[] = [structuredClone(DEFAULT_LOCATION)] + + protected readonly altitudeData: ChartData = { labels: ['12h', '13h', '14h', '15h', '16h', '17h', '18h', '19h', '20h', '21h', '22h', '23h', '0h', '1h', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', '10h', '11h', '12h'], datasets: [ // Day. @@ -262,7 +182,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, ], } - readonly altitudeOptions: ChartOptions = { + protected readonly altitudeOptions: ChartOptions = { responsive: true, plugins: { legend: { @@ -371,84 +291,99 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, }, } - private static readonly DEFAULT_SATELLITE_FILTERS: SatelliteGroupType[] = ['AMATEUR', 'BEIDOU', 'GALILEO', 'GLO_OPS', 'GNSS', 'GPS_OPS', 'ONEWEB', 'SCIENCE', 'STARLINK', 'STATIONS', 'VISUAL'] - - readonly ephemerisModel: SlideMenuItem[] = [ + protected readonly ephemerisModel: SlideMenuItem[] = [ { icon: 'mdi mdi-magnify', label: 'Find sky objects around this object', slideMenu: [], command: async () => { - this.skyObjectFilter.rightAscension = this.bodyPosition.rightAscensionJ2000 - this.skyObjectFilter.declination = this.bodyPosition.declinationJ2000 - if (this.skyObjectFilter.radius <= 0) this.skyObjectFilter.radius = 4 + this.skyObject.search.filter.rightAscension = this.position.rightAscensionJ2000 + this.skyObject.search.filter.declination = this.position.declinationJ2000 + if (this.skyObject.search.filter.radius <= 0) this.skyObject.search.filter.radius = 4 - this.tab = SkyAtlasTab.SKY_OBJECT + this.tab = BodyTabType.SKY_OBJECT await this.tabChanged() - await this.filterSkyObject() + await this.searchSkyObject() }, }, ] - private refreshTimer?: Subscription - private refreshTabCount = 0 + @ViewChild('deviceMenu') + private readonly deviceMenu!: DeviceListMenuComponent + + @ViewChild('dateTimeAndLocationPanel') + private readonly dateTimeAndLocationPanel!: OverlayPanel + + @ViewChild('chart') + private readonly chart!: UIChart + + get body() { + switch (this.tab) { + case BodyTabType.SUN: + return this.sun + case BodyTabType.MOON: + return this.moon + case BodyTabType.PLANET: + return this.planet + case BodyTabType.MINOR_PLANET: + return this.minorPlanet + case BodyTabType.SKY_OBJECT: + return this.skyObject + case BodyTabType.SATELLITE: + return this.satellite + default: + return this.sun + } + } - private location: Location + get position() { + return this.body.position + } - readonly settings: SettingsDialog = { - showDialog: false, + get refreshing() { + return this.refresh.position || this.refresh.chart } constructor( private readonly app: AppComponent, private readonly api: ApiService, - private readonly browserWindow: BrowserWindowService, + private readonly browserWindowService: BrowserWindowService, private readonly route: ActivatedRoute, - electron: ElectronService, - private readonly preference: PreferenceService, - private readonly skyObjectPipe: SkyObjectPipe, - private readonly prime: PrimeService, + electronService: ElectronService, + private readonly preferenceService: PreferenceService, + private readonly angularService: AngularService, ngZone: NgZone, ) { app.title = 'Sky Atlas' - app.topMenu.push({ - icon: 'mdi mdi-cog', - tooltip: 'Settings', - visible: false, - command: () => { - this.settings.showDialog = true - }, - }) app.topMenu.push({ icon: 'mdi mdi-calendar', - tooltip: 'Date & Time', + tooltip: 'Date Time and Location', command: (e) => { - this.calendarPanel.toggle(e.originalEvent) + this.dateTimeAndLocationPanel.toggle(e.originalEvent) }, }) - electron.on('LOCATION.CHANGED', async (event) => { - await ngZone.run(() => { - this.location = event - return this.refreshTab(true, true) + electronService.on('LOCATION.CHANGED', (location) => { + ngZone.run(() => { + this.loadLocations() + + if (this.dateTimeAndLocation.location.id === location.id) { + void this.refreshTab(true, true) + } }) }) - electron.on('DATA.CHANGED', async (event) => { + electronService.on('DATA.CHANGED', async (event) => { await this.loadTabFromData(event) }) - - this.location = this.preference.selectedLocation.get() - - // TODO: Refresh graph and twilight if hours past 12 (noon) } - async ngOnInit() { + ngOnInit() { + Chart.register(zoomPlugin) + this.loadPreference() - const types = await this.api.skyObjectTypes() - this.skyObjectFilter.types = ['ALL', ...types] } ngAfterContentInit() { @@ -462,8 +397,8 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, const now = new Date() const initialDelay = 60 * 1000 - (now.getSeconds() * 1000 + now.getMilliseconds()) - this.refreshTimer = timer(initialDelay, 60 * 1000).subscribe(async () => { - if (!this.useManualDateTime) { + this.refresh.timer = timer(initialDelay, 60 * 1000).subscribe(async () => { + if (!this.dateTimeAndLocation.manual) { await this.refreshTab() } }) @@ -476,285 +411,244 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, async ngAfterViewInit() { await this.refreshTab() - - this.calendarPanel.onOverlayClick = (e) => { - e.stopImmediatePropagation() - } } @HostListener('window:unload') ngOnDestroy() { - this.refreshTimer?.unsubscribe() + this.refresh.timer?.unsubscribe() } private async loadTabFromData(data?: SkyAtlasInput) { - if (data?.tab) { + if (data && data.tab >= BodyTabType.SUN) { this.tab = data.tab - if (this.tab === SkyAtlasTab.SKY_OBJECT) { - this.skyObjectFilter.rightAscension = data.filter?.rightAscension ?? this.skyObjectFilter.rightAscension - this.skyObjectFilter.declination = data.filter?.declination ?? this.skyObjectFilter.declination - this.skyObjectFilter.radius = (data.filter?.radius ?? this.skyObjectFilter.radius) || 4.0 - this.skyObjectFilter.constellation = data.filter?.constellation ?? this.skyObjectFilter.constellation - this.skyObjectFilter.magnitude = data.filter?.magnitude ?? this.skyObjectFilter.magnitude - this.skyObjectFilter.type = data.filter?.type ?? this.skyObjectFilter.type + if (this.tab === BodyTabType.SKY_OBJECT) { + this.skyObject.search.filter = searchFilterWithDefault(data.filter, this.skyObject.search.filter) await this.tabChanged() - await this.filterSkyObject() + await this.searchSkyObject() } } } - async tabChanged() { - await this.refreshTab(false, true) + protected tabChanged() { + return this.refreshTab(false, true) } - async planetChanged() { - await this.refreshTab(false, true) + protected async planetChanged() { + if (this.planet.selected) { + this.planet.name = this.planet.selected.name + await this.refreshTab(false, true) + } } - async searchMinorPlanet() { - this.refreshingPosition = true + protected async searchMinorPlanet() { + this.refresh.position = true try { - const minorPlanet = await this.api.searchMinorPlanet(this.minorPlanetSearchText) + const minorPlanet = await this.api.searchMinorPlanet(this.minorPlanet.search.text) if (minorPlanet.found) { - this.minorPlanet = minorPlanet + this.minorPlanet.search.result = minorPlanet + this.minorPlanet.name = minorPlanet.name + + const tags: BodyTag[] = [] + // if (minorPlanet.kind) tags.push({ label: minorPlanet.kind, severity: 'success' }) + if (minorPlanet.orbitType) tags.push({ label: minorPlanet.orbitType, severity: 'success' }) + if (minorPlanet.pha) tags.push({ label: 'PHA', severity: 'danger' }) + if (minorPlanet.neo) tags.push({ label: 'NEO', severity: 'warning' }) + this.minorPlanet.tags = tags + await this.refreshTab(false, true) - } else { - this.minorPlanetChoiceItems = minorPlanet.searchItems - this.showMinorPlanetChoiceDialog = true + } else if (minorPlanet.list.length) { + this.minorPlanet.list.items = minorPlanet.list + this.minorPlanet.list.showDialog = true } } finally { - this.refreshingPosition = false + this.refresh.position = false } } - async minorPlanetChoosen(event: ListboxChangeEvent) { - this.minorPlanetSearchText = (event.value as MinorPlanetSearchItem).pdes + protected async minorPlanetSelected(event: ListboxChangeEvent) { + const value = event.value as MinorPlanetListItem + this.minorPlanet.search.text = value.pdes + this.minorPlanet.list.showDialog = false await this.searchMinorPlanet() - this.showMinorPlanetChoiceDialog = false } - async closeApproachesForMinorPlanets() { - this.refreshingPosition = true + protected async closeApproachesOfMinorPlanets() { + this.refresh.position = true try { - this.closeApproaches = await this.api.closeApproachesForMinorPlanets(this.closeApproachDays, this.closeApproachDistance, this.dateTime) + this.minorPlanet.closeApproach.result = await this.api.closeApproachesOfMinorPlanets(this.minorPlanet.closeApproach.days, this.minorPlanet.closeApproach.lunarDistance, this.dateTimeAndLocation.dateTime) - if (!this.closeApproaches.length) { - this.prime.message('No close approaches found for the given days and lunar distance', 'warn') + if (!this.minorPlanet.closeApproach.result.length) { + this.angularService.message('No close approaches found for the given days and lunar distance', 'warn') } } finally { - this.refreshingPosition = false + this.refresh.position = false } } - async closeApproachChanged() { - if (this.closeApproach) { - this.minorPlanetSearchText = this.closeApproach.designation - this.minorPlanetTab = 0 + protected async closeApproachChanged() { + if (this.minorPlanet.closeApproach.selected) { + this.minorPlanet.search.text = this.minorPlanet.closeApproach.selected.designation + this.minorPlanet.tab = 0 await this.searchMinorPlanet() } } - starChanged() { - return this.refreshTab(false, true) - } - - dsoChanged() { - return this.refreshTab(false, true) - } - - skyObjectChanged() { - return this.refreshTab(false, true) - } - - satelliteChanged() { - return this.refreshTab(false, true) + protected async skyObjectChanged() { + if (this.skyObject.search.selected) { + this.skyObject.name = this.skyObject.search.selected.name + await this.refreshTab(false, true) + } } - showSkyObjectFilterDialog() { - this.showSkyObjectFilter = true + protected async satelliteChanged() { + if (this.satellite.search.selected) { + this.satellite.name = this.satellite.search.selected.name + await this.refreshTab(false, true) + } } - async searchSkyObject() { - const constellation = this.skyObjectFilter.constellation === 'ALL' ? undefined : this.skyObjectFilter.constellation - const type = this.skyObjectFilter.type === 'ALL' ? undefined : this.skyObjectFilter.type + protected async searchSkyObject() { + const constellation = this.skyObject.search.filter.constellation === 'ALL' ? undefined : this.skyObject.search.filter.constellation + const type = this.skyObject.search.filter.type === 'ALL' ? undefined : this.skyObject.search.filter.type - this.refreshingPosition = true + this.refresh.position = true try { - this.skyObjectItems = await this.api.searchSkyObject(this.skyObjectSearchText, this.skyObjectFilter.rightAscension, this.skyObjectFilter.declination, this.skyObjectFilter.radius, constellation, this.skyObjectFilter.magnitude[0], this.skyObjectFilter.magnitude[1], type) + const { text, rightAscension, declination, radius, magnitude } = this.skyObject.search.filter + this.skyObject.search.result = await this.api.searchSkyObject(text, rightAscension, declination, radius, constellation, magnitude[0], magnitude[1], type) } finally { - this.refreshingPosition = false + this.skyObject.search.showDialog = false + this.refresh.position = false } } - async filterSkyObject() { - await this.searchSkyObject() - this.showSkyObjectFilter = false - } - - async searchSatellite() { - this.refreshingPosition = true + protected async searchSatellite() { + this.refresh.position = true try { - this.savePreference() - const groups = SATELLITE_GROUPS.filter((e) => this.satelliteSearchGroup.get(e)) - this.satelliteItems = await this.api.searchSatellites(this.satelliteSearchText, groups) + const groups = SATELLITE_GROUPS.filter((e) => this.satellite.search.filter.groups[e]) + this.satellite.search.result = await this.api.searchSatellites(this.satellite.search.filter.text, groups) } finally { - this.refreshingPosition = false + this.satellite.search.showDialog = false + this.refresh.position = false } } - resetSatelliteFilter() { - for (const group of SATELLITE_GROUPS) { - const enabled = AtlasComponent.DEFAULT_SATELLITE_FILTERS.includes(group) - this.satelliteSearchGroup.set(group, enabled) - } - + protected resetSatelliteSearchGroups() { + resetSatelliteSearchGroup(this.satellite.search.filter.groups) this.savePreference() } - async filterSatellite() { - await this.searchSatellite() - this.showSatelliteFilterDialog = false - } - - async dateTimeChanged(dateChanged: boolean) { + protected async dateTimeChanged(dateChanged: boolean) { + this.savePreference() await this.refreshTab(dateChanged, true) } - async useManualDateTimeChanged() { - if (!this.useManualDateTime) { + protected async manualDateTimeChanged() { + this.savePreference() + + if (!this.dateTimeAndLocation.manual) { await this.refreshTab(true, true) } } - mountGoTo() { + protected locationChanged() { + this.savePreference() + return this.refreshTab(true, true) + } + + protected mountGoTo() { return this.executeMount((mount) => { - return this.api.mountGoTo(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) + return this.api.mountGoTo(mount, this.position.rightAscension, this.position.declination, false) }) } - mountSlew() { + protected mountSlew() { return this.executeMount((mount) => { - return this.api.mountSlew(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) + return this.api.mountSlew(mount, this.position.rightAscension, this.position.declination, false) }) } - mountSync() { + protected mountSync() { return this.executeMount((mount) => { - return this.api.mountSync(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) + return this.api.mountSync(mount, this.position.rightAscension, this.position.declination, false) }) } - frame() { - return this.browserWindow.openFraming({ - rightAscension: this.bodyPosition.rightAscensionJ2000, - declination: this.bodyPosition.declinationJ2000, + protected frame() { + return this.browserWindowService.openFraming({ + rightAscension: this.position.rightAscensionJ2000, + declination: this.position.declinationJ2000, }) } - async refreshTab(refreshTwilight: boolean = false, refreshChart: boolean = false) { - this.refreshingPosition = true - this.refreshTabCount++ + private async refreshTab(refreshTwilight: boolean = false, refreshChart: boolean = false) { + this.refresh.position = true + this.refresh.count++ - if (!this.useManualDateTime) { - this.dateTime = new Date() - this.dateTimeHour = this.dateTime.getHours() - this.dateTimeMinute = this.dateTime.getMinutes() - } else { - this.dateTime.setHours(this.dateTimeHour) - this.dateTime.setMinutes(this.dateTimeMinute) + if (!this.dateTimeAndLocation.manual) { + this.dateTimeAndLocation.dateTime = new Date() } - this.app.subTitle = `${this.location.name} · ${moment(this.dateTime).format('YYYY-MM-DD HH:mm')}` + const { dateTime, location } = this.dateTimeAndLocation + + this.app.subTitle = `${location.name} · ${extractDate(dateTime)} ${extractTime(dateTime, false)}` try { // Sun. - if (this.tab === SkyAtlasTab.SUN) { - this.name = 'Sun' - this.tags = [] - this.imageOfSun.nativeElement.src = `${this.api.baseUrl}/sky-atlas/sun/image` - const bodyPosition = await this.api.positionOfSun(this.dateTime) - Object.assign(this.bodyPosition, bodyPosition) + if (this.tab === BodyTabType.SUN) { + this.sun.image = `${this.api.baseUrl}/sky-atlas/sun/image` + const position = await this.api.positionOfSun(dateTime, location) + Object.assign(this.sun.position, position) } // Moon. - else if (this.tab === SkyAtlasTab.MOON) { - this.name = 'Moon' - this.tags = [] - const bodyPosition = await this.api.positionOfMoon(this.dateTime) - Object.assign(this.bodyPosition, bodyPosition) - this.moonIlluminated = this.bodyPosition.illuminated / 100.0 - this.moonWaning = this.bodyPosition.leading + else if (this.tab === BodyTabType.MOON) { + const position = await this.api.positionOfMoon(dateTime, location) + Object.assign(this.moon.position, position) } // Planet. - else if (this.tab === SkyAtlasTab.PLANET) { - this.tags = [] - - if (this.planet) { - this.name = this.planet.name - const bodyPosition = await this.api.positionOfPlanet(this.planet.code, this.dateTime) - Object.assign(this.bodyPosition, bodyPosition) - } else { - this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + else if (this.tab === BodyTabType.PLANET) { + if (this.planet.selected) { + const position = await this.api.positionOfPlanet(this.planet.selected.code, dateTime, location) + Object.assign(this.planet.position, position) } } // Minor Planet. - else if (this.tab === SkyAtlasTab.MINOR_PLANET) { - this.tags = [] - - if (this.minorPlanet) { - this.name = this.minorPlanet.name - // if (this.minorPlanet.kind) this.tags.push({ title: this.minorPlanet.kind, severity: 'success' }) - if (this.minorPlanet.orbitType) this.tags.push({ title: this.minorPlanet.orbitType, severity: 'success' }) - if (this.minorPlanet.pha) this.tags.push({ title: 'PHA', severity: 'danger' }) - if (this.minorPlanet.neo) this.tags.push({ title: 'NEO', severity: 'warning' }) - const code = `DES=${this.minorPlanet.spkId};` - const bodyPosition = await this.api.positionOfPlanet(code, this.dateTime) - Object.assign(this.bodyPosition, bodyPosition) - } else { - this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + else if (this.tab === BodyTabType.MINOR_PLANET) { + if (this.minorPlanet.search.result) { + const code = `DES=${this.minorPlanet.search.result.spkId};` + const position = await this.api.positionOfPlanet(code, dateTime, location) + Object.assign(this.minorPlanet.position, position) } } // Sky Object. - else if (this.tab === SkyAtlasTab.SKY_OBJECT) { - this.tags = [] + else if (this.tab === BodyTabType.SKY_OBJECT) { + const selected = this.skyObject.search.selected - if (this.skyObject) { - this.name = this.skyObjectPipe.transform(this.skyObject, 'name') - const bodyPosition = await this.api.positionOfSkyObject(this.skyObject, this.dateTime) - Object.assign(this.bodyPosition, bodyPosition) - } else { - this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + if (selected) { + const position = await this.api.positionOfSkyObject(selected, dateTime, location) + Object.assign(this.skyObject.position, position) } } // Satellite. else { - this.tags = [] - - if (this.satellite) { - this.name = this.satellite.name - const bodyPosition = await this.api.positionOfSatellite(this.satellite, this.dateTime) - Object.assign(this.bodyPosition, bodyPosition) - } else { - this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + if (this.satellite.search.selected) { + const position = await this.api.positionOfSatellite(this.satellite.search.selected, dateTime, location) + Object.assign(this.satellite.position, position) } } - this.refreshingPosition = false + this.refresh.position = false - if (this.refreshTabCount === 1 || refreshTwilight) { - this.refreshingChart = true + if (this.refresh.count === 1 || refreshTwilight) { + this.refresh.chart = true - const twilight = await this.api.twilight(this.dateTime) + const twilight = await this.api.twilight(dateTime, location) this.altitudeData.datasets[0].data = [ [0.0, 90], [twilight.civilDusk[0], 90], @@ -791,101 +685,108 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, [twilight.civilDawn[1], 90], [24.0, 90], ] + this.chart.refresh() } - if (this.refreshTabCount === 1 || refreshChart) { + if (this.refresh.count === 1 || refreshChart) { await this.refreshChart() } } finally { - this.refreshingPosition = false - this.refreshingChart = false + this.refresh.position = false + this.refresh.chart = false } } private async refreshChart() { - this.refreshingChart = true + this.refresh.chart = true + + const { dateTime, location } = this.dateTimeAndLocation try { // Sun. - if (this.tab === SkyAtlasTab.SUN) { - const points = await this.api.altitudePointsOfSun(this.dateTime) - AtlasComponent.belowZeroPoints(points) - this.altitudeData.datasets[9].data = points + if (this.tab === BodyTabType.SUN) { + const points = await this.api.altitudePointsOfSun(dateTime, location) + this.updateAltitudeDataPoints(points) } // Moon. - else if (this.tab === SkyAtlasTab.MOON) { - const points = await this.api.altitudePointsOfMoon(this.dateTime) - AtlasComponent.belowZeroPoints(points) - this.altitudeData.datasets[9].data = points + else if (this.tab === BodyTabType.MOON) { + const points = await this.api.altitudePointsOfMoon(dateTime, location) + this.updateAltitudeDataPoints(points) } // Planet. - else if (this.tab === SkyAtlasTab.PLANET && this.planet) { - const points = await this.api.altitudePointsOfPlanet(this.planet.code, this.dateTime) - AtlasComponent.belowZeroPoints(points) - this.altitudeData.datasets[9].data = points + else if (this.tab === BodyTabType.PLANET) { + if (this.planet.selected) { + const points = await this.api.altitudePointsOfPlanet(this.planet.selected.code, dateTime, location) + this.updateAltitudeDataPoints(points) + } else { + this.updateAltitudeDataPoints() + } } // Minor Planet. - else if (this.tab === SkyAtlasTab.MINOR_PLANET) { - if (this.minorPlanet) { - const code = `DES=${this.minorPlanet.spkId};` - const points = await this.api.altitudePointsOfPlanet(code, this.dateTime) - AtlasComponent.belowZeroPoints(points) - this.altitudeData.datasets[9].data = points + else if (this.tab === BodyTabType.MINOR_PLANET) { + if (this.minorPlanet.search.result) { + const code = `DES=${this.minorPlanet.search.result.spkId};` + const points = await this.api.altitudePointsOfPlanet(code, dateTime, location) + this.updateAltitudeDataPoints(points) } else { - this.altitudeData.datasets[9].data = [] + this.updateAltitudeDataPoints() } } // Sky Object. - else if (this.tab === SkyAtlasTab.SKY_OBJECT) { - if (this.skyObject) { - const points = await this.api.altitudePointsOfSkyObject(this.skyObject, this.dateTime) - AtlasComponent.belowZeroPoints(points) - this.altitudeData.datasets[9].data = points + else if (this.tab === BodyTabType.SKY_OBJECT) { + if (this.skyObject.search.selected) { + const points = await this.api.altitudePointsOfSkyObject(this.skyObject.search.selected, dateTime, location) + this.updateAltitudeDataPoints(points) } else { - this.altitudeData.datasets[9].data = [] + this.updateAltitudeDataPoints() } } // Satellite. - else if (this.tab === SkyAtlasTab.SATELLITE) { - if (this.satellite) { - const points = await this.api.altitudePointsOfSatellite(this.satellite, this.dateTime) - AtlasComponent.belowZeroPoints(points) - this.altitudeData.datasets[9].data = points + else { + if (this.satellite.search.selected) { + const points = await this.api.altitudePointsOfSatellite(this.satellite.search.selected, dateTime, location) + this.updateAltitudeDataPoints(points) } else { - this.altitudeData.datasets[9].data = [] + this.updateAltitudeDataPoints() } - } else { - return } this.chart.refresh() } finally { - this.refreshingChart = false + this.refresh.chart = false } } - private loadPreference() { - const preference = this.preference.skyAtlasPreference.get() - - for (const group of SATELLITE_GROUPS) { - const satellite = preference.satellites.find((e) => e.group === group) - const enabled = satellite?.enabled ?? AtlasComponent.DEFAULT_SATELLITE_FILTERS.includes(group) - this.satelliteSearchGroup.set(group, enabled) + private updateAltitudeDataPoints(points?: AltitudeDataPoint[]) { + if (points?.length) { + AtlasComponent.removePointsBelowZero(points) + this.altitudeData.datasets[9].data = points + } else { + this.altitudeData.datasets[9].data = [] } } - savePreference() { - const preference = this.preference.skyAtlasPreference.get() + private loadLocations() { + const settings = this.preferenceService.settings.get() + this.locations = settings.locations + this.dateTimeAndLocation.location = this.locations.find((e) => e.id === this.dateTimeAndLocation.location.id) ?? this.locations.find((e) => e.id === settings.location.id) ?? this.locations[0] + } - preference.satellites = SATELLITE_GROUPS.map((group) => { - return { group, enabled: this.satelliteSearchGroup.get(group) ?? false } - }) + private loadPreference() { + Object.assign(this.preference, this.preferenceService.skyAtlasPreference.get()) + this.satellite.search.filter.groups = this.preference.satellites + this.dateTimeAndLocation.location = this.preference.location + + this.loadLocations() + } - this.preference.skyAtlasPreference.set(preference) + protected savePreference() { + this.preference.location = this.dateTimeAndLocation.location + this.preferenceService.skyAtlasPreference.set(this.preference) } - private static belowZeroPoints(points: [number, number][]) { + private static removePointsBelowZero(points: AltitudeDataPoint[]) { for (const point of points) { if (point[1] < 0) { point[1] = NaN @@ -894,7 +795,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, } private async executeMount(action: (mount: Mount) => void | Promise) { - if (await this.prime.confirm('Are you sure that you want to proceed?')) { + if (await this.angularService.confirm('Are you sure that you want to proceed?')) { return false } diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html index 039328e12..d2ec84a7f 100644 --- a/desktop/src/app/autofocus/autofocus.component.html +++ b/desktop/src/app/autofocus/autofocus.component.html @@ -7,7 +7,7 @@ [(device)]="camera" (deviceChange)="cameraChanged()" /> @@ -37,7 +37,7 @@ pInputText readonly class="p-inputtext-sm border-0 max-w-full" - [value]="focuser.position" /> + [value]="focuser?.position ?? 0" />
@@ -71,28 +71,28 @@
+ spinnableNumber />
+ spinnableNumber />
@@ -105,7 +105,7 @@ [min]="1" [max]="10" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -146,7 +146,7 @@ [step]="0.1" (ngModelChange)="savePreference()" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -175,7 +175,7 @@ [min]="1" [max]="1000" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -189,7 +189,7 @@ [min]="1" [max]="1000" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber /> @@ -214,7 +214,7 @@
{ - if (event.device.id === this.camera.id) { + electronService.on('CAMERA.UPDATED', (event) => { + if (event.device.id === this.camera?.id) { ngZone.run(() => { - Object.assign(this.camera, event.device) + if (this.camera) { + Object.assign(this.camera, event.device) + } }) } }) - electron.on('CAMERA.ATTACHED', (event) => { + electronService.on('CAMERA.ATTACHED', (event) => { ngZone.run(() => { this.cameras.push(event.device) this.cameras.sort(deviceComparator) }) }) - electron.on('CAMERA.DETACHED', (event) => { + electronService.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) + Object.assign(this.camera, this.cameras[0] ?? DEFAULT_CAMERA) } this.cameras.splice(index, 1) @@ -272,28 +272,30 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { }) }) - electron.on('FOCUSER.UPDATED', (event) => { - if (event.device.id === this.focuser.id) { + electronService.on('FOCUSER.UPDATED', (event) => { + if (event.device.id === this.focuser?.id) { ngZone.run(() => { - Object.assign(this.focuser, event.device) + if (this.focuser) { + Object.assign(this.focuser, event.device) + } }) } }) - electron.on('FOCUSER.ATTACHED', (event) => { + electronService.on('FOCUSER.ATTACHED', (event) => { ngZone.run(() => { this.focusers.push(event.device) this.focusers.sort(deviceComparator) }) }) - electron.on('FOCUSER.DETACHED', (event) => { + electronService.on('FOCUSER.DETACHED', (event) => { ngZone.run(() => { const index = this.focusers.findIndex((e) => e.id === event.device.id) if (index >= 0) { if (this.focusers[index] === this.focuser) { - Object.assign(this.focuser, this.focusers[0] ?? EMPTY_FOCUSER) + Object.assign(this.focuser, this.focusers[0] ?? DEFAULT_FOCUSER) } this.focusers.splice(index, 1) @@ -301,7 +303,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { }) }) - electron.on('AUTO_FOCUS.ELAPSED', (event) => { + electronService.on('AUTO_FOCUS.ELAPSED', (event) => { ngZone.run(() => { this.status = event.state this.running = event.state !== 'FAILED' && event.state !== 'FINISHED' @@ -327,7 +329,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { } async ngAfterViewInit() { - this.pinger.register(this, 30000) + this.ticker.register(this, 30000) this.cameras = (await this.api.cameras()).sort(deviceComparator) this.focusers = (await this.api.focusers()).sort(deviceComparator) @@ -335,18 +337,18 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) void this.stop() } - async ping() { - if (this.camera.id) await this.api.cameraListen(this.camera) - if (this.focuser.id) await this.api.focuserListen(this.focuser) + async tick() { + if (this.camera?.id) await this.api.cameraListen(this.camera) + if (this.focuser?.id) await this.api.focuserListen(this.focuser) } - async cameraChanged() { - if (this.camera.id) { - await this.ping() + protected async cameraChanged() { + if (this.camera?.id) { + await this.tick() const camera = await this.api.camera(this.camera.id) Object.assign(this.camera, camera) @@ -354,39 +356,45 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { } } - async focuserChanged() { - if (this.focuser.id) { - await this.ping() + protected async focuserChanged() { + if (this.focuser?.id) { + await this.tick() const focuser = await this.api.focuser(this.focuser.id) Object.assign(this.focuser, focuser) } } - async showCameraDialog() { - if (this.camera.id) { - if (await CameraComponent.showAsDialog(this.browserWindow, 'AUTO_FOCUS', this.camera, this.request.capture)) { + protected async showCameraDialog() { + if (this.camera?.id) { + if (await CameraComponent.showAsDialog(this.browserWindowService, 'AUTO_FOCUS', this.camera, this.request.capture)) { this.savePreference() } } } - async start() { - await this.openCameraImage() + protected async start() { + if (this.camera?.id && this.focuser?.id) { + await this.openCameraImage() - this.clearChart() - this.stepSizeForScale = this.request.stepSize + this.clearChart() + this.stepSize = this.request.stepSize + Object.assign(this.request.starDetector, this.preferenceService.settings.get().starDetector[this.request.starDetector.type]) - this.request.starDetector = this.preference.starDetectionRequest('ASTAP').get() - return this.api.autoFocusStart(this.camera, this.focuser, this.request) + await this.api.autoFocusStart(this.camera, this.focuser, this.request) + } } - stop() { - return this.api.autoFocusStop(this.camera) + protected async stop() { + if (this.camera?.id) { + await this.api.autoFocusStop(this.camera) + } } - openCameraImage() { - return this.browserWindow.openCameraImage(this.camera, 'ALIGNMENT') + protected async openCameraImage() { + if (this.camera?.id) { + await this.browserWindowService.openCameraImage(this.camera, 'ALIGNMENT') + } } private updateChart(data: AutoFocusChart) { @@ -419,8 +427,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { } const scales = this.chartOptions.scales! - scales['x']!.min = Math.max(0, data.minX - this.stepSizeForScale) - scales['x']!.max = data.maxX + this.stepSizeForScale + scales['x']!.min = Math.max(0, data.minX - this.stepSize) + scales['x']!.max = data.maxX + this.stepSize scales['y']!.max = (data.maxY || 19) + 1 const zoom = this.chartOptions.plugins!.zoom! @@ -432,7 +440,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { } private clearChart() { - this.focusPoints = [] + this.focusPoints.length = 0 for (const dataset of this.chartData.datasets) { dataset.data = [] @@ -442,20 +450,9 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { } private loadPreference() { - const preference: Partial = this.preference.autoFocusPreference.get() - - this.request.fittingMode = preference.fittingMode ?? 'HYPERBOLIC' - this.request.initialOffsetSteps = preference.initialOffsetSteps ?? 4 - this.request.rSquaredThreshold = preference.rSquaredThreshold ?? 0.5 - this.request.stepSize = preference.stepSize ?? 100 - this.request.totalNumberOfAttempts = preference.totalNumberOfAttempts ?? 1 - this.request.backlashCompensation.mode = preference.backlashCompensation?.mode ?? 'NONE' - this.request.backlashCompensation.backlashIn = preference.backlashCompensation?.backlashIn ?? 0 - this.request.backlashCompensation.backlashOut = preference.backlashCompensation?.backlashOut ?? 0 - - if (this.camera.id) { - const cameraPreference = this.preference.cameraPreference(this.camera).get() - Object.assign(this.request.capture, this.preference.cameraStartCaptureForAutoFocus(this.camera).get(cameraPreference)) + if (this.camera?.id) { + Object.assign(this.preference, this.preferenceService.autoFocus(this.camera).get()) + this.request = this.preference.request if (this.camera.connected) { updateCameraStartCaptureFromCamera(this.request.capture, this.camera) @@ -463,13 +460,9 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { } } - savePreference() { - this.preference.cameraStartCaptureForAutoFocus(this.camera).set(this.request.capture) - - const preference: AutoFocusPreference = { - ...this.request, + protected savePreference() { + if (this.camera?.id) { + this.preferenceService.autoFocus(this.camera).set(this.preference) } - - this.preference.autoFocusPreference.set(preference) } } diff --git a/desktop/src/app/calculator/calculator.component.html b/desktop/src/app/calculator/calculator.component.html index 12e22ca7e..40d22736c 100644 --- a/desktop/src/app/calculator/calculator.component.html +++ b/desktop/src/app/calculator/calculator.component.html @@ -5,7 +5,6 @@ [(ngModel)]="formula" styleClass="border-0 p-inputtext-sm" [autoDisplayFirst]="false" - (ngModelChange)="formulaChanged()" [panelStyle]="{ maxWidth: '100px' }"> ; formula: CalculatorFormula }[] = [ + protected readonly formulae: { component: Type; formula: CalculatorFormula }[] = [ { component: FormulaComponent, formula: { @@ -211,11 +211,9 @@ export class CalculatorComponent { }, ] - formula = this.formulae[0] + protected formula = this.formulae[0] constructor(app: AppComponent) { app.title = 'Calculator' } - - formulaChanged() {} } diff --git a/desktop/src/app/calculator/formula/formula.component.html b/desktop/src/app/calculator/formula/formula.component.html index 5199b89c5..a41c6d5df 100644 --- a/desktop/src/app/calculator/formula/formula.component.html +++ b/desktop/src/app/calculator/formula/formula.component.html @@ -21,7 +21,7 @@ [showButtons]="true" styleClass="border-0 p-inputtext-sm" locale="en" - scrollableNumber /> + spinnableNumber />
diff --git a/desktop/src/app/calculator/formula/formula.component.ts b/desktop/src/app/calculator/formula/formula.component.ts index b32ebe4db..2bbb69408 100644 --- a/desktop/src/app/calculator/formula/formula.component.ts +++ b/desktop/src/app/calculator/formula/formula.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input } from '@angular/core' +import { Component, Input } from '@angular/core' import { CalculatorFormula } from '../../../shared/types/calculator.types' @Component({ @@ -6,11 +6,9 @@ import { CalculatorFormula } from '../../../shared/types/calculator.types' templateUrl: './formula.component.html', styleUrls: ['./formula.component.scss'], }) -export class FormulaComponent implements AfterViewInit { +export class FormulaComponent { @Input({ required: true }) - readonly formula!: CalculatorFormula - - ngAfterViewInit() {} + protected readonly formula!: CalculatorFormula calculateFormula() { const result = this.formula.calculate(...this.formula.operands.map((e) => e.value)) diff --git a/desktop/src/app/calibration/calibration.component.html b/desktop/src/app/calibration/calibration.component.html index bee121072..53a446168 100644 --- a/desktop/src/app/calibration/calibration.component.html +++ b/desktop/src/app/calibration/calibration.component.html @@ -1,120 +1,124 @@ -
-
-
- -
-
- - -
- @if (node.data.type === 'NAME') { - {{ node.label }} - } @else if (node.data.type === 'GROUP') { -
- - - - - - - -
- } @else if (node.data.type === 'FRAME') { -
- - - {{ node.data.data.path }} - -
- } -
- @if (node.data.type === 'NAME') { +
+
+ + @for (key of groups; track $index) { + @let value = frames.get(key) ?? []; + +
+
+
+ (onClick)="openFilesToUpload(key)" /> + (onClick)="openDirectoryToUpload(key)" /> +
+ {{ this.frames.get(key)?.length ?? 0 }} frames +
+ - } - + size="small" + (onClick)="groupMenu.toggle($event)" /> +
+
+
+ + +
+
+
+
+ +
+ {{ item.type }} +
+ {{ item.exposureTime | exposureTime }} + {{ item.width }}x{{ item.height }} + {{ item.binX }}x{{ item.binY }} + GAIN: {{ item.gain }} + + {{ item.temperature }}°C + +
+ {{ item.path }} +
+
+
+ + + +
+
+
+
+
+
- - -
+ + } +
@@ -122,18 +126,18 @@ + [(ngModel)]="groupDialog.group" />
+ (onClick)="groupDialog.save?.()" />
diff --git a/desktop/src/app/calibration/calibration.component.scss b/desktop/src/app/calibration/calibration.component.scss deleted file mode 100644 index 3f694e53b..000000000 --- a/desktop/src/app/calibration/calibration.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -:host { - ::ng-deep { - .p-treenode-label { - width: 100%; - } - - .p-tree-wrapper { - max-height: 288px; - } - - .p-tree { - .p-tree-container { - padding-right: 4px; - - .p-treenode { - padding: 0; - - .p-treenode-content { - padding: 0 0.5rem; - } - } - } - } - - .p-treenode-leaf > .p-treenode-content .p-tree-toggler { - display: none; - } - - .p-tree-empty-message { - padding: 1rem 0.5rem; - } - } -} diff --git a/desktop/src/app/calibration/calibration.component.ts b/desktop/src/app/calibration/calibration.component.ts index 70abd6d25..3a9ffa19c 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -1,263 +1,410 @@ -import { AfterViewInit, Component } from '@angular/core' +import { AfterViewInit, Component, HostListener, OnDestroy, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core' import { dirname } from 'path' -import { TreeDragDropService, TreeNode } from 'primeng/api' -import { TreeNodeDropEvent } from 'primeng/tree' +import { Listbox } from 'primeng/listbox' +import { MenuItem } from '../../shared/components/menu-item/menu-item.component' +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 { PreferenceService } from '../../shared/services/preference.service' -import { CalibrationFrame, CalibrationFrameGroup } from '../../shared/types/calibration.types' +import { CalibrationFrame, DEFAULT_CALIBRATION_GROUP_DIALOG, DEFAULT_CALIBRATION_PREFERENCE } from '../../shared/types/calibration.types' +import { textComparator } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' -export interface CalibrationNode extends TreeNode { - key: string - label: string - data: TreeNodeData - children: CalibrationNode[] - parent?: CalibrationNode -} - -export type TreeNodeData = { type: 'NAME'; data: string } | { type: 'GROUP'; data: CalibrationFrameGroup } | { type: 'FRAME'; data: CalibrationFrame } - @Component({ selector: 'neb-calibration', templateUrl: './calibration.component.html', - styleUrls: ['./calibration.component.scss'], - providers: [TreeDragDropService], + encapsulation: ViewEncapsulation.None, }) -export class CalibrationComponent implements AfterViewInit { - readonly frames: CalibrationNode[] = [] +export class CalibrationComponent implements AfterViewInit, OnDestroy { + protected readonly frames = new Map() + protected readonly preference = structuredClone(DEFAULT_CALIBRATION_PREFERENCE) + protected readonly groupDialog = structuredClone(DEFAULT_CALIBRATION_GROUP_DIALOG) + protected selectedFrames: CalibrationFrame[] = [] + + protected tab = 0 + private frameId = '' + + private readonly renameSelectedFramesMenuItem: MenuItem = { + icon: 'mdi mdi-pencil', + label: 'Rename Group', + badge: '0', + visible: false, + command: () => { + this.showEditGroupDialogForSelectedFrames() + }, + } + + private readonly deleteSelectedFramesMenuItem: MenuItem = { + icon: 'mdi mdi-delete', + severity: 'danger', + label: 'Delete', + badge: '0', + visible: false, + command: () => this.deleteSelectedFrames(), + } + + protected activeGroup = '' + protected readonly groupModel: MenuItem[] = [ + { + icon: 'mdi mdi-checkbox-marked', + label: 'Select All', + command: () => { + const frames = this.activeFrames + + if (frames.length) { + const selectedFrames = new Set(this.selectedFrames) + + for (const frame of frames) { + selectedFrames.add(frame) + } + + this.selectedFrames = Array.from(selectedFrames) + this.frameSelected() + } + }, + }, + { + icon: 'mdi mdi-checkbox-blank-outline', + label: 'Unselect All', + command: () => { + const frames = this.activeFrames + + if (frames.length) { + const selectedFrames = new Set(this.selectedFrames) + + for (const frame of frames) { + selectedFrames.delete(frame) + } + + this.selectedFrames = Array.from(selectedFrames) + this.frameSelected() + } + }, + }, + SEPARATOR_MENU_ITEM, + { + icon: 'mdi mdi-checkbox-marked', + label: 'Enable All', + command: async () => { + const frames = this.activeFrames + + for (const frame of frames) { + if (!frame.enabled) { + await this.toggleFrame(frame, true) + } + } + }, + }, + { + icon: 'mdi mdi-checkbox-blank-outline', + label: 'Disable All', + command: async () => { + const frames = this.activeFrames + + for (const frame of frames) { + if (frame.enabled) { + await this.toggleFrame(frame, false) + } + } + }, + }, + SEPARATOR_MENU_ITEM, + { + icon: 'mdi mdi-pencil', + label: 'Rename Group', + command: () => { + if (this.activeGroup && this.activeFrames.length) { + this.showEditGroupDialog(this.activeGroup) + } + }, + }, + { + icon: 'mdi mdi-delete', + label: 'Delete All', + iconClass: 'text-danger', + command: async () => { + if (this.activeGroup && this.activeFrames.length) { + await this.deleteFrameGroup(this.activeGroup) + } + }, + }, + ] - showNewGroupDialog = false - newGroupName = '' - newGroupDialogSave: () => void = () => {} + @ViewChildren('frameListBox') + private readonly frameListBoxes!: QueryList + + get groups() { + return Array.from(this.frames.keys()).sort(textComparator) + } + + get activeFrames() { + return this.frames.get(this.activeGroup) ?? [] + } constructor( app: AppComponent, private readonly api: ApiService, - private readonly electron: ElectronService, - private readonly browserWindow: BrowserWindowService, - private readonly preference: PreferenceService, + private readonly electronService: ElectronService, + private readonly browserWindowService: BrowserWindowService, + private readonly preferenceService: PreferenceService, ) { app.title = 'Calibration' + + app.topMenu.push({ + icon: 'mdi mdi-plus', + label: 'New Group', + command: () => { + this.showNewGroupDialog() + }, + }) + + app.topMenu.push(this.renameSelectedFramesMenuItem) + app.topMenu.push(this.deleteSelectedFramesMenuItem) } - async ngAfterViewInit() { - await this.load() + ngAfterViewInit() { + this.loadPreference() + + return this.load() } - private makeTreeNode(key: string, label: string, data: TreeNodeData, parent?: CalibrationNode): CalibrationNode { - const draggable = data.type === 'FRAME' - const droppable = data.type === 'NAME' - return { key, label, data, children: [], parent, draggable, droppable } + @HostListener('window:unload') + ngOnDestroy() { + void this.closeFrameWindow() } - addGroup(name: string) { - const node = this.frames.find((e) => e.label === name) ?? this.makeTreeNode(`group-${name}`, name, { type: 'NAME', data: name }) + protected frameSelected() { + const count = this.selectedFrames.length + const visible = count > 0 - if (!this.frames.includes(node)) { - this.frames.push(node) - } + this.renameSelectedFramesMenuItem.visible = visible + this.deleteSelectedFramesMenuItem.visible = visible - return node + if (visible) { + this.renameSelectedFramesMenuItem.badge = `${count}` + this.deleteSelectedFramesMenuItem.badge = `${count}` + } } - addFrameGroup(name: string | CalibrationNode, group: CalibrationFrameGroup) { - const parent = typeof name === 'string' ? this.frames.find((e) => e.label === name) : name + protected async openFilesToUpload(group: string) { + const paths = await this.electronService.openImages({ defaultPath: this.preference.filePath }) - if (parent) { - const node = this.makeTreeNode(`frame-group-${group.id}`, `Frame`, { type: 'GROUP', data: group }, parent) - parent.children.push(node) - return node - } + if (paths && paths.length) { + this.preference.filePath = dirname(paths[0]) + this.savePreference() - return undefined + for (const path of paths) { + await this.upload(group, path) + } + } } - addFrame(group: string | CalibrationNode, frame: CalibrationFrame) { - const parent = typeof group === 'string' ? this.frames.find((e) => e.label === group) : group + protected async openDirectoryToUpload(group: string) { + const path = await this.electronService.openDirectory({ defaultPath: this.preference.directoryPath }) - if (parent) { - const node = this.makeTreeNode(`frame-${frame.id}`, `Frame`, { type: 'FRAME', data: frame }, parent) - parent.children.push(node) - return node + if (path) { + this.preference.directoryPath = path + this.savePreference() + await this.upload(group, path) } - - return undefined } - async openFileToUpload(node: CalibrationNode) { - if (node.data.type === 'NAME') { - const preference = this.preference.calibrationPreference.get() - const path = await this.electron.openImage({ defaultPath: preference.openPath }) + private async upload(group: string, path: string) { + const frames = await this.api.uploadCalibrationFrame(group, path) - if (path) { - preference.openPath = dirname(path) - this.preference.calibrationPreference.set(preference) - await this.upload(node, path) - } + if (frames.length > 0) { + await this.electronService.calibrationChanged() + await this.loadGroup(group) } } - async openDirectoryToUpload(node: CalibrationNode) { - if (node.data.type === 'NAME') { - const preference = this.preference.calibrationPreference.get() - const path = await this.electron.openDirectory({ defaultPath: preference.openPath }) + private async loadGroup(group: string) { + const frames = await this.api.calibrationFrames(group) - if (path) { - preference.openPath = path - this.preference.calibrationPreference.set(preference) - await this.upload(node, path) + for (let i = 0; i < this.selectedFrames.length; i++) { + for (const frame of frames) { + if (frame.id === this.selectedFrames[i].id) { + this.selectedFrames[i] = frame + } } } - } - private async upload(node: CalibrationNode, path: string) { - if (node.data.type === 'NAME') { - const frames = await this.api.uploadCalibrationFrame(node.data.data, path) + this.frames.set(group, frames) + } - if (frames.length > 0) { - await this.electron.calibrationChanged() - await this.load() - } + private loadDefaultGroupIfEmpty() { + if (!this.frames.size) { + this.frames.set('Group 1', []) } } private async load() { - this.frames.length = 0 - - const names = await this.api.calibrationGroups() + this.frames.clear() - for (const name of names) { - const nameNode = this.addGroup(name) + const groups = await this.api.calibrationGroups() - const groups = await this.api.calibrationFrames(name) + for (const group of groups) { + await this.loadGroup(group) + } - for (const group of groups) { - const frameGroupNode = this.addFrameGroup(nameNode, group) + this.loadDefaultGroupIfEmpty() + } - if (frameGroupNode) { - for (const frame of group.frames) { - this.addFrame(frameGroupNode, frame) - } - } - } - } + protected openImage(frame: CalibrationFrame) { + return this.browserWindowService.openImage({ path: frame.path, source: 'PATH' }) } - openImage(frame: CalibrationFrame) { - return this.browserWindow.openImage({ path: frame.path, source: 'PATH' }) + protected toggleFrame(frame: CalibrationFrame, enabled: boolean) { + frame.enabled = enabled + return this.api.updateCalibrationFrame(frame) } - async toggleCalibrationFrame(node: CalibrationNode, enabled: boolean) { - if (node.data.type === 'FRAME') { - await this.api.editCalibrationFrame(node.data.data) - } + protected async openFrame(frame: CalibrationFrame) { + this.frameId = await this.browserWindowService.openImage({ path: frame.path, id: 'calibration', source: 'PATH' }) } - async deleteFrame(node: CalibrationNode) { - const deleteFromParent = async () => { - if (node.parent) { - const idx = node.parent.children.indexOf(node) + protected async deleteFrame(frame: CalibrationFrame, box?: Listbox) { + await this.api.deleteCalibrationFrame(frame) - if (idx >= 0) { - node.parent.children.splice(idx, 1) - console.info('frame deleted', node) - } + let index = this.selectedFrames.indexOf(frame) - if (!node.parent.children.length) { - await this.deleteFrame(node.parent) - } - } else { - const idx = this.frames.indexOf(node) + if (index >= 0) { + this.selectedFrames.splice(index, 1) + this.frameSelected() + } - if (idx >= 0) { - this.frames.splice(idx, 1) - console.info('frame deleted', node) - await this.electron.calibrationChanged() - } + const frames = this.frames.get(frame.group) + + if (frames?.length) { + index = frames.indexOf(frame) + + if (index >= 0) { + frames.splice(index, 1) + box?.cd.markForCheck() } } + } - if (node.data.type === 'FRAME') { - await this.api.deleteCalibrationFrame(node.data.data) - await deleteFromParent() - } else { - for (const frame of Array.from(node.children)) { - await this.deleteFrame(frame) - } + private async deleteSelectedFrames() { + const groups = new Set() + const frames = Array.from(this.selectedFrames) - if (!node.children.length) { - await deleteFromParent() - } + for (const frame of frames) { + groups.add(frame.group) + await this.deleteFrame(frame) } + + this.markFrameListBoxesForCheck() } - private calibrationFrameFromNode(node: CalibrationNode) { - const frames: CalibrationFrame[] = [] + private async deleteFrameGroup(group: string) { + const frames = Array.from(this.frames.get(group) ?? []) - function recursive(node: TreeNode) { - if (node.data) { - if (node.data.type === 'NAME' || node.data.type === 'GROUP') { - if (node.children) { - for (const child of node.children) { - recursive(child) - } - } - } else { - frames.push(node.data.data) + for (const frame of frames) { + await this.deleteFrame(frame) + } + + this.markFrameListBoxesForCheck() + } + + private showEditGroupDialogForSelectedFrames() { + this.groupDialog.save = async () => { + const groups = new Set() + + groups.add(this.groupDialog.group) + + for (const frame of this.selectedFrames) { + if (this.groupDialog.group !== frame.group) { + groups.add(frame.group) + frame.group = this.groupDialog.group + await this.api.updateCalibrationFrame(frame) } } + + this.groupDialog.showDialog = false + + for (const group of groups) { + await this.loadGroup(group) + } + + await this.electronService.calibrationChanged() } - recursive(node) + this.groupDialog.group = '' + this.groupDialog.showDialog = true + } - return frames + private async closeFrameWindow() { + if (this.frameId) { + await this.electronService.closeWindow(undefined, this.frameId) + } } - showNewGroupDialogForAdd() { - this.newGroupDialogSave = () => { - this.addGroup(this.newGroupName) - this.showNewGroupDialog = false + private showNewGroupDialog() { + this.groupDialog.save = () => { + if (this.groupDialog.group) { + this.frames.set(this.groupDialog.group, []) + this.groupDialog.showDialog = false + } } - this.newGroupName = '' - this.showNewGroupDialog = true + this.groupDialog.group = '' + this.groupDialog.showDialog = true } - showNewGroupDialogForEdit(node: CalibrationNode) { - if (node.data.type === 'NAME') { - this.newGroupDialogSave = async () => { - const frames = this.calibrationFrameFromNode(node) + protected showEditGroupDialog(value: CalibrationFrame | string) { + if (typeof value === 'string') { + this.groupDialog.save = async () => { + const frames = this.frames.get(value) - for (const frame of frames) { - frame.name = this.newGroupName - await this.api.editCalibrationFrame(frame) - await this.electron.calibrationChanged() + if (frames?.length && this.groupDialog.group) { + for (const frame of frames) { + frame.group = this.groupDialog.group + await this.api.updateCalibrationFrame(frame) + } + + await this.loadGroup(value) + await this.loadGroup(this.groupDialog.group) + await this.electronService.calibrationChanged() } - this.showNewGroupDialog = false - await this.load() + this.groupDialog.showDialog = false } - this.newGroupName = node.data.data - this.showNewGroupDialog = true + this.groupDialog.group = value + } else { + this.groupDialog.save = async () => { + const prevGroup = value.group + + if (this.groupDialog.group !== prevGroup) { + value.group = this.groupDialog.group + await this.api.updateCalibrationFrame(value) + await this.loadGroup(prevGroup) + await this.loadGroup(value.group) + await this.electronService.calibrationChanged() + } + + this.groupDialog.showDialog = false + } + + this.groupDialog.group = value.group } + + this.groupDialog.showDialog = true } - editGroupName() { - this.showNewGroupDialog = false + private loadPreference() { + Object.assign(this.preference, this.preferenceService.calibrationPreference.get()) } - async frameDropped(event: TreeNodeDropEvent) { - const dragNode = event.dragNode as CalibrationNode - const dropNode = event.dropNode as CalibrationNode + protected savePreference() { + this.preferenceService.calibrationPreference.set(this.preference) + } - if (dragNode.data.type === 'FRAME' && dropNode.data.type === 'NAME' && dragNode.data.data.name !== dropNode.data.data) { - dragNode.data.data.name = dropNode.data.data - await this.api.editCalibrationFrame(dragNode.data.data) - await this.electron.calibrationChanged() - await this.load() + private markFrameListBoxesForCheck() { + for (const box of this.frameListBoxes) { + box.cd.markForCheck() } } } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 7f00245aa..f14507963 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -1,4 +1,4 @@ -
+
@@ -47,7 +47,7 @@ [rounded]="true" icon="mdi mdi-wrench" (onClick)="calibrationMenu.show()" - pTooltip="CALIBRATION: {{ this.request.calibrationGroup ?? 'None' }}" + pTooltip="Calibration" tooltipPosition="bottom" size="small" /> - {{ savePath || capturesPath }} + {{ preference.request.savePath || camera.capturesPath }}
+ spinnableNumber />
-
- - - - - - -
+
@@ -225,7 +210,7 @@ + spinnableNumber />
+ spinnableNumber />
@@ -272,7 +257,7 @@
+ spinnableNumber />
+ spinnableNumber />
+ spinnableNumber />
+ spinnableNumber />
@@ -347,7 +332,7 @@ Subframe
@@ -374,7 +359,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -391,7 +376,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -422,7 +407,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -439,7 +424,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -468,33 +453,33 @@ severity="info" (click)="liveStacking.showDialog = true" /> + (click)="openMount(preference.mount)" /> + (click)="openFocuser(preference.focuser)" /> + (click)="openWheel(preference.wheel)" /> + (click)="openRotator(preference.rotator)" />
@if (pausingOrPaused) { @@ -595,7 +580,7 @@ locale="en" [minFractionDigits]="1" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -610,7 +595,7 @@ [(ngModel)]="dither.request.afterExposures" [step]="1" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -660,7 +645,7 @@ [disabled]="!liveStacking.request.enabled" [directory]="false" label="Dark File" - key="LIVE_STACKER_DARK_PATH" + key="liveStacker.darkPath" [(path)]="liveStacking.request.darkPath" class="w-full" (pathChange)="savePreference()" /> @@ -670,7 +655,7 @@ [disabled]="!liveStacking.request.enabled" [directory]="false" label="Flat File" - key="LIVE_STACKER_FLAT_PATH" + key="liveStacker.flatPath" [(path)]="liveStacking.request.flatPath" class="w-full" (pathChange)="savePreference()" /> @@ -680,7 +665,7 @@ [disabled]="!liveStacking.request.enabled || liveStacking.request.type !== 'PIXINSIGHT'" [directory]="false" label="Bias File" - key="LIVE_STACKER_BIAS_PATH" + key="liveStacker.darkPath" [(path)]="liveStacking.request.biasPath" class="w-full" (pathChange)="savePreference()" /> diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index f69f3ca93..2362f69bb 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -1,92 +1,42 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' -import { MenuItem, MenuItemCommandEvent, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { MenuItemCommandEvent, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' -import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' +import { Tickable, Ticker } from '../../shared/services/ticker.service' import { Camera, + cameraCaptureNamingFormatWithDefault, CameraDialogInput, - CameraDialogMode, CameraDitherDialog, CameraLiveStackingDialog, + CameraMode, CameraNamingFormatDialog, - CameraPreference, CameraStartCapture, - EMPTY_CAMERA, - EMPTY_CAMERA_START_CAPTURE, - ExposureMode, - ExposureTimeUnit, + DEFAULT_CAMERA, + DEFAULT_CAMERA_PREFERENCE, FrameType, updateCameraStartCaptureFromCamera, } from '../../shared/types/camera.types' -import { Device } from '../../shared/types/device.types' +import { Device, DeviceType } from '../../shared/types/device.types' import { Focuser } from '../../shared/types/focuser.types' -import { Equipment } from '../../shared/types/home.types' import { Mount } from '../../shared/types/mount.types' import { Rotator } from '../../shared/types/rotator.types' import { resetCameraCaptureNamingFormat } from '../../shared/types/settings.types' -import { FilterWheel } from '../../shared/types/wheel.types' -import { Undefinable } from '../../shared/utils/types' +import { Wheel } from '../../shared/types/wheel.types' import { AppComponent } from '../app.component' @Component({ selector: 'neb-camera', templateUrl: './camera.component.html', }) -export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { - readonly camera = structuredClone(EMPTY_CAMERA) - readonly equipment: Equipment = {} - - savePath = '' - capturesPath = '' - mode: CameraDialogMode = 'CAPTURE' - - get canShowMenu() { - return this.hasCalibration || this.hasLiveStacking || this.hasDither || this.canSnoopDevices - } - - get canShowSavePath() { - return this.mode === 'CAPTURE' - } - - get canShowInfo() { - return this.mode === 'CAPTURE' - } - - get canExposureMode() { - return this.mode === 'CAPTURE' - } - - get canExposureTime() { - return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' || this.mode === 'TPPA' || this.mode === 'AUTO_FOCUS' - } - - get canExposureTimeUnit() { - return this.mode !== 'DARV' - } - - get canExposureAmount() { - return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' || this.mode === 'AUTO_FOCUS' - } - - get canFrameType() { - return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' - } - - get canStartOrAbort() { - return this.mode === 'CAPTURE' - } - - get canSave() { - return this.mode !== 'CAPTURE' - } - - calibrationModel: SlideMenuItem[] = [] +export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { + protected readonly camera = structuredClone(DEFAULT_CAMERA) + protected calibrationModel: SlideMenuItem[] = [] private readonly ditherMenuItem: SlideMenuItem = { icon: 'mdi mdi-pulse', @@ -142,70 +92,34 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { ], } - readonly cameraModel: SlideMenuItem[] = [this.ditherMenuItem, this.liveStackingMenuItem, this.namingFormatMenuItem, this.snoopDevicesMenuItem] - - running = false - hasDewHeater = false - setpointTemperature = 0.0 - exposureTimeMin = 1 - exposureTimeMax = 1 - exposureTimeUnit = ExposureTimeUnit.MICROSECOND - exposureMode: ExposureMode = 'SINGLE' - subFrame = false + protected readonly cameraModel: SlideMenuItem[] = [this.ditherMenuItem, this.liveStackingMenuItem, this.namingFormatMenuItem, this.snoopDevicesMenuItem] - readonly request = structuredClone(EMPTY_CAMERA_START_CAPTURE) + protected running = false + protected hasDewHeater = false + protected readonly preference = structuredClone(DEFAULT_CAMERA_PREFERENCE) + protected request = this.preference.request + protected mode: CameraMode = 'CAPTURE' - readonly dither: CameraDitherDialog = { + protected readonly dither: CameraDitherDialog = { showDialog: false, request: this.request.dither, } - readonly liveStacking: CameraLiveStackingDialog = { + protected readonly liveStacking: CameraLiveStackingDialog = { showDialog: false, request: this.request.liveStacking, } - readonly namingFormat: CameraNamingFormatDialog = { + protected readonly namingFormat: CameraNamingFormatDialog = { showDialog: false, format: this.request.namingFormat, } - readonly exposureTimeUnitModel: MenuItem[] = [ - { - label: 'Minute (m)', - command: () => { - this.updateExposureUnit(ExposureTimeUnit.MINUTE) - this.savePreference() - }, - }, - { - label: 'Second (s)', - command: () => { - this.updateExposureUnit(ExposureTimeUnit.SECOND) - this.savePreference() - }, - }, - { - label: 'Millisecond (ms)', - command: () => { - this.updateExposureUnit(ExposureTimeUnit.MILLISECOND) - this.savePreference() - }, - }, - { - label: 'Microsecond (µs)', - command: () => { - this.updateExposureUnit(ExposureTimeUnit.MICROSECOND) - this.savePreference() - }, - }, - ] - @ViewChild('cameraExposure') private readonly cameraExposure?: CameraExposureComponent get status() { - return this.cameraExposure?.state ?? 'IDLE' + return this.cameraExposure?.currentState ?? 'IDLE' } get pausingOrPaused() { @@ -228,19 +142,63 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { return !this.app.modal } + get canShowMenu() { + return this.hasCalibration || this.hasLiveStacking || this.hasDither || this.canSnoopDevices + } + + get canShowSavePath() { + return this.mode === 'CAPTURE' + } + + get canShowInfo() { + return this.mode === 'CAPTURE' + } + + get canExposureMode() { + return this.mode === 'CAPTURE' + } + + get canExposureTime() { + return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' || this.mode === 'TPPA' || this.mode === 'AUTO_FOCUS' + } + + get canExposureTimeUnit() { + return this.mode !== 'DARV' + } + + get canExposureAmount() { + return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' || this.mode === 'AUTO_FOCUS' + } + + get canFrameType() { + return this.mode === 'CAPTURE' || this.mode === 'SEQUENCER' + } + + get canStartOrAbort() { + return this.mode === 'CAPTURE' + } + + get canSave() { + return this.mode !== 'CAPTURE' + } + + get currentWheelFilter() { + return this.preference.wheel?.names[this.preference.wheel.position - 1] + } + constructor( private readonly app: AppComponent, private readonly api: ApiService, - private readonly browserWindow: BrowserWindowService, - private readonly electron: ElectronService, - private readonly preference: PreferenceService, + private readonly browserWindowService: BrowserWindowService, + private readonly electronService: ElectronService, + private readonly preferenceService: PreferenceService, private readonly route: ActivatedRoute, - private readonly pinger: Pinger, + private readonly ticker: Ticker, ngZone: NgZone, ) { app.title = 'Camera' - electron.on('CAMERA.UPDATED', (event) => { + electronService.on('CAMERA.UPDATED', (event) => { if (event.device.id === this.camera.id) { ngZone.run(() => { Object.assign(this.camera, event.device) @@ -249,15 +207,15 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } }) - electron.on('CAMERA.DETACHED', (event) => { + electronService.on('CAMERA.DETACHED', (event) => { if (event.device.id === this.camera.id) { ngZone.run(() => { - Object.assign(this.camera, EMPTY_CAMERA) + Object.assign(this.camera, DEFAULT_CAMERA) }) } }) - electron.on('CAMERA.CAPTURE_ELAPSED', (event) => { + electronService.on('CAMERA.CAPTURE_ELAPSED', (event) => { if (event.camera.id === this.camera.id) { ngZone.run(() => { this.running = this.cameraExposure?.handleCameraCaptureEvent(event) ?? false @@ -265,51 +223,115 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } }) - electron.on('MOUNT.UPDATED', (event) => { - if (event.device.id === this.equipment.mount?.id) { + electronService.on('MOUNT.UPDATED', (event) => { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.mount?.id) { ngZone.run(() => { - if (this.equipment.mount) { - Object.assign(this.equipment.mount, event.device) + if (this.preference.mount) { + Object.assign(this.preference.mount, event.device) } }) } }) - electron.on('WHEEL.UPDATED', (event) => { - if (event.device.id === this.equipment.wheel?.id) { + electronService.on('MOUNT.ATTACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('MOUNT') + }) + } + }) + + electronService.on('MOUNT.DETACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('MOUNT') + }) + } + }) + + electronService.on('WHEEL.UPDATED', (event) => { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.wheel?.id) { ngZone.run(() => { - if (this.equipment.wheel) { - Object.assign(this.equipment.wheel, event.device) + if (this.preference.wheel) { + Object.assign(this.preference.wheel, event.device) } }) } }) - electron.on('FOCUSER.UPDATED', (event) => { - if (event.device.id === this.equipment.focuser?.id) { + electronService.on('WHEEL.ATTACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('WHEEL') + }) + } + }) + + electronService.on('WHEEL.DETACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('WHEEL') + }) + } + }) + + electronService.on('FOCUSER.UPDATED', (event) => { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.focuser?.id) { ngZone.run(() => { - if (this.equipment.focuser) { - Object.assign(this.equipment.focuser, event.device) + if (this.preference.focuser) { + Object.assign(this.preference.focuser, event.device) } }) } }) - electron.on('ROTATOR.UPDATED', (event) => { - if (event.device.id === this.equipment.rotator?.id) { + electronService.on('FOCUSER.ATTACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('FOCUSER') + }) + } + }) + + electronService.on('FOCUSER.DETACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('FOCUSER') + }) + } + }) + + electronService.on('ROTATOR.UPDATED', (event) => { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.rotator?.id) { ngZone.run(() => { - if (this.equipment.rotator) { - Object.assign(this.equipment.rotator, event.device) + if (this.preference.rotator) { + Object.assign(this.preference.rotator, event.device) } }) } }) - electron.on('CALIBRATION.CHANGED', async () => { - await ngZone.run(() => this.loadCalibrationGroups()) + electronService.on('ROTATOR.ATTACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('ROTATOR') + }) + } + }) + + electronService.on('ROTATOR.DETACHED', () => { + if (this.mode === 'CAPTURE') { + void ngZone.run(() => { + return this.loadEquipment('ROTATOR') + }) + } + }) + + electronService.on('CALIBRATION.CHANGED', () => { + void ngZone.run(() => this.loadCalibrationGroups()) }) - electron.on('ROI.SELECTED', (event) => { + electronService.on('ROI.SELECTED', (event) => { if (event.camera.id === this.camera.id) { ngZone.run(() => { this.request.x = event.x @@ -320,39 +342,38 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } }) - this.snoopDevicesMenuItem.visible = !app.modal + this.snoopDevicesMenuItem.visible = this.canSnoopDevices } ngAfterContentInit() { this.route.queryParams.subscribe(async (e) => { - const decodedData = JSON.parse(decodeURIComponent(e['data'] as string)) as unknown + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as unknown if (this.app.modal) { - await this.loadCameraStartCaptureForDialogMode(decodedData as CameraDialogInput) + await this.loadCameraStartCaptureForDialogMode(data as CameraDialogInput) } else { - await this.cameraChanged(decodedData as Camera) + await this.cameraChanged(data as Camera) } - this.pinger.register(this, 30000) + this.ticker.register(this, 30000) - if (!this.app.modal) { + if (this.mode === 'CAPTURE') { await this.loadEquipment() + await this.loadCalibrationGroups() } - - await this.loadCalibrationGroups() }) } @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) if (this.mode === 'CAPTURE') { void this.abortCapture() } } - async ping() { + async tick() { if (this.camera.id) { await this.api.cameraListen(this.camera) } @@ -361,31 +382,40 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { private async loadCameraStartCaptureForDialogMode(data?: CameraDialogInput) { if (data) { this.mode = data.mode - Object.assign(this.request, data.request) await this.cameraChanged(data.camera) - this.loadDefaultsForMode(data.mode) - this.normalizeExposureTimeAndUnit(this.request.exposureTime) + Object.assign(this.request, data.request) + this.loadDefaultsForMode(this.mode) } } - private loadDefaultsForMode(mode: CameraDialogMode) { + private loadDefaultsForMode(mode: CameraMode) { if (mode === 'SEQUENCER' || mode === 'AUTO_FOCUS') { - this.exposureMode = 'FIXED' + this.preference.exposureMode = 'FIXED' } else if (this.mode === 'FLAT_WIZARD') { - this.exposureMode = 'SINGLE' + this.preference.exposureMode = 'SINGLE' this.request.frameType = 'FLAT' } else if (mode === 'TPPA') { - this.exposureMode = 'FIXED' + this.preference.exposureMode = 'FIXED' this.request.exposureAmount = 1 } else if (mode === 'DARV') { - this.exposureTimeUnit = ExposureTimeUnit.SECOND + this.preference.exposureTimeUnit = 'SECOND' } this.ditherMenuItem.visible = this.hasDither this.liveStackingMenuItem.visible = this.hasLiveStacking } - async cameraChanged(camera?: Camera) { + private updateSubTitle() { + let subTitle = this.camera.name + + if (this.mode !== 'CAPTURE') { + subTitle += ` · ${this.mode}` + } + + this.app.subTitle = subTitle + } + + protected async cameraChanged(camera?: Camera) { if (camera?.id) { camera = await this.api.camera(camera.id) Object.assign(this.camera, camera) @@ -394,88 +424,124 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { this.update() } - this.app.subTitle = camera?.name ?? '' - - if (this.mode !== 'CAPTURE') { - this.app.subTitle += ` · ${this.mode}` - } + this.updateSubTitle() } - private async loadEquipment() { - const makeItem = (selected: boolean, command: () => void, device?: Device) => { + private async loadEquipment(type?: DeviceType) { + const makeMenuItem = (selected: boolean, command: () => Promise | void, device?: Device) => { return { icon: device ? 'mdi mdi-connection' : 'mdi mdi-close', label: device?.name ?? 'None', selected, slideMenu: [], - command: (event: MenuItemCommandEvent) => { - command() - this.preference.equipmentForDevice(this.camera).set(this.equipment) + command: async (event: MenuItemCommandEvent) => { + await command() + this.savePreference() event.parentItem?.slideMenu?.forEach((item) => (item.selected = item === event.item)) }, } as SlideMenuItem } - const slideMenu = this.snoopDevicesMenuItem.slideMenu + const menu = this.snoopDevicesMenuItem.slideMenu // MOUNT - const mounts = await this.api.mounts() - this.equipment.mount = mounts.find((e) => e.name === this.equipment.mount?.name) + if (!type || type === 'MOUNT') { + menu[0].slideMenu.length = 0 - const makeMountItem = (mount?: Mount) => { - return makeItem(this.equipment.mount?.name === mount?.name, () => (this.equipment.mount = mount), mount) - } + const mounts = await this.api.mounts() + this.preference.mount = mounts.find((e) => e.name === this.preference.mount?.name) + + const makeMountItem = (mount?: Mount) => { + return makeMenuItem( + this.preference.mount?.name === mount?.name, + async () => { + this.preference.mount = mount && (await this.api.mount(mount.id)) + }, + mount, + ) + } - slideMenu[0].slideMenu.push(makeMountItem()) + menu[0].slideMenu.push(makeMountItem()) - for (const mount of mounts) { - slideMenu[0].slideMenu.push(makeMountItem(mount)) + for (const mount of mounts) { + menu[0].slideMenu.push(makeMountItem(mount)) + } } - // FILTER WHEEL + // WHEEL - const wheels = await this.api.wheels() - this.equipment.wheel = wheels.find((e) => e.name === this.equipment.wheel?.name) + if (!type || type === 'WHEEL') { + menu[1].slideMenu.length = 0 - const makeWheelItem = (wheel?: FilterWheel) => { - return makeItem(this.equipment.wheel?.name === wheel?.name, () => (this.equipment.wheel = wheel), wheel) - } + const wheels = await this.api.wheels() + this.preference.wheel = wheels.find((e) => e.name === this.preference.wheel?.name) + + const makeWheelItem = (wheel?: Wheel) => { + return makeMenuItem( + this.preference.wheel?.name === wheel?.name, + async () => { + this.preference.wheel = wheel && (await this.api.wheel(wheel.id)) + }, + wheel, + ) + } - slideMenu[1].slideMenu.push(makeWheelItem()) + menu[1].slideMenu.push(makeWheelItem()) - for (const wheel of wheels) { - slideMenu[1].slideMenu.push(makeWheelItem(wheel)) + for (const wheel of wheels) { + menu[1].slideMenu.push(makeWheelItem(wheel)) + } } // FOCUSER - const focusers = await this.api.focusers() - this.equipment.focuser = focusers.find((e) => e.name === this.equipment.focuser?.name) + if (!type || type === 'FOCUSER') { + menu[2].slideMenu.length = 0 - const makeFocuserItem = (focuser?: Focuser) => { - return makeItem(this.equipment.focuser?.name === focuser?.name, () => (this.equipment.focuser = focuser), focuser) - } + const focusers = await this.api.focusers() + this.preference.focuser = focusers.find((e) => e.name === this.preference.focuser?.name) - slideMenu[2].slideMenu.push(makeFocuserItem()) + const makeFocuserItem = (focuser?: Focuser) => { + return makeMenuItem( + this.preference.focuser?.name === focuser?.name, + async () => { + this.preference.focuser = focuser && (await this.api.focuser(focuser.id)) + }, + focuser, + ) + } + + menu[2].slideMenu.push(makeFocuserItem()) - for (const focuser of focusers) { - slideMenu[2].slideMenu.push(makeFocuserItem(focuser)) + for (const focuser of focusers) { + menu[2].slideMenu.push(makeFocuserItem(focuser)) + } } // ROTATOR - const rotators = await this.api.rotators() - this.equipment.rotator = rotators.find((e) => e.name === this.equipment.rotator?.name) + if (!type || type === 'ROTATOR') { + menu[3].slideMenu.length = 0 - const makeRotatorItem = (rotator?: Rotator) => { - return makeItem(this.equipment.rotator?.name === rotator?.name, () => (this.equipment.rotator = rotator), rotator) - } + const rotators = await this.api.rotators() + this.preference.rotator = rotators.find((e) => e.name === this.preference.rotator?.name) + + const makeRotatorItem = (rotator?: Rotator) => { + return makeMenuItem( + this.preference.rotator?.name === rotator?.name, + async () => { + this.preference.rotator = rotator && (await this.api.rotator(rotator.id)) + }, + rotator, + ) + } - slideMenu[3].slideMenu.push(makeRotatorItem()) + menu[3].slideMenu.push(makeRotatorItem()) - for (const rotator of rotators) { - slideMenu[3].slideMenu.push(makeRotatorItem(rotator)) + for (const rotator of rotators) { + menu[3].slideMenu.push(makeRotatorItem(rotator)) + } } } @@ -508,7 +574,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { label: 'Open Calibration', slideMenu: [], command: () => { - return this.browserWindow.openCalibration({ bringToFront: true }) + return this.browserWindowService.openCalibration({ bringToFront: true }) }, }) @@ -522,7 +588,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { this.calibrationModel = menu } - connect() { + protected connect() { if (this.camera.connected) { return this.api.cameraDisconnect(this.camera) } else { @@ -530,12 +596,12 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } } - toggleAutoSaveAllExposures() { + protected toggleAutoSaveAllExposures() { this.request.autoSave = !this.request.autoSave this.savePreference() } - toggleAutoSubFolder() { + protected toggleAutoSubFolder() { switch (this.request.autoSubFolderMode) { case 'OFF': this.request.autoSubFolderMode = 'NOON' @@ -551,26 +617,26 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { this.savePreference() } - async chooseSavePath() { - const defaultPath = this.savePath || this.capturesPath - const path = await this.electron.openDirectory({ defaultPath }) + protected async chooseSavePath() { + const defaultPath = this.preference.request.savePath || this.camera.capturesPath + const path = await this.electronService.openDirectory({ defaultPath }) if (path) { - this.savePath = path + this.preference.request.savePath = path this.savePreference() } } - applySetpointTemperature() { + protected applySetpointTemperature() { this.savePreference() - return this.api.cameraSetpointTemperature(this.camera, this.setpointTemperature) + return this.api.cameraSetpointTemperature(this.camera, this.preference.setpointTemperature) } - toggleCooler() { + protected toggleCooler() { return this.api.cameraCooler(this.camera, this.camera.cooler) } - fullsize() { + protected fullsize() { this.request.x = this.camera.minX this.request.y = this.camera.minY this.request.width = this.camera.maxWidth @@ -578,50 +644,45 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { this.savePreference() } - openMount(mount: Mount) { - return this.browserWindow.openMount(mount) + protected openMount(mount: Mount) { + return this.browserWindowService.openMount(mount) } - openFocuser(focuser: Focuser) { - return this.browserWindow.openFocuser(focuser) + protected openFocuser(focuser: Focuser) { + return this.browserWindowService.openFocuser(focuser) } - openWheel(wheel: FilterWheel) { - return this.browserWindow.openWheel(wheel) + protected openWheel(wheel: Wheel) { + return this.browserWindowService.openWheel(wheel) } - openRotator(rotator: Rotator) { - return this.browserWindow.openRotator(rotator) + protected openRotator(rotator: Rotator) { + return this.browserWindowService.openRotator(rotator) } - openCameraImage() { - return this.browserWindow.openCameraImage(this.camera, 'CAMERA', this.request) + protected openCameraImage() { + return this.browserWindowService.openCameraImage(this.camera, 'CAMERA', this.request) } private makeCameraStartCapture(): CameraStartCapture { - const x = this.subFrame ? this.request.x : this.camera.minX - const y = this.subFrame ? this.request.y : this.camera.minY - const width = this.subFrame ? this.request.width : this.camera.maxWidth - const height = this.subFrame ? this.request.height : this.camera.maxHeight - const exposureFactor = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) - const exposureTime = Math.trunc((this.request.exposureTime * 60000000) / exposureFactor) + const subFrame = this.preference.subFrame + const x = subFrame ? this.request.x : this.camera.minX + const y = subFrame ? this.request.y : this.camera.minY + const width = subFrame ? this.request.width : this.camera.maxWidth + const height = subFrame ? this.request.height : this.camera.maxHeight const exposureAmount = - this.exposureMode === 'LOOP' ? 0 - : this.exposureMode === 'FIXED' ? this.request.exposureAmount + this.preference.exposureMode === 'LOOP' ? 0 + : this.preference.exposureMode === 'FIXED' ? this.request.exposureAmount : 1 - const savePath = this.mode !== 'CAPTURE' ? this.request.savePath : this.savePath - - const liveStackingRequest = this.preference.liveStackingRequest(this.request.liveStacking.type).get() - this.request.liveStacking.executablePath = liveStackingRequest.executablePath - this.request.liveStacking.slot = liveStackingRequest.slot || 1 - let shutterPosition: Undefinable + let shutterPosition = 0 - if (this.equipment.wheel) { - const wheelPreference = this.preference.wheelPreference(this.equipment.wheel).get() - shutterPosition = wheelPreference.shutterPosition + if (this.preference.wheel) { + shutterPosition = this.preferenceService.wheel(this.preference.wheel).get().shutterPosition } + Object.assign(this.request.liveStacking, this.preferenceService.settings.get().liveStacker[this.request.liveStacking.type]) + return { ...this.request, shutterPosition, @@ -629,183 +690,74 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { y, width, height, - exposureTime, exposureAmount, - savePath, } } - async startCapture() { + protected async startCapture() { try { this.running = true await this.openCameraImage() - await this.api.cameraStartCapture(this.camera, this.makeCameraStartCapture(), this.equipment) - this.preference.equipmentForDevice(this.camera).set(this.equipment) + const { mount, wheel, focuser, rotator } = this.preference + await this.api.cameraStartCapture(this.camera, this.makeCameraStartCapture(), mount, wheel, focuser, rotator) } catch { this.running = false } } - pauseCapture() { + protected pauseCapture() { return this.api.cameraPauseCapture(this.camera) } - unpauseCapture() { + protected unpauseCapture() { return this.api.cameraUnpauseCapture(this.camera) } - abortCapture() { + protected abortCapture() { return this.api.cameraAbortCapture(this.camera) } - static exposureUnitFactor(unit: ExposureTimeUnit) { - switch (unit) { - case ExposureTimeUnit.MINUTE: - return 1 - case ExposureTimeUnit.SECOND: - return 60 - case ExposureTimeUnit.MILLISECOND: - return 60000 - case ExposureTimeUnit.MICROSECOND: - return 60000000 - default: - return 0 - } - } - - 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((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)) - this.exposureTimeUnit = unit - } - } - - private normalizeExposureTimeAndUnit(exposureTime: number) { - 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.id) { if (this.camera.connected) { updateCameraStartCaptureFromCamera(this.request, this.camera) - this.updateExposureUnit(this.exposureTimeUnit) } - - this.capturesPath = this.camera.capturesPath } } - clearSavePath() { - this.savePath = '' + protected clearSavePath() { + this.preference.request.savePath = '' this.savePreference() } - resetCameraCaptureNamingFormat(type: FrameType) { - const namingFormatPreference = this.preference.cameraCaptureNamingFormatPreference.get() - resetCameraCaptureNamingFormat(type, this.namingFormat.format, namingFormatPreference) + protected resetCameraCaptureNamingFormat(type: FrameType) { + resetCameraCaptureNamingFormat(type, this.namingFormat.format, this.preferenceService.settings.get().namingFormat) this.savePreference() } - apply() { + protected apply() { return this.app.close(this.makeCameraStartCapture()) } private loadPreference() { - if (this.mode === 'CAPTURE' && this.camera.name) { - const cameraPreference: Partial = this.preference.cameraPreference(this.camera).get() - - this.request.autoSave = cameraPreference.autoSave ?? false - this.savePath = cameraPreference.savePath ?? '' - this.request.autoSubFolderMode = cameraPreference.autoSubFolderMode ?? 'OFF' - this.setpointTemperature = cameraPreference.setpointTemperature ?? 0 - this.request.exposureTime = cameraPreference.exposureTime ?? this.camera.exposureMin - this.exposureTimeUnit = cameraPreference.exposureTimeUnit ?? ExposureTimeUnit.MICROSECOND - this.exposureMode = cameraPreference.exposureMode ?? 'SINGLE' - this.request.exposureDelay = cameraPreference.exposureDelay ?? 0 - this.request.exposureAmount = cameraPreference.exposureAmount ?? 1 - this.request.x = cameraPreference.x ?? this.camera.minX - this.request.y = cameraPreference.y ?? this.camera.minY - this.request.width = cameraPreference.width ?? this.camera.maxWidth - this.request.height = cameraPreference.height ?? this.camera.maxHeight - this.subFrame = cameraPreference.subFrame ?? false - this.request.binX = cameraPreference.binX ?? 1 - this.request.binY = cameraPreference.binY ?? 1 - this.request.frameType = cameraPreference.frameType ?? 'LIGHT' - this.request.gain = cameraPreference.gain ?? 0 - this.request.offset = cameraPreference.offset ?? 0 - this.request.frameFormat = cameraPreference.frameFormat ?? (this.camera.frameFormats[0] || '') - this.request.calibrationGroup = cameraPreference.calibrationGroup - - this.request.dither.enabled = cameraPreference.dither?.enabled ?? false - this.request.dither.amount = cameraPreference.dither?.amount ?? 1.5 - this.request.dither.raOnly = cameraPreference.dither?.raOnly ?? false - this.request.dither.afterExposures = cameraPreference.dither?.afterExposures ?? 1 - - this.request.liveStacking.enabled = cameraPreference.liveStacking?.enabled ?? false - this.request.liveStacking.type = cameraPreference.liveStacking?.type ?? 'SIRIL' - this.request.liveStacking.executablePath = cameraPreference.liveStacking?.executablePath ?? '' - this.request.liveStacking.darkPath = cameraPreference.liveStacking?.darkPath - this.request.liveStacking.flatPath = cameraPreference.liveStacking?.flatPath - this.request.liveStacking.biasPath = cameraPreference.liveStacking?.biasPath - this.request.liveStacking.use32Bits = cameraPreference.liveStacking?.use32Bits ?? false - this.request.liveStacking.slot = cameraPreference.liveStacking?.slot ?? 1 - - const cameraCaptureNamingFormatPreference = this.preference.cameraCaptureNamingFormatPreference.get() - this.request.namingFormat.light = cameraPreference.namingFormat?.light ?? cameraCaptureNamingFormatPreference.light - this.request.namingFormat.dark = cameraPreference.namingFormat?.dark ?? cameraCaptureNamingFormatPreference.dark - this.request.namingFormat.flat = cameraPreference.namingFormat?.flat ?? cameraCaptureNamingFormatPreference.flat - this.request.namingFormat.bias = cameraPreference.namingFormat?.bias ?? cameraCaptureNamingFormatPreference.bias - - Object.assign(this.equipment, this.preference.equipmentForDevice(this.camera).get()) + if (this.mode === 'CAPTURE' && this.camera.id) { + Object.assign(this.preference, this.preferenceService.camera(this.camera).get()) + this.request = this.preference.request + this.dither.request = this.request.dither + this.liveStacking.request = this.request.liveStacking + this.namingFormat.format = cameraCaptureNamingFormatWithDefault(this.request.namingFormat, this.preferenceService.settings.get().namingFormat) } } - savePreference() { + protected savePreference() { if (this.mode === 'CAPTURE' && this.camera.connected) { - const preference: CameraPreference = { - ...this.request, - setpointTemperature: this.setpointTemperature, - exposureTimeUnit: this.exposureTimeUnit, - exposureMode: this.exposureMode, - subFrame: this.subFrame, - savePath: this.request.savePath || this.savePath, - } - - this.preference.cameraPreference(this.camera).set(preference) + Object.assign(this.preference.request, this.request) + this.preferenceService.camera(this.camera).set(this.preference) } } - static async showAsDialog(window: BrowserWindowService, mode: CameraDialogMode, camera: Camera, request: CameraStartCapture) { - const result = await window.openCameraDialog({ mode, camera, request }) + static async showAsDialog(service: BrowserWindowService, mode: CameraMode, camera: Camera, request: CameraStartCapture) { + const result = await service.openCameraDialog({ mode, camera, request }) if (result) { Object.assign(request, result) diff --git a/desktop/src/app/camera/exposure-time.component.html b/desktop/src/app/camera/exposure-time.component.html new file mode 100644 index 000000000..960f08306 --- /dev/null +++ b/desktop/src/app/camera/exposure-time.component.html @@ -0,0 +1,32 @@ +
+ + + + + + +
diff --git a/desktop/src/app/camera/exposure-time.component.ts b/desktop/src/app/camera/exposure-time.component.ts new file mode 100644 index 000000000..d1ab3ecb8 --- /dev/null +++ b/desktop/src/app/camera/exposure-time.component.ts @@ -0,0 +1,223 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core' +import { MenuItem } from '../../shared/components/menu-item/menu-item.component' +import { ExposureTimeUnit } from '../../shared/types/camera.types' + +@Component({ + selector: 'neb-exposure-time', + templateUrl: './exposure-time.component.html', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExposureTimeComponent implements AfterViewInit, OnChanges { + @Input({ required: true }) + protected exposureTime: number = 0 + + @Output() + readonly exposureTimeChange = new EventEmitter() + + @Input() + protected unit: ExposureTimeUnit = 'MICROSECOND' + + @Output() + readonly unitChange = new EventEmitter() + + @Input() + protected readonly min: number = 0 + + @Input() + protected readonly max: number = 600000000 + + @Input() + protected readonly disabled: boolean = false + + @Input() + protected readonly canExposureTime: boolean = true + + @Input() + protected readonly canExposureTimeUnit: boolean = true + + @Input() + protected readonly normalized: boolean = true + + @Input() + protected readonly label?: string + + protected readonly current = { + exposureTime: this.exposureTime, + min: this.min, + max: this.max, + } + + protected readonly model: MenuItem[] = [ + { + label: 'Minute (m)', + command: () => { + this.exposureTimeUnitChanged('MINUTE') + }, + }, + { + label: 'Second (s)', + command: () => { + this.exposureTimeUnitChanged('SECOND') + }, + }, + { + label: 'Millisecond (ms)', + command: () => { + this.exposureTimeUnitChanged('MILLISECOND') + }, + }, + { + label: 'Microsecond (µs)', + command: () => { + this.exposureTimeUnitChanged('MICROSECOND') + }, + }, + ] + + private exposureTimeInMicroseconds = 0 + + ngOnChanges(changes: SimpleChanges) { + for (const key in changes) { + const change = changes[key] + + // if (change.currentValue === change.previousValue && !change.firstChange) continue + + switch (key) { + case 'unit': + this.exposureTimeUnitChanged(change.currentValue) + break + case 'exposureTime': + this.exposureTimeChanged(change.currentValue, 'MICROSECOND', this.normalized && this.exposureTimeInMicroseconds !== change.currentValue) + break + case 'min': + case 'max': + this.exposureTimeMinMaxChanged() + break + case 'normalized': + this.normalize(this.exposureTime) + break + } + } + } + + ngAfterViewInit() { + this.updateExposureTime(this.current.exposureTime, this.unit, this.unit) + } + + protected exposureTimeUnitChanged(value: ExposureTimeUnit) { + this.updateExposureTime(this.current.exposureTime, value, this.unit, false) + } + + protected exposureTimeChanged(value: number, from: ExposureTimeUnit = this.unit, normalize: boolean = false) { + this.updateExposureTime(value, this.unit, from, normalize) + } + + protected exposureTimeMinMaxChanged() { + this.updateExposureTime(this.current.exposureTime, this.unit, this.unit, false) + } + + protected exposureTimeUnitWheeled(event: WheelEvent) { + if (event.deltaY) { + const units: ExposureTimeUnit[] = ['MINUTE', 'SECOND', 'MILLISECOND', 'MICROSECOND'] + const index = units.indexOf(this.unit) + + if (index >= 0) { + if (event.deltaY > 0) { + const next = (index + 1) % units.length + this.exposureTimeUnitChanged(units[next]) + } else { + const next = (index + units.length - 1) % units.length + this.exposureTimeUnitChanged(units[next]) + } + } + } + } + + private updateExposureTime(value: number, unit: ExposureTimeUnit, from: ExposureTimeUnit, normalize: boolean = this.normalized) { + const a = ExposureTimeComponent.exposureUnitFactor(from) + const b = ExposureTimeComponent.exposureUnitFactor(unit) + + if (!a || !b) return + + this.current.min = Math.max(1, Math.trunc(((this.min || 1) * b) / 60000000)) + this.current.max = Math.max(1, Math.trunc(((this.max || 600000000) * b) / 60000000)) + this.current.exposureTime = Math.max(this.current.min, Math.min(Math.trunc((value * b) / a), this.current.max)) + + const exposureTimeInMicroseconds = Math.trunc((this.current.exposureTime * 60000000) / b) + + if (normalize) { + if (this.normalize(exposureTimeInMicroseconds)) { + return + } + } + + if (this.exposureTime !== exposureTimeInMicroseconds) { + this.exposureTime = exposureTimeInMicroseconds + this.exposureTimeInMicroseconds = exposureTimeInMicroseconds + this.exposureTimeChange.emit(exposureTimeInMicroseconds) + } + + if (this.unit !== unit) { + this.unit = unit + this.unitChange.emit(unit) + } + } + + private normalize(exposureTime: number) { + if (!this.normalized) { + return false + } + + const factors: { unit: ExposureTimeUnit; time: number }[] = [ + { unit: 'MINUTE', time: 60000000 }, + { unit: 'SECOND', time: 1000000 }, + { unit: '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.updateExposureTime(exposureTime, unit, 'MICROSECOND', false) + return true + } + } + } + + return false + } + + static computeExposureTime(exposureTime: number, to: ExposureTimeUnit, from: ExposureTimeUnit = 'MICROSECOND') { + if (to === from) { + return exposureTime + } + + const a = ExposureTimeComponent.exposureUnitFactor(from) + const b = ExposureTimeComponent.exposureUnitFactor(to) + + return Math.trunc((exposureTime * b) / a) + } + + static exposureUnitFactor(unit: ExposureTimeUnit) { + switch (unit) { + case 'MINUTE': + case 'm' as ExposureTimeUnit: + return 1 + case 'SECOND': + case 's' as ExposureTimeUnit: + return 60 + case 'MILLISECOND': + case 'ms' as ExposureTimeUnit: + return 60000 + case 'MICROSECOND': + case 'us' as ExposureTimeUnit: + case 'µs' as ExposureTimeUnit: + return 60000000 + default: + return 0 + } + } +} diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index b1cf6463e..627f61436 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -144,14 +144,14 @@ + spinnableNumber />
diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 835648182..1cc4c9bbf 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -6,12 +6,11 @@ import { Subject, Subscription, debounceTime } from 'rxjs' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' -import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' -import { CameraStartCapture, EMPTY_CAMERA_START_CAPTURE } from '../../shared/types/camera.types' +import { Tickable, Ticker } from '../../shared/services/ticker.service' +import { CameraStartCapture, DEFAULT_CAMERA_START_CAPTURE } from '../../shared/types/camera.types' import { Focuser } from '../../shared/types/focuser.types' -import { EMPTY_WHEEL, FilterSlot, FilterWheel, WheelDialogInput, WheelDialogMode, WheelPreference, makeFilterSlots } from '../../shared/types/wheel.types' -import { Undefinable } from '../../shared/utils/types' +import { DEFAULT_WHEEL, DEFAULT_WHEEL_PREFERENCE, Filter, Wheel, WheelDialogInput, WheelMode, makeFilter } from '../../shared/types/wheel.types' import { AppComponent } from '../app.component' @Component({ @@ -19,22 +18,26 @@ import { AppComponent } from '../app.component' templateUrl: './filterwheel.component.html', styleUrls: ['./filterwheel.component.scss'], }) -export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingable { - readonly wheel = structuredClone(EMPTY_WHEEL) - readonly request = structuredClone(EMPTY_CAMERA_START_CAPTURE) +export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickable { + protected readonly wheel = structuredClone(DEFAULT_WHEEL) + protected readonly request = structuredClone(DEFAULT_CAMERA_START_CAPTURE) + protected readonly preference = structuredClone(DEFAULT_WHEEL_PREFERENCE) - focusers: Focuser[] = [] - focuser?: Focuser - focusOffset = 0 - focusOffsetMin = 0 - focusOffsetMax = 0 + protected focusers: Focuser[] = [] + protected focuser?: Focuser + protected focuserOffset = 0 + protected focuserMinPosition = 0 + protected focuserMaxPosition = 0 - moving = false - position = 0 - filters: FilterSlot[] = [] - filter?: FilterSlot + protected moving = false + protected position = 0 + protected filters: Filter[] = [] + protected filter?: Filter - mode: WheelDialogMode = 'CAPTURE' + protected mode: WheelMode = 'CAPTURE' + + private readonly filterPublisher = new Subject() + private readonly filterSubscription?: Subscription get canShowInfo() { return this.mode === 'CAPTURE' @@ -52,25 +55,22 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab return this.mode !== 'CAPTURE' } - get currentFilter(): Undefinable { + get currentFilter(): Filter | undefined { return this.filters[this.position - 1] } - private readonly filterChangePublisher = new Subject() - private readonly filterChangeSubscription?: Subscription - constructor( private readonly app: AppComponent, private readonly api: ApiService, - private readonly electron: ElectronService, - private readonly preference: PreferenceService, + private readonly electronService: ElectronService, + private readonly preferenceService: PreferenceService, private readonly route: ActivatedRoute, - private readonly pinger: Pinger, + private readonly ticker: Ticker, ngZone: NgZone, ) { app.title = 'Filter Wheel' - electron.on('WHEEL.UPDATED', (event) => { + electronService.on('WHEEL.UPDATED', (event) => { if (event.device.id === this.wheel.id) { ngZone.run(() => { Object.assign(this.wheel, event.device) @@ -79,15 +79,15 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } }) - electron.on('WHEEL.DETACHED', (event) => { + electronService.on('WHEEL.DETACHED', (event) => { if (event.device.id === this.wheel.id) { ngZone.run(() => { - Object.assign(this.wheel, EMPTY_WHEEL) + Object.assign(this.wheel, DEFAULT_WHEEL) }) } }) - electron.on('FOCUSER.UPDATED', (event) => { + electronService.on('FOCUSER.UPDATED', (event) => { if (event.device.id === this.focuser?.id) { ngZone.run(() => { if (this.focuser) { @@ -97,7 +97,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } }) - electron.on('FOCUSER.DETACHED', (event) => { + electronService.on('FOCUSER.DETACHED', (event) => { if (event.device.id === this.focuser?.id) { ngZone.run(() => { this.focuser = undefined @@ -112,12 +112,10 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } }) - this.filterChangeSubscription = this.filterChangePublisher.pipe(debounceTime(1500)).subscribe(async (filter) => { - this.savePreference() - + this.filterSubscription = this.filterPublisher.pipe(debounceTime(1500)).subscribe(async (filter) => { const names = this.filters.map((e) => e.name) await this.api.wheelSync(this.wheel, names) - await this.electron.send('WHEEL.RENAMED', { wheel: this.wheel, filter }) + await this.electronService.send('WHEEL.RENAMED', { wheel: this.wheel, filter }) }) hotkeys('enter', (event) => { @@ -172,18 +170,15 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab async ngAfterContentInit() { this.route.queryParams.subscribe(async (e) => { - const decodedData = JSON.parse(decodeURIComponent(e['data'] as string)) as unknown + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as unknown if (this.app.modal) { - const request = decodedData as WheelDialogInput - Object.assign(this.request, request.request) - this.mode = request.mode - await this.wheelChanged(request.wheel) + await this.loadCameraStartCaptureForDialogMode(data as WheelDialogInput) } else { - await this.wheelChanged(decodedData as FilterWheel) + await this.wheelChanged(data as Wheel) } - this.pinger.register(this, 30000) + this.ticker.register(this, 30000) }) this.focusers = await this.api.focusers() @@ -196,20 +191,28 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) - this.filterChangeSubscription?.unsubscribe() + this.ticker.unregister(this) + this.filterSubscription?.unsubscribe() } - async ping() { + async tick() { if (this.wheel.id) await this.api.wheelListen(this.wheel) if (this.focuser?.id) await this.api.focuserListen(this.focuser) } - async wheelChanged(wheel?: FilterWheel) { + private async loadCameraStartCaptureForDialogMode(data?: WheelDialogInput) { + if (data) { + this.mode = data.mode + await this.wheelChanged(data.wheel) + Object.assign(this.request, data.request) + } + } + + protected async wheelChanged(wheel?: Wheel) { if (wheel?.id) { wheel = await this.api.wheel(wheel.id) - await this.ping() + await this.tick() Object.assign(this.wheel, wheel) @@ -220,7 +223,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab this.app.subTitle = wheel?.name ?? '' } - connect() { + protected connect() { if (this.wheel.connected) { return this.api.wheelDisconnect(this.wheel) } else { @@ -228,11 +231,11 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } } - filterChanged() { + protected filterChanged() { this.updateFocusOffset() } - async moveTo(filter: FilterSlot) { + protected async moveTo(filter: Filter) { try { if (this.currentFilter) { this.moving = true @@ -245,8 +248,6 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab const offset = nextFocusOffset - currentFocusOffset if (this.focuser && offset !== 0) { - console.info('moving focuser %d steps', offset) - if (offset < 0) await this.api.focuserMoveIn(this.focuser, -offset) else await this.api.focuserMoveOut(this.focuser, offset) } @@ -257,21 +258,21 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } } - async moveToSelectedFilter() { + protected async moveToSelectedFilter() { if (this.filter) { await this.moveTo(this.filter) } } - moveUp() { + protected moveUp() { return this.moveToPosition(this.wheel.position - 1) } - moveDown() { + protected moveDown() { return this.moveToPosition(this.wheel.position + 1) } - async moveToIndex(index: number) { + protected async moveToIndex(index: number) { if (!this.moving) { index = index >= 0 && index < this.filters.length ? index @@ -282,7 +283,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } } - async moveToPosition(position: number) { + protected async moveToPosition(position: number) { if (!this.moving) { position = position >= 1 && position <= this.wheel.count ? position @@ -298,40 +299,41 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } } - shutterToggled(filter: FilterSlot, event: CheckboxChangeEvent) { + protected shutterToggled(filter: Filter, event: CheckboxChangeEvent) { this.filters.forEach((e) => (e.dark = !!event.checked && e === filter)) - this.filterChangePublisher.next(structuredClone(filter)) + this.preference.shutterPosition = this.filters.find((e) => e.dark)?.position ?? 0 + this.savePreference() } - filterNameChanged(filter: FilterSlot) { + protected filterNameChanged(filter: Filter) { if (filter.name) { - this.filterChangePublisher.next(structuredClone(filter)) + this.filterPublisher.next(structuredClone(filter)) } } - async focuserChanged() { + protected async focuserChanged() { if (this.focuser) { - await this.ping() + await this.tick() - this.focusOffsetMax = this.focuser.maxPosition - this.focusOffsetMin = -this.focusOffsetMax + this.focuserMaxPosition = this.focuser.maxPosition + this.focuserMinPosition = -this.focuserMaxPosition this.updateFocusOffset() } } - focusOffsetForFilter(filter: FilterSlot) { - return this.focuser ? this.preference.focusOffsets(this.wheel, this.focuser).get()[filter.position - 1] ?? 0 : 0 + protected focusOffsetForFilter(filter: Filter) { + return this.focuser ? (this.preferenceService.focusOffsets(this.wheel, this.focuser).get()[filter.position - 1] ?? 0) : 0 } private updateFocusOffset() { - this.focusOffset = this.filter ? this.focusOffsetForFilter(this.filter) : 0 + this.focuserOffset = this.filter ? this.focusOffsetForFilter(this.filter) : 0 } - focusOffsetChanged() { + protected focusOffsetChanged() { if (this.filter && this.focuser) { - const offsets = this.preference.focusOffsets(this.wheel, this.focuser).get() - offsets[this.filter.position - 1] = this.focusOffset - this.preference.focusOffsets(this.wheel, this.focuser).set(offsets) + const offsets = this.preferenceService.focusOffsets(this.wheel, this.focuser).get() + offsets[this.filter.position - 1] = this.focuserOffset + this.preferenceService.focusOffsets(this.wheel, this.focuser).set(offsets) } } @@ -349,36 +351,21 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab if (this.moving) return - const preference = this.preference.wheelPreference(this.wheel).get() - const filters = makeFilterSlots(this.wheel, this.filters, preference.shutterPosition) - - if (filters !== this.filters) { - this.filters = filters - this.filter = filters[(this.filter?.position ?? this.position) - 1] ?? filters[0] - } + this.filters = makeFilter(this.wheel, this.filters, this.preference.shutterPosition) + this.filter = this.filters[(this.filter?.position ?? this.position) - 1] ?? this.filters[0] this.updateFocusOffset() } private loadPreference() { if (this.mode === 'CAPTURE' && this.wheel.name) { - const preference = this.preference.wheelPreference(this.wheel).get() - const shutterPosition = preference.shutterPosition ?? 0 - this.filters.forEach((e) => (e.dark = e.position === shutterPosition)) + Object.assign(this.preference, this.preferenceService.wheel(this.wheel).get()) } } private savePreference() { if (this.mode === 'CAPTURE' && this.wheel.connected) { - const dark = this.filters.find((e) => e.dark) - - const preference: WheelPreference = { - shutterPosition: dark?.position ?? 0, - } - - this.preference.wheelPreference(this.wheel).set(preference) - - // TODO: this.api.wheelSync(this.wheel, preference.names!) + this.preferenceService.wheel(this.wheel).set(this.preference) } } @@ -389,12 +376,12 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab } } - apply() { + protected apply() { return this.app.close(this.makeCameraStartCapture()) } - static async showAsDialog(window: BrowserWindowService, mode: WheelDialogMode, wheel: FilterWheel, request: CameraStartCapture) { - const result = await window.openWheelDialog({ mode, wheel, request }) + static async showAsDialog(service: BrowserWindowService, mode: WheelMode, wheel: Wheel, request: CameraStartCapture) { + const result = await service.openWheelDialog({ 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 ab62448c0..3539bd486 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.html +++ b/desktop/src/app/flat-wizard/flat-wizard.component.html @@ -8,7 +8,7 @@ [(device)]="camera" (deviceChange)="cameraChanged()" /> @@ -27,19 +27,13 @@
- - - {{ savedPath }} -
+ spinnableNumber />
+ spinnableNumber />
+ spinnableNumber />
+ spinnableNumber />
@@ -139,7 +133,7 @@
{ + electronService.on('FLAT_WIZARD.ELAPSED', (event) => { ngZone.run(() => { - if (event.state === 'EXPOSURING' && event.capture && event.capture.camera.id === this.camera.id) { + if (event.state === 'EXPOSURING' && event.capture && event.capture.camera.id === this.camera?.id) { this.running = true this.cameraExposure.handleCameraCaptureEvent(event.capture, true) } else if (event.state === 'CAPTURED') { this.running = false - this.savedPath = event.savedPath - this.prime.message(`Flat frame captured`) + this.angularService.message('Flat frame captured') } else if (event.state === 'FAILED') { this.running = false - this.savedPath = undefined - this.prime.message(`Failed to find an optimal exposure time from given parameters`, 'error') + this.angularService.message('Failed to find an optimal exposure time from given parameters', 'error') } }) }) - electron.on('CAMERA.UPDATED', async (event) => { - if (event.device.id === this.camera.id) { - await ngZone.run(() => { - Object.assign(this.camera, event.device) - return this.cameraChanged() + electronService.on('CAMERA.UPDATED', (event) => { + if (event.device.id === this.camera?.id) { + ngZone.run(() => { + if (this.camera) { + Object.assign(this.camera, event.device) + void this.cameraChanged() + } }) } }) - electron.on('CAMERA.ATTACHED', (event) => { + electronService.on('CAMERA.ATTACHED', (event) => { ngZone.run(() => { this.cameras.push(event.device) this.cameras.sort(deviceComparator) }) }) - electron.on('CAMERA.DETACHED', (event) => { + electronService.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) + Object.assign(this.camera, this.cameras[0] ?? DEFAULT_CAMERA) } this.cameras.splice(index, 1) @@ -108,41 +102,41 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Pingable { }) }) - electron.on('WHEEL.UPDATED', async (event) => { - if (event.device.id === this.wheel.id) { - await ngZone.run(() => { - Object.assign(this.wheel, event.device) - return this.wheelChanged() + electronService.on('WHEEL.UPDATED', (event) => { + if (event.device.id === this.wheel?.id) { + ngZone.run(() => { + if (this.wheel) { + Object.assign(this.wheel, event.device) + void this.wheelChanged() + } }) } }) - electron.on('WHEEL.ATTACHED', (event) => { + electronService.on('WHEEL.ATTACHED', (event) => { ngZone.run(() => { this.wheels.push(event.device) this.wheels.sort(deviceComparator) }) }) - electron.on('WHEEL.DETACHED', (event) => { + electronService.on('WHEEL.DETACHED', (event) => { ngZone.run(() => { 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) + Object.assign(this.wheel, this.wheels[0] ?? DEFAULT_WHEEL) } this.wheels.splice(index, 1) } }) }) - - this.request.capture.frameType = 'FLAT' } async ngAfterViewInit() { - this.pinger.register(this, 30000) + this.ticker.register(this, 30000) this.cameras = (await this.api.cameras()).sort(deviceComparator) this.wheels = (await this.api.wheels()).sort(deviceComparator) @@ -150,27 +144,26 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Pingable { @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) void this.stop() } - async ping() { - if (this.camera.id) await this.api.cameraListen(this.camera) - if (this.wheel.id) await this.api.wheelListen(this.wheel) + async tick() { + if (this.camera?.id) await this.api.cameraListen(this.camera) + if (this.wheel?.id) await this.api.wheelListen(this.wheel) } - async showCameraDialog() { - if (this.camera.id && (await CameraComponent.showAsDialog(this.browserWindow, 'FLAT_WIZARD', this.camera, this.request.capture))) { - this.preference.cameraStartCaptureForFlatWizard(this.camera).set(this.request.capture) + protected async showCameraDialog() { + if (this.camera?.id && (await CameraComponent.showAsDialog(this.browserWindowService, 'FLAT_WIZARD', this.camera, this.request.capture))) { + this.savePreference() } } - async cameraChanged() { - if (this.camera.id) { - await this.ping() + protected async cameraChanged() { + if (this.camera?.id) { + await this.tick() - const cameraPreference = this.preference.cameraPreference(this.camera).get() - this.request.capture = this.preference.cameraStartCaptureForFlatWizard(this.camera).get(cameraPreference) + this.loadPreference() this.updateEntryFromCamera(this.camera) this.request.capture.frameType = 'FLAT' } @@ -179,15 +172,16 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Pingable { private updateEntryFromCamera(camera?: Camera) { if (camera?.connected) { updateCameraStartCaptureFromCamera(this.request.capture, camera) + this.savePreference() } } - async wheelChanged() { - if (this.wheel.id) { - await this.ping() + protected async wheelChanged() { + if (this.wheel?.id) { + await this.tick() - const preference = this.preference.wheelPreference(this.wheel).get() - const filters = makeFilterSlots(this.wheel, this.filters, preference.shutterPosition) + const shutterPosition = this.preferenceService.wheel(this.wheel).get().shutterPosition + const filters = makeFilter(this.wheel, this.filters, shutterPosition) if (filters !== this.filters) { this.filters = filters @@ -196,20 +190,31 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Pingable { } } - async start() { - 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. - await this.api.flatWizardStart(this.camera, this.request) + protected async start() { + if (this.camera) { + await this.browserWindowService.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. + await this.api.flatWizardStart(this.camera, this.request) + } + } + + protected async stop() { + if (this.camera) { + await this.api.flatWizardStop(this.camera) + } } - stop() { - return this.api.flatWizardStop(this.camera) + private loadPreference() { + if (this.camera?.id) { + Object.assign(this.preference, this.preferenceService.flatWizard(this.camera).get()) + this.request = this.preference.request + } } - savePreference() { - if (this.camera.id) { - this.preference.cameraStartCaptureForFlatWizard(this.camera).set(this.request.capture) + protected savePreference() { + if (this.camera?.id) { + this.preferenceService.flatWizard(this.camera).set(this.preference) } } } diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html index 4ae7750ce..42e26e757 100644 --- a/desktop/src/app/focuser/focuser.component.html +++ b/desktop/src/app/focuser/focuser.component.html @@ -11,7 +11,7 @@
- {{ moving ? 'moving' : 'idle' }} + {{ focuser.moving ? 'moving' : 'idle' }}
- + spinnableNumber /> +
+ spinnableNumber />
-
- +
+ + spinnableNumber /> { + electronService.on('FOCUSER.UPDATED', (event) => { if (event.device.id === this.focuser.id) { ngZone.run(() => { Object.assign(this.focuser, event.device) @@ -39,10 +36,10 @@ export class FocuserComponent implements AfterViewInit, OnDestroy, Pingable { } }) - electron.on('FOCUSER.DETACHED', (event) => { + electronService.on('FOCUSER.DETACHED', (event) => { if (event.device.id === this.focuser.id) { ngZone.run(() => { - Object.assign(this.focuser, EMPTY_FOCUSER) + Object.assign(this.focuser, DEFAULT_FOCUSER) }) } }) @@ -81,43 +78,47 @@ export class FocuserComponent implements AfterViewInit, OnDestroy, Pingable { }) hotkeys('up', (event) => { event.preventDefault() - this.stepsRelative = Math.min(this.focuser.maxPosition, this.stepsRelative + 1) + this.preference.stepsRelative = Math.min(this.focuser.maxPosition, this.preference.stepsRelative + 1) + this.savePreference() }) hotkeys('down', (event) => { event.preventDefault() - this.stepsRelative = Math.max(0, this.stepsRelative - 1) + this.preference.stepsRelative = Math.max(0, this.preference.stepsRelative - 1) + this.savePreference() }) hotkeys('ctrl+up', (event) => { event.preventDefault() - this.stepsAbsolute = Math.max(0, this.stepsAbsolute - 1) + this.preference.stepsAbsolute = Math.max(0, this.preference.stepsAbsolute - 1) + this.savePreference() }) hotkeys('ctrl+down', (event) => { event.preventDefault() - this.stepsAbsolute = Math.min(this.focuser.maxPosition, this.stepsAbsolute + 1) + this.preference.stepsAbsolute = Math.min(this.focuser.maxPosition, this.preference.stepsAbsolute + 1) + this.savePreference() }) } ngAfterViewInit() { this.route.queryParams.subscribe(async (e) => { - const focuser = JSON.parse(decodeURIComponent(e['data'] as string)) as Focuser - await this.focuserChanged(focuser) - this.pinger.register(this, 30000) + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as Focuser + await this.focuserChanged(data) + this.ticker.register(this, 30000) }) } @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) void this.abort() } - async ping() { + async tick() { if (this.focuser.id) { await this.api.focuserListen(this.focuser) } } - async focuserChanged(focuser?: Focuser) { + protected async focuserChanged(focuser?: Focuser) { if (focuser?.id) { focuser = await this.api.focuser(focuser.id) Object.assign(this.focuser, focuser) @@ -129,7 +130,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy, Pingable { this.app.subTitle = focuser?.name ?? '' } - connect() { + protected connect() { if (this.focuser.connected) { return this.api.focuserDisconnect(this.focuser) } else { @@ -137,61 +138,46 @@ export class FocuserComponent implements AfterViewInit, OnDestroy, Pingable { } } - async moveIn(stepSize: number = 1) { - if (!this.moving) { - this.moving = true - await this.api.focuserMoveIn(this.focuser, Math.trunc(this.stepsRelative * stepSize)) - this.savePreference() + protected async moveIn(stepSize: number = 1) { + if (!this.focuser.moving && stepSize) { + await this.api.focuserMoveIn(this.focuser, Math.trunc(this.preference.stepsRelative * stepSize)) } } - async moveOut(stepSize: number = 1) { - if (!this.moving) { - this.moving = true - await this.api.focuserMoveOut(this.focuser, Math.trunc(this.stepsRelative * stepSize)) - this.savePreference() + protected async moveOut(stepSize: number = 1) { + if (!this.focuser.moving && stepSize) { + await this.api.focuserMoveOut(this.focuser, Math.trunc(this.preference.stepsRelative * stepSize)) } } - async moveTo() { - if (!this.moving && this.stepsAbsolute !== this.focuser.position) { - this.moving = true - await this.api.focuserMoveTo(this.focuser, this.stepsAbsolute) - this.savePreference() + protected async moveTo() { + if (!this.focuser.moving && this.preference.stepsAbsolute !== this.focuser.position) { + await this.api.focuserMoveTo(this.focuser, this.preference.stepsAbsolute) } } - async sync() { - if (!this.moving) { - await this.api.focuserSync(this.focuser, this.stepsAbsolute) - this.savePreference() + protected async sync() { + if (!this.focuser.moving) { + await this.api.focuserSync(this.focuser, this.preference.stepsAbsolute) } } - abort() { + protected abort() { return this.api.focuserAbort(this.focuser) } - private update() { - if (this.focuser.id) { - this.moving = this.focuser.moving - } - } + private update() {} private loadPreference() { if (this.focuser.id) { - const preference = this.preference.focuserPreference(this.focuser).get() - this.stepsRelative = preference.stepsRelative ?? 100 - this.stepsAbsolute = preference.stepsAbsolute ?? this.focuser.position + Object.assign(this.preference, this.preferenceService.focuser(this.focuser).get()) + this.preference.stepsAbsolute = this.focuser.position } } - private savePreference() { + protected savePreference() { if (this.focuser.connected) { - const preference = this.preference.focuserPreference(this.focuser).get() - preference.stepsAbsolute = this.stepsAbsolute - preference.stepsRelative = this.stepsRelative - this.preference.focuserPreference(this.focuser).set(preference) + this.preferenceService.focuser(this.focuser).set(this.preference) } } } diff --git a/desktop/src/app/framing/framing.component.html b/desktop/src/app/framing/framing.component.html index eb480fb86..8186c7931 100644 --- a/desktop/src/app/framing/framing.component.html +++ b/desktop/src/app/framing/framing.component.html @@ -4,7 +4,8 @@ + [(ngModel)]="preference.rightAscension" + (ngModelChange)="savePreference()" />
@@ -13,7 +14,8 @@ + [(ngModel)]="preference.declination" + (ngModelChange)="savePreference()" />
@@ -25,9 +27,10 @@ class="w-full" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="width" + [(ngModel)]="preference.width" + (ngModelChange)="savePreference()" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -39,9 +42,10 @@ class="w-full" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="height" + [(ngModel)]="preference.height" + (ngModelChange)="savePreference()" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -54,10 +58,11 @@ class="w-full" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="fov" + [(ngModel)]="preference.fov" + (ngModelChange)="savePreference()" locale="en" [minFractionDigits]="1" - scrollableNumber /> + spinnableNumber />
@@ -70,10 +75,11 @@ class="w-full" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="rotation" + [(ngModel)]="preference.rotation" + (ngModelChange)="savePreference()" locale="en" [minFractionDigits]="1" - scrollableNumber /> + spinnableNumber />
@@ -81,7 +87,8 @@ @@ -107,7 +114,7 @@
{ + electronService.on('DATA.CHANGED', (event: LoadFraming) => { return ngZone.run(() => this.frameFromData(event)) }) - - this.loadPreference() } async ngAfterViewInit() { @@ -73,93 +41,69 @@ export class FramingComponent implements AfterViewInit, OnDestroy { try { this.hipsSurveys = await this.api.hipsSurveys() - this.hipsSurvey = this.hipsSurveys.find((e) => e.id === this.hipsSurvey?.id) ?? this.hipsSurveys[0] + this.loadPreference() } finally { this.loading = false } this.route.queryParams.subscribe((e) => { - const data = JSON.parse(decodeURIComponent(e['data'] as string)) as FramingData + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as LoadFraming return this.frameFromData(data) }) } @HostListener('window:unload') ngOnDestroy() { - void this.closeFrameImage() - void this.electron.closeWindow(undefined, this.frameId) + void this.closeImageWindow() } - private async frameFromData(data: FramingData) { - this.rightAscension = data.rightAscension || this.rightAscension - this.declination = data.declination || this.declination - this.width = data.width || this.width - this.height = data.height || this.height - this.fov = data.fov || this.fov - if (data.rotation === 0 || data.rotation) this.rotation = data.rotation + private async frameFromData(data: LoadFraming) { + this.preference.rightAscension = data.rightAscension || this.preference.rightAscension + this.preference.declination = data.declination || this.preference.declination + this.preference.width = data.width || this.preference.width + this.preference.height = data.height || this.preference.height + this.preference.fov = data.fov || this.preference.fov + if (data.rotation === 0 || data.rotation) this.preference.rotation = data.rotation + + this.savePreference() if (data.rightAscension && data.declination) { await this.frame() } } - async frame() { - if (!this.hipsSurvey) return - - await this.closeFrameImage() + protected async frame() { + if (!this.preference.hipsSurvey) return this.loading = true try { - const path = await this.api.frame(this.rightAscension, this.declination, this.width, this.height, this.fov, this.rotation, this.hipsSurvey) - const title = `Framing ・ ${this.rightAscension} ・ ${this.declination}` - - this.framePath = path - this.frameId = await this.browserWindow.openImage({ path, source: 'FRAMING', id: 'framing', title }) + const { rightAscension, declination, width, height, fov, rotation, hipsSurvey } = this.preference + const path = await this.api.frame(rightAscension, declination, width, height, fov, rotation, hipsSurvey) + const title = `Framing ・ ${rightAscension} ・ ${declination}` - this.savePreference() + this.frameId = await this.browserWindowService.openImage({ path, source: 'FRAMING', id: 'framing', title }) } catch (e) { console.error(e) - this.prime.message('Failed to retrieve the image', 'error') + this.angularService.message('Failed to retrieve the image', 'error') } finally { this.loading = false } } private loadPreference() { - const preference = this.storage.get(FRAMING_KEY, {}) - - this.rightAscension = preference.rightAscension ?? '00h00m00s' - this.declination = preference.declination ?? `+00°00'00"` - this.width = preference.width ?? 1280 - this.height = preference.height ?? 720 - this.fov = preference.fov ?? 1 - this.rotation = preference.rotation ?? 0 - - if (preference.hipsSurvey) { - this.hipsSurveys = [preference.hipsSurvey] - this.hipsSurvey = this.hipsSurveys[0] - } + Object.assign(this.preference, this.preferenceService.framing.get()) + this.preference.hipsSurvey = this.hipsSurveys.find((e) => e.id === this.preference.hipsSurvey?.id) ?? this.hipsSurveys[0] } - private savePreference() { - const preference: FramingPreference = { - rightAscension: this.rightAscension, - declination: this.declination, - width: this.width, - height: this.height, - fov: this.fov, - rotation: this.rotation, - hipsSurvey: this.hipsSurvey, - } - - this.storage.set(FRAMING_KEY, preference) + protected savePreference() { + this.preferenceService.framing.set(this.preference) } - private async closeFrameImage() { - if (this.framePath) { - await this.api.closeImage(this.framePath) + private async closeImageWindow() { + if (this.frameId) { + await this.electronService.closeWindow(undefined, this.frameId) } } } diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index c602ba772..38e80746d 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -9,41 +9,47 @@ + [(ngModel)]="preference.host" + (ngModelChange)="savePreference()" />
+ spinnableNumber />
+ [text]="true" + pTooltip="Disconnect" + tooltipPosition="bottom" /> + [text]="true" + pTooltip="Connect" + tooltipPosition="bottom" />
- {{ guideState | enum | lowercase }} + {{ guider.state | enum | lowercase }} - {{ message }} + {{ guider.message }}
-
+
+ value="{{ chartInfo.rmsRA.toFixed(2) + ' (' + (chartInfo.rmsRA * chartInfo.pixelScale).toFixed(2) + '" )' }}" />
@@ -78,7 +84,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - value="{{ rmsDEC.toFixed(2) + ' (' + (rmsDEC * pixelScale).toFixed(2) + '" )' }}" /> + value="{{ chartInfo.rmsDEC.toFixed(2) + ' (' + (chartInfo.rmsDEC * chartInfo.pixelScale).toFixed(2) + '" )' }}" />
@@ -88,7 +94,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - value="{{ rmsTotal.toFixed(2) + ' (' + (rmsTotal * pixelScale).toFixed(2) + '" )' }}" /> + value="{{ chartInfo.rmsTotal.toFixed(2) + ' (' + (chartInfo.rmsTotal * chartInfo.pixelScale).toFixed(2) + '" )' }}" />
@@ -98,7 +104,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="guideStep?.starMass ?? 0" /> + [value]="guider.step?.starMass ?? 0" />
@@ -108,7 +114,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="guideStep?.hfd ?? 0" /> + [value]="guider.step?.hfd ?? 0" /> @@ -118,7 +124,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="guideStep?.snr ?? 0" /> + [value]="guider.step?.snr ?? 0" /> @@ -126,7 +132,7 @@
-
- +
+ + spinnableNumber />
-
- +
+ + spinnableNumber />
-
- +
+ + spinnableNumber />
@@ -231,7 +237,7 @@ + spinnableNumber />
@@ -284,15 +291,16 @@
+ spinnableNumber />
@@ -300,7 +308,7 @@
+ spinnableNumber />
@@ -393,16 +402,17 @@
+ spinnableNumber />
diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index e58c9f05c..f0f13b9bc 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -1,64 +1,45 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' -import { Title } from '@angular/platform-browser' -import { ChartData, ChartOptions } from 'chart.js' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { Chart, ChartData, ChartOptions } from 'chart.js' +import zoomPlugin from 'chartjs-plugin-zoom' import { UIChart } from 'primeng/chart' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' -import { Pingable, Pinger } from '../../shared/services/pinger.service' -import { GuideDirection, GuideOutput, GuideState, GuideStep, Guider, GuiderHistoryStep, GuiderPlotMode, GuiderYAxisUnit } from '../../shared/types/guider.types' +import { PreferenceService } from '../../shared/services/preference.service' +import { Tickable, Ticker } from '../../shared/services/ticker.service' +import { DEFAULT_GUIDER_CHART_INFO, DEFAULT_GUIDER_PHD2, DEFAULT_GUIDER_PREFERENCE, DEFAULT_GUIDER_PULSE, GuideDirection, GuideOutput, Guider, GuiderHistoryStep } from '../../shared/types/guider.types' +import { AppComponent } from '../app.component' @Component({ selector: 'neb-guider', templateUrl: './guider.component.html', }) -export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { - guideOutputs: GuideOutput[] = [] - guideOutput?: GuideOutput - guideOutputConnected = false - pulseGuiding = false - - guideNorthDuration = 1000 - guideSouthDuration = 1000 - guideWestDuration = 1000 - guideEastDuration = 1000 - - connected = false - host = 'localhost' - port = 4400 - guideState: GuideState = 'STOPPED' - guideStep?: GuideStep - message = '' - - settleAmount = 1.5 - settleTime = 10 - settleTimeout = 30 - readonly phdGuideHistory: GuiderHistoryStep[] = [] - private phdDurationScale = 1.0 - - pixelScale = 1.0 - rmsRA = 0.0 - rmsDEC = 0.0 - rmsTotal = 0.0 - - plotMode: GuiderPlotMode = 'RA/DEC' - yAxisUnit: GuiderYAxisUnit = 'ARCSEC' +export class GuiderComponent implements OnInit, AfterViewInit, OnDestroy, Tickable { + protected guideOutputs: GuideOutput[] = [] + protected guideOutput?: GuideOutput + + protected readonly preference = structuredClone(DEFAULT_GUIDER_PREFERENCE) + protected readonly guider = structuredClone(DEFAULT_GUIDER_PHD2) + protected readonly pulse = structuredClone(DEFAULT_GUIDER_PULSE) + protected readonly chartInfo = structuredClone(DEFAULT_GUIDER_CHART_INFO) + + private readonly guideHistory: GuiderHistoryStep[] = [] @ViewChild('chart') private readonly chart!: UIChart get stopped() { - return this.guideState === 'STOPPED' + return this.guider.state === 'STOPPED' } get looping() { - return this.guideState === 'LOOPING' + return this.guider.state === 'LOOPING' } get guiding() { - return this.guideState === 'GUIDING' + return this.guider.state === 'GUIDING' } - readonly chartData: ChartData = { + protected readonly chartData: ChartData = { labels: Array.from({ length: 100 }, (_, i) => `${i}`), datasets: [ // RA. @@ -96,7 +77,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { ], } - readonly chartOptions: ChartOptions = { + protected readonly chartOptions: ChartOptions = { responsive: true, plugins: { legend: { @@ -116,10 +97,10 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const barType = context.dataset.type === 'bar' const raType = context.datasetIndex === 0 || context.datasetIndex === 2 - const scale = barType ? this.phdDurationScale : 1.0 + const scale = barType ? this.chartInfo.durationScale : 1.0 const y = context.parsed.y * scale const prefix = raType ? 'RA: ' : 'DEC: ' - const lineSuffix = this.yAxisUnit === 'ARCSEC' ? '"' : 'px' + const lineSuffix = this.preference.yAxisUnit === 'ARCSEC' ? '"' : 'px' const formattedY = prefix + (barType ? y.toFixed(0) + ' ms' : y.toFixed(2) + lineSuffix) return formattedY }, @@ -214,15 +195,16 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { } constructor( - title: Title, + app: AppComponent, private readonly api: ApiService, - private readonly pinger: Pinger, - electron: ElectronService, + private readonly ticker: Ticker, + private readonly preferenceService: PreferenceService, + electronService: ElectronService, ngZone: NgZone, ) { - title.setTitle('Guider') + app.title = 'Guider' - electron.on('GUIDE_OUTPUT.UPDATED', (event) => { + electronService.on('GUIDE_OUTPUT.UPDATED', (event) => { if (event.device.id === this.guideOutput?.id) { ngZone.run(() => { if (this.guideOutput) { @@ -233,69 +215,67 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { } }) - electron.on('GUIDE_OUTPUT.ATTACHED', (event) => { + electronService.on('GUIDE_OUTPUT.ATTACHED', (event) => { ngZone.run(() => { this.guideOutputs.push(event.device) }) }) - electron.on('GUIDE_OUTPUT.DETACHED', (event) => { + electronService.on('GUIDE_OUTPUT.DETACHED', (event) => { ngZone.run(() => { const index = this.guideOutputs.findIndex((e) => e.id === event.device.id) if (index >= 0) this.guideOutputs.splice(index, 1) }) }) - electron.on('GUIDER.CONNECTED', () => { + electronService.on('GUIDER.CONNECTED', () => { ngZone.run(() => { - this.connected = true + this.guider.connected = true }) }) - electron.on('GUIDER.DISCONNECTED', () => { + electronService.on('GUIDER.DISCONNECTED', () => { ngZone.run(() => { - this.connected = false + this.guider.connected = false }) }) - electron.on('GUIDER.UPDATED', (event) => { + electronService.on('GUIDER.UPDATED', (event) => { ngZone.run(() => { this.processGuiderStatus(event.data) }) }) - electron.on('GUIDER.STEPPED', (event) => { + electronService.on('GUIDER.STEPPED', (event) => { ngZone.run(() => { - if (this.phdGuideHistory.length >= 100) { - this.phdGuideHistory.splice(0, this.phdGuideHistory.length - 99) + if (this.guideHistory.length >= 100) { + this.guideHistory.splice(0, this.guideHistory.length - 99) } - this.phdGuideHistory.push(event.data) + this.guideHistory.push(event.data) this.updateGuideHistoryChart() if (event.data.guideStep) { - this.guideStep = event.data.guideStep + this.guider.step = event.data.guideStep } else { // Dithering. } }) }) - electron.on('GUIDER.MESSAGE_RECEIVED', (event) => { + electronService.on('GUIDER.MESSAGE_RECEIVED', (event) => { ngZone.run(() => { - this.message = event.data + this.guider.message = event.data }) }) } - async ngAfterViewInit() { - this.pinger.register(this, 30000) - - const settle = await this.api.getGuidingSettle() + ngOnInit() { + Chart.register(zoomPlugin) + } - this.settleAmount = settle.amount - this.settleTime = settle.time - this.settleTimeout = settle.timeout + async ngAfterViewInit() { + this.ticker.register(this, 30000) this.guideOutputs = await this.api.guideOutputs() @@ -303,46 +283,50 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { this.processGuiderStatus(status) const history = await this.api.guidingHistory() - this.phdGuideHistory.push(...history) + this.guideHistory.push(...history) this.updateGuideHistoryChart() + + this.loadPreference() } @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) } - async ping() { + async tick() { if (this.guideOutput?.id) await this.api.guideOutputListen(this.guideOutput) } private processGuiderStatus(event: Guider) { - this.connected = event.connected - this.guideState = event.state - this.pixelScale = event.pixelScale + this.guider.connected = event.connected + this.guider.state = event.state + this.chartInfo.pixelScale = event.pixelScale } - plotModeChanged() { + protected plotModeChanged() { this.updateGuideHistoryChart() + this.savePreference() } - yAxisUnitChanged() { + protected yAxisUnitChanged() { this.updateGuideHistoryChart() + this.savePreference() } private updateGuideHistoryChart() { - if (this.phdGuideHistory.length > 0) { - const history = this.phdGuideHistory[this.phdGuideHistory.length - 1] - this.rmsTotal = history.rmsTotal - this.rmsDEC = history.rmsDEC - this.rmsRA = history.rmsRA + if (this.guideHistory.length > 0) { + const history = this.guideHistory[this.guideHistory.length - 1] + this.chartInfo.rmsTotal = history.rmsTotal + this.chartInfo.rmsDEC = history.rmsDEC + this.chartInfo.rmsRA = history.rmsRA } else { return } - const startId = this.phdGuideHistory[0].id - const guideSteps = this.phdGuideHistory.filter((e) => e.guideStep !== undefined) - const scale = this.yAxisUnit === 'ARCSEC' ? this.pixelScale : 1.0 + const startId = this.guideHistory[0].id + const guideSteps = this.guideHistory.filter((e) => e.guideStep !== undefined) + const scale = this.preference.yAxisUnit === 'ARCSEC' ? this.chartInfo.pixelScale : 1.0 let maxDuration = 0 @@ -351,9 +335,9 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { maxDuration = Math.max(maxDuration, Math.abs(step.guideStep!.decDuration)) } - this.phdDurationScale = maxDuration / 16.0 + this.chartInfo.durationScale = maxDuration / 16.0 - if (this.plotMode === 'RA/DEC') { + if (this.preference.plotMode === 'RA/DEC') { this.chartData.datasets[0].data = guideSteps.map((e) => [e.id - startId, -e.guideStep!.raDistance * scale]) this.chartData.datasets[1].data = guideSteps.map((e) => [e.id - startId, e.guideStep!.decDistance * scale]) } else { @@ -362,18 +346,18 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { } const durationScale = (direction?: GuideDirection) => { - return !direction || direction === 'NORTH' || direction === 'WEST' ? this.phdDurationScale : -this.phdDurationScale + return !direction || direction === 'NORTH' || direction === 'WEST' ? this.chartInfo.durationScale : -this.chartInfo.durationScale } - this.chartData.datasets[2].data = this.phdGuideHistory.map((e) => (e.guideStep?.raDuration ?? 0) / durationScale(e.guideStep?.raDirection)) - this.chartData.datasets[3].data = this.phdGuideHistory.map((e) => (e.guideStep?.decDuration ?? 0) / durationScale(e.guideStep?.decDirection)) + this.chartData.datasets[2].data = this.guideHistory.map((e) => (e.guideStep?.raDuration ?? 0) / durationScale(e.guideStep?.raDirection)) + this.chartData.datasets[3].data = this.guideHistory.map((e) => (e.guideStep?.decDuration ?? 0) / durationScale(e.guideStep?.decDirection)) this.chart.refresh() } - async guideOutputChanged() { + protected async guideOutputChanged() { if (this.guideOutput?.id) { - await this.ping() + await this.tick() const guideOutput = await this.api.guideOutput(this.guideOutput.id) Object.assign(this.guideOutput, guideOutput) @@ -382,28 +366,28 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { } } - async guidePulseStart(...directions: GuideDirection[]) { + protected async guidePulseStart(...directions: GuideDirection[]) { if (this.guideOutput) { for (const direction of directions) { switch (direction) { case 'NORTH': - await this.api.guideOutputPulse(this.guideOutput, direction, this.guideNorthDuration * 1000) + await this.api.guideOutputPulse(this.guideOutput, direction, this.preference.pulseDuration.north * 1000) break case 'SOUTH': - await this.api.guideOutputPulse(this.guideOutput, direction, this.guideSouthDuration * 1000) + await this.api.guideOutputPulse(this.guideOutput, direction, this.preference.pulseDuration.south * 1000) break case 'WEST': - await this.api.guideOutputPulse(this.guideOutput, direction, this.guideWestDuration * 1000) + await this.api.guideOutputPulse(this.guideOutput, direction, this.preference.pulseDuration.west * 1000) break case 'EAST': - await this.api.guideOutputPulse(this.guideOutput, direction, this.guideEastDuration * 1000) + await this.api.guideOutputPulse(this.guideOutput, direction, this.preference.pulseDuration.east * 1000) break } } } } - async guidePulseStop() { + protected async guidePulseStop() { if (this.guideOutput) { await this.api.guideOutputPulse(this.guideOutput, 'NORTH', 0) await this.api.guideOutputPulse(this.guideOutput, 'SOUTH', 0) @@ -412,40 +396,41 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { } } - guidingConnect() { - if (this.connected) { + protected guidingConnect() { + if (this.guider.connected) { return this.api.guidingDisconnect() } else { - return this.api.guidingConnect(this.host, this.port) + return this.api.guidingConnect(this.preference.host, this.preference.port) } } - async guidingStart(event: MouseEvent) { + protected async guidingStart(event: MouseEvent) { await this.api.guidingLoop(true) + await this.api.guidingSettle(this.preference.settle) await this.api.guidingStart(event.shiftKey) } - async settleChanged() { - await this.api.setGuidingSettle({ - amount: this.settleAmount, - time: this.settleTime, - timeout: this.settleTimeout, - }) - } - - guidingClearHistory() { - this.phdGuideHistory.length = 0 + protected guidingClearHistory() { + this.guideHistory.length = 0 return this.api.guidingClearHistory() } - guidingStop() { + protected guidingStop() { return this.api.guidingStop() } + private loadPreference() { + Object.assign(this.preference, this.preferenceService.guider.get()) + } + + protected savePreference() { + this.preferenceService.guider.set(this.preference) + } + private update() { - if (this.guideOutput) { - this.guideOutputConnected = this.guideOutput.connected - this.pulseGuiding = this.guideOutput.pulseGuiding + if (this.guideOutput?.id) { + this.pulse.connected = this.guideOutput.connected + this.pulse.pulsing = this.guideOutput.pulseGuiding } } } diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index bbd3239fd..960215797 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -13,7 +13,7 @@
- {{ item?.name }} + {{ connection?.name }}
{{ item.name }} - {{ item.host }}:{{ item.port }} + {{ item.type }} | {{ item.host }}:{{ item.port }}
{{ (item.connectedAt | date: 'yyyy-MM-dd HH:mm:ss') ?? 'never' }} @@ -239,7 +239,16 @@
+ +
Calibration
+
+
+
+ @@ -278,11 +287,11 @@
@@ -292,18 +301,16 @@ header="Connection" [modal]="true" [draggable]="false" - [(visible)]="showConnectionDialog" + [(visible)]="connectionDialog.showDialog" [style]="{ width: '90vw' }"> -
+
+ [(ngModel)]="connectionDialog.connection.name" />
@@ -312,7 +319,7 @@ + [(ngModel)]="connectionDialog.connection.host" />
@@ -322,25 +329,24 @@ [showButtons]="true" styleClass="border-0 p-inputtext-sm w-full" placeholder="7624" - [(ngModel)]="newConnection[0].port" + [(ngModel)]="connectionDialog.connection.port" [min]="80" [max]="65535" [format]="false" - scrollableNumber /> + spinnableNumber />
- + (deviceDisconnect)="deviceDisconnected($event)" + [toolbarBuilder]="deviceMenuToolbarBuilder" /> diff --git a/desktop/src/app/home/home.component.scss b/desktop/src/app/home/home.component.scss index c9b5bbcca..fac79d445 100644 --- a/desktop/src/app/home/home.component.scss +++ b/desktop/src/app/home/home.component.scss @@ -1,6 +1,6 @@ -:host { - ::ng-deep { - .buttons p-button { +neb-home { + .buttons { + p-button { display: contents; > button { diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index c8d706bec..5bb19a589 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,31 +1,22 @@ -import { AfterContentInit, Component, NgZone, ViewChild } from '@angular/core' +import { AfterContentInit, Component, NgZone, ViewChild, ViewEncapsulation } from '@angular/core' import { dirname } from 'path' import { DeviceChooserComponent } from '../../shared/components/device-chooser/device-chooser.component' import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { MenuItem, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { PrimeService } from '../../shared/services/prime.service' -import { Camera } from '../../shared/types/camera.types' -import { Device } from '../../shared/types/device.types' -import { Focuser } from '../../shared/types/focuser.types' -import { CONNECTION_TYPES, ConnectionDetails, EMPTY_CONNECTION_DETAILS, HomeWindowType } from '../../shared/types/home.types' -import { Mount } from '../../shared/types/mount.types' -import { Rotator } from '../../shared/types/rotator.types' -import { FilterWheel } from '../../shared/types/wheel.types' -import { Undefinable } from '../../shared/utils/types' +import { Camera, isCamera } from '../../shared/types/camera.types' +import { Device, DeviceType } from '../../shared/types/device.types' +import { Focuser, isFocuser } from '../../shared/types/focuser.types' +import { ConnectionDetails, DEFAULT_CONNECTION_DETAILS, DEFAULT_HOME_CONNECTION_DIALOG, DEFAULT_HOME_PREFERENCE, HomeWindowType } from '../../shared/types/home.types' +import { isMount, Mount } from '../../shared/types/mount.types' +import { isRotator, Rotator } from '../../shared/types/rotator.types' +import { isWheel, Wheel } from '../../shared/types/wheel.types' import { AppComponent } from '../app.component' -interface MappedDevice { - CAMERA: Camera - MOUNT: Mount - FOCUSER: Focuser - WHEEL: FilterWheel - ROTATOR: Rotator -} - function scrollPageOf(element: Element) { return parseInt(element.getAttribute('scroll-page') ?? '0') } @@ -34,29 +25,54 @@ function scrollPageOf(element: Element) { selector: 'neb-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class HomeComponent implements AfterContentInit { - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent + protected readonly preference = structuredClone(DEFAULT_HOME_PREFERENCE) + protected connection?: ConnectionDetails + protected readonly connectionDialog = structuredClone(DEFAULT_HOME_CONNECTION_DIALOG) - @ViewChild('imageMenu') - private readonly imageMenu!: DeviceListMenuComponent + protected cameras: Camera[] = [] + protected mounts: Mount[] = [] + protected focusers: Focuser[] = [] + protected wheels: Wheel[] = [] + protected rotators: Rotator[] = [] + protected domes: Camera[] = [] + protected switches: Camera[] = [] - readonly connectionTypes = Array.from(CONNECTION_TYPES) - showConnectionDialog = false - connections: ConnectionDetails[] = [] - connection?: ConnectionDetails - newConnection?: [ConnectionDetails, Undefinable] + protected page = 0 - cameras: Camera[] = [] - mounts: Mount[] = [] - focusers: Focuser[] = [] - wheels: FilterWheel[] = [] - rotators: Rotator[] = [] - domes: Camera[] = [] - switches: Camera[] = [] + protected readonly deviceModel: MenuItem[] = [] - currentPage = 0 + protected readonly imageModel: SlideMenuItem[] = [ + { + icon: 'mdi mdi-image-plus', + label: 'Open new image', + slideMenu: [], + command: () => { + return this.openImage() + }, + }, + ] + + protected readonly deviceMenuToolbarBuilder = (device: Device): MenuItem[] => { + if (isCamera(device)) { + return [ + { + icon: 'mdi mdi-image', + label: 'View Image', + command: () => { + return this.browserWindowService.openCameraImage(device) + }, + }, + ] + } else { + return [] + } + } + + @ViewChild('deviceMenu') + private readonly deviceMenu!: DeviceListMenuComponent get connected() { return !!this.connection && this.connection.connected @@ -122,152 +138,109 @@ export class HomeComponent implements AfterContentInit { return this.connection?.type === 'ALPACA' && this.hasDevices } - readonly deviceModel: MenuItem[] = [] - - readonly imageModel: SlideMenuItem[] = [ - { - icon: 'mdi mdi-image-plus', - label: 'Open new image', - slideMenu: [], - command: () => { - return this.openImage(true) - }, - }, - ] + constructor( + app: AppComponent, + private readonly electronService: ElectronService, + private readonly browserWindowService: BrowserWindowService, + private readonly api: ApiService, + private readonly angularService: AngularService, + private readonly preferenceService: PreferenceService, + ngZone: NgZone, + ) { + app.title = 'Nebulosa' - private startListening(type: K, onAdd: (device: MappedDevice[K]) => number, onRemove: (device: MappedDevice[K]) => number, onUpdate: (device: MappedDevice[K]) => void) { - this.electron.on(`${type}.ATTACHED`, (event) => { - this.ngZone.run(() => { - onAdd(event.device as never) + electronService.on('CAMERA.ATTACHED', (event) => { + ngZone.run(() => { + this.deviceAdded(event.device) }) }) - - this.electron.on(`${type}.DETACHED`, (event) => { - this.ngZone.run(() => { - onRemove(event.device as never) + electronService.on(`CAMERA.DETACHED`, (event) => { + ngZone.run(() => { + this.deviceRemoved(event.device) }) }) - - this.electron.on(`${type}.UPDATED`, (event) => { - this.ngZone.run(() => { - onUpdate(event.device as never) + electronService.on(`CAMERA.UPDATED`, (event) => { + ngZone.run(() => { + this.deviceUpdated(event.device) }) }) - } - - constructor( - app: AppComponent, - private readonly electron: ElectronService, - private readonly browserWindow: BrowserWindowService, - private readonly api: ApiService, - private readonly prime: PrimeService, - private readonly preference: PreferenceService, - private readonly ngZone: NgZone, - ) { - app.title = 'Nebulosa' - - this.startListening( - 'CAMERA', - (device) => { - return this.cameras.push(device) - }, - (device) => { - const found = this.cameras.findIndex((e) => e.id === device.id) - this.cameras.splice(found, 1) - return this.cameras.length - }, - (device) => { - const found = this.cameras.find((e) => e.id === device.id) - if (!found) return - Object.assign(found, device) - }, - ) - this.startListening( - 'MOUNT', - (device) => { - return this.mounts.push(device) - }, - (device) => { - const found = this.mounts.findIndex((e) => e.id === device.id) - this.mounts.splice(found, 1) - return this.mounts.length - }, - (device) => { - const found = this.mounts.find((e) => e.id === device.id) - if (!found) return - Object.assign(found, device) - }, - ) + electronService.on('MOUNT.ATTACHED', (event) => { + ngZone.run(() => { + this.deviceAdded(event.device) + }) + }) + electronService.on(`MOUNT.DETACHED`, (event) => { + ngZone.run(() => { + this.deviceRemoved(event.device) + }) + }) + electronService.on(`MOUNT.UPDATED`, (event) => { + ngZone.run(() => { + this.deviceUpdated(event.device) + }) + }) - this.startListening( - 'FOCUSER', - (device) => { - return this.focusers.push(device) - }, - (device) => { - const found = this.focusers.findIndex((e) => e.id === device.id) - this.focusers.splice(found, 1) - return this.focusers.length - }, - (device) => { - const found = this.focusers.find((e) => e.id === device.id) - if (!found) return - Object.assign(found, device) - }, - ) + electronService.on('FOCUSER.ATTACHED', (event) => { + ngZone.run(() => { + this.deviceAdded(event.device) + }) + }) + electronService.on(`FOCUSER.DETACHED`, (event) => { + ngZone.run(() => { + this.deviceRemoved(event.device) + }) + }) + electronService.on(`FOCUSER.UPDATED`, (event) => { + ngZone.run(() => { + this.deviceUpdated(event.device) + }) + }) - this.startListening( - 'WHEEL', - (device) => { - return this.wheels.push(device) - }, - (device) => { - const found = this.wheels.findIndex((e) => e.id === device.id) - this.wheels.splice(found, 1) - return this.wheels.length - }, - (device) => { - const found = this.wheels.find((e) => e.id === device.id) - if (!found) return - Object.assign(found, device) - }, - ) + electronService.on('WHEEL.ATTACHED', (event) => { + ngZone.run(() => { + this.deviceAdded(event.device) + }) + }) + electronService.on(`WHEEL.DETACHED`, (event) => { + ngZone.run(() => { + this.deviceRemoved(event.device) + }) + }) + electronService.on(`WHEEL.UPDATED`, (event) => { + ngZone.run(() => { + this.deviceUpdated(event.device) + }) + }) - this.startListening( - 'ROTATOR', - (device) => { - return this.rotators.push(device) - }, - (device) => { - const found = this.rotators.findIndex((e) => e.id === device.id) - this.rotators.splice(found, 1) - return this.rotators.length - }, - (device) => { - const found = this.rotators.find((e) => e.id === device.id) - if (!found) return - Object.assign(found, device) - }, - ) + electronService.on('ROTATOR.ATTACHED', (event) => { + ngZone.run(() => { + this.deviceAdded(event.device) + }) + }) + electronService.on(`ROTATOR.DETACHED`, (event) => { + ngZone.run(() => { + this.deviceRemoved(event.device) + }) + }) + electronService.on(`ROTATOR.UPDATED`, (event) => { + ngZone.run(() => { + this.deviceUpdated(event.device) + }) + }) - electron.on('CONNECTION.CLOSED', async (event) => { + electronService.on('CONNECTION.CLOSED', async (event) => { if (this.connection?.id === event.id) { await ngZone.run(() => { return this.updateConnection() }) } }) - - this.connections = preference.connections.get().sort((a, b) => (b.connectedAt ?? 0) - (a.connectedAt ?? 0)) - this.connections.forEach((e) => { - e.id = undefined - e.connected = false - }) - this.connection = this.connections[0] } async ngAfterContentInit() { + this.loadPreference() + await this.updateConnection() if (this.connected) { @@ -279,54 +252,103 @@ export class HomeComponent implements AfterContentInit { } } - addConnection() { - this.newConnection = [structuredClone(EMPTY_CONNECTION_DETAILS), undefined] - this.showConnectionDialog = true + private deviceAdded(device: Device) { + if (isCamera(device)) { + this.cameras.push(device) + } else if (isMount(device)) { + this.mounts.push(device) + } else if (isFocuser(device)) { + this.focusers.push(device) + } else if (isWheel(device)) { + this.wheels.push(device) + } else if (isRotator(device)) { + this.rotators.push(device) + } } - editConnection(connection: ConnectionDetails, event: MouseEvent) { - this.newConnection = [structuredClone(connection), connection] - this.showConnectionDialog = true + private deviceRemoved(device: Device) { + if (isCamera(device)) { + const found = this.cameras.findIndex((e) => e.id === device.id) + this.cameras.splice(found, 1) + } else if (isMount(device)) { + const found = this.mounts.findIndex((e) => e.id === device.id) + this.mounts.splice(found, 1) + } else if (isFocuser(device)) { + const found = this.focusers.findIndex((e) => e.id === device.id) + this.focusers.splice(found, 1) + } else if (isWheel(device)) { + const found = this.wheels.findIndex((e) => e.id === device.id) + this.wheels.splice(found, 1) + } else if (isRotator(device)) { + const found = this.rotators.findIndex((e) => e.id === device.id) + this.rotators.splice(found, 1) + } + } + + private deviceUpdated(device: Device) { + if (isCamera(device)) { + const found = this.cameras.find((e) => e.id === device.id) + found && Object.assign(found, device) + } else if (isMount(device)) { + const found = this.mounts.find((e) => e.id === device.id) + found && Object.assign(found, device) + } else if (isFocuser(device)) { + const found = this.focusers.find((e) => e.id === device.id) + found && Object.assign(found, device) + } else if (isWheel(device)) { + const found = this.wheels.find((e) => e.id === device.id) + found && Object.assign(found, device) + } else if (isRotator(device)) { + const found = this.rotators.find((e) => e.id === device.id) + found && Object.assign(found, device) + } + } + + protected addConnection() { + this.connectionDialog.edited = false + this.connectionDialog.connection = structuredClone(DEFAULT_CONNECTION_DETAILS) + this.connectionDialog.showDialog = true + } + + protected editConnection(connection: ConnectionDetails, event: MouseEvent) { + this.connectionDialog.edited = true + this.connectionDialog.connection = connection + this.connectionDialog.showDialog = true event.stopImmediatePropagation() } - deleteConnection(connection: ConnectionDetails, event: MouseEvent) { - const index = this.connections.findIndex((e) => e === connection) + protected deleteConnection(connection: ConnectionDetails, event: MouseEvent) { + const index = this.preference.connections.findIndex((e) => e === connection) if (index >= 0 && !connection.connected) { - this.connections.splice(index, 1) + this.preference.connections.splice(index, 1) + + if (!this.preference.connections.length) { + this.preference.connections.push(structuredClone(DEFAULT_CONNECTION_DETAILS)) + } if (connection === this.connection) { - this.connection = this.connections[0] + this.connection = this.preference.connections[0] } - this.preference.connections.set(this.connections) + this.savePreference() } event.stopImmediatePropagation() } - saveConnection() { - if (this.newConnection) { - // Edit. - if (this.newConnection[1]) { - Object.assign(this.newConnection[1], this.newConnection[0]) - } - // New. - else { - const newConnection = structuredClone(this.newConnection[0]) - this.connections = [...this.connections, newConnection] - this.connection = newConnection - } + protected saveConnection() { + if (!this.connectionDialog.edited) { + this.connection = this.connectionDialog.connection + this.preference.connections.push(this.connection) } - this.preference.connections.set(this.connections) + this.savePreference() - this.newConnection = undefined - this.showConnectionDialog = false + this.connectionDialog.showDialog = false } - async connect() { + protected async connect() { try { if (this.connection && !this.connection.connected) { this.connection.id = await this.api.connect(this.connection.host, this.connection.port, this.connection.type) @@ -334,13 +356,13 @@ export class HomeComponent implements AfterContentInit { } catch (e) { console.error(e) - this.prime.message('Connection failed', 'error') + this.angularService.message('Connection failed', 'error') } finally { await this.updateConnection() } } - async disconnect() { + protected async disconnect() { try { if (this.connection?.id && this.connection.connected) { await this.api.disconnect(this.connection.id) @@ -364,7 +386,7 @@ export class HomeComponent implements AfterContentInit { return DeviceChooserComponent.handleDisconnectDevice(this.api, event.device, event.item) } - private async openDevice(type: K) { + private async openDevice(type: DeviceType) { this.deviceModel.length = 0 const devices: Device[] = @@ -377,54 +399,45 @@ export class HomeComponent implements AfterContentInit { if (devices.length === 0) return - this.deviceMenu.header = type - const device = await this.deviceMenu.show(devices) + const device = await this.deviceMenu.show(devices, undefined, type) if (device && device !== 'NONE') { - await this.openDeviceWindow(type, device as never) + await this.openDeviceWindow(device) } } - private async openDeviceWindow(type: K, device: MappedDevice[K]) { - switch (type) { + private async openDeviceWindow(device: Device) { + switch (device.type) { case 'MOUNT': - await this.browserWindow.openMount(device as Mount, { bringToFront: true }) + await this.browserWindowService.openMount(device as Mount, { bringToFront: true }) break case 'CAMERA': - await this.browserWindow.openCamera(device as Camera, { bringToFront: true }) + await this.browserWindowService.openCamera(device as Camera, { bringToFront: true }) break case 'FOCUSER': - await this.browserWindow.openFocuser(device as Focuser, { bringToFront: true }) + await this.browserWindowService.openFocuser(device as Focuser, { bringToFront: true }) break case 'WHEEL': - await this.browserWindow.openWheel(device as FilterWheel, { bringToFront: true }) + await this.browserWindowService.openWheel(device as Wheel, { bringToFront: true }) break case 'ROTATOR': - await this.browserWindow.openRotator(device as Rotator, { bringToFront: true }) + await this.browserWindowService.openRotator(device as Rotator, { bringToFront: true }) break } } - private async openImage(force: boolean = false) { - if (force || this.cameras.length === 0) { - const preference = this.preference.homePreference.get() - const path = await this.electron.openImage({ defaultPath: preference.imagePath }) + private async openImage() { + const path = await this.electronService.openImage({ defaultPath: this.preference.imagePath }) - if (path) { - preference.imagePath = dirname(path) - this.preference.homePreference.set(preference) - await this.browserWindow.openImage({ path, source: 'PATH' }) - } - } else { - const camera = await this.imageMenu.show(this.cameras) + if (path) { + this.preference.imagePath = dirname(path) + this.savePreference() - if (camera && camera !== 'NONE') { - await this.browserWindow.openCameraImage(camera) - } + await this.browserWindowService.openImage({ path, source: 'PATH' }) } } - async open(type: HomeWindowType) { + protected async open(type: HomeWindowType) { switch (type) { case 'MOUNT': case 'CAMERA': @@ -434,43 +447,46 @@ export class HomeComponent implements AfterContentInit { await this.openDevice(type) break case 'GUIDER': - await this.browserWindow.openGuider({ bringToFront: true }) + await this.browserWindowService.openGuider({ bringToFront: true }) break case 'SKY_ATLAS': - await this.browserWindow.openSkyAtlas(undefined, { bringToFront: true }) + await this.browserWindowService.openSkyAtlas(undefined, { bringToFront: true }) break case 'FRAMING': - await this.browserWindow.openFraming(undefined, { bringToFront: true }) + await this.browserWindowService.openFraming(undefined, { bringToFront: true }) break case 'ALIGNMENT': - await this.browserWindow.openAlignment({ bringToFront: true }) + await this.browserWindowService.openAlignment({ bringToFront: true }) break case 'SEQUENCER': - await this.browserWindow.openSequencer({ bringToFront: true }) + await this.browserWindowService.openSequencer({ bringToFront: true }) break case 'AUTO_FOCUS': - await this.browserWindow.openAutoFocus({ bringToFront: true }) + await this.browserWindowService.openAutoFocus({ bringToFront: true }) break case 'FLAT_WIZARD': - await this.browserWindow.openFlatWizard({ bringToFront: true }) + await this.browserWindowService.openFlatWizard({ bringToFront: true }) break case 'STACKER': - await this.browserWindow.openStacker({ bringToFront: true }) + await this.browserWindowService.openStacker({ bringToFront: true }) break case 'INDI': - await this.browserWindow.openINDI(undefined, { bringToFront: true }) + await this.browserWindowService.openINDI(undefined, { bringToFront: true }) break case 'IMAGE': await this.openImage() break case 'SETTINGS': - await this.browserWindow.openSettings() + await this.browserWindowService.openSettings() break case 'CALCULATOR': - await this.browserWindow.openCalculator() + await this.browserWindowService.openCalculator() + break + case 'CALIBRATION': + await this.browserWindowService.openCalibration() break case 'ABOUT': - await this.browserWindow.openAbout() + await this.browserWindowService.openAbout() break } } @@ -482,7 +498,7 @@ export class HomeComponent implements AfterContentInit { if (status && !this.connection.connected) { this.connection.connectedAt = Date.now() - this.preference.connections.set(this.connections) + this.savePreference() this.connection.connected = true } else if (!status) { this.connection.connected = false @@ -494,7 +510,7 @@ export class HomeComponent implements AfterContentInit { const statuses = await this.api.connectionStatuses() for (const status of statuses) { - for (const connection of this.connections) { + for (const connection of this.preference.connections) { if (!connection.connected && (status.host === connection.host || status.ip === connection.host) && status.port === connection.port) { connection.id = status.id connection.type = status.type @@ -521,7 +537,7 @@ export class HomeComponent implements AfterContentInit { } } - scrolled(event: Event) { + protected scrolled(event: Event) { function isVisible(element: Element) { const bound = element.getBoundingClientRect() @@ -539,16 +555,18 @@ export class HomeComponent implements AfterContentInit { } } - this.currentPage = page + this.page = page + + event.stopImmediatePropagation() } - scrollTo(event: Event, page: number) { - this.currentPage = page + protected scrollTo(event: Event, page: number) { + this.page = page this.scrollToPage(page) event.stopImmediatePropagation() } - scrollToPage(page: number) { + protected scrollToPage(page: number) { const scrollChidren = document.getElementsByClassName('scroll-child') for (let i = 0; i < scrollChidren.length; i++) { @@ -560,4 +578,25 @@ export class HomeComponent implements AfterContentInit { } } } + + private loadPreference() { + Object.assign(this.preference, this.preferenceService.home.get()) + + this.preference.connections + .sort((a, b) => (b.connectedAt ?? 0) - (a.connectedAt ?? 0)) + .forEach((e) => { + e.id = undefined + e.connected = false + }) + + if (!this.preference.connections.length) { + this.preference.connections.push(structuredClone(DEFAULT_CONNECTION_DETAILS)) + } + + this.connection = this.preference.connections[0] + } + + protected savePreference() { + this.preferenceService.home.set(this.preference) + } } diff --git a/desktop/src/app/image/crosshair.component.ts b/desktop/src/app/image/crosshair.component.ts new file mode 100644 index 000000000..22f9e44cf --- /dev/null +++ b/desktop/src/app/image/crosshair.component.ts @@ -0,0 +1,53 @@ +import { Component, ViewEncapsulation } from '@angular/core' + +@Component({ + selector: 'neb-crosshair', + template: ` + + + + + + + + `, + styles: ` + :host { + width: 100%; + height: 100%; + pointer-events: none; + } + `, + encapsulation: ViewEncapsulation.None, +}) +export class CrossHairComponent {} diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index a9670890b..3bec82bfe 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -1,60 +1,25 @@ - {{ imageZoom.toFixed(1) }}x + {{ zoom.scale.toFixed(1) }}x
+ style="backface-visibility: hidden"> - - - - - - - + + (click)="drawDetectedStar(s)"> + [model]="contextMenuModel"> @@ -177,7 +142,9 @@
Stars & DSOs
@@ -185,8 +152,9 @@
@@ -208,7 +176,9 @@
Minor Planets
@@ -217,27 +187,35 @@
+ spinnableNumber />
+ @if (annotation.request.minorPlanetsMagLimit >= 20 || annotation.request.includeMinorPlanetsWithoutMagnitude) { +
+ + Can take a long time +
+ }

Minor planets Annotation uses the @@ -265,17 +243,17 @@

- +
@@ -285,7 +263,7 @@ @@ -295,55 +273,55 @@
+ *ngIf="astronomicalObject.info.constellation">
+ *ngIf="astronomicalObject.info.magnitude">
+ *ngIf="astronomicalObject.info.type">
+ *ngIf="astronomicalObject.info.distance"> @@ -351,10 +329,10 @@ @@ -362,32 +340,32 @@
+ [(ngModel)]="solver.request.blind" />
@@ -428,9 +406,9 @@ + [(ngModel)]="solver.request.centerRA" />
@@ -438,9 +416,9 @@ + [(ngModel)]="solver.request.centerDEC" />
@@ -449,17 +427,17 @@
+ [(ngModel)]="solver.request.radius" + spinnableNumber />
- @if (solver.type === 'SIRIL' || solver.type === 'PIXINSIGHT') { + @if (solver.request.type === 'SIRIL' || solver.request.type === 'PIXINSIGHT') {
+ spinnableNumber />
@@ -482,10 +460,10 @@ [step]="0.01" styleClass="p-inputtext-sm border-0 w-full" [showButtons]="true" - [(ngModel)]="solver.pixelSize" + [(ngModel)]="solver.request.pixelSize" [allowEmpty]="false" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -624,9 +602,9 @@ [max]="65536" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="stretchShadow" + [(ngModel)]="stretch.transformation.shadow" locale="en" - scrollableNumber /> + spinnableNumber /> @@ -635,9 +613,9 @@ [max]="65536" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="stretchHighlight" + [(ngModel)]="stretch.transformation.highlight" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -646,8 +624,8 @@ class="mt-3 px-2" [min]="0" [max]="65536" - [ngModel]="stretchShadowAndHighlight()" - (ngModelChange)="stretchShadow.set($event[0]); stretchHighlight.set($event[1])" + [ngModel]="[stretch.transformation.shadow, stretch.transformation.highlight]" + (ngModelChange)="stretch.transformation.shadow = $event[0]; stretch.transformation.highlight = $event[1]" [range]="true" />
@@ -658,9 +636,9 @@ [max]="65536" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="stretchMidtone" + [(ngModel)]="stretch.transformation.midtone" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -669,7 +647,7 @@ class="mt-3 px-2" [min]="0" [max]="65536" - [(ngModel)]="stretchMidtone" /> + [(ngModel)]="stretch.transformation.midtone" />
@@ -708,18 +686,19 @@
+ spinnableNumber />
@@ -760,13 +739,13 @@
@@ -791,7 +770,7 @@ @@ -804,7 +783,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="imageInfo.statistics.count" /> + [value]="statistics.statistics.count" />
@@ -814,7 +793,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="(imageInfo.statistics.mean * statisticsBitLength.rangeMax).toFixed(8)" /> + [value]="(statistics.statistics.mean * statistics.bitOption.rangeMax).toFixed(8)" />
@@ -824,7 +803,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="(imageInfo.statistics.median * statisticsBitLength.rangeMax).toFixed(8)" /> + [value]="(statistics.statistics.median * statistics.bitOption.rangeMax).toFixed(8)" />
@@ -834,7 +813,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="(imageInfo.statistics.variance * statisticsBitLength.rangeMax * statisticsBitLength.rangeMax).toFixed(8)" /> + [value]="(statistics.statistics.variance * statistics.bitOption.rangeMax * statistics.bitOption.rangeMax).toFixed(8)" />
@@ -844,7 +823,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="(imageInfo.statistics.avgDev * statisticsBitLength.rangeMax).toFixed(8)" /> + [value]="(statistics.statistics.avgDev * statistics.bitOption.rangeMax).toFixed(8)" />
@@ -854,7 +833,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="(imageInfo.statistics.stdDev * statisticsBitLength.rangeMax).toFixed(8)" /> + [value]="(statistics.statistics.stdDev * statistics.bitOption.rangeMax).toFixed(8)" />
@@ -864,7 +843,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="(imageInfo.statistics.minimum * statisticsBitLength.rangeMax).toFixed(8)" /> + [value]="(statistics.statistics.minimum * statistics.bitOption.rangeMax).toFixed(8)" />
@@ -874,20 +853,20 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="(imageInfo.statistics.maximum * statisticsBitLength.rangeMax).toFixed(8)" /> + [value]="(statistics.statistics.maximum * statistics.bitOption.rangeMax).toFixed(8)" />
+ (ngModelChange)="savePreference()" />
@@ -903,7 +882,7 @@ @@ -912,7 +891,7 @@
+ *ngIf="starDetector.request.type !== 'SIRIL'"> + spinnableNumber />
+ *ngIf="starDetector.request.type === 'SIRIL'"> + spinnableNumber />
@@ -966,7 +945,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="starDetection.stars.length" /> + [value]="starDetector.stars.length" />
@@ -976,7 +955,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - value="{{ starDetection.computed.hfd.toFixed(2) }} | {{ starDetection.computed.stdDev.toFixed(4) }}" /> + value="{{ starDetector.computed.hfd.toFixed(2) }} | {{ starDetector.computed.stdDev.toFixed(4) }}" />
@@ -986,7 +965,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="starDetection.computed.snr.toFixed(1)" /> + [value]="starDetector.computed.snr.toFixed(1)" />
@@ -996,7 +975,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - value="{{ starDetection.computed.fluxMin.toFixed(0) }} | {{ starDetection.computed.fluxMax.toFixed(0) }}" /> + value="{{ starDetector.computed.fluxMin.toFixed(0) }} | {{ starDetector.computed.fluxMax.toFixed(0) }}" />
@@ -1016,7 +995,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - value="{{ starDetection.selected.x.toFixed(0) }} | {{ starDetection.selected.y.toFixed(0) }}" /> + value="{{ starDetector.selected.x.toFixed(0) }} | {{ starDetector.selected.y.toFixed(0) }}" />
@@ -1026,7 +1005,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="starDetection.selected.flux.toFixed(0)" /> + [value]="starDetector.selected.flux.toFixed(0)" />
@@ -1036,7 +1015,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="starDetection.selected.hfd.toFixed(2)" /> + [value]="starDetector.selected.hfd.toFixed(2)" /> @@ -1046,7 +1025,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="starDetection.selected.snr.toFixed(1)" /> + [value]="starDetector.selected.snr.toFixed(1)" /> @@ -1055,7 +1034,7 @@
+ [(ngModel)]="fov.selected.focalLength" + (ngModelChange)="saveFOV()" + spinnableNumber />
@@ -1099,8 +1081,9 @@ + [(ngModel)]="fov.selected.aperture" + (ngModelChange)="saveFOV()" + spinnableNumber /> @@ -1109,7 +1092,7 @@ @@ -1121,8 +1104,9 @@ [showButtons]="true" [min]="1" [max]="9999" - [(ngModel)]="fov.cameraSize.width" - scrollableNumber /> + [(ngModel)]="fov.selected.cameraSize.width" + (ngModelChange)="saveFOV()" + spinnableNumber /> @@ -1133,8 +1117,9 @@ [showButtons]="true" [min]="1" [max]="9999" - [(ngModel)]="fov.cameraSize.height" - scrollableNumber /> + [(ngModel)]="fov.selected.cameraSize.height" + (ngModelChange)="saveFOV()" + spinnableNumber /> @@ -1148,9 +1133,10 @@ [step]="0.01" [minFractionDigits]="0" [maxFractionDigits]="2" - [(ngModel)]="fov.pixelSize.width" + [(ngModel)]="fov.selected.pixelSize.width" + (ngModelChange)="saveFOV()" locale="en" - scrollableNumber /> + spinnableNumber /> @@ -1164,13 +1150,14 @@ [step]="0.01" [minFractionDigits]="0" [maxFractionDigits]="2" - [(ngModel)]="fov.pixelSize.height" + [(ngModel)]="fov.selected.pixelSize.height" + (ngModelChange)="saveFOV()" locale="en" - scrollableNumber /> + spinnableNumber /> -
+
+ spinnableNumber />
@@ -1193,8 +1181,9 @@ [showButtons]="true" [min]="1" [max]="5" - [(ngModel)]="fov.bin" - scrollableNumber /> + [(ngModel)]="fov.selected.bin" + (ngModelChange)="saveFOV()" + spinnableNumber />
@@ -1209,97 +1198,81 @@ [minFractionDigits]="0" [maxFractionDigits]="2" [(ngModel)]="fov.rotation" + (ngModelChange)="saveFOV(false)" locale="en" - scrollableNumber /> + spinnableNumber /> --> -
+
- -
+ style="max-height: 112px"> @for (item of fov.fovs; track $index) {
+ class="flex align-items-center gap-2 border-left-3 p-2 border-round cursor-pointer" + [class.bg-blue-900]="fov.selected === item" + [style.border-color]="item.color" + (click)="selectFOV(item)">
+ [(ngModel)]="item.enabled" + (onChange)="$event.originalEvent?.stopImmediatePropagation()" />
-
+
+ value="FL: {{ item.focalLength }} mm" /> + value="AP: {{ item.aperture }} mm" /> + value="RES: {{ item.cameraSize.width }}x{{ item.cameraSize.height }}" /> + value="PS: {{ item.pixelSize.width }}x{{ item.pixelSize.height }} µm" /> + value="MULT: {{ item.barlowReducer.toFixed(2) }}x" /> + value="BIN: {{ item.bin }}" /> + value="ANGLE: {{ item.rotation }}°" /> @if (item.computed) { + value="F/{{ item.computed.focalRatio.toFixed(1) }}" /> + value="SCALE: {{ item.computed.cameraResolution.width.toFixed(2) }}"x{{ item.computed.cameraResolution.height.toFixed(2) }}"" /> + value="FOV: {{ item.computed.fieldSize.width.toFixed(2) }}°x{{ item.computed.fieldSize.height.toFixed(2) }}°" /> }
- + pTooltip="Remove" + tooltipPosition="bottom" + (onClick)="deleteFOV(item); $event.stopImmediatePropagation()" />
} @@ -1451,6 +1424,28 @@ + +
+
+ + +
+
+
+
diff --git a/desktop/src/app/image/image.component.scss b/desktop/src/app/image/image.component.scss index 3f4411614..affaac825 100644 --- a/desktop/src/app/image/image.component.scss +++ b/desktop/src/app/image/image.component.scss @@ -15,33 +15,33 @@ image-rendering: pixelated; border-radius: 4px; } -} -.roi { - width: 128px; - height: 128px; - box-sizing: border-box; -} + .roi { + width: 128px; + height: 128px; + box-sizing: border-box; + } -.roi-coordinates { - background: rgba(0, 0, 0, 0.5); - padding: 4px 8px; - border-radius: 2px; - font-size: 12px !important; - min-width: 71px; - top: 46px; - left: 50%; - white-space: nowrap; - transform: translate(-50%, 0%); -} + .roi-coordinates { + background: rgba(0, 0, 0, 0.5); + padding: 4px 8px; + border-radius: 2px; + font-size: 12px !important; + min-width: 71px; + top: 46px; + left: 50%; + white-space: nowrap; + transform: translate(-50%, 0%); + } -.coordinates { - bottom: 8px; - left: 50%; - padding: 8px; - border-radius: 2px; - background: rgba(0, 0, 0, 0.65); - border: 1px solid rgba(255, 255, 255, 0.15); - width: 260px; - transform: translate(-50%, 0px); + .coordinates { + bottom: 8px; + left: 50%; + padding: 8px; + border-radius: 2px; + background: rgba(0, 0, 0, 0.65); + border: 1px solid rgba(255, 255, 255, 0.15); + width: 260px; + transform: translate(-50%, 0px); + } } diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index f7a960a32..ca3e8607e 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -1,50 +1,57 @@ -import { AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, ViewChild, computed, model } from '@angular/core' +import { AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import hotkeys from 'hotkeys-js' import { NgxLegacyMoveableComponent, OnDrag, OnResize, OnRotate } from 'ngx-moveable' -import createPanZoom, { PanZoom } from 'panzoom' +import createPanZoom from 'panzoom' import { basename, dirname, extname } from 'path' import { ContextMenu } from 'primeng/contextmenu' import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { HistogramComponent } from '../../shared/components/histogram/histogram.component' import { MenuItem } from '../../shared/components/menu-item/menu-item.component' import { SEPARATOR_MENU_ITEM } from '../../shared/constants' +import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { PrimeService } from '../../shared/services/prime.service' -import { Angle, EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' +import { EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' import { - AnnotationInfoDialog, + AstronomicalObjectDialog, DEFAULT_FOV, + DEFAULT_IMAGE_ANNOTATION_DIALOG, + DEFAULT_IMAGE_CALIBRATION, + DEFAULT_IMAGE_DATA, + DEFAULT_IMAGE_FOV_DIALOG, + DEFAULT_IMAGE_LIVE_STACKING, + DEFAULT_IMAGE_MOUSE_COORDINATES, + DEFAULT_IMAGE_MOUSE_POSITION, + DEFAULT_IMAGE_PREFERENCE, + DEFAULT_IMAGE_ROI, + DEFAULT_IMAGE_SAVE_DIALOG, + DEFAULT_IMAGE_SETTINGS_DIALOG, + DEFAULT_IMAGE_SOLVED, + DEFAULT_IMAGE_SOLVER_DIALOG, + DEFAULT_IMAGE_STATISTICS_DIALOG, + DEFAULT_IMAGE_ZOOM, + DEFAULT_STAR_DETECTOR_DIALOG, DetectedStar, - EMPTY_IMAGE_SOLVED, - FITSHeaderItem, FOV, - IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, - ImageAnnotationDialog, - ImageChannel, - ImageData, - ImageFITSHeadersDialog, - ImageFOVDialog, + imageFormatFromExtension, + ImageHeaderItem, + ImageHeadersDialog, ImageInfo, - ImageROI, ImageSCNRDialog, - ImageSaveDialog, ImageSolved, - ImageSolverDialog, - ImageStatisticsBitOption, ImageStretchDialog, - ImageTransformation, LiveStackingMode, OpenImage, - StarDetectionDialog, } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' -import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' +import { PlateSolverRequest } from '../../shared/types/platesolver.types' +import { StarDetectionRequest } from '../../shared/types/stardetector.types' +import { CoordinateInterpolator } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @Component({ @@ -53,191 +60,60 @@ import { AppComponent } from '../app.component' styleUrls: ['./image.component.scss'], }) export class ImageComponent implements AfterViewInit, OnDestroy { - @ViewChild('image') - private readonly image!: ElementRef - - @ViewChild('roi') - private readonly roi!: ElementRef - - @ViewChild('menu') - private readonly menu!: ContextMenu - - @ViewChild('deviceMenu') - private readonly deviceMenu!: DeviceListMenuComponent - - @ViewChild('histogram') - private readonly histogram?: HistogramComponent - - @ViewChild('detectedStarCanvas') - private readonly detectedStarCanvas!: ElementRef - - @ViewChild('moveable') - private readonly moveable!: NgxLegacyMoveableComponent - - imageInfo?: ImageInfo - private imageURL!: string - imageData: ImageData = {} - liveStackingMode: LiveStackingMode = 'NONE' - imageZoom = 1 - - readonly scnrChannels: { name: string; value?: ImageChannel }[] = [ - { name: 'None', value: undefined }, - { name: 'Red', value: 'RED' }, - { name: 'Green', value: 'GREEN' }, - { name: 'Blue', value: 'BLUE' }, - ] - readonly scnr: ImageSCNRDialog = { - showDialog: false, - amount: 0.5, - method: 'AVERAGE_NEUTRAL', - } - - readonly stretch: ImageStretchDialog = { + protected readonly preference = structuredClone(DEFAULT_IMAGE_PREFERENCE) + protected readonly solver = structuredClone(DEFAULT_IMAGE_SOLVER_DIALOG) + protected readonly starDetector = structuredClone(DEFAULT_STAR_DETECTOR_DIALOG) + protected transformation = this.preference.transformation + protected readonly fov = structuredClone(DEFAULT_IMAGE_FOV_DIALOG) + protected readonly annotation = structuredClone(DEFAULT_IMAGE_ANNOTATION_DIALOG) + protected readonly imageROI = structuredClone(DEFAULT_IMAGE_ROI) + protected readonly saveAs = structuredClone(DEFAULT_IMAGE_SAVE_DIALOG) + protected readonly statistics = structuredClone(DEFAULT_IMAGE_STATISTICS_DIALOG) + protected readonly mouseCoordinate = structuredClone(DEFAULT_IMAGE_MOUSE_COORDINATES) + protected readonly liveStacking = structuredClone(DEFAULT_IMAGE_LIVE_STACKING) + protected readonly zoom = structuredClone(DEFAULT_IMAGE_ZOOM) + protected readonly settings = structuredClone(DEFAULT_IMAGE_SETTINGS_DIALOG) + private readonly calibration = structuredClone(DEFAULT_IMAGE_CALIBRATION) + private readonly mouseMountCoordinate = structuredClone(DEFAULT_IMAGE_MOUSE_POSITION) + private readonly imageData = structuredClone(DEFAULT_IMAGE_DATA) + + protected readonly stretch: ImageStretchDialog = { showDialog: false, - auto: true, - shadow: 0, - highlight: 1, - midtone: 0.5, + transformation: this.transformation.stretch, } - readonly stretchShadow = model(0) - readonly stretchHighlight = model(65536) - readonly stretchMidtone = model(32768) - readonly stretchShadowAndHighlight = computed(() => [this.stretchShadow(), this.stretchHighlight()]) - - readonly transformation: ImageTransformation = { - force: false, - debayer: true, - stretch: this.stretch, - mirrorHorizontal: false, - mirrorVertical: false, - invert: false, - scnr: this.scnr, - } - - calibrationViaCamera = true - - readonly annotation: ImageAnnotationDialog = { + protected readonly scnr: ImageSCNRDialog = { showDialog: false, - running: false, - visible: false, - useStarsAndDSOs: true, - useMinorPlanets: false, - minorPlanetsMagLimit: 18.0, - includeMinorPlanetsWithoutMagnitude: true, - useSimbad: false, - data: [], + transformation: this.transformation.scnr, } - readonly annotationInfo: AnnotationInfoDialog = { + protected readonly astronomicalObject: AstronomicalObjectDialog = { showDialog: false, } - readonly starDetection: StarDetectionDialog = { - showDialog: false, - running: false, - type: 'ASTAP', - minSNR: 0, - maxStars: 0, - visible: false, - stars: [], - computed: { - hfd: 0, - snr: 0, - stdDev: 0, - fluxMax: 0, - fluxMin: 0, - }, - selected: { - x: 0, - y: 0, - snr: 0, - hfd: 0, - flux: 0, - }, - } - - readonly solver: ImageSolverDialog = { - showDialog: false, - running: false, - type: 'ASTAP', - blind: true, - centerRA: '', - centerDEC: '', - radius: 4, - focalLength: 0, - pixelSize: 0, - solved: structuredClone(EMPTY_IMAGE_SOLVED), - } - - crossHair = false - - readonly fitsHeaders: ImageFITSHeadersDialog = { + protected readonly headers: ImageHeadersDialog = { showDialog: false, headers: [], } - showStatisticsDialog = false - - readonly statisticsBitOptions: ImageStatisticsBitOption[] = IMAGE_STATISTICS_BIT_OPTIONS - statisticsBitLength = this.statisticsBitOptions[0] - - readonly fov: ImageFOVDialog = { - ...structuredClone(DEFAULT_FOV), - showDialog: false, - fovs: [], - showCameraDialog: false, - cameras: [], - showTelescopeDialog: false, - telescopes: [], - } - - get canAddFOV() { - return this.fov.aperture && this.fov.focalLength && this.fov.cameraSize.width && this.fov.cameraSize.height && this.fov.pixelSize.width && this.fov.pixelSize.height && this.fov.bin - } - - private panZoom?: PanZoom - private imageMouseX = 0 - private imageMouseY = 0 - - readonly imageROI: ImageROI = { - show: false, - x: 0, - y: 0, - width: 128, - height: 128, - } - - readonly saveAs: ImageSaveDialog = { - showDialog: false, - format: 'FITS', - bitpix: 'BYTE', - path: '', - shouldBeTransformed: true, - transformation: this.transformation, - } + protected imageInfo?: ImageInfo private readonly saveAsMenuItem: MenuItem = { label: 'Save as...', icon: 'mdi mdi-content-save', command: async () => { - const preference = this.preference.imagePreference.get() - - const path = await this.electron.saveImage({ defaultPath: preference.savePath }) + const path = await this.electronService.saveImage({ defaultPath: this.preference.savePath }) if (path) { const extension = extname(path).toLowerCase() - this.saveAs.format = - extension === '.xisf' ? 'XISF' - : extension === '.png' ? 'PNG' - : extension === '.jpg' ? 'JPG' - : 'FITS' + this.saveAs.format = imageFormatFromExtension(extension) this.saveAs.bitpix = this.imageInfo?.bitpix ?? 'BYTE' this.saveAs.path = path - this.saveAs.showDialog = true - preference.savePath = dirname(path) - this.preference.imagePreference.set(preference) + this.preference.savePath = dirname(path) + this.savePreference() + + this.saveAs.showDialog = true } }, } @@ -283,7 +159,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { command: () => { this.transformation.mirrorHorizontal = !this.transformation.mirrorHorizontal this.horizontalMirrorMenuItem.selected = this.transformation.mirrorHorizontal - void this.loadImage() + this.savePreference() + return this.loadImage() }, } @@ -294,7 +171,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { command: () => { this.transformation.mirrorVertical = !this.transformation.mirrorVertical this.verticalMirrorMenuItem.selected = this.transformation.mirrorVertical - void this.loadImage() + this.savePreference() + return this.loadImage() }, } @@ -317,7 +195,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-chart-histogram', label: 'Statistics', command: () => { - this.showStatisticsDialog = true + this.statistics.showDialog = true return this.computeHistogram() }, } @@ -326,7 +204,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-list-box', label: 'FITS Header', command: () => { - this.fitsHeaders.showDialog = true + this.headers.showDialog = true }, } @@ -339,7 +217,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { if (path) { void this.executeMount((mount) => { - return this.api.pointMountHere(mount, path, this.imageMouseX, this.imageMouseY) + return this.api.pointMountHere(mount, path, this.mouseMountCoordinate) }) } }, @@ -350,7 +228,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-image', disabled: true, command: () => { - const coordinate = this.mouseCoordinateInterpolation?.interpolate(this.imageMouseX, this.imageMouseY, false, false) + const coordinate = this.mouseCoordinate.interpolator?.interpolate(this.mouseMountCoordinate.x, this.mouseMountCoordinate.y, false, false) if (coordinate) { void this.frame(coordinate) @@ -377,7 +255,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.annotation.showDialog = true }, check: (event) => { - event.originalEvent?.stopImmediatePropagation() this.annotation.visible = !!event.checked }, } @@ -389,11 +266,10 @@ export class ImageComponent implements AfterViewInit, OnDestroy { checkable: false, selected: false, command: () => { - this.starDetection.showDialog = true + this.starDetector.showDialog = true }, check: (event) => { - this.starDetection.visible = !!event.checked - event.originalEvent?.stopImmediatePropagation() + this.starDetector.visible = !!event.checked }, } @@ -425,7 +301,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { items: [this.crosshairMenuItem, this.annotationMenuItem, this.detectStarsMenuItem, this.roiMenuItem, this.fovMenuItem], } - readonly contextMenuItems = [ + protected readonly contextMenuModel = [ this.saveAsMenuItem, SEPARATOR_MENU_ITEM, this.plateSolveMenuItem, @@ -446,25 +322,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.frameAtThisCoordinateMenuItem, ] - mouseCoordinate?: InterpolatedCoordinate & Partial<{ x: number; y: number }> - private mouseCoordinateInterpolation?: CoordinateInterpolator - - get isMouseCoordinateVisible() { - return !!this.mouseCoordinate && !this.transformation.mirrorHorizontal && !this.transformation.mirrorVertical - } - - get imagePath() { - if (this.liveStackingMode === 'NONE' || this.liveStackingMode === 'RAW' || !this.imageData.liveStackedPath) { - return this.imageData.path - } else { - return this.imageData.liveStackedPath - } - } - - get canPlateSolve() { - return (this.solver.type !== 'SIRIL' && this.solver.type !== 'PIXINSIGHT') || (this.solver.focalLength > 0 && this.solver.pixelSize > 0) - } - private readonly liveStackingMenuItem: MenuItem = { label: 'RAW', icon: 'mdi mdi-image-multiple', @@ -486,14 +343,56 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ], } + @ViewChild('image') + private readonly image!: ElementRef + + @ViewChild('roi') + private readonly roi!: ElementRef + + @ViewChild('menu') + private readonly menu!: ContextMenu + + @ViewChild('deviceMenu') + private readonly deviceMenu!: DeviceListMenuComponent + + @ViewChild('histogram') + private readonly histogram?: HistogramComponent + + @ViewChild('detectedStarCanvas') + private readonly detectedStarCanvas!: ElementRef + + @ViewChild('moveable') + private readonly moveable!: NgxLegacyMoveableComponent + + get isMouseCoordinateVisible() { + return this.mouseCoordinate.show && !!this.mouseCoordinate.interpolator && !this.transformation.mirrorHorizontal && !this.transformation.mirrorVertical + } + + get imagePath() { + if (this.liveStacking.mode === 'NONE' || this.liveStacking.mode === 'RAW' || !this.liveStacking.path) { + return this.imageData.path + } else { + return this.liveStacking.path + } + } + + get canPlateSolve() { + return (this.solver.request.type !== 'SIRIL' && this.solver.request.type !== 'PIXINSIGHT') || (this.solver.request.focalLength > 0 && this.solver.request.pixelSize > 0) + } + + get canAddFOV() { + const fov = this.fov.selected + return fov.aperture && fov.focalLength && fov.cameraSize.width && fov.cameraSize.height && fov.pixelSize.width && fov.pixelSize.height && fov.bin + } + constructor( private readonly app: AppComponent, private readonly route: ActivatedRoute, private readonly api: ApiService, - private readonly electron: ElectronService, - private readonly browserWindow: BrowserWindowService, - private readonly preference: PreferenceService, - private readonly prime: PrimeService, + private readonly electronService: ElectronService, + private readonly browserWindowService: BrowserWindowService, + private readonly preferenceService: PreferenceService, + private readonly angularService: AngularService, ngZone: NgZone, ) { app.title = 'Image' @@ -538,22 +437,18 @@ export class ImageComponent implements AfterViewInit, OnDestroy { }, }) - this.stretchShadow.subscribe((value) => { - this.stretch.shadow = value / 65536 - }) - - this.stretchHighlight.subscribe((value) => { - this.stretch.highlight = value / 65536 - }) - - this.stretchMidtone.subscribe((value) => { - this.stretch.midtone = value / 65536 + app.topMenu.push({ + icon: 'mdi mdi-cog', + label: 'Settings', + command: () => { + this.settings.showDialog = true + }, }) - electron.on('CAMERA.CAPTURE_ELAPSED', async (event) => { + electronService.on('CAMERA.CAPTURE_ELAPSED', async (event) => { if (event.state === 'EXPOSURE_FINISHED' && event.camera.id === this.imageData.camera?.id) { await ngZone.run(async () => { - if (this.liveStackingMode === 'NONE') { + if (this.liveStacking.mode === 'NONE') { if (event.liveStackedPath) { await this.changeLiveStackingMode('STACKED') } @@ -562,9 +457,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } this.imageData.path = event.savedPath - this.imageData.liveStackedPath = event.liveStackedPath this.imageData.capture = event.capture this.imageData.exposureCount = event.exposureCount + this.liveStacking.path = event.liveStackedPath this.clearOverlay() @@ -573,13 +468,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } }) - electron.on('DATA.CHANGED', (event: OpenImage) => { + electronService.on('DATA.CHANGED', (event: OpenImage) => { return ngZone.run(() => { return this.loadImageFromOpenImage(event) }) }) - electron.on('CALIBRATION.CHANGED', async () => { + electronService.on('CALIBRATION.CHANGED', async () => { return ngZone.run(() => { return this.loadCalibrationGroups() }) @@ -640,19 +535,21 @@ export class ImageComponent implements AfterViewInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { - void this.closeImage(true) + this.zoom.panZoom?.dispose() + void this.closeImage() } - private markCalibrationGroupItem(name?: string) { + private markCalibrationGroupItem(name: string | undefined = this.transformation.calibrationGroup) { const items = this.calibrationMenuItem.items + const calibrationViaCamera = this.calibration.source === 'CAMERA' if (items) { items[2].disabled = !this.imageInfo?.camera?.id - items[2].selected = this.calibrationViaCamera + items[2].selected = calibrationViaCamera for (let i = 3; i < items.length; i++) { const item = items[i] - item.selected = !this.calibrationViaCamera && item.data === name + item.selected = !calibrationViaCamera && item.data === name } } } @@ -665,7 +562,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { if (!found) { reloadImage = !!this.transformation.calibrationGroup this.transformation.calibrationGroup = undefined - this.calibrationViaCamera = true + this.savePreference() + this.calibration.source = 'CAMERA' } const makeItem = (name?: string) => { @@ -675,11 +573,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { return { label, icon, - selected: !this.calibrationViaCamera && this.transformation.calibrationGroup === name, + selected: this.calibration.source === 'MENU' && this.transformation.calibrationGroup === name, data: name, command: () => { - this.calibrationViaCamera = false + this.calibration.source = 'MENU' this.transformation.calibrationGroup = name + this.savePreference() this.markCalibrationGroupItem(name) void this.loadImage() }, @@ -692,7 +591,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { label: 'Open', icon: 'mdi mdi-wrench', command: () => { - return this.browserWindow.openCalibration() + return this.browserWindowService.openCalibration() }, }) @@ -701,13 +600,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy { menu.push({ label: 'Camera', icon: 'mdi mdi-camera-iris', - selected: this.calibrationViaCamera, + selected: this.calibration.source === 'CAMERA', disabled: !this.imageInfo?.camera?.id, data: 0, command: () => { if (this.imageInfo?.camera?.id) { - this.calibrationViaCamera = !this.calibrationViaCamera - this.markCalibrationGroupItem(this.transformation.calibrationGroup) + this.calibration.source = this.calibration.source === 'CAMERA' ? 'MENU' : 'CAMERA' + this.markCalibrationGroupItem() void this.loadImage() } }, @@ -720,7 +619,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } this.calibrationMenuItem.items = menu - this.menu.model = this.contextMenuItems + this.menu.model = this.contextMenuModel this.menu.cd.markForCheck() if (reloadImage) { @@ -728,29 +627,28 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - private async closeImage(force: boolean = false) { - if (this.imageData.path && force) { - await this.api.closeImage(this.imageData.path) - } - if (this.imageData.liveStackedPath && force) { - await this.api.closeImage(this.imageData.liveStackedPath) + private async closeImage() { + const path = this.imagePath + + if (path) { + await this.api.closeImage(path) } } - private async changeLiveStackingMode(mode: LiveStackingMode) { - this.liveStackingMode = mode + private changeLiveStackingMode(mode: LiveStackingMode) { + this.liveStacking.mode = mode - if (this.liveStackingMode !== 'NONE') { + if (this.liveStacking.mode !== 'NONE') { this.disableCalibration(true) } - this.liveStackingMenuItem.visible = this.liveStackingMode !== 'NONE' + this.liveStackingMenuItem.visible = this.liveStacking.mode !== 'NONE' this.liveStackingMenuItem.label = mode - await this.loadImage(true) + return this.loadImage(true) } - roiDrag(event: OnDrag) { + protected roiDrag(event: OnDrag) { const { target, transform } = event target.style.transform = transform @@ -759,7 +657,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.imageROI.y = Math.trunc(rect.top) } - roiResize(event: OnResize) { + protected roiResize(event: OnResize) { const { target, width, height, transform } = event target.style.transform = transform @@ -774,19 +672,19 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.imageROI.height = Math.trunc(height) } - roiRotate(event: OnRotate) { + protected roiRotate(event: OnRotate) { const { target, transform } = event target.style.transform = transform } - roiForCamera() { + protected roiForCamera() { return this.executeCamera((camera) => { const x = Math.max(0, Math.min(camera.x + this.imageROI.x, camera.maxX)) const y = Math.max(0, Math.min(camera.y + this.imageROI.y, camera.maxY)) const width = Math.max(0, Math.min(camera.binX * this.imageROI.width, camera.maxWidth)) const height = Math.max(0, Math.min(camera.binY * this.imageROI.height, camera.maxHeight)) - return this.electron.send('ROI.SELECTED', { camera, x, y, width, height }) + return this.electronService.send('ROI.SELECTED', { camera, x, y, width, height }) }, false) } @@ -794,9 +692,10 @@ export class ImageComponent implements AfterViewInit, OnDestroy { Object.assign(this.imageData, data) // Not clicked on menu item. - if (this.calibrationViaCamera && this.transformation.calibrationGroup !== data.capture?.calibrationGroup) { + if (this.calibration.source === 'CAMERA' && this.transformation.calibrationGroup !== data.capture?.calibrationGroup) { this.transformation.calibrationGroup = data.capture?.calibrationGroup - this.markCalibrationGroupItem(this.transformation.calibrationGroup) + this.savePreference() + this.markCalibrationGroupItem() } if (data.source === 'FRAMING') { @@ -820,41 +719,38 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.annotation.visible = false this.annotationMenuItem.checkable = false - this.starDetection.stars = [] - this.starDetection.visible = false + this.starDetector.stars = [] + this.starDetector.visible = false this.detectStarsMenuItem.checkable = false - Object.assign(this.solver.solved, EMPTY_IMAGE_SOLVED) + Object.assign(this.solver.solved, DEFAULT_IMAGE_SOLVED) this.histogram?.update([]) } - private async computeHistogram() { + protected async computeHistogram() { const path = this.imagePath if (path) { - const data = await this.api.imageHistogram(path, this.statisticsBitLength.bitLength) + const data = await this.api.imageHistogram(path, this.statistics.bitOption.bitLength) this.histogram?.update(data) } } - statisticsBitLengthChanged() { - return this.computeHistogram() - } - - async detectStars() { + protected async detectStars() { const path = this.imagePath if (path) { - const options = this.preference.starDetectionRequest(this.starDetection.type).get() - options.minSNR = this.starDetection.minSNR - options.maxStars = this.starDetection.maxStars + const request: StarDetectionRequest = { + ...this.starDetector.request, + ...this.preferenceService.settings.get().starDetector[this.starDetector.request.type], + } try { - this.starDetection.running = true - this.starDetection.stars = await this.api.detectStars(path, options) + this.starDetector.running = true + this.starDetector.stars = await this.api.detectStars(path, request) } finally { - this.starDetection.running = false + this.starDetector.running = false } let hfd = 0 @@ -863,12 +759,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { let fluxMin = 0 let fluxMax = 0 - const starCount = this.starDetection.stars.length + const starCount = this.starDetector.stars.length if (starCount) { - fluxMax = this.starDetection.stars[0].flux + fluxMax = this.starDetector.stars[0].flux - for (const star of this.starDetection.stars) { + for (const star of this.starDetector.stars) { hfd += star.hfd snr += star.snr fluxMax = Math.min(fluxMax, star.flux) @@ -880,29 +776,29 @@ export class ImageComponent implements AfterViewInit, OnDestroy { let squared = 0 - for (const star of this.starDetection.stars) { + for (const star of this.starDetector.stars) { squared += Math.pow(star.hfd - hfd, 2) } stdDev = Math.sqrt(squared / starCount) } - this.starDetection.computed.hfd = hfd - this.starDetection.computed.stdDev = stdDev - this.starDetection.computed.snr = snr - this.starDetection.computed.fluxMax = fluxMin - this.starDetection.computed.fluxMin = fluxMax + this.starDetector.computed.hfd = hfd + this.starDetector.computed.stdDev = stdDev + this.starDetector.computed.snr = snr + this.starDetector.computed.fluxMax = fluxMin + this.starDetector.computed.fluxMin = fluxMax this.savePreference() - this.starDetection.visible = this.starDetection.stars.length > 0 - this.detectStarsMenuItem.checkable = this.starDetection.visible - this.detectStarsMenuItem.checked = this.starDetection.visible + this.starDetector.visible = this.starDetector.stars.length > 0 + this.detectStarsMenuItem.checkable = this.starDetector.visible + this.detectStarsMenuItem.checked = this.starDetector.visible } } - selectDetectedStar(star: DetectedStar) { - Object.assign(this.starDetection.selected, star) + protected drawDetectedStar(star: DetectedStar) { + Object.assign(this.starDetector.selected, star) const canvas = this.detectedStarCanvas.nativeElement const ctx = canvas.getContext('2d') @@ -910,7 +806,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async loadImage(force: boolean = false) { - await this.closeImage(force) + if (force) { + await this.closeImage() + } const path = this.imagePath @@ -941,7 +839,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { const image = this.image.nativeElement const transformation = structuredClone(this.transformation) - if (this.calibrationViaCamera && this.liveStackingMode !== 'NONE') transformation.calibrationGroup = this.imageData.capture?.calibrationGroup + if (this.calibration.source === 'CAMERA' && this.liveStacking.mode !== 'NONE') transformation.calibrationGroup = this.imageData.capture?.calibrationGroup const { info, blob } = await this.api.openImage(path, transformation, this.imageData.camera) if (!blob || !info) return @@ -949,29 +847,25 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.imageInfo = info this.scnrMenuItem.disabled = info.mono - if (info.rightAscension) this.solver.centerRA = info.rightAscension - if (info.declination) this.solver.centerDEC = info.declination - this.solver.blind = !this.solver.centerRA || !this.solver.centerDEC + if (info.rightAscension) this.solver.request.centerRA = info.rightAscension + if (info.declination) this.solver.request.centerDEC = info.declination + this.solver.request.blind = !this.solver.request.centerRA || !this.solver.request.centerDEC - if (this.stretch.auto) { - this.stretchShadow.set(Math.trunc(info.stretchShadow * 65536)) - this.stretchHighlight.set(Math.trunc(info.stretchHighlight * 65536)) - this.stretchMidtone.set(Math.trunc(info.stretchMidtone * 65536)) + if (this.stretch.transformation.auto) { + Object.assign(this.stretch.transformation, info.stretch) } this.updateImageSolved(info.solved) - this.fitsHeaders.headers = info.headers + this.headers.headers = info.headers this.retrieveInfoFromImageHeaders(info.headers) - if (this.imageURL) window.URL.revokeObjectURL(this.imageURL) - this.imageURL = window.URL.createObjectURL(blob) - image.src = this.imageURL + image.src = URL.createObjectURL(blob) if (!info.camera?.id) { - this.calibrationViaCamera = false - this.markCalibrationGroupItem(this.transformation.calibrationGroup) + this.calibration.source = 'MENU' + this.markCalibrationGroupItem() } else if (this.calibrationMenuItem.items) { this.calibrationMenuItem.items[2].disabled = false } @@ -979,46 +873,38 @@ export class ImageComponent implements AfterViewInit, OnDestroy { return this.retrieveCoordinateInterpolation() } - private retrieveInfoFromImageHeaders(headers: FITSHeaderItem[]) { - const imagePreference = this.preference.imagePreference.get() - + private retrieveInfoFromImageHeaders(headers: ImageHeaderItem[]) { for (const item of headers) { if (item.name === 'FOCALLEN') { - this.solver.focalLength = parseFloat(item.value) + this.solver.request.focalLength = parseFloat(item.value) } else if (item.name === 'XPIXSZ') { - this.solver.pixelSize = parseFloat(item.value) + this.solver.request.pixelSize = parseFloat(item.value) } } - - this.solver.focalLength ||= imagePreference.solver?.focalLength ?? 0 - this.solver.pixelSize ||= imagePreference.solver?.pixelSize ?? 0 } - imageClicked(event: MouseEvent, contextMenu: boolean) { - this.imageMouseX = event.offsetX - this.imageMouseY = event.offsetY + protected imageClicked(event: MouseEvent, contextMenu: boolean) { + this.mouseMountCoordinate.x = event.offsetX + this.mouseMountCoordinate.y = event.offsetY if (contextMenu) { this.menu.show(event) } } - imageMouseMoved(event: MouseEvent) { + protected imageMouseMoved(event: MouseEvent) { this.imageMouseMovedWithCoordinates(event.offsetX, event.offsetY) } - imageMouseMovedWithCoordinates(x: number, y: number) { - if (!this.menu.visible()) { - this.mouseCoordinate = this.mouseCoordinateInterpolation?.interpolateAsText(x, y, true, true, false) - - if (this.mouseCoordinate) { - this.mouseCoordinate.x = x - this.mouseCoordinate.y = y - } + private imageMouseMovedWithCoordinates(x: number, y: number) { + if (!this.menu.visible() && this.mouseCoordinate.interpolator) { + Object.assign(this.mouseCoordinate, this.mouseCoordinate.interpolator.interpolateAsText(x, y, true, true, false)) + this.mouseCoordinate.x = x + this.mouseCoordinate.y = y } } - async saveImageAs() { + protected async saveImageAs() { const path = this.imagePath if (path) { @@ -1027,13 +913,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - async annotateImage() { + protected async annotateImage() { const path = this.imagePath if (path) { try { this.annotation.running = true - this.annotation.data = await this.api.annotationsOfImage(path, this.annotation.useStarsAndDSOs, this.annotation.useMinorPlanets, this.annotation.minorPlanetsMagLimit, this.annotation.includeMinorPlanetsWithoutMagnitude, this.annotation.useSimbad) + this.annotation.data = await this.api.annotationsOfImage(path, this.annotation.request) this.annotation.visible = this.annotation.data.length > 0 this.annotationMenuItem.checkable = this.annotation.visible this.annotationMenuItem.checked = this.annotation.visible @@ -1044,94 +930,101 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - showAnnotationInfo(annotation: ImageAnnotation) { - this.annotationInfo.info = annotation.star ?? annotation.dso ?? annotation.minorPlanet - this.annotationInfo.showDialog = true + protected showAnnotationInfo(annotation: ImageAnnotation) { + this.astronomicalObject.info = annotation.star ?? annotation.dso ?? annotation.minorPlanet + this.astronomicalObject.showDialog = true } private disableAutoStretch() { - this.stretch.auto = false + this.stretch.transformation.auto = false + this.savePreference() this.autoStretchMenuItem.selected = false } private disableCalibration(canEnable: boolean = true) { this.transformation.calibrationGroup = undefined + this.savePreference() this.markCalibrationGroupItem(undefined) this.calibrationMenuItem.disabled = !canEnable } - autoStretch() { - this.stretch.auto = true + protected autoStretch() { + this.stretch.transformation.auto = true + this.savePreference() this.autoStretchMenuItem.selected = true return this.loadImage() } - async resetStretch(load: boolean = true) { - this.stretchShadow.set(0) - this.stretchHighlight.set(65536) - this.stretchMidtone.set(32768) + protected async resetStretch(load: boolean = true) { + this.stretch.transformation.shadow = 0 + this.stretch.transformation.highlight = 65536 + this.stretch.transformation.midtone = 32768 + this.savePreference() if (load) { await this.stretchImage() } } - async toggleStretch() { - this.stretch.auto = !this.stretch.auto - this.autoStretchMenuItem.selected = this.stretch.auto + private async toggleStretch() { + this.stretch.transformation.auto = !this.stretch.transformation.auto + this.savePreference() + this.autoStretchMenuItem.selected = this.stretch.transformation.auto - if (!this.stretch.auto) { - await this.resetStretch() + if (this.stretch.transformation.auto) { + return this.loadImage() } else { - await this.loadImage() + return this.resetStretch() } } - stretchImage() { + protected stretchImage() { this.disableAutoStretch() return this.loadImage() } - invertImage() { + private invertImage() { this.transformation.invert = !this.transformation.invert this.invertMenuItem.selected = this.transformation.invert + this.savePreference() return this.loadImage() } - scnrImage() { + protected scnrImage() { return this.loadImage() } - toggleCrosshair() { - this.crossHair = !this.crossHair - this.crosshairMenuItem.selected = this.crossHair + private toggleCrosshair() { + this.preference.crossHair = !this.preference.crossHair + this.savePreference() + this.crosshairMenuItem.selected = this.preference.crossHair } - zoomIn() { - if (!this.panZoom) return - const { scale } = this.panZoom.getTransform() - this.panZoom.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, scale * 1.1) + private zoomIn() { + if (!this.zoom.panZoom) return + const { scale } = this.zoom.panZoom.getTransform() + this.zoom.panZoom.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, scale * 1.1) } - zoomOut() { - if (!this.panZoom) return - const { scale } = this.panZoom.getTransform() - this.panZoom.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, scale * 0.9) + private zoomOut() { + if (!this.zoom.panZoom) return + const { scale } = this.zoom.panZoom.getTransform() + this.zoom.panZoom.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, scale * 0.9) } - center() { + private center() { const { width, height } = this.image.nativeElement.getBoundingClientRect() - this.panZoom?.moveTo(window.innerWidth / 2 - width / 2, (window.innerHeight - 42) / 2 - height / 2) + this.zoom.panZoom?.moveTo(window.innerWidth / 2 - width / 2, (window.innerHeight - 42) / 2 - height / 2) } - resetZoom(fitToScreen: boolean = false, center: boolean = true) { + private resetZoom(fitToScreen: boolean = false, center: boolean = true) { if (fitToScreen) { const { width, height } = this.image.nativeElement const factor = Math.min(window.innerWidth, window.innerHeight - 42) / Math.min(width, height) - this.panZoom?.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, factor) + this.zoom.panZoom?.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, factor) } else { - this.panZoom?.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, 1.0) + this.zoom.panZoom?.smoothZoomAbs(window.innerWidth / 2, window.innerHeight / 2, 1.0) } if (center) { @@ -1139,46 +1032,49 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - async enterFullscreen() { - this.app.showTopBar = !(await this.electron.fullscreenWindow(true)) + private async enterFullscreen() { + this.app.showTopBar = !(await this.electronService.fullscreenWindow(true)) } - async exitFullscreen() { - this.app.showTopBar = !(await this.electron.fullscreenWindow(false)) + private async exitFullscreen() { + this.app.showTopBar = !(await this.electronService.fullscreenWindow(false)) } private async retrieveCoordinateInterpolation() { const path = this.imagePath if (path) { - const coordinate = await this.api.coordinateInterpolation(this.imagePath) + const coordinate = await this.api.coordinateInterpolation(path) if (coordinate && this.imageInfo) { const { ma, md, x0, y0, x1, y1, delta } = coordinate - const x = Math.max(0, Math.min(this.mouseCoordinate?.x ?? 0, this.imageInfo.width)) - const y = Math.max(0, Math.min(this.mouseCoordinate?.y ?? 0, this.imageInfo.height)) - this.mouseCoordinateInterpolation = new CoordinateInterpolator(ma, md, x0, y0, x1, y1, delta) + const x = Math.max(0, Math.min(this.mouseCoordinate.x, this.imageInfo.width)) + const y = Math.max(0, Math.min(this.mouseCoordinate.y, this.imageInfo.height)) + this.mouseCoordinate.interpolator = new CoordinateInterpolator(ma, md, x0, y0, x1, y1, delta) + this.mouseCoordinate.show = true this.imageMouseMovedWithCoordinates(x, y) - } else { - this.mouseCoordinateInterpolation = undefined - this.mouseCoordinate = undefined + return } } + + this.mouseCoordinate.interpolator = undefined + this.mouseCoordinate.show = false } - async solverStart() { + protected async solverStart() { const path = this.imagePath if (path) { this.solver.running = true try { - const solver = this.preference.plateSolverRequest(this.solver.type).get() - solver.pixelSize = this.solver.pixelSize - solver.focalLength = this.solver.focalLength - const solved = await this.api.solverStart(solver, path, this.solver.blind, this.solver.centerRA, this.solver.centerDEC, this.solver.radius) + const request: PlateSolverRequest = { + ...this.solver.request, + ...this.preferenceService.settings.get().plateSolver[this.solver.request.type], + } + + const solved = await this.api.solverStart(request, path) - this.savePreference() this.updateImageSolved(solved) } catch { this.updateImageSolved(this.imageInfo?.solved) @@ -1192,12 +1088,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - solverStop() { + protected solverStop() { return this.api.solverStop() } private updateImageSolved(solved?: ImageSolved) { - Object.assign(this.solver.solved, solved ?? EMPTY_IMAGE_SOLVED) + Object.assign(this.solver.solved, solved ?? DEFAULT_IMAGE_SOLVED) this.annotationMenuItem.disabled = !this.solver.solved.solved this.fovMenuItem.disabled = !this.solver.solved.solved this.pointMountHereMenuItem.disabled = !this.solver.solved.solved @@ -1207,27 +1103,27 @@ export class ImageComponent implements AfterViewInit, OnDestroy { else this.fov.fovs.forEach((e) => (e.computed = undefined)) } - mountSync(coordinate: EquatorialCoordinateJ2000) { + protected mountSync(coordinate: EquatorialCoordinateJ2000) { return this.executeMount((mount) => { return this.api.mountSync(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) }) } - mountGoTo(coordinate: EquatorialCoordinateJ2000) { + protected mountGoTo(coordinate: EquatorialCoordinateJ2000) { return this.executeMount((mount) => { return this.api.mountGoTo(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) }) } - mountSlew(coordinate: EquatorialCoordinateJ2000) { + protected mountSlew(coordinate: EquatorialCoordinateJ2000) { return this.executeMount((mount) => { return this.api.mountSlew(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) }) } - async frame(coordinate: EquatorialCoordinateJ2000) { + protected async frame(coordinate: EquatorialCoordinateJ2000) { if (this.solver.solved.solved) { - await this.browserWindow.openFraming({ + await this.browserWindowService.openFraming({ rightAscension: coordinate.rightAscensionJ2000, declination: coordinate.declinationJ2000, fov: this.solver.solved.width / 60, @@ -1236,11 +1132,15 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - imageLoaded() { - const imageWrapperElement = this.image.nativeElement.parentElement + protected imageLoaded() { + const image = this.image.nativeElement + const imageWrapper = image.parentElement + + URL.revokeObjectURL(image.src) + console.log(image.src) - if (!this.panZoom && imageWrapperElement) { - this.panZoom = createPanZoom(imageWrapperElement, { + if (!this.zoom.panZoom && imageWrapper) { + const panZoom = createPanZoom(imageWrapper, { minZoom: 0.1, maxZoom: 500.0, autocenter: true, @@ -1249,23 +1149,24 @@ export class ImageComponent implements AfterViewInit, OnDestroy { filterKey: () => { return true }, - beforeWheel: () => { - return false // e.target !== this.image.nativeElement && e.target !== this.roi.nativeElement + beforeWheel: (e) => { + return e.target !== this.image.nativeElement && e.target !== this.roi.nativeElement && (e.target as HTMLElement).tagName !== 'circle' }, beforeMouseDown: (e) => { - // return e.target !== this.image.nativeElement - return e.target === this.roi.nativeElement + return e.target !== this.image.nativeElement && (e.target as HTMLElement).tagName !== 'circle' }, }) - this.panZoom.on('zoom', () => { - const { scale } = this.panZoom!.getTransform() - this.imageZoom = scale + panZoom.on('transform', () => { + const { scale } = panZoom.getTransform() + this.zoom.scale = scale }) + + this.zoom.panZoom = panZoom } } - async showFOVCameras() { + protected async showFOVCameraDialog() { if (!this.fov.cameras.length) { this.fov.cameras = await this.api.fovCameras() } @@ -1274,7 +1175,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.fov.showCameraDialog = true } - async showFOVTelescopes() { + protected async showFOVTelescopeDialog() { if (!this.fov.telescopes.length) { this.fov.telescopes = await this.api.fovTelescopes() } @@ -1283,47 +1184,49 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.fov.showTelescopeDialog = true } - chooseCamera() { + protected chooseCamera() { if (this.fov.camera) { - this.fov.cameraSize.width = this.fov.camera.width - this.fov.cameraSize.height = this.fov.camera.height - this.fov.pixelSize.width = this.fov.camera.pixelSize - this.fov.pixelSize.height = this.fov.camera.pixelSize + this.fov.selected.cameraSize.width = this.fov.camera.width + this.fov.selected.cameraSize.height = this.fov.camera.height + this.fov.selected.pixelSize.width = this.fov.camera.pixelSize + this.fov.selected.pixelSize.height = this.fov.camera.pixelSize this.fov.camera = undefined this.fov.showCameraDialog = false } } - chooseTelescope() { + protected chooseTelescope() { if (this.fov.telescope) { - this.fov.aperture = this.fov.telescope.aperture - this.fov.focalLength = this.fov.telescope.focalLength + this.fov.selected.aperture = this.fov.telescope.aperture + this.fov.selected.focalLength = this.fov.telescope.focalLength this.fov.telescope = undefined this.fov.showTelescopeDialog = false } } - addFOV() { - if (this.computeFOV(this.fov)) { - this.fov.fovs.push(structuredClone(this.fov)) - this.preference.imageFOVs.set(this.fov.fovs) + protected addFOV() { + if (this.computeFOV(this.fov.selected)) { + this.fov.fovs.push(structuredClone(this.fov.selected)) + this.savePreference() } } - editFOV(fov: FOV) { - Object.assign(this.fov, structuredClone(fov)) - this.fov.edited = fov + private removeSelectedFOV() { + this.fov.selected = structuredClone(DEFAULT_FOV) } - cancelEditFOV() { - this.fov.edited = undefined + protected selectFOV(fov: FOV) { + if (this.fov.selected === fov) { + this.removeSelectedFOV() + } else { + this.fov.selected = fov + } } - saveFOV() { - if (this.fov.edited && this.computeFOV(this.fov)) { - Object.assign(this.fov.edited, structuredClone(this.fov)) - this.preference.imageFOVs.set(this.fov.fovs) - this.fov.edited = undefined + protected saveFOV(compute: boolean = true) { + // Edited. + if (this.fov.fovs.includes(this.fov.selected) && (!compute || this.computeFOV(this.fov.selected))) { + this.savePreference() } } @@ -1365,56 +1268,44 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - deleteFOV(fov: FOV) { + protected deleteFOV(fov: FOV) { const index = this.fov.fovs.indexOf(fov) if (index >= 0) { - if (this.fov.fovs[index] === this.fov.edited) { - this.fov.edited = undefined - } - this.fov.fovs.splice(index, 1) - this.preference.imageFOVs.set(this.fov.fovs) + this.savePreference() + + if (this.fov.selected === this.fov.fovs[index]) { + this.removeSelectedFOV() + } } } private loadPreference() { - const preference = this.preference.imagePreference.get() - this.solver.radius = preference.solver?.radius ?? this.solver.radius - this.solver.type = preference.solver?.type ?? 'ASTAP' - this.solver.focalLength = preference.solver?.focalLength ?? 0 - this.solver.pixelSize = preference.solver?.pixelSize ?? 0 - this.starDetection.type = preference.starDetection?.type ?? this.starDetection.type - this.starDetection.minSNR = preference.starDetection?.minSNR ?? this.preference.starDetectionRequest(this.starDetection.type).get().minSNR ?? this.starDetection.minSNR - this.starDetection.maxStars = preference.starDetection?.maxStars ?? this.preference.starDetectionRequest(this.starDetection.type).get().maxStars ?? this.starDetection.maxStars - - this.fov.fovs = this.preference.imageFOVs.get() - this.fov.fovs.forEach((e) => { - e.enabled = false - e.computed = undefined - }) + Object.assign(this.preference, this.preferenceService.imagePreference.get()) + this.solver.request = this.preference.solver + this.starDetector.request = this.preference.starDetector + this.settings.preference = this.preference + this.transformation = this.preference.transformation + this.saveAs.transformation = this.transformation + this.stretch.transformation = this.transformation.stretch + this.scnr.transformation = this.transformation.scnr + this.annotation.request = this.preference.annotation + this.fov.fovs = this.preference.fovs + + this.autoStretchMenuItem.selected = this.transformation.stretch.auto + this.invertMenuItem.selected = this.transformation.invert + this.horizontalMirrorMenuItem.selected = this.transformation.mirrorHorizontal + this.verticalMirrorMenuItem.selected = this.transformation.mirrorVertical + this.crosshairMenuItem.selected = this.preference.crossHair } - private savePreference() { - const preference = this.preference.imagePreference.get() - - preference.solver = { - type: this.solver.type, - focalLength: this.solver.focalLength, - pixelSize: this.solver.pixelSize, - radius: this.solver.radius, - } - preference.starDetection = { - type: this.starDetection.type, - maxStars: this.starDetection.maxStars, - minSNR: this.starDetection.minSNR, - } - - this.preference.imagePreference.set(preference) + protected savePreference() { + this.preferenceService.imagePreference.set(this.preference) } private async executeCamera(action: (camera: Camera) => void | Promise, showConfirmation: boolean = true) { - if (showConfirmation && (await this.prime.confirm('Are you sure that you want to proceed?'))) { + if (showConfirmation && (await this.angularService.confirm('Are you sure that you want to proceed?'))) { return false } @@ -1424,8 +1315,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { await action(cameras[0]) return true } else { - this.deviceMenu.header = 'CAMERA' - const camera = await this.deviceMenu.show(cameras) + const camera = await this.deviceMenu.show(cameras, undefined, 'CAMERA') if (camera && camera !== 'NONE' && camera.connected) { await action(camera) @@ -1437,7 +1327,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async executeMount(action: (mount: Mount) => void | Promise, showConfirmation: boolean = true) { - if (showConfirmation && (await this.prime.confirm('Are you sure that you want to proceed?'))) { + if (showConfirmation && (await this.angularService.confirm('Are you sure that you want to proceed?'))) { return false } @@ -1447,8 +1337,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { await action(mounts[0]) return true } else { - this.deviceMenu.header = 'MOUNT' - const mount = await this.deviceMenu.show(mounts) + const mount = await this.deviceMenu.show(mounts, undefined, 'MOUNT') if (mount && mount !== 'NONE' && mount.connected) { await action(mount) diff --git a/desktop/src/app/indi/indi.component.scss b/desktop/src/app/indi/indi.component.scss index 6c27e4f87..465bcc5e8 100644 --- a/desktop/src/app/indi/indi.component.scss +++ b/desktop/src/app/indi/indi.component.scss @@ -1,16 +1,16 @@ -:host { - ::ng-deep { - .p-listbox-list-wrapper { - max-height: calc(100vh - 175px) !important; - } +neb-indi { + .properties { + height: calc(100vh - 100px); + overflow-y: auto; } -} -.properties { - height: calc(100vh - 100px); - overflow-y: auto; -} + .properties::-webkit-scrollbar { + display: none; + } -.properties::-webkit-scrollbar { - display: none; + .p-listbox-filter.p-inputtext { + border: 0px; + border-radius: 4px; + background: #1a1a1a; + } } diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index c33d85881..f528be883 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { MenuItem } from 'primeng/api' import { Listbox } from 'primeng/listbox' @@ -12,31 +12,32 @@ import { AppComponent } from '../app.component' selector: 'neb-indi', templateUrl: './indi.component.html', styleUrls: ['./indi.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class INDIComponent implements AfterViewInit, OnDestroy { - devices: Device[] = [] - properties: INDIProperty[] = [] - groups: MenuItem[] = [] + protected devices: Device[] = [] + protected properties: INDIProperty[] = [] + protected groups: MenuItem[] = [] - device?: Device - group = '' - showLog = false - messages: string[] = [] + protected device?: Device + protected group = '' + protected showLog = false + protected messages: string[] = [] @ViewChild('listbox') - readonly messageListbox!: Listbox + protected readonly messageBox!: Listbox constructor( app: AppComponent, private readonly route: ActivatedRoute, private readonly api: ApiService, - electron: ElectronService, + electronService: ElectronService, ngZone: NgZone, ) { app.title = 'INDI' - electron.on('DEVICE.PROPERTY_CHANGED', (event) => { - if (this.device?.id === event.device.id) { + electronService.on('DEVICE.PROPERTY_CHANGED', (event) => { + if (event.device.id === this.device?.id) { ngZone.run(() => { if (event.property) { this.addOrUpdateProperty(event.property) @@ -46,8 +47,8 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } }) - electron.on('DEVICE.PROPERTY_DELETED', (event) => { - if (this.device?.id === event.device.id) { + electronService.on('DEVICE.PROPERTY_DELETED', (event) => { + if (event.device.id === this.device?.id) { const index = this.properties.findIndex((e) => e.name === event.property?.name) if (index >= 0) { @@ -59,12 +60,12 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } }) - electron.on('DEVICE.MESSAGE_RECEIVED', (event) => { - if (this.device && event.device.id === this.device.id) { + electronService.on('DEVICE.MESSAGE_RECEIVED', (event) => { + if (event.device.id === this.device?.id) { ngZone.run(() => { if (event.message) { this.messages.splice(0, 0, event.message) - this.messageListbox.cd.markForCheck() + this.messageBox.cd.markForCheck() } }) } @@ -80,7 +81,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } }) - this.devices = [...(await this.api.cameras()), ...(await this.api.mounts()), ...(await this.api.focusers()), ...(await this.api.wheels())].sort(deviceComparator) + this.devices = [...(await this.api.cameras()), ...(await this.api.mounts()), ...(await this.api.focusers()), ...(await this.api.wheels()), ...(await this.api.rotators())].sort(deviceComparator) if (this.devices.length) { this.device = this.devices[0] @@ -95,7 +96,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } } - async deviceChanged(device: Device) { + protected async deviceChanged(device: Device) { if (this.device) { await this.api.indiUnlisten(this.device) } @@ -107,7 +108,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { this.messages = await this.api.indiLog(device) } - changeGroup(group: string) { + protected changeGroup(group: string) { this.showLog = false this.group = group } diff --git a/desktop/src/app/indi/property/indi-property.component.scss b/desktop/src/app/indi/property/indi-property.component.scss index fc6120b85..3c6597153 100644 --- a/desktop/src/app/indi/property/indi-property.component.scss +++ b/desktop/src/app/indi/property/indi-property.component.scss @@ -1,21 +1,21 @@ -:host { +neb-indi-property { background: rgba(0, 0, 0, 0.1); border-radius: 8px; display: block; margin-bottom: 4px; -} -.mdi.mdi-circle { - &.IDLE { - color: #039be5; - } - &.OK { - color: #43a047; - } - &.BUSY { - color: #f57c00; - } - &.ALERT { - color: #e53935; + .mdi.mdi-circle { + &.IDLE { + color: #039be5; + } + &.OK { + color: #43a047; + } + &.BUSY { + color: #f57c00; + } + &.ALERT { + color: #e53935; + } } } diff --git a/desktop/src/app/indi/property/indi-property.component.ts b/desktop/src/app/indi/property/indi-property.component.ts index 19be45587..7ccde2aa4 100644 --- a/desktop/src/app/indi/property/indi-property.component.ts +++ b/desktop/src/app/indi/property/indi-property.component.ts @@ -1,17 +1,18 @@ -import { AfterContentInit, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core' +import { AfterContentInit, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core' import { INDIProperty, INDIPropertyItem, INDISendProperty, INDISendPropertyItem } from '../../../shared/types/device.types' @Component({ selector: 'neb-indi-property', templateUrl: './indi-property.component.html', styleUrls: ['./indi-property.component.scss'], + encapsulation: ViewEncapsulation.None, }) -export class INDIPropertyComponent implements AfterContentInit, OnDestroy { +export class INDIPropertyComponent implements AfterContentInit { @Input({ required: true }) - property!: INDIProperty + protected property!: INDIProperty @Input() - disabled = false + protected disabled = false @Output() readonly onSend = new EventEmitter() @@ -24,8 +25,6 @@ export class INDIPropertyComponent implements AfterContentInit, OnDestroy { } } - ngOnDestroy() {} - sendSwitch(item: INDIPropertyItem) { const property: INDISendProperty = { name: this.property.name, diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index e8acceb0a..153f63b0a 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -11,7 +11,7 @@ - {{ parking ? 'parking' : parked ? 'parked' : slewing ? 'slewing' : tracking ? 'tracking' : 'idle' }} + {{ mount.parking ? 'parking' : mount.parked ? 'parked' : mount.slewing ? 'slewing' : tracking ? 'tracking' : 'idle' }}
@@ -55,7 +55,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="rightAscensionJ2000" /> + [value]="currentComputedLocation.rightAscensionJ2000" />
@@ -65,7 +65,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="declinationJ2000" /> + [value]="currentComputedLocation.declinationJ2000" />
@@ -75,7 +75,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="rightAscension" /> + [value]="mount.rightAscension" /> @@ -85,7 +85,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="declination" /> + [value]="mount.declination" /> @@ -95,7 +95,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="azimuth" /> + [value]="currentComputedLocation.azimuth" /> @@ -105,7 +105,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="altitude" /> + [value]="currentComputedLocation.altitude" /> @@ -115,7 +115,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="lst" /> + [value]="currentComputedLocation.lst" /> @@ -125,7 +125,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="constellation ?? '-'" /> + [value]="currentComputedLocation.constellation" /> @@ -135,7 +135,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - value="{{ meridianAt }} (-{{ timeLeftToMeridianFlip }})" /> + value="{{ currentComputedLocation.meridianAt }} (-{{ currentComputedLocation.timeLeftToMeridianFlip }})" /> @@ -145,7 +145,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="pierSide" /> + [value]="currentComputedLocation.pierSide" /> @@ -162,9 +162,9 @@
@@ -197,9 +197,9 @@ @@ -209,9 +209,9 @@ @@ -219,7 +219,7 @@
- +
@@ -430,12 +430,12 @@ [min]="1024" [max]="65535" placeholder="10001" - scrollableNumber /> + spinnableNumber />
- + Use together with the - + Use together with the
() private readonly computeTargetCoordinatePublisher = new Subject() private readonly computeCoordinateSubscriptions: Subscription[] = [] private readonly moveToDirection = [false, false] - readonly ephemerisModel: SlideMenuItem[] = [ + protected tracking = false + protected trackMode: TrackMode = 'SIDEREAL' + protected slewRate?: SlewRate + protected slewingDirection?: MountSlewDirection + + protected readonly ephemerisModel: SlideMenuItem[] = [ { icon: 'mdi mdi-image', label: 'Frame', slideMenu: [], command: () => { - const data: FramingData = { rightAscension: this.rightAscensionJ2000, declination: this.declinationJ2000 } - return this.browserWindow.openFraming(data) + return this.browserWindowService.openFraming({ rightAscension: this.currentComputedLocation.rightAscensionJ2000, declination: this.currentComputedLocation.declinationJ2000 }) }, }, SEPARATOR_MENU_ITEM, @@ -71,24 +50,25 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { label: 'Find sky objects around the coordinates', slideMenu: [], command: () => { - const data: SkyAtlasInput = { - tab: SkyAtlasTab.SKY_OBJECT, - filter: { rightAscension: this.rightAscensionJ2000, declination: this.declinationJ2000 }, - } - - return this.browserWindow.openSkyAtlas(data, { bringToFront: true }) + return this.browserWindowService.openSkyAtlas( + { + tab: BodyTabType.SKY_OBJECT, + filter: { rightAscension: this.currentComputedLocation.rightAscensionJ2000, declination: this.currentComputedLocation.declinationJ2000 }, + }, + { bringToFront: true }, + ) }, }, ] - readonly targetCoordinateModel: SlideMenuItem[] = [ + protected readonly targetCoordinateModel: SlideMenuItem[] = [ { icon: 'mdi mdi-telescope', label: 'Go To', slideMenu: [], command: () => { this.targetCoordinateCommand = this.targetCoordinateModel[0] - return this.goTo() + this.savePreference() }, }, { @@ -97,7 +77,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { slideMenu: [], command: () => { this.targetCoordinateCommand = this.targetCoordinateModel[1] - return this.slewTo() + this.savePreference() }, }, { @@ -106,7 +86,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { slideMenu: [], command: () => { this.targetCoordinateCommand = this.targetCoordinateModel[2] - return this.sync() + this.savePreference() }, }, { @@ -114,8 +94,13 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { label: 'Frame', slideMenu: [], command: () => { - const data: FramingData = { rightAscension: this.targetRightAscension, declination: this.targetDeclination } - return this.browserWindow.openFraming(data) + const { targetRightAscension, targetDeclination, targetCoordinateType } = this.preference + + if (targetCoordinateType === 'J2000') { + return this.browserWindowService.openFraming({ rightAscension: targetRightAscension, declination: targetDeclination }) + } else { + return this.browserWindowService.openFraming({ rightAscension: this.targetComputedLocation.rightAscensionJ2000, declination: this.targetComputedLocation.declinationJ2000 }) + } }, }, SEPARATOR_MENU_ITEM, @@ -128,9 +113,9 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { label: 'Current location', slideMenu: [], command: () => { - this.targetRightAscension = this.rightAscension - this.targetDeclination = this.declination - this.targetCoordinateType = 'JNOW' + this.preference.targetRightAscension = this.mount.rightAscension + this.preference.targetDeclination = this.mount.declination + this.preference.targetCoordinateType = 'JNOW' }, }, { @@ -138,9 +123,9 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { label: 'Current location (J2000)', slideMenu: [], command: () => { - this.targetRightAscension = this.rightAscensionJ2000 - this.targetDeclination = this.declinationJ2000 - this.targetCoordinateType = 'J2000' + this.preference.targetRightAscension = this.currentComputedLocation.rightAscensionJ2000 + this.preference.targetDeclination = this.currentComputedLocation.declinationJ2000 + this.preference.targetCoordinateType = 'J2000' }, }, { @@ -216,30 +201,22 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { }, ] - targetCoordinateCommand = this.targetCoordinateModel[0] - - readonly remoteControl: MountRemoteControlDialog = { - showDialog: false, - type: 'LX200', - host: '0.0.0.0', - port: 10001, - data: [], - } + protected targetCoordinateCommand = this.targetCoordinateModel[0] constructor( private readonly app: AppComponent, private readonly api: ApiService, - private readonly browserWindow: BrowserWindowService, - electron: ElectronService, - private readonly preference: PreferenceService, + private readonly browserWindowService: BrowserWindowService, + electronService: ElectronService, + private readonly preferenceService: PreferenceService, private readonly route: ActivatedRoute, - private readonly prime: PrimeService, - private readonly pinger: Pinger, + private readonly angularService: AngularService, + private readonly ticker: Ticker, ngZone: NgZone, ) { app.title = 'Mount' - electron.on('MOUNT.UPDATED', async (event) => { + electronService.on('MOUNT.UPDATED', async (event) => { if (event.device.id === this.mount.id) { await ngZone.run(async () => { const wasConnected = this.mount.connected @@ -253,10 +230,10 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { } }) - electron.on('MOUNT.DETACHED', (event) => { + electronService.on('MOUNT.DETACHED', (event) => { if (event.device.id === this.mount.id) { ngZone.run(() => { - Object.assign(this.mount, EMPTY_MOUNT) + Object.assign(this.mount, DEFAULT_MOUNT) }) } }) @@ -314,15 +291,15 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { ngAfterContentInit() { this.route.queryParams.subscribe(async (e) => { - const mount = JSON.parse(decodeURIComponent(e['data'] as string)) as Mount - await this.mountChanged(mount) - this.pinger.register(this, 30000) + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as Mount + await this.mountChanged(data) + this.ticker.register(this, 30000) }) } @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) this.computeCoordinateSubscriptions.forEach((e) => { e.unsubscribe() @@ -331,13 +308,13 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { void this.abort() } - async ping() { + async tick() { if (this.mount.id) { await this.api.mountListen(this.mount) } } - async mountChanged(mount?: Mount) { + protected async mountChanged(mount?: Mount) { if (mount?.id) { mount = await this.api.mount(mount.id) Object.assign(this.mount, mount) @@ -349,7 +326,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { this.app.subTitle = mount?.name ?? '' } - connect() { + protected connect() { if (this.mount.connected) { return this.api.mountDisconnect(this.mount) } else { @@ -357,41 +334,44 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { } } - async showRemoteControlDialog() { - this.remoteControl.data = await this.api.mountRemoteControlList(this.mount) + protected async showRemoteControlDialog() { + this.remoteControl.controls = await this.api.mountRemoteControlList(this.mount) this.remoteControl.showDialog = true } - async startRemoteControl() { + protected async startRemoteControl() { try { - await this.api.mountRemoteControlStart(this.mount, this.remoteControl.type, this.remoteControl.host, this.remoteControl.port) - this.remoteControl.data = await this.api.mountRemoteControlList(this.mount) + await this.api.mountRemoteControlStart(this.mount, this.remoteControl.protocol, this.remoteControl.host, this.remoteControl.port) + this.remoteControl.controls = await this.api.mountRemoteControlList(this.mount) } catch { - this.prime.message('Failed to start remote control', 'error') + this.angularService.message('Failed to start remote control', 'error') } } - async stopRemoteControl(type: MountRemoteControlType) { - await this.api.mountRemoteControlStop(this.mount, type) - this.remoteControl.data = await this.api.mountRemoteControlList(this.mount) + protected async stopRemoteControl(protocol: MountRemoteControlProtocol) { + await this.api.mountRemoteControlStop(this.mount, protocol) + this.remoteControl.controls = await this.api.mountRemoteControlList(this.mount) } - async goTo() { - await this.api.mountGoTo(this.mount, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') + protected async goTo() { + const { targetRightAscension, targetDeclination, targetCoordinateType } = this.preference + await this.api.mountGoTo(this.mount, targetRightAscension, targetDeclination, targetCoordinateType === 'J2000') this.savePreference() } - async slewTo() { - await this.api.mountSlew(this.mount, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') + protected async slewTo() { + const { targetRightAscension, targetDeclination, targetCoordinateType } = this.preference + await this.api.mountSlew(this.mount, targetRightAscension, targetDeclination, targetCoordinateType === 'J2000') this.savePreference() } - async sync() { - await this.api.mountSync(this.mount, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') + protected async sync() { + const { targetRightAscension, targetDeclination, targetCoordinateType } = this.preference + await this.api.mountSync(this.mount, targetRightAscension, targetDeclination, targetCoordinateType === 'J2000') this.savePreference() } - async targetCoordinateCommandClicked() { + protected async targetCoordinateCommandClicked() { if (this.targetCoordinateCommand === this.targetCoordinateModel[0]) { await this.goTo() } else if (this.targetCoordinateCommand === this.targetCoordinateModel[1]) { @@ -401,7 +381,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { } } - moveTo(direction: MoveDirectionType, pressed: boolean, event?: MouseEvent) { + protected moveTo(direction: MountSlewDirection, pressed: boolean, event?: MouseEvent) { if (!event || event.button === 0) { this.slewingDirection = pressed ? direction : undefined @@ -441,50 +421,40 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { } } - abort() { + protected abort() { return this.api.mountAbort(this.mount) } - trackingToggled() { + protected trackingToggled() { return this.api.mountTracking(this.mount, this.tracking) } - trackModeChanged() { + protected trackModeChanged() { return this.api.mountTrackMode(this.mount, this.trackMode) } - async slewRateChanged() { + protected async slewRateChanged() { if (this.slewRate) { await this.api.mountSlewRate(this.mount, this.slewRate) } } - park() { + protected park() { return this.api.mountPark(this.mount) } - unpark() { + protected unpark() { return this.api.mountUnpark(this.mount) } - home() { + protected home() { return this.api.mountHome(this.mount) } private update() { if (this.mount.id) { - this.slewing = this.mount.slewing - this.parking = this.mount.parking - this.parked = this.mount.parked - this.canPark = this.mount.canPark - this.canHome = this.mount.canHome - this.trackModes = this.mount.trackModes this.trackMode = this.mount.trackMode - this.slewRates = this.mount.slewRates this.slewRate = this.mount.slewRate - this.rightAscension = this.mount.rightAscension - this.declination = this.mount.declination - this.pierSide = this.mount.pierSide this.tracking = this.mount.tracking this.computeCoordinatePublisher.next() @@ -493,56 +463,43 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { private async computeCoordinates() { if (this.mount.connected) { - const computedCoordinates = await this.api.mountComputeLocation(this.mount, false, this.mount.rightAscension, this.mount.declination, true, true, true) - this.rightAscensionJ2000 = computedCoordinates.rightAscensionJ2000 - this.declinationJ2000 = computedCoordinates.declinationJ2000 - this.azimuth = computedCoordinates.azimuth - this.altitude = computedCoordinates.altitude - this.constellation = computedCoordinates.constellation - this.meridianAt = computedCoordinates.meridianAt - this.timeLeftToMeridianFlip = computedCoordinates.timeLeftToMeridianFlip - this.lst = computedCoordinates.lst + Object.assign(this.currentComputedLocation, await this.api.mountComputeLocation(this.mount, false, this.mount.rightAscension, this.mount.declination, true, true, true)) } } - async computeTargetCoordinates() { + protected async computeTargetCoordinates() { if (this.mount.connected) { - const computedLocation = await this.api.mountComputeLocation(this.mount, this.targetCoordinateType === 'J2000', this.targetRightAscension, this.targetDeclination, true, true, true) - this.targetComputedLocation = computedLocation + const { targetRightAscension, targetDeclination, targetCoordinateType } = this.preference + Object.assign(this.targetComputedLocation, await this.api.mountComputeLocation(this.mount, targetCoordinateType === 'J2000', targetRightAscension, targetDeclination, true, true, true)) } } private updateTargetCoordinate(coordinates: ComputedLocation) { - if (this.targetCoordinateType === 'J2000') { - this.targetRightAscension = coordinates.rightAscensionJ2000 - this.targetDeclination = coordinates.declinationJ2000 + if (this.preference.targetCoordinateType === 'J2000') { + this.preference.targetRightAscension = coordinates.rightAscensionJ2000 + this.preference.targetDeclination = coordinates.declinationJ2000 } else { - this.targetRightAscension = coordinates.rightAscension - this.targetDeclination = coordinates.declination + this.preference.targetRightAscension = coordinates.rightAscension + this.preference.targetDeclination = coordinates.declination } + this.savePreference() + this.computeTargetCoordinatePublisher.next() } private loadPreference() { if (this.mount.id) { - const mountPreference: Partial = this.preference.mountPreference(this.mount).get() - this.targetCoordinateType = mountPreference.targetCoordinateType ?? 'JNOW' - this.targetRightAscension = mountPreference.targetRightAscension ?? '00h00m00s' - this.targetDeclination = mountPreference.targetDeclination ?? `00°00'00"` + Object.assign(this.preference, this.preferenceService.mount(this.mount).get()) + this.targetCoordinateCommand = this.targetCoordinateModel[this.preference.targetCoordinateCommand] ?? this.targetCoordinateModel[0] this.computeTargetCoordinatePublisher.next() } } private savePreference() { if (this.mount.connected) { - const preference: MountPreference = { - targetCoordinateType: this.targetCoordinateType, - targetRightAscension: this.targetRightAscension, - targetDeclination: this.targetDeclination, - } - - this.preference.mountPreference(this.mount).set(preference) + this.preference.targetCoordinateCommand = this.targetCoordinateModel.indexOf(this.targetCoordinateCommand) + this.preferenceService.mount(this.mount).set(this.preference) } } } diff --git a/desktop/src/app/rotator/rotator.component.html b/desktop/src/app/rotator/rotator.component.html index 7e9cc5dfd..1e415be90 100644 --- a/desktop/src/app/rotator/rotator.component.html +++ b/desktop/src/app/rotator/rotator.component.html @@ -11,7 +11,7 @@
- {{ moving ? 'moving' : 'idle' }} + {{ rotator.moving ? 'moving' : 'idle' }}
@@ -45,13 +45,13 @@ styleClass="p-inputtext-sm border-0 max-w-full" [(ngModel)]="rotator.angle" locale="en" - scrollableNumber /> + spinnableNumber />
Reversed + [(ngModel)]="rotator.reversed" />
@@ -85,14 +85,15 @@ [max]="rotator.maxAngle" [showButtons]="true" styleClass="p-inputtext-sm border-0 max-w-full" - [(ngModel)]="angle" + [(ngModel)]="preference.angle" + (ngModelChange)="savePreference()" [allowEmpty]="false" locale="en" - scrollableNumber /> + spinnableNumber /> { + electronService.on('ROTATOR.UPDATED', (event) => { if (event.device.id === this.rotator.id) { ngZone.run(() => { Object.assign(this.rotator, event.device) @@ -38,10 +35,10 @@ export class RotatorComponent implements AfterViewInit, OnDestroy, Pingable { } }) - electron.on('ROTATOR.DETACHED', (event) => { + electronService.on('ROTATOR.DETACHED', (event) => { if (event.device.id === this.rotator.id) { ngZone.run(() => { - Object.assign(this.rotator, EMPTY_ROTATOR) + Object.assign(this.rotator, DEFAULT_ROTATOR) }) } }) @@ -49,25 +46,25 @@ export class RotatorComponent implements AfterViewInit, OnDestroy, Pingable { ngAfterViewInit() { this.route.queryParams.subscribe(async (e) => { - const rotator = JSON.parse(decodeURIComponent(e['data'] as string)) as Rotator - await this.rotatorChanged(rotator) - this.pinger.register(this, 30000) + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as Rotator + await this.rotatorChanged(data) + this.ticker.register(this, 30000) }) } @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) void this.abort() } - async ping() { + async tick() { if (this.rotator.id) { await this.api.rotatorListen(this.rotator) } } - async rotatorChanged(rotator?: Rotator) { + protected async rotatorChanged(rotator?: Rotator) { if (rotator?.id) { rotator = await this.api.rotator(rotator.id) Object.assign(this.rotator, rotator) @@ -79,7 +76,7 @@ export class RotatorComponent implements AfterViewInit, OnDestroy, Pingable { this.app.subTitle = rotator?.name ?? '' } - connect() { + protected connect() { if (this.rotator.connected) { return this.api.rotatorDisconnect(this.rotator) } else { @@ -87,52 +84,37 @@ export class RotatorComponent implements AfterViewInit, OnDestroy, Pingable { } } - reverse(enabled: boolean) { + protected reverse(enabled: boolean) { return this.api.rotatorReverse(this.rotator, enabled) } - async move() { - if (!this.moving) { - this.moving = true - await this.api.rotatorMove(this.rotator, this.angle) - this.savePreference() - } + protected move() { + return this.api.rotatorMove(this.rotator, this.preference.angle) } - async sync() { - if (!this.moving) { - await this.api.rotatorSync(this.rotator, this.angle) - this.savePreference() - } + protected sync() { + return this.api.rotatorSync(this.rotator, this.preference.angle) } - abort() { + protected abort() { return this.api.rotatorAbort(this.rotator) } - home() { + protected home() { return this.api.rotatorHome(this.rotator) } - private update() { - if (this.rotator.id) { - this.moving = this.rotator.moving - this.reversed = this.rotator.reversed - } - } + private update() {} private loadPreference() { if (this.rotator.id) { - const preference = this.preference.rotatorPreference(this.rotator).get() - this.angle = preference.angle ?? 0 + Object.assign(this.preference, this.preferenceService.rotator(this.rotator).get()) } } - private savePreference() { + protected savePreference() { if (this.rotator.connected) { - const preference = this.preference.rotatorPreference(this.rotator).get() - preference.angle = this.angle - this.preference.rotatorPreference(this.rotator).set(preference) + this.preferenceService.rotator(this.rotator).set(this.preference) } } } diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index 10c057c2e..13ddc495a 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -18,8 +18,8 @@ locale="en" styleClass="p-inputtext-sm border-0" [allowEmpty]="false" - (ngModelChange)="savePlan()" - scrollableNumber /> + (ngModelChange)="savePreference()" + spinnableNumber />
@@ -27,12 +27,12 @@ + (ngModelChange)="savePreference()" /> @@ -40,7 +40,7 @@ @@ -59,10 +59,10 @@ tooltipPosition="bottom" [positionTop]="8"> @@ -76,7 +76,7 @@ [binary]="true" [disabled]="running" [(ngModel)]="plan.dither.enabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" />
@@ -86,7 +86,7 @@ [disabled]="running || !plan.dither.enabled" label="RA only" [(ngModel)]="plan.dither.raOnly" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" />
@@ -100,8 +100,8 @@ [step]="0.1" locale="en" [minFractionDigits]="1" - (ngModelChange)="savePlan()" - scrollableNumber /> + (ngModelChange)="savePreference()" + spinnableNumber />
@@ -115,8 +115,8 @@ [max]="1000" [(ngModel)]="plan.dither.afterExposures" [step]="1" - (ngModelChange)="savePlan()" - scrollableNumber /> + (ngModelChange)="savePreference()" + spinnableNumber /> @@ -131,7 +131,7 @@ [binary]="true" [disabled]="running" [(ngModel)]="plan.autoFocus.enabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" />
@@ -140,19 +140,19 @@ [disabled]="running || !plan.autoFocus.enabled" label="On start" [(ngModel)]="plan.autoFocus.onStart" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" />
+ (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" + spinnableNumber />
@@ -172,7 +172,7 @@ [binary]="true" [disabled]="running || !plan.autoFocus.enabled" [(ngModel)]="plan.autoFocus.afterExposuresEnabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" + spinnableNumber />
@@ -192,7 +192,7 @@ [binary]="true" [disabled]="running || !plan.autoFocus.enabled" [(ngModel)]="plan.autoFocus.afterTemperatureChangeEnabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" + spinnableNumber /> @@ -212,7 +212,7 @@ [binary]="true" [disabled]="running || !plan.autoFocus.enabled" [(ngModel)]="plan.autoFocus.afterHFDIncreaseEnabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" + spinnableNumber /> @@ -239,7 +239,7 @@ pInputText class="p-inputtext-sm border-0" [(ngModel)]="plan.namingFormat.light" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" /> + (ngModelChange)="savePreference()" /> @@ -382,7 +382,7 @@ cdkDropList (cdkDropListDropped)="drop($event)">
@@ -393,36 +393,36 @@
- + (onClick)="showCameraDialog(sequence)" size="small" /> -->
+ (click)="showSequenceMenu(sequence, entryMenu)" /> + (onClick)="deleteSequence(sequence, i)" /> + (onClick)="duplicateSequence(sequence, i)" /> + [(ngModel)]="sequence.enabled" + (ngModelChange)="savePreference()" />
+ [info]="sequence" + [wheel]="plan.wheel" + [canRemoveFilter]="sequence.enabled" + (filterRemoved)="filterRemoved(sequence)" />
@@ -481,7 +483,7 @@ [text]="true" /> } @else if (!running) {
@@ -530,96 +532,84 @@ [text]="true" icon="mdi mdi-checkbox-marked" label="Select All" - (onClick)="updateAllAvailableEntryPropertiesToApply(true)" /> + (onClick)="selectSequenceProperty(true)" /> + (onClick)="selectSequenceProperty(false)" />
+ [(ngModel)]="property.properties.EXPOSURE_TIME" />
+ [(ngModel)]="property.properties.EXPOSURE_AMOUNT" />
+ [(ngModel)]="property.properties.EXPOSURE_DELAY" />
+ [(ngModel)]="property.properties.FRAME_TYPE" />
+ [(ngModel)]="property.properties.X" />
+ [(ngModel)]="property.properties.Y" />
+ [(ngModel)]="property.properties.WIDTH" />
+ [(ngModel)]="property.properties.HEIGHT" />
+ [(ngModel)]="property.properties.BIN" />
+ [(ngModel)]="property.properties.FRAME_FORMAT" />
+ [(ngModel)]="property.properties.GAIN" />
+ [(ngModel)]="property.properties.OFFSET" />
@@ -628,11 +618,11 @@ icon="mdi mdi-check" label="Apply" size="small" - (onClick)="applyCameraStartCaptureToEntries()" /> + (onClick)="copySequencePropertyToSequencies()" />
diff --git a/desktop/src/app/sequencer/sequencer.component.scss b/desktop/src/app/sequencer/sequencer.component.scss index bf78461c8..bbe2b5908 100644 --- a/desktop/src/app/sequencer/sequencer.component.scss +++ b/desktop/src/app/sequencer/sequencer.component.scss @@ -1,24 +1,22 @@ -:host { - ::ng-deep { - .p-card { - .p-card-body { - padding: 0px; - padding-left: 1rem !important; - padding-right: 1rem !important; - } - - .p-card-content { - padding: 0px; - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } +neb-sequencer { + .p-card { + .p-card-body { + padding: 0px; + padding-left: 1rem !important; + padding-right: 1rem !important; } - .p-orderlist-controls { - display: none; + .p-card-content { + padding: 0px; + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } } + .p-orderlist-controls { + display: none; + } + .mdi.mdi-progress-indicator:before { font-size: 11px; } diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index f70f87ee4..c1a92261b 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -1,63 +1,56 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop' -import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, QueryList, ViewChildren } from '@angular/core' +import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core' +import { dirname } from 'path' import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component' import { DialogMenuComponent } from '../../shared/components/dialog-menu/dialog-menu.component' -import { SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { MenuItem, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' +import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' -import { Pingable, Pinger } from '../../shared/services/pinger.service' import { PreferenceService } from '../../shared/services/preference.service' -import { PrimeService } from '../../shared/services/prime.service' +import { Tickable, Ticker } from '../../shared/services/ticker.service' import { JsonFile } from '../../shared/types/app.types' -import { Camera, CameraCaptureEvent, CameraStartCapture, FrameType, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' +import { Camera, cameraCaptureNamingFormatWithDefault, FrameType, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { Focuser } from '../../shared/types/focuser.types' import { Mount } from '../../shared/types/mount.types' import { Rotator } from '../../shared/types/rotator.types' -import { EMPTY_SEQUENCE_PLAN, SEQUENCE_ENTRY_PROPERTIES, SequenceCaptureMode, SequenceEntryProperty, SequencePlan, SequencerEvent } from '../../shared/types/sequencer.types' -import { DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, resetCameraCaptureNamingFormat } from '../../shared/types/settings.types' -import { FilterWheel } from '../../shared/types/wheel.types' +import { DEFAULT_SEQUENCE, DEFAULT_SEQUENCE_PROPERTY_DIALOG, DEFAULT_SEQUENCER_PLAN, DEFAULT_SEQUENCER_PREFERENCE, Sequence, SequenceProperty, SequencerEvent, SequencerPlan } from '../../shared/types/sequencer.types' +import { resetCameraCaptureNamingFormat } from '../../shared/types/settings.types' +import { Wheel } from '../../shared/types/wheel.types' import { deviceComparator } from '../../shared/utils/comparators' -import { Undefinable } from '../../shared/utils/types' import { AppComponent } from '../app.component' import { CameraComponent } from '../camera/camera.component' import { FilterWheelComponent } from '../filterwheel/filterwheel.component' -export const SEQUENCER_SAVED_PATH_KEY = 'sequencer.savedPath' - @Component({ selector: 'neb-sequencer', templateUrl: './sequencer.component.html', styleUrls: ['./sequencer.component.scss'], + encapsulation: ViewEncapsulation.None, }) -export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable { - cameras: Camera[] = [] - mounts: Mount[] = [] - wheels: FilterWheel[] = [] - focusers: Focuser[] = [] - rotators: Rotator[] = [] - - camera?: Camera - mount?: Mount - wheel?: FilterWheel - focuser?: Focuser - rotator?: Rotator - - readonly captureModes: SequenceCaptureMode[] = ['FULLY', 'INTERLEAVED'] - readonly plan = structuredClone(EMPTY_SEQUENCE_PLAN) - - private entryToApply?: CameraStartCapture - private entryToApplyCount: [number, number] = [0, 0] - readonly availableEntryPropertiesToApply = new Map() - showEntryPropertiesToApplyDialog = false - readonly entryMenuModel: SlideMenuItem[] = [ +export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable { + protected cameras: Camera[] = [] + protected mounts: Mount[] = [] + protected wheels: Wheel[] = [] + protected focusers: Focuser[] = [] + protected rotators: Rotator[] = [] + + protected readonly property = structuredClone(DEFAULT_SEQUENCE_PROPERTY_DIALOG) + protected readonly preference = structuredClone(DEFAULT_SEQUENCER_PREFERENCE) + protected plan = this.preference.plan + protected event?: SequencerEvent + protected running = false + + // NOTE: Remove the "plan.sequences.length <= 1" on layout if add more options + protected readonly sequenceModel: SlideMenuItem[] = [ { icon: 'mdi mdi-content-copy', label: 'Apply to all', slideMenu: [], command: () => { - this.entryToApplyCount = [-1000, 1000] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [-1000, 1000] + this.property.showDialog = true }, }, { @@ -65,8 +58,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable label: 'Apply to all above', slideMenu: [], command: () => { - this.entryToApplyCount = [-1000, 0] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [-1000, 0] + this.property.showDialog = true }, }, { @@ -74,8 +67,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable label: 'Apply to above', slideMenu: [], command: () => { - this.entryToApplyCount = [-1, 0] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [-1, 0] + this.property.showDialog = true }, }, { @@ -83,8 +76,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable label: 'Apply to below', slideMenu: [], command: () => { - this.entryToApplyCount = [1, 0] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [1, 0] + this.property.showDialog = true }, }, { @@ -92,104 +85,103 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable label: 'Apply to all below', slideMenu: [], command: () => { - this.entryToApplyCount = [1000, 0] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [1000, 0] + this.property.showDialog = true }, }, ] - readonly sequenceEvents: CameraCaptureEvent[] = [] + private readonly createNewMenuItem: MenuItem = { + icon: 'mdi mdi-plus', + label: 'Create new', + command: () => { + this.preference.loadPath = undefined + this.savePreference() + + this.app.subTitle = undefined + this.saveMenuItem.visible = false + this.saveMenuItem.disabled = true + + if (!this.loadPlan(structuredClone(DEFAULT_SEQUENCER_PLAN))) { + this.add() + } + }, + } + + private readonly saveMenuItem: MenuItem = { + icon: 'mdi mdi-content-save', + label: 'Save', + visible: false, + command: () => this.savePlanToJson(false), + } + + private readonly saveAsMenuItem: MenuItem = { + icon: 'mdi mdi-content-save-edit', + label: 'Save as', + command: () => this.savePlanToJson(true), + } - event?: SequencerEvent - running = false + private readonly loadMenuItem: MenuItem = { + icon: 'mdi mdi-folder-open', + label: 'Load', + command: async () => { + const defaultPath = this.preference.loadPath ? dirname(this.preference.loadPath) : undefined + const file = await this.electronService.openJson({ defaultPath }) + + if (file !== false) { + this.loadPlanFromJson(file) + } + }, + } @ViewChildren('cameraExposure') private readonly cameraExposures!: QueryList get canStart() { - return !!this.camera && this.camera.connected && !!this.plan.entries.find((e) => e.enabled) + return !!this.plan.camera?.connected && !!this.plan.sequences.find((e) => e.enabled) } get pausingOrPaused() { return this.event?.state === 'PAUSING' || this.event?.state === 'PAUSED' } - get savedPath() { - return this.app.subTitle - } - - set savedPath(value: Undefinable) { - this.app.subTitle = value - } - constructor( private readonly app: AppComponent, private readonly api: ApiService, - private readonly browserWindow: BrowserWindowService, - private readonly electron: ElectronService, - private readonly preference: PreferenceService, - private readonly prime: PrimeService, - private readonly pinger: Pinger, + private readonly browserWindowService: BrowserWindowService, + private readonly electronService: ElectronService, + private readonly preferenceService: PreferenceService, + private readonly angularService: AngularService, + private readonly ticker: Ticker, ngZone: NgZone, ) { app.title = 'Sequencer' - app.topMenu.push({ - icon: 'mdi mdi-plus', - label: 'Create new', - command: () => { - this.updateSavedPath() + app.topMenu.push(this.createNewMenuItem) + app.topMenu.push(this.saveMenuItem) + app.topMenu.push(this.saveAsMenuItem) + app.topMenu.push(this.loadMenuItem) - Object.assign(this.plan, structuredClone(EMPTY_SEQUENCE_PLAN)) - this.add() - }, - }) - app.topMenu.push({ - icon: 'mdi mdi-content-save', - label: 'Save', - command: async () => { - const file = await electron.saveJson({ path: this.savedPath, json: this.plan }) - - if (file !== false) { - this.afterSavedJsonFile(file) - } - }, - }) - app.topMenu.push({ - icon: 'mdi mdi-content-save-edit', - label: 'Save as', - command: async () => { - const file = await electron.saveJson({ json: this.plan }) - - if (file !== false) { - this.afterSavedJsonFile(file) - } - }, - }) - app.topMenu.push({ - icon: 'mdi mdi-folder-open', - label: 'Load', - command: async () => { - const file = await electron.openJson() - - if (file !== false) { - this.loadSavedJsonFile(file) - } - }, - }) + app.beforeClose = async () => { + if (!this.saveMenuItem.disabled) { + return !(await angularService.confirm('Are you sure you want to close the window? Please make sure to save before exiting to avoid losing any important changes.')) + } else { + return true + } + } - electron.on('CAMERA.UPDATED', (event) => { + electronService.on('CAMERA.UPDATED', (event) => { const camera = this.cameras.find((e) => e.id === event.device.id) if (camera) { ngZone.run(() => { Object.assign(camera, event.device) - this.updateEntriesFromCamera(this.camera) + this.updateSequencesFromCamera(camera) }) } }) - electron.on('MOUNT.UPDATED', (event) => { + electronService.on('MOUNT.UPDATED', (event) => { const mount = this.mounts.find((e) => e.id === event.device.id) if (mount) { @@ -199,7 +191,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } }) - electron.on('WHEEL.UPDATED', (event) => { + electronService.on('WHEEL.UPDATED', (event) => { const wheel = this.wheels.find((e) => e.id === event.device.id) if (wheel) { @@ -209,7 +201,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } }) - electron.on('FOCUSER.UPDATED', (event) => { + electronService.on('FOCUSER.UPDATED', (event) => { const focuser = this.focusers.find((e) => e.id === event.device.id) if (focuser) { @@ -219,7 +211,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } }) - electron.on('ROTATOR.UPDATED', (event) => { + electronService.on('ROTATOR.UPDATED', (event) => { const rotator = this.rotators.find((e) => e.id === event.device.id) if (rotator) { @@ -229,7 +221,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } }) - electron.on('SEQUENCER.ELAPSED', (event) => { + electronService.on('SEQUENCER.ELAPSED', (event) => { ngZone.run(() => { if (this.running !== event.remainingTime > 0) { this.enableOrDisableTopbarMenu(event.remainingTime <= 0) @@ -246,14 +238,10 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } }) }) - - for (const p of SEQUENCE_ENTRY_PROPERTIES) { - this.availableEntryPropertiesToApply.set(p, true) - } } async ngAfterContentInit() { - this.pinger.register(this, 30000) + this.ticker.register(this, 30000) this.cameras = (await this.api.cameras()).sort(deviceComparator) this.mounts = (await this.api.mounts()).sort(deviceComparator) @@ -261,150 +249,140 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable this.focusers = (await this.api.focusers()).sort(deviceComparator) this.rotators = (await this.api.rotators()).sort(deviceComparator) - await this.loadSavedJsonFileFromPathOrAddDefault() + this.loadPreference() - // this.route.queryParams.subscribe(e => { }) + await this.loadPlanFromPath() } @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) } - async ping() { - if (this.camera?.id) await this.api.cameraListen(this.camera) - if (this.mount?.id) await this.api.mountListen(this.mount) - if (this.focuser?.id) await this.api.focuserListen(this.focuser) - if (this.wheel?.id) await this.api.wheelListen(this.wheel) - if (this.rotator?.id) await this.api.rotatorListen(this.rotator) + async tick() { + if (this.plan.camera?.id) await this.api.cameraListen(this.plan.camera) + if (this.plan.mount?.id) await this.api.mountListen(this.plan.mount) + if (this.plan.focuser?.id) await this.api.focuserListen(this.plan.focuser) + if (this.plan.wheel?.id) await this.api.wheelListen(this.plan.wheel) + if (this.plan.rotator?.id) await this.api.rotatorListen(this.plan.rotator) } - private enableOrDisableTopbarMenu(enable: boolean) { - this.app.topMenu.forEach((e) => (e.disabled = !enable)) + private enableOrDisableTopbarMenu(enabled: boolean) { + this.createNewMenuItem.disabled = !enabled + this.loadMenuItem.disabled = !enabled } - add() { - const camera = this.camera ?? (this.cameras[0] as Undefinable) - // const wheel = this.wheel ?? this.wheels[0] - // const focuser = this.focuser ?? this.focusers[0] - // const rotator = this.rotator ?? this.rotators[0] + protected add() { + const camera = this.plan.camera - this.plan.entries.push({ - enabled: true, - exposureTime: 1000000, - exposureAmount: 1, - exposureDelay: 0, + const sequence: Sequence = { + ...structuredClone(DEFAULT_SEQUENCE), x: camera?.minX ?? 0, y: camera?.minY ?? 0, width: camera?.maxWidth ?? 0, height: camera?.maxHeight ?? 0, - frameType: 'LIGHT', - binX: 1, - binY: 1, - gain: 0, - offset: 0, frameFormat: camera?.frameFormats[0], - autoSave: true, - autoSubFolderMode: 'OFF', - dither: { - enabled: false, - amount: 0, - raOnly: false, - afterExposures: 0, - }, - liveStacking: { - enabled: false, - type: 'SIRIL', - executablePath: '', - use32Bits: false, - slot: 1, - }, - namingFormat: this.plan.namingFormat, - }) + } - this.savePlan() - } + if (camera?.connected) { + updateCameraStartCaptureFromCamera(sequence, camera) + } - drop(event: CdkDragDrop) { - moveItemInArray(this.plan.entries, event.previousIndex, event.currentIndex) + this.plan.sequences.push(sequence) + + this.savePreference() } - private afterSavedJsonFile(file: JsonFile) { - if (file.path) { - this.updateSavedPath(file.path) + protected drop(event: CdkDragDrop) { + if (event.previousIndex !== event.currentIndex) { + moveItemInArray(this.plan.sequences, event.previousIndex, event.currentIndex) + this.savePreference() } } - private loadSavedJsonFile(file: JsonFile) { - if (this.loadPlan(file.json)) { - this.afterSavedJsonFile(file) - } else { - this.prime.message(`No entry found for the saved Sequence at: ${file.path}`, 'warn') - + private loadPlanFromJson(file: JsonFile) { + if (!this.loadPlan(file.json)) { + this.angularService.message('No sequence found', 'warn') this.add() } - } - private async loadSavedJsonFileFromPathOrAddDefault() { - const sequencerPreference = this.preference.sequencerPreference.get() + this.preference.loadPath = file.path + this.savePreference() + + this.app.subTitle = file.path + this.saveMenuItem.visible = !!file.path + this.saveMenuItem.disabled = true + } - if (sequencerPreference.savedPath) { - const file = await this.electron.readJson(sequencerPreference.savedPath) + private async loadPlanFromPath() { + if (this.preference.loadPath) { + const file = await this.electronService.readJson(this.preference.loadPath) - if (file !== false) { - this.loadSavedJsonFile(file) + if (file !== false && file.path) { + this.loadPlanFromJson(file) return } - this.prime.message(`Failed to load the saved Sequence at: ${sequencerPreference.savedPath}`, 'error') + this.angularService.message('Failed to load the file', 'error') - sequencerPreference.savedPath = undefined - this.preference.sequencerPreference.set(sequencerPreference) + this.preference.loadPath = undefined + this.savePreference() } - if (!this.loadPlan()) { + this.saveMenuItem.visible = false + + if (!this.loadPlan(this.plan)) { this.add() } } - private updateSavedPath(savedPath?: string) { - this.savedPath = savedPath - const sequencerPreference = this.preference.sequencerPreference.get() - sequencerPreference.savedPath = savedPath - this.preference.sequencerPreference.set(sequencerPreference) - } + private loadPlan(plan: SequencerPlan) { + if (this.plan !== plan) { + Object.assign(this.plan, plan) + } - private loadPlan(plan?: SequencePlan) { - const sequencerPreference = this.preference.sequencerPreference.get() + this.plan.camera = this.cameras.find((e) => e.id === plan.camera?.id) + this.plan.mount = this.mounts.find((e) => e.id === plan.mount?.id) + this.plan.wheel = this.wheels.find((e) => e.id === plan.wheel?.id) + this.plan.focuser = this.focusers.find((e) => e.id === plan.focuser?.id) + this.plan.rotator = this.rotators.find((e) => e.id === plan.rotator?.id) - plan ??= sequencerPreference.plan + const settings = this.preferenceService.settings.get() + cameraCaptureNamingFormatWithDefault(this.plan.namingFormat, settings.namingFormat) - if (this.plan !== plan) { - Object.assign(this.plan, structuredClone(plan)) - } + return this.plan.sequences.length + } - this.camera = this.cameras.find((e) => e.id === this.plan.camera) ?? this.cameras[0] - this.mount = this.mounts.find((e) => e.id === this.plan.mount) ?? this.mounts[0] - this.wheel = this.wheels.find((e) => e.id === this.plan.wheel) ?? this.wheels[0] - this.focuser = this.focusers.find((e) => e.id === this.plan.focuser) ?? this.focusers[0] - this.rotator = this.rotators.find((e) => e.id === this.plan.rotator) ?? this.rotators[0] + private async savePlanToJson(createNew: boolean) { + const path = createNew ? undefined : this.preference.loadPath + const file = await this.electronService.saveJson({ json: this.plan, path }) - const cameraCaptureNamingFormatPreference = this.preference.cameraCaptureNamingFormatPreference.get() - this.plan.namingFormat.light ??= cameraCaptureNamingFormatPreference.light ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.light - this.plan.namingFormat.dark ??= cameraCaptureNamingFormatPreference.dark ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.dark - this.plan.namingFormat.flat ??= cameraCaptureNamingFormatPreference.flat ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.flat - this.plan.namingFormat.bias ??= cameraCaptureNamingFormatPreference.bias ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.bias + if (file !== false) { + this.preference.loadPath = file.path + this.savePreference() - return this.plan.entries.length + this.app.subTitle = file.path + this.saveMenuItem.disabled = true + } } - resetCameraCaptureNamingFormat(type: FrameType) { - const namingFormatPreference = this.preference.cameraCaptureNamingFormatPreference.get() - resetCameraCaptureNamingFormat(type, this.plan.namingFormat, namingFormatPreference) - this.savePlan() + protected resetCameraCaptureNamingFormat(type?: FrameType) { + const settings = this.preferenceService.settings.get() + const cameraNamingFormat = this.plan.camera?.id ? this.preferenceService.camera(this.plan.camera).get().request.namingFormat : settings.namingFormat + + if (type) { + resetCameraCaptureNamingFormat(type, this.plan.namingFormat, cameraNamingFormat) + } else { + resetCameraCaptureNamingFormat('LIGHT', this.plan.namingFormat, cameraNamingFormat) + resetCameraCaptureNamingFormat('DARK', this.plan.namingFormat, cameraNamingFormat) + resetCameraCaptureNamingFormat('FLAT', this.plan.namingFormat, cameraNamingFormat) + resetCameraCaptureNamingFormat('BIAS', this.plan.namingFormat, cameraNamingFormat) + } + + this.savePreference() } - toggleAutoSubFolder() { + protected toggleAutoSubFolder() { if (!this.running) { switch (this.plan.autoSubFolderMode) { case 'OFF': @@ -418,175 +396,197 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable break } - this.savePlan() + this.savePreference() } } - async showCameraDialog(entry: CameraStartCapture) { - if (this.camera && (await CameraComponent.showAsDialog(this.browserWindow, 'SEQUENCER', this.camera, entry))) { - this.savePlan() + protected async showCameraDialog(sequence: Sequence) { + if (this.plan.camera && (await CameraComponent.showAsDialog(this.browserWindowService, 'SEQUENCER', this.plan.camera, sequence))) { + this.savePreference() } } - async showWheelDialog(entry: CameraStartCapture) { - if (this.wheel && (await FilterWheelComponent.showAsDialog(this.browserWindow, 'SEQUENCER', this.wheel, entry))) { - this.savePlan() + protected async showWheelDialog(sequence: Sequence) { + if (this.plan.wheel && (await FilterWheelComponent.showAsDialog(this.browserWindowService, 'SEQUENCER', this.plan.wheel, sequence))) { + this.savePreference() } } - async cameraChanged() { - await this.ping() - - this.updateEntriesFromCamera(this.camera) - } - - private updateEntriesFromCamera(camera?: Camera) { + private updateSequencesFromCamera(camera?: Camera) { if (camera?.connected) { - for (const entry of this.plan.entries) { - updateCameraStartCaptureFromCamera(entry, camera) + for (const sequence of this.plan.sequences) { + updateCameraStartCaptureFromCamera(sequence, camera) } } } - mountChanged() { - return this.ping() + protected async cameraChanged() { + if (this.plan.camera) { + await this.api.cameraListen(this.plan.camera) + this.updateSequencesFromCamera(this.plan.camera) + } + + this.savePreference() } - focuserChanged() { - return this.ping() + protected async mountChanged() { + if (this.plan.mount) { + await this.api.mountListen(this.plan.mount) + } + + this.savePreference() } - wheelChanged() { - return this.ping() + protected async focuserChanged() { + if (this.plan.focuser) { + await this.api.focuserListen(this.plan.focuser) + } + + this.savePreference() } - rotatorChanged() { - return this.ping() + protected async wheelChanged() { + if (this.plan.wheel) { + await this.api.wheelListen(this.plan.wheel) + } + + this.savePreference() } - savePlan() { - const sequencerPreference = this.preference.sequencerPreference.get() - sequencerPreference.savedPath = this.savedPath - this.plan.camera = this.camera?.id - this.plan.mount = this.mount?.id - this.plan.wheel = this.wheel?.id - this.plan.focuser = this.focuser?.id - this.plan.rotator = this.rotator?.id - Object.assign(sequencerPreference.plan, this.plan) - this.preference.sequencerPreference.set(sequencerPreference) + protected async rotatorChanged() { + if (this.plan.rotator) { + await this.api.rotatorListen(this.plan.rotator) + } + + this.savePreference() } - showEntryMenu(entry: CameraStartCapture, dialogMenu: DialogMenuComponent) { - this.entryToApply = entry - const index = this.plan.entries.indexOf(entry) + protected showSequenceMenu(sequence: Sequence, dialogMenu: DialogMenuComponent) { + this.property.sequence = sequence - this.entryMenuModel.forEach((e) => (e.visible = true)) + const index = this.plan.sequences.indexOf(sequence) + const lastIndex = this.plan.sequences.length - 1 - 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 - } + this.sequenceModel[1].visible = index >= 2 // ALL ABOBE + this.sequenceModel[2].visible = index >= 1 // ABOBE + this.sequenceModel[3].visible = index < lastIndex // BELOW + this.sequenceModel[4].visible = index < lastIndex - 1 // ALL BELOW + this.sequenceModel[0].visible = this.sequenceModel[2].visible && this.sequenceModel[3].visible - 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 + if (this.sequenceModel.find((e) => e.visible)) { + dialogMenu.show() } - - dialogMenu.show() } - updateAllAvailableEntryPropertiesToApply(selected: boolean) { - for (const p of SEQUENCE_ENTRY_PROPERTIES) { - this.availableEntryPropertiesToApply.set(p, selected) + protected selectSequenceProperty(selected: boolean) { + for (const [key] of Object.entries(this.property.properties)) { + this.property.properties[key as SequenceProperty] = selected } } - applyCameraStartCaptureToEntries() { - const source = this.entryToApply + protected copySequencePropertyToSequencies() { + const source = this.property.sequence + if (!source) return - const index = this.plan.entries.indexOf(source) - for (let count of this.entryToApplyCount) { + const index = this.plan.sequences.indexOf(source) + + for (const count of this.property.count) { if (index < 0 || count === 0) continue const below = Math.sign(count) - count = Math.abs(count) - - for (let i = 1; i <= count; i++) { + for (let i = 1; i <= Math.abs(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 + if (pos >= 0 && pos < this.plan.sequences.length) { + const dest = this.plan.sequences[pos] + + if (!dest.enabled || dest === source) continue + + if (this.property.properties.EXPOSURE_TIME) dest.exposureTime = source.exposureTime + if (this.property.properties.EXPOSURE_AMOUNT) dest.exposureAmount = source.exposureAmount + if (this.property.properties.EXPOSURE_DELAY) dest.exposureDelay = source.exposureDelay + if (this.property.properties.FRAME_TYPE) dest.frameType = source.frameType + if (this.property.properties.X) dest.x = source.x + if (this.property.properties.Y) dest.y = source.y + if (this.property.properties.WIDTH) dest.width = source.width + if (this.property.properties.HEIGHT) dest.height = source.height + if (this.property.properties.BIN) dest.binX = source.binX + if (this.property.properties.BIN) dest.binY = source.binY + if (this.property.properties.FRAME_FORMAT) dest.frameFormat = source.frameFormat + if (this.property.properties.GAIN) dest.gain = source.gain + if (this.property.properties.OFFSET) dest.offset = source.offset } else { break } } } - this.savePlan() + this.savePreference() - this.showEntryPropertiesToApplyDialog = false + this.property.showDialog = false } - deleteEntry(entry: CameraStartCapture, index: number) { - if (entry === this.plan.entries[index]) { - this.plan.entries.splice(index, 1) - this.savePlan() + protected deleteSequence(sequence: Sequence, index: number) { + if (sequence === this.plan.sequences[index]) { + this.plan.sequences.splice(index, 1) + this.savePreference() } } - duplicateEntry(entry: CameraStartCapture, index: number) { - this.plan.entries.splice(index + 1, 0, structuredClone(entry)) - this.savePlan() + protected duplicateSequence(sequence: Sequence, index: number) { + this.plan.sequences.splice(index + 1, 0, structuredClone(sequence)) + this.savePreference() } - async start() { - if (this.camera) { + protected filterRemoved(sequence: Sequence) { + sequence.filterPosition = 0 + this.savePreference() + } + + protected async start() { + if (this.plan.camera) { for (let i = 0; i < this.cameraExposures.length; i++) { this.cameraExposures.get(i)?.reset() } - this.savePlan() + await this.browserWindowService.openCameraImage(this.plan.camera, 'SEQUENCER') + await this.api.sequencerStart(this.plan.camera, this.plan) + } + } - await this.browserWindow.openCameraImage(this.camera, 'SEQUENCER') - await this.api.sequencerStart(this.camera, this.plan) + protected async pause() { + if (this.plan.camera) { + await this.api.sequencerPause(this.plan.camera) } } - pause() { - return this.api.sequencerPause(this.camera!) + protected async unpause() { + if (this.plan.camera) { + await this.api.sequencerUnpause(this.plan.camera) + } } - unpause() { - return this.api.sequencerUnpause(this.camera!) + protected async stop() { + if (this.plan.camera) { + await this.api.sequencerStop(this.plan.camera) + } } - stop() { - return this.api.sequencerStop(this.camera!) + private loadPreference() { + Object.assign(this.preference, this.preferenceService.sequencerPreference.get()) + this.plan = this.preference.plan + this.property.properties = this.preference.properties + + this.loadPlan(this.plan) + } + + protected savePreference() { + this.preferenceService.sequencerPreference.set(this.preference) + + if (this.preference.loadPath) { + this.saveMenuItem.disabled = false + } } } diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index a096e3ffa..999c36db7 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -18,9 +18,9 @@
- {{ location.name || '?' }} + {{ preference.location.name || '?' }}
@@ -44,7 +44,7 @@ tooltipPosition="bottom" (onClick)="addLocation()" />
+ [location]="preference.location" + (locationChange)="locationChanged($event)" />
@@ -73,7 +73,6 @@ optionsValue="value" [(ngModel)]="plateSolverType" styleClass="p-inputtext-sm border-0" - (ngModelChange)="plateSolvers.get(plateSolverType)!.type = $event; save()" [autoDisplayFirst]="false" /> @@ -82,12 +81,12 @@ class="col-12" *ngIf="plateSolverType !== 'ASTROMETRY_NET_ONLINE'"> + (pathChange)="savePreference()" /> @if (plateSolverType === 'ASTROMETRY_NET_ONLINE') {
@@ -95,8 +94,8 @@ + [(ngModel)]="plateSolver.apiUrl" + (ngModelChange)="savePreference()" />
@@ -105,8 +104,8 @@ + [(ngModel)]="plateSolver.apiKey" + (ngModelChange)="savePreference()" /> @@ -116,12 +115,12 @@ + spinnableNumber /> @@ -129,12 +128,12 @@ + spinnableNumber /> @@ -149,10 +148,10 @@ [showButtons]="true" class="w-full" styleClass="p-inputtext-sm border-0 w-full" - [ngModel]="plateSolvers.get(plateSolverType)!.slot" - (ngModelChange)="plateSolvers.get(plateSolverType)!.slot = $event; save()" + [(ngModel)]="plateSolver.slot" + (ngModelChange)="savePreference()" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber /> @@ -171,68 +170,29 @@ optionsValue="value" [(ngModel)]="starDetectorType" styleClass="p-inputtext-sm border-0" - (ngModelChange)="starDetectors.get(starDetectorType)!.type = $event; save()" [autoDisplayFirst]="false" />
-
-
- - - - -
-
- - - - + (pathChange)="savePreference()" />
+ spinnableNumber />
@@ -247,10 +207,10 @@ [showButtons]="true" class="w-full" styleClass="p-inputtext-sm border-0 w-full" - [ngModel]="starDetectors.get(starDetectorType)!.slot" - (ngModelChange)="starDetectors.get(starDetectorType)!.slot = $event; save()" + [(ngModel)]="starDetector.slot" + (ngModelChange)="savePreference()" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber /> @@ -268,19 +228,18 @@ optionsValue="value" [(ngModel)]="liveStackerType" styleClass="p-inputtext-sm border-0" - (ngModelChange)="liveStackers.get(liveStackerType)!.type = $event; save()" [autoDisplayFirst]="false" />
+ (pathChange)="savePreference()" />
+ spinnableNumber />
@@ -314,19 +273,18 @@ optionsValue="value" [(ngModel)]="stackerType" styleClass="p-inputtext-sm border-0" - (ngModelChange)="stackers.get(stackerType)!.type = $event; save()" [autoDisplayFirst]="false" />
+ (pathChange)="savePreference()" />
+ spinnableNumber />
@@ -357,8 +315,8 @@ + [(ngModel)]="preference.namingFormat.light" + (ngModelChange)="savePreference()" /> + [(ngModel)]="preference.namingFormat.dark" + (ngModelChange)="savePreference()" /> + [(ngModel)]="preference.namingFormat.flat" + (ngModelChange)="savePreference()" /> + [(ngModel)]="preference.namingFormat.bias" + (ngModelChange)="savePreference()" /> () - - starDetectorType: StarDetectorType = 'ASTAP' - readonly starDetectors = new Map() + private readonly locationChangePublisher = new Subject() + private readonly locationChangeSubscription?: Subscription - liveStackerType: LiveStackerType = 'SIRIL' - readonly liveStackers = new Map() + get plateSolver() { + return this.preference.plateSolver[this.plateSolverType] + } - stackerType: StackerType = 'PIXINSIGHT' - readonly stackers = new Map() + get starDetector() { + return this.preference.starDetector[this.starDetectorType] + } - readonly cameraCaptureNamingFormat = structuredClone(DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT) + get liveStacker() { + return this.preference.liveStacker[this.liveStackerType] + } - private readonly locationChangePublisher = new Subject() - private readonly locationChangeSubscription?: Subscription + get stacker() { + return this.preference.stacker[this.stackerType] + } constructor( app: AppComponent, - private readonly preference: PreferenceService, - private readonly electron: ElectronService, - private readonly dropdownOptions: DropdownOptionsPipe, + private readonly preferenceService: PreferenceService, + private readonly electronService: ElectronService, ) { app.title = 'Settings' - this.locations = preference.locations.get() - const selectedLocation = preference.selectedLocation.get(this.locations[0]) - this.location = this.locations.find(e => e.id === selectedLocation.id) ?? this.locations[0] - - for (const type of dropdownOptions.transform('PLATE_SOLVER')) { - this.plateSolvers.set(type, preference.plateSolverRequest(type).get()) - } - for (const type of dropdownOptions.transform('STAR_DETECTOR')) { - this.starDetectors.set(type, preference.starDetectionRequest(type).get()) - } - for (const type of dropdownOptions.transform('LIVE_STACKER')) { - this.liveStackers.set(type, preference.liveStackingRequest(type).get()) - } - for (const type of dropdownOptions.transform('STACKER')) { - this.stackers.set(type, preference.stackingRequest(type).get()) - } - - Object.assign(this.cameraCaptureNamingFormat, preference.cameraCaptureNamingFormatPreference.get(this.cameraCaptureNamingFormat)) - this.locationChangeSubscription = this.locationChangePublisher.pipe(debounceTime(2000)).subscribe((location) => { - return this.electron.send('LOCATION.CHANGED', location) + return this.electronService.locationChanged(location) }) } + ngAfterViewInit() { + this.loadPreference() + } + + @HostListener('window:unload') ngOnDestroy() { this.locationChangeSubscription?.unsubscribe() } - addLocation() { - const location = structuredClone(EMPTY_LOCATION) + protected addLocation() { + const location = structuredClone(DEFAULT_LOCATION) location.id = +new Date() - this.locations.push(location) - this.location = location - this.save() - this.locationChangePublisher.next(this.location) + this.preference.locations.push(location) + this.locationChanged(location) } - deleteLocation() { - if (this.locations.length > 1) { - const index = this.locations.findIndex((e) => e.id === this.location.id) + protected deleteLocation() { + if (this.preference.locations.length > 1) { + const index = this.preference.locations.findIndex((e) => e.id === this.preference.location.id) if (index >= 0) { - this.locations.splice(index, 1) - this.location = this.locations[0]! - - this.save() - this.locationChangePublisher.next(this.location) + this.preference.locations.splice(index, 1) + this.locationChanged(this.preference.locations[0]) } } } - locationChanged() { - console.log(this.locations) - this.save() - this.locationChangePublisher.next(this.location) + protected locationChanged(location?: Location) { + if (location) { + this.preference.location = location + this.savePreference() + this.locationChangePublisher.next(location) + } } - resetCameraCaptureNamingFormat(type: FrameType) { - resetCameraCaptureNamingFormat(type, this.cameraCaptureNamingFormat, DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT) - this.save() + protected resetCameraCaptureNamingFormat(type: FrameType) { + resetCameraCaptureNamingFormat(type, this.preference.namingFormat, DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT) + this.savePreference() } - save() { - if (this.location.name) { - this.preference.locations.set(this.locations) - this.preference.selectedLocation.set(this.location) - } - - for (const type of this.dropdownOptions.transform('PLATE_SOLVER')) { - this.preference.plateSolverRequest(type).set(this.plateSolvers.get(type)) - } - for (const type of this.dropdownOptions.transform('STAR_DETECTOR')) { - this.preference.starDetectionRequest(type).set(this.starDetectors.get(type)) - } - for (const type of this.dropdownOptions.transform('LIVE_STACKER')) { - this.preference.liveStackingRequest(type).set(this.liveStackers.get(type)) - } - for (const type of this.dropdownOptions.transform('STACKER')) { - this.preference.stackingRequest(type).set(this.stackers.get(type)) - } + private loadPreference() { + Object.assign(this.preference, this.preferenceService.settings.get()) + this.preference.location = this.preference.locations.find((e) => e.id === this.preference.location.id) ?? this.preference.locations[0] + } - this.preference.cameraCaptureNamingFormatPreference.set(this.cameraCaptureNamingFormat) + protected savePreference() { + this.preferenceService.settings.set(this.preference) } } diff --git a/desktop/src/app/stacker/stacker.component.html b/desktop/src/app/stacker/stacker.component.html index 6f5cc6800..4cc290230 100644 --- a/desktop/src/app/stacker/stacker.component.html +++ b/desktop/src/app/stacker/stacker.component.html @@ -1,7 +1,7 @@
+ emptyMessage="No frames"> @@ -109,7 +109,7 @@ [disabled]="!request.darkEnabled" [directory]="false" label="Dark File" - key="STACKER_DARK_PATH" + key="stacker.darkPath" [(path)]="request.darkPath" class="w-full" (pathChange)="savePreference()" /> @@ -123,7 +123,7 @@ [disabled]="!request.flatEnabled" [directory]="false" label="Flat File" - key="STACKER_FLAT_PATH" + key="stacker.flatPath" [(path)]="request.flatPath" class="w-full" (pathChange)="savePreference()" /> @@ -137,7 +137,7 @@ [disabled]="!request.biasEnabled" [directory]="false" label="Bias File" - key="STACKER_BIAS_PATH" + key="stacker.biasPath" [(path)]="request.biasPath" class="w-full" (pathChange)="savePreference()" /> diff --git a/desktop/src/app/stacker/stacker.component.ts b/desktop/src/app/stacker/stacker.component.ts index 9464cb456..35ed8c021 100644 --- a/desktop/src/app/stacker/stacker.component.ts +++ b/desktop/src/app/stacker/stacker.component.ts @@ -1,19 +1,22 @@ -import { AfterViewInit, Component } from '@angular/core' +import { AfterViewInit, Component, HostListener, OnDestroy } from '@angular/core' import { dirname } from 'path' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { EMPTY_STACKING_REQUEST, StackingRequest, StackingTarget } from '../../shared/types/stacker.types' +import { DEFAULT_STACKER_PREFERENCE, StackingRequest, StackingTarget } from '../../shared/types/stacker.types' import { AppComponent } from '../app.component' @Component({ selector: 'neb-stacker', templateUrl: './stacker.component.html', }) -export class StackerComponent implements AfterViewInit { - running = false - readonly request = structuredClone(EMPTY_STACKING_REQUEST) +export class StackerComponent implements AfterViewInit, OnDestroy { + protected running = false + protected readonly preference = structuredClone(DEFAULT_STACKER_PREFERENCE) + protected request = this.preference.request + + private frameId = '' get referenceTarget() { return this.request.targets.find((e) => e.enabled && e.reference && e.type === 'LIGHT') @@ -29,10 +32,10 @@ export class StackerComponent implements AfterViewInit { constructor( app: AppComponent, - private readonly electron: ElectronService, + private readonly electronService: ElectronService, private readonly api: ApiService, - private readonly preference: PreferenceService, - private readonly browserWindow: BrowserWindowService, + private readonly preferenceService: PreferenceService, + private readonly browserWindowService: BrowserWindowService, ) { app.title = 'Stacker' } @@ -43,12 +46,16 @@ export class StackerComponent implements AfterViewInit { this.running = await this.api.stackerIsRunning() } - async openImages() { + @HostListener('window:unload') + ngOnDestroy() { + void this.closeFrameWindow() + } + + protected async openImages() { try { this.running = true - const stackerPreference = this.preference.stackerPreference.get() - const images = await this.electron.openImages({ defaultPath: stackerPreference.defaultPath }) + const images = await this.electronService.openImages({ defaultPath: this.preference.defaultPath }) if (images && images.length) { const targets: StackingTarget[] = [...this.request.targets] @@ -70,15 +77,15 @@ export class StackerComponent implements AfterViewInit { this.request.targets = targets - stackerPreference.defaultPath = dirname(images[0]) - this.preference.stackerPreference.set(stackerPreference) + this.preference.defaultPath = dirname(images[0]) + this.savePreference() } } finally { this.running = false } } - referenceChanged(target: StackingTarget, enabled: boolean) { + protected referenceChanged(target: StackingTarget, enabled: boolean) { if (enabled) { for (const item of this.request.targets) { if (item.reference && item !== target) { @@ -88,11 +95,11 @@ export class StackerComponent implements AfterViewInit { } } - openTargetImage(target: StackingTarget) { - return this.browserWindow.openImage({ path: target.path, id: 'stacker', source: 'PATH' }) + protected async openTargetImage(target: StackingTarget) { + this.frameId = await this.browserWindowService.openImage({ path: target.path, id: 'stacker', source: 'PATH' }) } - deleteTarget(target: StackingTarget) { + protected deleteTarget(target: StackingTarget) { const index = this.request.targets.findIndex((e) => e === target) if (index >= 0) { @@ -100,14 +107,19 @@ export class StackerComponent implements AfterViewInit { } } - async startStacking() { - const stackingRequest = this.preference.stackingRequest(this.request.type).get() - this.request.executablePath = stackingRequest.executablePath - this.request.slot = stackingRequest.slot || 1 - this.request.referencePath = this.referenceTarget!.path + private async closeFrameWindow() { + if (this.frameId) { + await this.electronService.closeWindow(undefined, this.frameId) + } + } + + protected async startStacking() { + const settings = this.preferenceService.settings.get() const request: StackingRequest = { ...this.request, + ...settings.stacker[this.request.type], + referencePath: this.referenceTarget!.path, targets: this.request.targets.filter((e) => e.enabled), } @@ -118,40 +130,23 @@ export class StackerComponent implements AfterViewInit { const path = await this.api.stackerStart(request) if (path) { - await this.browserWindow.openImage({ path, source: 'STACKER' }) + await this.browserWindowService.openImage({ path, source: 'STACKER' }) } } finally { this.running = false } } - stopStacking() { + protected stopStacking() { return this.api.stackerStop() } private loadPreference() { - const stackerPreference = this.preference.stackerPreference.get() - - this.request.outputDirectory = stackerPreference.outputDirectory ?? '' - this.request.darkPath = stackerPreference.darkPath - this.request.darkEnabled = stackerPreference.darkEnabled ?? false - this.request.flatPath = stackerPreference.flatPath - this.request.flatEnabled = stackerPreference.flatEnabled ?? false - this.request.biasPath = stackerPreference.biasPath - this.request.biasEnabled = stackerPreference.biasEnabled ?? false - this.request.type = stackerPreference.type ?? 'PIXINSIGHT' + Object.assign(this.preference, this.preferenceService.stacker.get()) + this.request = this.preference.request } - savePreference() { - const stackerPreference = this.preference.stackerPreference.get() - stackerPreference.outputDirectory = this.request.outputDirectory - stackerPreference.darkPath = this.request.darkPath - stackerPreference.darkEnabled = this.request.darkEnabled - stackerPreference.flatPath = this.request.flatPath - stackerPreference.flatEnabled = this.request.flatEnabled - stackerPreference.biasPath = this.request.biasPath - stackerPreference.biasEnabled = this.request.biasEnabled - stackerPreference.type = this.request.type - this.preference.stackerPreference.set(stackerPreference) + protected savePreference() { + this.preferenceService.stacker.set(this.preference) } } diff --git a/desktop/src/assets/data/.gitignore b/desktop/src/assets/data/.gitignore new file mode 100644 index 000000000..7850aa44f --- /dev/null +++ b/desktop/src/assets/data/.gitignore @@ -0,0 +1 @@ +nebulosa.json diff --git a/desktop/src/assets/icons/calibration.png b/desktop/src/assets/icons/calibration.png new file mode 100644 index 000000000..77f50c6ae Binary files /dev/null and b/desktop/src/assets/icons/calibration.png differ diff --git a/desktop/src/assets/icons/photo-filter.png b/desktop/src/assets/icons/photo-filter.png deleted file mode 100644 index d3f7a892d..000000000 Binary files a/desktop/src/assets/icons/photo-filter.png and /dev/null differ 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 e01882614..1c51b1a50 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_STEP_INFO } from '../../types/camera.types' +import { CameraCaptureEvent, CameraCaptureState, DEFAULT_CAMERA_CAPTURE_INFO, DEFAULT_CAMERA_STEP_INFO } from '../../types/camera.types' @Component({ selector: 'neb-camera-exposure', @@ -8,18 +8,22 @@ import { CameraCaptureEvent, CameraCaptureState, EMPTY_CAMERA_CAPTURE_INFO, EMPT }) export class CameraExposureComponent { @Input() - info?: string + protected info?: string @Input() - showRemainingTime: boolean = true + protected showRemainingTime: boolean = true @Input() - readonly step = structuredClone(EMPTY_CAMERA_STEP_INFO) + protected readonly step = structuredClone(DEFAULT_CAMERA_STEP_INFO) @Input() - readonly capture = structuredClone(EMPTY_CAMERA_CAPTURE_INFO) + protected readonly capture = structuredClone(DEFAULT_CAMERA_CAPTURE_INFO) - state?: CameraCaptureState = 'IDLE' + protected state: CameraCaptureState = 'IDLE' + + get currentState() { + return this.state + } handleCameraCaptureEvent(event: CameraCaptureEvent, looping: boolean = false) { this.capture.elapsedTime = event.captureElapsedTime @@ -50,13 +54,13 @@ export class CameraExposureComponent { this.state = event.state } - return this.state !== undefined && this.state !== 'CAPTURE_FINISHED' && this.state !== 'IDLE' + return this.state !== 'CAPTURE_FINISHED' && this.state !== 'IDLE' } reset() { this.state = 'IDLE' - Object.assign(this.step, EMPTY_CAMERA_STEP_INFO) - Object.assign(this.capture, EMPTY_CAMERA_CAPTURE_INFO) + Object.assign(this.step, DEFAULT_CAMERA_STEP_INFO) + Object.assign(this.capture, DEFAULT_CAMERA_CAPTURE_INFO) } } diff --git a/desktop/src/shared/components/camera-info/camera-info.component.html b/desktop/src/shared/components/camera-info/camera-info.component.html index bd1ffe2ed..3444ae02c 100644 --- a/desktop/src/shared/components/camera-info/camera-info.component.html +++ b/desktop/src/shared/components/camera-info/camera-info.component.html @@ -1,56 +1,64 @@
- {{ info.frameType }} - + class="flex flex-column align-items-center"> + + {{ info.frameType }}
- {{ info.exposureAmount || '∞' }} / {{ info.exposureTime | exposureTime }} - + class="flex flex-column align-items-center"> + + {{ info.exposureAmount || '∞' }} / {{ info.exposureTime | exposureTime }}
- {{ info.exposureDelay * 1000000 | exposureTime }} - + class="flex flex-column align-items-center"> + + {{ info.exposureDelay * 1000000 | exposureTime }}
- {{ info.x }} {{ info.y }} {{ info.width }} {{ info.height }} - + class="flex flex-column align-items-center"> + + {{ info.x }} {{ info.y }} {{ info.width }} {{ info.height }}
- {{ info.binX }}x{{ info.binY }} - + class="flex flex-column align-items-center"> + + {{ info.binX }}x{{ info.binY }}
- {{ info.gain }} - + class="flex flex-column align-items-center"> + + {{ info.gain }}
- {{ info.offset }} - + class="flex flex-column align-items-center"> + + {{ info.offset }}
- {{ info.frameFormat }} - + class="flex flex-column align-items-center"> + + {{ info.frameFormat }}
- {{ filter }} - + class="flex flex-row gap-1 align-items-center"> +
+ + {{ filter }} +
+
diff --git a/desktop/src/shared/components/camera-info/camera-info.component.scss b/desktop/src/shared/components/camera-info/camera-info.component.scss deleted file mode 100644 index a608020ad..000000000 --- a/desktop/src/shared/components/camera-info/camera-info.component.scss +++ /dev/null @@ -1,28 +0,0 @@ -.tag { - position: relative; - display: flex; - align-items: end; - border-radius: 4px; - - span { - display: block; - text-align: center; - margin-top: 9px; - font-size: 0.875rem !important; - } - - label { - border-radius: 2px; - font-size: 9px !important; - font-weight: bold; - top: -0.5rem !important; - background-color: #151515d0; - padding: 2px 4px; - position: absolute; - pointer-events: none; - left: 50%; - transform: translate(-50%, 0%); - display: block; - width: max-content; - } -} diff --git a/desktop/src/shared/components/camera-info/camera-info.component.ts b/desktop/src/shared/components/camera-info/camera-info.component.ts index dd180f422..c3472e027 100644 --- a/desktop/src/shared/components/camera-info/camera-info.component.ts +++ b/desktop/src/shared/components/camera-info/camera-info.component.ts @@ -1,24 +1,30 @@ -import { Component, Input } from '@angular/core' +import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core' import { CameraStartCapture } from '../../types/camera.types' -import { FilterWheel } from '../../types/wheel.types' +import { Wheel } from '../../types/wheel.types' @Component({ selector: 'neb-camera-info', templateUrl: './camera-info.component.html', - styleUrls: ['./camera-info.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class CameraInfoComponent { @Input({ required: true }) - readonly info!: CameraStartCapture + protected readonly info!: CameraStartCapture @Input() - readonly wheel?: FilterWheel + protected readonly wheel?: Wheel @Input() - readonly hasType: boolean = true + protected readonly hasType: boolean = true @Input() - readonly hasExposure: boolean = true + protected readonly hasExposure: boolean = true + + @Input() + protected readonly canRemoveFilter = false + + @Output() + protected readonly filterRemoved = new EventEmitter() get hasFilter() { return !!this.wheel && !!this.info.filterPosition && this.wheel.connected diff --git a/desktop/src/shared/components/device-chooser/device-chooser.component.scss b/desktop/src/shared/components/device-chooser/device-chooser.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/shared/components/device-chooser/device-chooser.component.ts b/desktop/src/shared/components/device-chooser/device-chooser.component.ts index 3af1562c6..c77fa36f4 100644 --- a/desktop/src/shared/components/device-chooser/device-chooser.component.ts +++ b/desktop/src/shared/components/device-chooser/device-chooser.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core' import { ApiService } from '../../services/api.service' import { Device } from '../../types/device.types' import { Undefinable } from '../../utils/types' @@ -8,26 +8,26 @@ import { MenuItem } from '../menu-item/menu-item.component' @Component({ selector: 'neb-device-chooser', templateUrl: './device-chooser.component.html', - styleUrls: ['./device-chooser.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class DeviceChooserComponent { @Input({ required: true }) - readonly title!: string + protected readonly title!: string @Input() - readonly noDeviceMessage?: string + protected readonly noDeviceMessage?: string @Input({ required: true }) - readonly icon!: string + protected readonly icon!: string @Input({ required: true }) - readonly devices!: T[] + protected readonly devices!: T[] @Input() - readonly hasNone: boolean = false + protected readonly hasNone: boolean = false @Input() - device?: T + protected device?: T @Output() readonly deviceChange = new EventEmitter() @@ -67,48 +67,66 @@ export class DeviceChooserComponent { } static async handleConnectDevice(api: ApiService, device: Device, item: MenuItem) { + if (device.connected) return undefined + await api.indiDeviceConnect(device) item.disabled = true - return new Promise>((resolve) => { - setTimeout(async () => { + return new Promise((resolve) => { + let counter = 0 + + const timer = setInterval(async () => { Object.assign(device, await api.indiDevice(device)) if (device.connected) { item.icon = 'mdi mdi-close' item.severity = 'danger' item.label = 'Disconnect' + clearInterval(timer) resolve({ device, item }) - } else { + } else if (counter >= 10) { + clearInterval(timer) resolve(undefined) + } else { + counter++ + return } item.disabled = false - }, 1000) + }, 1500) }) } static async handleDisconnectDevice(api: ApiService, device: Device, item: MenuItem) { + if (!device.connected) return undefined + await api.indiDeviceDisconnect(device) item.disabled = true return new Promise>((resolve) => { - setTimeout(async () => { + let counter = 0 + + const timer = setTimeout(async () => { Object.assign(device, await api.indiDevice(device)) if (!device.connected) { item.icon = 'mdi mdi-connection' item.severity = 'info' item.label = 'Connect' + clearInterval(timer) resolve({ device, item }) - } else { + } else if (counter >= 10) { + clearInterval(timer) resolve(undefined) + } else { + counter++ + return } item.disabled = false - }, 1000) + }, 1500) }) } } diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss index 5581d9d1b..ef8a048d3 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss @@ -1,5 +1,5 @@ -:host { - ::ng-deep .p-menuitem-link { +neb-device-list-menu { + .p-menuitem-link { padding: 0.5rem 0.75rem; min-height: 43px; } 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 5af2dc2b5..5d879ee0e 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts @@ -1,6 +1,6 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core' import { SEPARATOR_MENU_ITEM } from '../../constants' -import { PrimeService } from '../../services/prime.service' +import { AngularService } from '../../services/angular.service' import { isGuideHead } from '../../types/camera.types' import { Device } from '../../types/device.types' import { deviceComparator } from '../../utils/comparators' @@ -17,22 +17,26 @@ export interface DeviceConnectionCommandEvent { selector: 'neb-device-list-menu', templateUrl: './device-list-menu.component.html', styleUrls: ['./device-list-menu.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class DeviceListMenuComponent { @Input() - readonly model: SlideMenuItem[] = [] + protected readonly model: SlideMenuItem[] = [] @Input() - readonly modelAtFirst: boolean = true + protected readonly modelAtFirst: boolean = true @Input() - readonly disableIfDeviceIsNotConnected: boolean = true + protected readonly disableIfDeviceIsNotConnected: boolean = true @Input() - header?: string + protected header?: string @Input() - readonly hasNone: boolean = false + protected readonly hasNone: boolean = false + + @Input() + protected readonly toolbarBuilder?: (device: Device) => MenuItem[] @Output() readonly deviceConnect = new EventEmitter() @@ -43,15 +47,17 @@ export class DeviceListMenuComponent { @ViewChild('menu') private readonly menu!: DialogMenuComponent - constructor(private readonly prime: PrimeService) {} + constructor(private readonly angularService: AngularService) {} - show(devices: T[], selected?: NoInfer) { + show(devices: T[], selected?: NoInfer, header?: string) { const model: SlideMenuItem[] = [] + if (header) this.header = header + return new Promise>((resolve) => { if (devices.length <= 0) { resolve(undefined) - this.prime.message('Please connect your equipment first!', 'warn') + this.angularService.message('Please connect your equipment first!', 'warn') return } @@ -92,12 +98,15 @@ export class DeviceListMenuComponent { } for (const device of devices.sort(deviceComparator)) { + const toolbarMenu = this.toolbarBuilder?.(device) ?? [] + model.push({ label: device.name, selected: selected === device, disabled: this.disableIfDeviceIsNotConnected && !device.connected, slideMenu: [], toolbarMenu: [ + ...toolbarMenu, { icon: 'mdi ' + (device.connected ? 'mdi-close' : 'mdi-connection'), severity: device.connected ? 'danger' : 'info', @@ -122,8 +131,7 @@ export class DeviceListMenuComponent { populateWithModel() } - this.menu.model = model - this.menu.show() + this.menu.show(model) }) } diff --git a/desktop/src/shared/components/dialog-menu/dialog-menu.component.html b/desktop/src/shared/components/dialog-menu/dialog-menu.component.html index 1bd0f9b85..453f951ab 100644 --- a/desktop/src/shared/components/dialog-menu/dialog-menu.component.html +++ b/desktop/src/shared/components/dialog-menu/dialog-menu.component.html @@ -10,9 +10,9 @@ (onHide)="hide()" [style]="{ width: 'auto' }"> - {{ header }} + {{ currentHeader }} () @Input() - model: SlideMenuItem[] = [] + protected model: SlideMenuItem[] = [] @Input() - header?: string + protected header?: string @Input() - updateHeaderWithMenuLabel: boolean = true + protected updateHeaderWithMenuLabel: boolean = true + protected currentHeader = this.header private readonly navigationHeader: Undefinable[] = [] - show() { + show(model?: SlideMenuItem[]) { + if (model?.length) this.model = model + this.currentHeader = this.header this.visible = true this.visibleChange.emit(true) } @@ -36,14 +40,14 @@ export class DialogMenuComponent { this.visibleChange.emit(false) } - next(event: MenuItemCommandEvent) { + protected next(event: MenuItemCommandEvent) { if (!event.item?.slideMenu?.length) { this.hide() } else { - this.navigationHeader.push(this.header) + this.navigationHeader.push(this.currentHeader) if (this.updateHeaderWithMenuLabel) { - this.header = event.item.label + this.currentHeader = event.item.label } } } @@ -53,7 +57,7 @@ export class DialogMenuComponent { const header = this.navigationHeader.splice(this.navigationHeader.length - 1, 1)[0] if (this.updateHeaderWithMenuLabel) { - this.header = header + this.currentHeader = header } } } diff --git a/desktop/src/shared/components/histogram/histogram.component.scss b/desktop/src/shared/components/histogram/histogram.component.scss deleted file mode 100644 index a5051dfe6..000000000 --- a/desktop/src/shared/components/histogram/histogram.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - position: relative; -} - -.minX, -.maxX { - bottom: -12px; -} diff --git a/desktop/src/shared/components/histogram/histogram.component.ts b/desktop/src/shared/components/histogram/histogram.component.ts index 1b78ff066..742910ff7 100644 --- a/desktop/src/shared/components/histogram/histogram.component.ts +++ b/desktop/src/shared/components/histogram/histogram.component.ts @@ -1,9 +1,9 @@ -import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core' +import { AfterViewInit, Component, ElementRef, ViewChild, ViewEncapsulation } from '@angular/core' @Component({ selector: 'neb-histogram', templateUrl: './histogram.component.html', - styleUrls: ['./histogram.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class HistogramComponent implements AfterViewInit { @ViewChild('canvas') diff --git a/desktop/src/shared/dialogs/location/location.dialog.html b/desktop/src/shared/components/location/location.dialog.html similarity index 95% rename from desktop/src/shared/dialogs/location/location.dialog.html rename to desktop/src/shared/components/location/location.dialog.html index e7b01f44e..8bb727601 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.html +++ b/desktop/src/shared/components/location/location.dialog.html @@ -21,7 +21,7 @@ (ngModelChange)="locationChanged()" [showButtons]="true" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber />
@@ -37,7 +37,7 @@ (ngModelChange)="locationChanged()" [showButtons]="true" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber />
@@ -52,7 +52,7 @@ [(ngModel)]="location.latitude" (ngModelChange)="locationChanged()" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber /> @@ -67,7 +67,7 @@ [(ngModel)]="location.longitude" (ngModelChange)="locationChanged()" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber /> diff --git a/desktop/src/shared/dialogs/location/location.dialog.ts b/desktop/src/shared/components/location/location.dialog.ts similarity index 68% rename from desktop/src/shared/dialogs/location/location.dialog.ts rename to desktop/src/shared/components/location/location.dialog.ts index 1b7e979b8..90a7ebef0 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.ts +++ b/desktop/src/shared/components/location/location.dialog.ts @@ -1,16 +1,15 @@ import { AfterViewInit, Component, EventEmitter, Input, Optional, Output, ViewChild } from '@angular/core' import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog' -import { MapComponent } from '../../components/map/map.component' -import { EMPTY_LOCATION, Location } from '../../types/atlas.types' +import { DEFAULT_LOCATION, Location } from '../../types/atlas.types' +import { MapComponent } from '../map/map.component' @Component({ selector: 'neb-location', templateUrl: './location.dialog.html', - styleUrls: ['./location.dialog.scss'], }) -export class LocationDialog implements AfterViewInit { +export class LocationComponent implements AfterViewInit { @ViewChild('map') - private readonly map!: MapComponent + private readonly map?: MapComponent @Input() readonly location!: Location @@ -27,12 +26,12 @@ export class LocationDialog implements AfterViewInit { @Optional() config?: DynamicDialogConfig, ) { if (config) { - this.location = config.data ?? structuredClone(EMPTY_LOCATION) + this.location = config.data ?? structuredClone(DEFAULT_LOCATION) } } ngAfterViewInit() { - this.map.refresh() + this.map?.refresh() } save() { diff --git a/desktop/src/shared/components/map/map.component.html b/desktop/src/shared/components/map/map.component.html index f742cb275..6a925dbe8 100644 --- a/desktop/src/shared/components/map/map.component.html +++ b/desktop/src/shared/components/map/map.component.html @@ -1,4 +1,4 @@
+ style="height: 150px" + class="border-round-md relative"> diff --git a/desktop/src/shared/components/map/map.component.scss b/desktop/src/shared/components/map/map.component.scss index bb0bcd3bd..2486878d8 100644 --- a/desktop/src/shared/components/map/map.component.scss +++ b/desktop/src/shared/components/map/map.component.scss @@ -2,7 +2,3 @@ display: block; width: 100%; } - -::ng-deep .leaflet-marker-shadow { - display: none; -} diff --git a/desktop/src/shared/components/map/map.component.ts b/desktop/src/shared/components/map/map.component.ts index 99d80671a..7a41533ab 100644 --- a/desktop/src/shared/components/map/map.component.ts +++ b/desktop/src/shared/components/map/map.component.ts @@ -7,21 +7,21 @@ import * as L from 'leaflet' styleUrls: ['./map.component.scss'], }) export class MapComponent implements AfterViewInit, OnChanges { - @ViewChild('map') - private readonly mapRef!: ElementRef - @Input() - latitude = 0 + protected latitude = 0 @Output() readonly latitudeChange = new EventEmitter() @Input() - longitude = 0 + protected longitude = 0 @Output() readonly longitudeChange = new EventEmitter() + @ViewChild('map') + private readonly mapRef!: ElementRef + private map?: L.Map private marker?: L.Marker diff --git a/desktop/src/shared/components/menu-bar/menu-bar.component.html b/desktop/src/shared/components/menu-bar/menu-bar.component.html index 3a594981a..19ed08fea 100644 --- a/desktop/src/shared/components/menu-bar/menu-bar.component.html +++ b/desktop/src/shared/components/menu-bar/menu-bar.component.html @@ -22,7 +22,7 @@ [binary]="true" [disabled]="item.disabled ?? false" [(ngModel)]="item.checked" - (onChange)="item.check?.($event)" /> + (onChange)="item.check?.($event); $event.originalEvent?.stopImmediatePropagation()" /> } @else if (item.label && item.splitButtonMenu?.length) { + [severity]="item.badgeSeverity ?? 'danger'" + [value]="item.badge" + styleClass="absolute flex justify-content-center align-items-center top-0" + [style]="{ width: '14px', minWidth: '14px', height: '14px', minHeight: '14px', right: '-2px' }" /> () diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html index 42f945147..b65c070a2 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.html +++ b/desktop/src/shared/components/menu-item/menu-item.component.html @@ -12,7 +12,7 @@ + (onChange)="item.check?.($event); $event.originalEvent?.stopImmediatePropagation()" /> } @if (item.items?.length || item.slideMenu?.length) { diff --git a/desktop/src/shared/components/menu-item/menu-item.component.scss b/desktop/src/shared/components/menu-item/menu-item.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/shared/components/menu-item/menu-item.component.ts b/desktop/src/shared/components/menu-item/menu-item.component.ts index 85f0ff880..d980aff44 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.ts +++ b/desktop/src/shared/components/menu-item/menu-item.component.ts @@ -1,7 +1,7 @@ -import { Component, Input } from '@angular/core' +import { Component, Input, ViewEncapsulation } from '@angular/core' import { CheckboxChangeEvent } from 'primeng/checkbox' import { InputSwitchChangeEvent } from 'primeng/inputswitch' -import { Severity, TooltipPosition } from '../../types/app.types' +import { Severity, TooltipPosition } from '../../types/angular.types' export interface MenuItemCommandEvent { originalEvent?: Event @@ -44,6 +44,9 @@ export interface MenuItem { command?: (event: MenuItemCommandEvent) => void check?: (event: CheckboxChangeEvent) => void toggle?: (event: InputSwitchChangeEvent) => void + + styleClass?: string + iconClass?: string } export interface SlideMenuItem extends MenuItem { @@ -53,9 +56,9 @@ export interface SlideMenuItem extends MenuItem { @Component({ selector: 'neb-menu-item', templateUrl: './menu-item.component.html', - styleUrls: ['./menu-item.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class MenuItemComponent { @Input({ required: true }) - readonly item!: MenuItem + protected readonly item!: MenuItem } diff --git a/desktop/src/shared/components/moon/moon.component.html b/desktop/src/shared/components/moon/moon.component.html index e8086e306..960765765 100644 --- a/desktop/src/shared/components/moon/moon.component.html +++ b/desktop/src/shared/components/moon/moon.component.html @@ -1,4 +1,5 @@ + [width]="width" + style="filter: brightness(1.5); background-repeat: no-repeat; background-position: center"> diff --git a/desktop/src/shared/components/moon/moon.component.scss b/desktop/src/shared/components/moon/moon.component.scss deleted file mode 100644 index 184d65ea9..000000000 --- a/desktop/src/shared/components/moon/moon.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -canvas { - background-repeat: no-repeat; - background-position: center; -} diff --git a/desktop/src/shared/components/moon/moon.component.ts b/desktop/src/shared/components/moon/moon.component.ts index 77a77e97c..16f13b385 100644 --- a/desktop/src/shared/components/moon/moon.component.ts +++ b/desktop/src/shared/components/moon/moon.component.ts @@ -1,25 +1,25 @@ -import { AfterViewInit, Component, ElementRef, Input, OnChanges, ViewChild } from '@angular/core' +import { AfterViewInit, Component, ElementRef, Input, OnChanges, ViewChild, ViewEncapsulation } from '@angular/core' @Component({ selector: 'neb-moon', templateUrl: './moon.component.html', - styleUrls: ['./moon.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class MoonComponent implements AfterViewInit, OnChanges { - @ViewChild('moon') - private readonly moon?: ElementRef - @Input() - height = 256 + protected height = 256 @Input() - width = 256 + protected width = 256 @Input() - illuminationRatio = 0 + protected illuminationRatio = 0 @Input() - waning = false + protected waning = false + + @ViewChild('moon') + private readonly moon?: ElementRef ngAfterViewInit() { this.draw() @@ -37,17 +37,14 @@ export class MoonComponent implements AfterViewInit, OnChanges { ctx.clearRect(0, 0, canvas.width, canvas.height) - const offset = 32 - const offset4 = offset / 4 - - const height = canvas.height - offset - const width = canvas.width - offset + const height = canvas.height + const width = canvas.width canvas.style.backgroundImage = `url('assets/images/moon.png')` - canvas.style.backgroundSize = `${height + offset4 * 2 - 2}px` + canvas.style.backgroundSize = `${height - 2}px` - const cx = width / 2 + offset4 - const cy = height / 2 + offset4 + const cx = width / 2 + const cy = height / 2 const pointsA: [number, number][] = [] const pointsB: [number, number][] = [] @@ -56,20 +53,20 @@ export class MoonComponent implements AfterViewInit, OnChanges { const angle = ((a - 90) * Math.PI) / 180 let x1 = Math.ceil(Math.cos(angle) * cx) const y1 = Math.ceil(Math.sin(angle) * cy) - const moonWidth = x1 * 2 - let x2 = Math.floor(moonWidth * this.illuminationRatio) + const w = x1 * 2 + let x2 = Math.floor(w * this.illuminationRatio) if (this.waning) { x1 = cx + x1 - x2 = x1 - (moonWidth - x2) + x2 = x1 - (w - x2) } else { x1 = cx - x1 - x2 = x1 + (moonWidth - x2) + x2 = x1 + (w - x2) } const y2 = cy + y1 - const p1: [number, number] = [x1 + offset4, y2 + offset4] - const p2: [number, number] = [x2 + offset4, y2 + offset4] + const p1: [number, number] = [x1, y2] + const p2: [number, number] = [x2, y2] pointsA.push(p1) pointsB.push(p2) @@ -78,7 +75,7 @@ export class MoonComponent implements AfterViewInit, OnChanges { const newPoints = pointsA.concat(pointsB.reverse()) ctx.beginPath() - ctx.fillStyle = '#121212D8' + ctx.fillStyle = '#121212E8' ctx.filter = 'blur(1px)' let first = true diff --git a/desktop/src/shared/components/path-chooser/path-chooser.component.scss b/desktop/src/shared/components/path-chooser/path-chooser.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/shared/components/path-chooser/path-chooser.component.ts b/desktop/src/shared/components/path-chooser/path-chooser.component.ts index 090302db1..92b525181 100644 --- a/desktop/src/shared/components/path-chooser/path-chooser.component.ts +++ b/desktop/src/shared/components/path-chooser/path-chooser.component.ts @@ -1,54 +1,44 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges } from '@angular/core' +import { Component, EventEmitter, Input, Output } from '@angular/core' import { dirname } from 'path' import { ElectronService } from '../../services/electron.service' -import { Undefinable } from '../../utils/types' @Component({ selector: 'neb-path-chooser', templateUrl: './path-chooser.component.html', - styleUrls: ['./path-chooser.component.scss'], }) -export class PathChooserComponent implements OnChanges { +export class PathChooserComponent { @Input({ required: true }) - readonly key!: string + protected readonly key!: string @Input() - readonly label?: string + protected readonly label?: string @Input() - readonly placeholder?: string + protected readonly placeholder?: string @Input() - readonly disabled: boolean = false + protected readonly disabled: boolean = false @Input() - readonly readonly: boolean = false + protected readonly readonly: boolean = false @Input({ required: true }) - readonly directory!: boolean + protected readonly directory!: boolean @Input() - path?: string + protected path?: string @Output() readonly pathChange = new EventEmitter() - constructor(private readonly electron: ElectronService) {} + constructor(private readonly electronService: ElectronService) {} - ngOnChanges(changes: SimpleChanges) { - const pathChanged = changes['path'] as Undefinable - - if (pathChanged?.currentValue) { - this.path = pathChanged.currentValue as string - } - } - - async choosePath() { + protected async choosePath() { const key = `pathChooser.${this.key}.defaultPath` - const storedPath = localStorage.getItem(key) - const defaultPath = storedPath && !this.directory ? dirname(storedPath) : this.path + const lastPath = localStorage.getItem(key) || undefined + const defaultPath = lastPath && !this.directory ? dirname(lastPath) : lastPath - const path = await (this.directory ? this.electron.openDirectory({ defaultPath }) : this.electron.openFile({ defaultPath })) + const path = await (this.directory ? this.electronService.openDirectory({ defaultPath }) : this.electronService.openFile({ defaultPath })) if (path) { this.path = path diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.scss b/desktop/src/shared/components/slide-menu/slide-menu.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.ts b/desktop/src/shared/components/slide-menu/slide-menu.component.ts index 0b1df0c79..838e60a04 100644 --- a/desktop/src/shared/components/slide-menu/slide-menu.component.ts +++ b/desktop/src/shared/components/slide-menu/slide-menu.component.ts @@ -1,11 +1,11 @@ -import { Component, ElementRef, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core' +import { Component, ElementRef, EventEmitter, Input, OnInit, Output, TemplateRef, ViewEncapsulation } from '@angular/core' import { Nullable } from '../../utils/types' import { MenuItemCommandEvent, SlideMenuItem } from '../menu-item/menu-item.component' @Component({ selector: 'neb-slide-menu', templateUrl: './slide-menu.component.html', - styleUrls: ['./slide-menu.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class SlideMenuComponent implements OnInit { @Input({ required: true }) @@ -20,7 +20,7 @@ export class SlideMenuComponent implements OnInit { @Output() readonly onBack = new EventEmitter() - currentMenu!: SlideMenuItem[] + protected currentMenu!: SlideMenuItem[] private readonly navigation: SlideMenuItem[][] = [] diff --git a/desktop/src/shared/constants.ts b/desktop/src/shared/constants.ts index 744d74377..cf7016eb0 100644 --- a/desktop/src/shared/constants.ts +++ b/desktop/src/shared/constants.ts @@ -7,11 +7,13 @@ export const TWO_DIGITS_FORMATTER = new Intl.NumberFormat('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0, }) + export const THREE_DIGITS_FORMATTER = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 3, minimumFractionDigits: 0, maximumFractionDigits: 0, }) + export const ONE_DECIMAL_PLACE_FORMATTER = new Intl.NumberFormat('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1, diff --git a/desktop/src/shared/dialogs/confirm/confirm.dialog.scss b/desktop/src/shared/dialogs/confirm/confirm.dialog.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/shared/dialogs/confirm/confirm.dialog.ts b/desktop/src/shared/dialogs/confirm/confirm.dialog.ts index 802fb7a07..0f65686bd 100644 --- a/desktop/src/shared/dialogs/confirm/confirm.dialog.ts +++ b/desktop/src/shared/dialogs/confirm/confirm.dialog.ts @@ -1,11 +1,10 @@ import { Component } from '@angular/core' import { ConfirmEventType, Confirmation } from 'primeng/api' import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog' -import { PrimeService } from '../../services/prime.service' +import { AngularService } from '../../services/angular.service' @Component({ templateUrl: './confirm.dialog.html', - styleUrls: ['./confirm.dialog.scss'], }) export class ConfirmDialog { readonly header: string @@ -27,8 +26,8 @@ export class ConfirmDialog { this.dialogRef.close(ConfirmEventType.ACCEPT) } - static async open(prime: PrimeService, message: string) { + static async open(service: AngularService, message: string) { const data: Confirmation = { message } - return (await prime.open(ConfirmDialog, { header: 'Confirmation', data, style: { maxWidth: '320px' } })) ?? ConfirmEventType.CANCEL + return (await service.open(ConfirmDialog, { header: 'Confirmation', data, style: { maxWidth: '320px' } })) ?? ConfirmEventType.CANCEL } } diff --git a/desktop/src/shared/dialogs/location/location.dialog.scss b/desktop/src/shared/dialogs/location/location.dialog.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/shared/directives/input-number-scrollable.ts b/desktop/src/shared/directives/spinnable-number.directive.ts similarity index 83% rename from desktop/src/shared/directives/input-number-scrollable.ts rename to desktop/src/shared/directives/spinnable-number.directive.ts index a7797ddd4..a3b588cd2 100644 --- a/desktop/src/shared/directives/input-number-scrollable.ts +++ b/desktop/src/shared/directives/spinnable-number.directive.ts @@ -1,8 +1,8 @@ import { Directive, Host, HostListener } from '@angular/core' import { InputNumber } from 'primeng/inputnumber' -@Directive({ selector: '[scrollableNumber]' }) -export class ScrollableNumberDirective { +@Directive({ selector: '[spinnableNumber]' }) +export class SpinnableNumberDirective { constructor(@Host() private readonly inputNumber: InputNumber) {} @HostListener('wheel', ['$event']) diff --git a/desktop/src/shared/directives/stop-propagation.directive.ts b/desktop/src/shared/directives/stop-propagation.directive.ts index 29726b68b..5de9883da 100644 --- a/desktop/src/shared/directives/stop-propagation.directive.ts +++ b/desktop/src/shared/directives/stop-propagation.directive.ts @@ -3,13 +3,13 @@ import { Directive, HostListener, Input } from '@angular/core' @Directive({ selector: '[stopPropagation]' }) export class StopPropagationDirective { @Input('spEnabled') - readonly enabled: boolean = true + protected readonly enabled: boolean = true @Input('spImmediate') - readonly immediate: boolean = true + protected readonly immediate: boolean = true @Input('spPreventDefault') - readonly preventDefault: boolean = false + protected readonly preventDefault: boolean = false @HostListener('click', ['$event']) @HostListener('contextmenu', ['$event']) diff --git a/desktop/src/shared/interceptors/confirmation.interceptor.ts b/desktop/src/shared/interceptors/confirmation.interceptor.ts index 3a29b4d5e..ec5be3093 100644 --- a/desktop/src/shared/interceptors/confirmation.interceptor.ts +++ b/desktop/src/shared/interceptors/confirmation.interceptor.ts @@ -6,7 +6,7 @@ import { IdempotencyKeyInterceptor } from './idempotency-key.interceptor' @Injectable({ providedIn: 'root' }) export class ConfirmationInterceptor implements HttpInterceptor { - constructor(private readonly confirmation: ConfirmationService) {} + constructor(private readonly confirmationService: ConfirmationService) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { const hasConfirmation = req.urlWithParams.includes('hasConfirmation') @@ -15,8 +15,7 @@ export class ConfirmationInterceptor implements HttpInterceptor { const idempotencyKey = req.headers.get(IdempotencyKeyInterceptor.HEADER_KEY) if (idempotencyKey) { - console.info('registered confirmation:', req.method, req.urlWithParams, idempotencyKey) - this.confirmation.register(idempotencyKey) + this.confirmationService.register(idempotencyKey) } const res = next.handle(req) @@ -24,8 +23,7 @@ export class ConfirmationInterceptor implements HttpInterceptor { if (idempotencyKey) { return res.pipe( finalize(() => { - console.info('unregistered confirmation:', req.method, req.urlWithParams, idempotencyKey) - this.confirmation.unregister(idempotencyKey) + this.confirmationService.unregister(idempotencyKey) }), ) } diff --git a/desktop/src/shared/interceptors/location.interceptor.ts b/desktop/src/shared/interceptors/location.interceptor.ts index 3efc4f402..8e2af3dbc 100644 --- a/desktop/src/shared/interceptors/location.interceptor.ts +++ b/desktop/src/shared/interceptors/location.interceptor.ts @@ -7,15 +7,33 @@ import { PreferenceService } from '../services/preference.service' export class LocationInterceptor implements HttpInterceptor { static readonly HEADER_KEY = 'X-Location' - constructor(private readonly preference: PreferenceService) {} + constructor(private readonly preferenceService: PreferenceService) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { if (req.urlWithParams.includes('hasLocation')) { - const location = this.preference.selectedLocation.get() + const params = new URLSearchParams(req.urlWithParams) + const hasLocation = params.get('hasLocation') - req = req.clone({ - headers: req.headers.set(LocationInterceptor.HEADER_KEY, JSON.stringify(location)), - }) + if (!hasLocation || hasLocation === 'true') { + const location = this.preferenceService.settings.get().location + + req = req.clone({ + headers: req.headers.set(LocationInterceptor.HEADER_KEY, JSON.stringify(location)), + }) + } else { + const id = parseInt(hasLocation) + + if (id) { + const locations = this.preferenceService.settings.get().locations + const location = locations.find((e) => e.id === id) + + if (location) { + req = req.clone({ + headers: req.headers.set(LocationInterceptor.HEADER_KEY, JSON.stringify(location)), + }) + } + } + } } return next.handle(req) diff --git a/desktop/src/shared/pipes/dropdown-options.pipe.ts b/desktop/src/shared/pipes/dropdown-options.pipe.ts index 0f231c974..4055016d2 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -1,12 +1,14 @@ import { Pipe, PipeTransform } from '@angular/core' import { Hemisphere } from '../types/alignment.types' +import { Constellation, CONSTELLATIONS, SATELLITE_GROUPS, SatelliteGroupType, SKY_OBJECT_TYPES, SkyObjectType } from '../types/atlas.types' import { AutoFocusFittingMode, BacklashCompensationMode } from '../types/autofocus.type' import { ExposureMode, FrameType, LiveStackerType } from '../types/camera.types' import { GuideDirection, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider.types' -import { Bitpix, ImageChannel, ImageFormat, SCNRProtectionMethod } from '../types/image.types' -import { MountRemoteControlType } from '../types/mount.types' +import { ConnectionType } from '../types/home.types' +import { Bitpix, IMAGE_STATISTICS_BIT_OPTIONS, ImageChannel, ImageFormat, ImageStatisticsBitOption, SCNRProtectionMethod } from '../types/image.types' +import { MountRemoteControlProtocol } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' -import { SequenceCaptureMode } from '../types/sequencer.types' +import { SequencerCaptureMode } from '../types/sequencer.types' import { SettingsTabKey } from '../types/settings.types' import { StackerGroupType, StackerType } from '../types/stacker.types' import { StarDetectorType } from '../types/stardetector.types' @@ -21,7 +23,7 @@ export interface DropdownOptions { IMAGE_FORMAT: ImageFormat[] IMAGE_BITPIX: Bitpix[] IMAGE_CHANNEL: ImageChannel[] - MOUNT_REMOTE_CONTROL_TYPE: MountRemoteControlType[] + MOUNT_REMOTE_CONTROL_PROTOCOL: MountRemoteControlProtocol[] FRAME_TYPE: FrameType[] EXPOSURE_MODE: ExposureMode[] GUIDE_DIRECTION: GuideDirection[] @@ -30,10 +32,16 @@ export interface DropdownOptions { HEMISPHERE: Hemisphere[] GUIDER_PLOT_MODE: GuiderPlotMode[] GUIDER_Y_AXIS_UNIT: GuiderYAxisUnit[] - SEQUENCE_CAPTURE_MODE: SequenceCaptureMode[] + SEQUENCE_CAPTURE_MODE: SequencerCaptureMode[] STACKER: StackerType[] SETTINGS_TAB: SettingsTabKey[] STACKER_GROUP_TYPE: StackerGroupType[] + CONNECTION_TYPE: ConnectionType[] + IMAGE_STATISTICS_BIT_OPTION: ImageStatisticsBitOption[] + SATELLITE_GROUP_TYPE: SatelliteGroupType[] + CONSTELLATION: Constellation[] + SKY_OBJECT_TYPE: SkyObjectType[] + SEQUENCER_CAPTURE_MODE: SequencerCaptureMode[] } @Pipe({ name: 'dropdownOptions' }) @@ -58,7 +66,7 @@ export class DropdownOptionsPipe implements PipeTransform { return ['BYTE', 'SHORT', 'INTEGER', 'FLOAT', 'DOUBLE'] as DropdownOptions[K] case 'IMAGE_CHANNEL': return ['RED', 'GREEN', 'BLUE', 'GRAY'] as DropdownOptions[K] - case 'MOUNT_REMOTE_CONTROL_TYPE': + case 'MOUNT_REMOTE_CONTROL_PROTOCOL': return ['LX200', 'STELLARIUM'] as DropdownOptions[K] case 'FRAME_TYPE': return ['LIGHT', 'DARK', 'FLAT', 'BIAS'] as DropdownOptions[K] @@ -84,6 +92,18 @@ export class DropdownOptionsPipe implements PipeTransform { return ['LOCATION', 'PLATE_SOLVER', 'STAR_DETECTOR', 'LIVE_STACKER', 'STACKER', 'CAPTURE_NAMING_FORMAT'] as DropdownOptions[K] case 'STACKER_GROUP_TYPE': return ['LUMINANCE', 'RED', 'GREEN', 'BLUE', 'MONO', 'RGB'] as DropdownOptions[K] + case 'CONNECTION_TYPE': + return ['INDI', 'ALPACA'] as DropdownOptions[K] + case 'IMAGE_STATISTICS_BIT_OPTION': + return IMAGE_STATISTICS_BIT_OPTIONS as DropdownOptions[K] + case 'SATELLITE_GROUP_TYPE': + return SATELLITE_GROUPS as unknown as DropdownOptions[K] + case 'CONSTELLATION': + return CONSTELLATIONS as unknown as DropdownOptions[K] + case 'SKY_OBJECT_TYPE': + return SKY_OBJECT_TYPES as unknown as DropdownOptions[K] + case 'SEQUENCER_CAPTURE_MODE': + return ['FULLY', 'INTERLEAVED'] as DropdownOptions[K] } return [] diff --git a/desktop/src/shared/pipes/enum-dropdown.pipe.ts b/desktop/src/shared/pipes/enum-dropdown.pipe.ts index f544b2972..1e02fa89e 100644 --- a/desktop/src/shared/pipes/enum-dropdown.pipe.ts +++ b/desktop/src/shared/pipes/enum-dropdown.pipe.ts @@ -1,16 +1,12 @@ import { Pipe, PipeTransform } from '@angular/core' -import { EnumPipe, EnumPipeKey } from './enum.pipe' - -export interface EnumDropdownItem { - label: string - value: EnumPipeKey -} +import { DropdownItem } from '../types/angular.types' +import { EnumPipe } from './enum.pipe' @Pipe({ name: 'enumDropdown' }) export class EnumDropdownPipe implements PipeTransform { constructor(private readonly enumPipe: EnumPipe) {} - transform(value: EnumPipeKey[]): EnumDropdownItem[] { + transform(value: T[]): DropdownItem[] { return value.map((value) => { return { label: this.enumPipe.transform(value), value } }) diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index a681c0a5a..a2d2822b8 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -2,13 +2,13 @@ import { Pipe, PipeTransform } from '@angular/core' import { DARVState, Hemisphere, TPPAState } from '../types/alignment.types' import { Constellation, SatelliteGroupType, SkyObjectType } from '../types/atlas.types' import { AutoFocusFittingMode, AutoFocusState, BacklashCompensationMode } from '../types/autofocus.type' -import { CameraCaptureState, ExposureMode, FrameType, LiveStackerType } from '../types/camera.types' +import { CameraCaptureState, ExposureMode, ExposureTimeUnit, FrameType, LiveStackerType } from '../types/camera.types' import { FlatWizardState } from '../types/flat-wizard.types' import { GuideDirection, GuideState, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider.types' -import { Bitpix, SCNRProtectionMethod } from '../types/image.types' -import { MountRemoteControlType } from '../types/mount.types' +import { Bitpix, ImageChannel, SCNRProtectionMethod } from '../types/image.types' +import { MountRemoteControlProtocol } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' -import { SequenceCaptureMode, SequencerState } from '../types/sequencer.types' +import { SequencerCaptureMode, SequencerState } from '../types/sequencer.types' import { SettingsTabKey } from '../types/settings.types' import { StackerGroupType, StackerType } from '../types/stacker.types' import { StarDetectorType } from '../types/stardetector.types' @@ -36,13 +36,15 @@ export type EnumPipeKey = | LiveStackerType | GuiderPlotMode | GuiderYAxisUnit - | MountRemoteControlType - | SequenceCaptureMode + | MountRemoteControlProtocol + | SequencerCaptureMode | Bitpix | StackerType | StackerGroupType | SettingsTabKey | SequencerState + | ExposureTimeUnit + | ImageChannel | 'ALL' @Pipe({ name: 'enum' }) @@ -205,6 +207,7 @@ export class EnumPipe implements PipeTransform { GRAVITATIONALLY_LENSED_IMAGE_OF_A_GALAXY: 'Gravitationally Lensed Image of a Galaxy', GRAVITATIONALLY_LENSED_IMAGE_OF_A_QUASAR: 'Gravitationally Lensed Image of a Quasar', GRAVITATIONALLY_LENSED_IMAGE: 'Gravitationally Lensed Image', + GRAY: 'Gray', GREEN: 'Green', GROUP_OF_GALAXIES: 'Group of Galaxies', GRU: 'Grus', @@ -271,11 +274,14 @@ export class EnumPipe implements PipeTransform { MEN: 'Mensa', METRIC_RADIO_SOURCE: 'Metric Radio Source', MIC: 'Microscopium', + MICROSECOND: 'µs', MICRO_LENSING_EVENT: '(Micro)Lensing Event', MID_IR_SOURCE_3_TO_30_M: 'Mid-IR Source (3 to 30 µm)', MILITARY: 'Miscellaneous Military', + MILLISECOND: 'ms', MILLIMETRIC_RADIO_SOURCE: 'Millimetric Radio Source', MINIMUM_NEUTRAL: 'Minimum Neutral', + MINUTE: 'm', MIRA_VARIABLE: 'Mira Variable', MOLECULAR_CLOUD: 'Molecular Cloud', MOLNIYA: 'Molniya', @@ -362,6 +368,7 @@ export class EnumPipe implements PipeTransform { SCO: 'Scorpius', SCT: 'Scutum', SELECTED: 'Selected', + SECOND: 's', SER: 'Serpens', SES: 'SES', SETTLING: 'Settling', diff --git a/desktop/src/shared/pipes/exposureTime.pipe.ts b/desktop/src/shared/pipes/exposureTime.pipe.ts index 210175fae..a97fdfcb6 100644 --- a/desktop/src/shared/pipes/exposureTime.pipe.ts +++ b/desktop/src/shared/pipes/exposureTime.pipe.ts @@ -38,7 +38,7 @@ const secondFormatter = formatter(TWO_DIGITS_FORMATTER, 's') function format(value: number, factors: [number, number], formatters: [UnitFormatter, UnitFormatter]) { const a = value / factors[0] const b = (a - Math.trunc(a)) * factors[1] - return `${formatters[0](Math.trunc(a))}${formatters[1](Math.trunc(b))}` + return `${formatters[0](a)}${formatters[1](b)}` } function hours(value: number) { diff --git a/desktop/src/shared/pipes/path.pipe.ts b/desktop/src/shared/pipes/path.pipe.ts new file mode 100644 index 000000000..dd61d2ea5 --- /dev/null +++ b/desktop/src/shared/pipes/path.pipe.ts @@ -0,0 +1,26 @@ +import { Pipe, PipeTransform } from '@angular/core' +import * as path from 'path' + +export type PathCommand = 'normalize' | 'basename' | 'dirname' | 'extname' | 'namespaced' + +@Pipe({ name: 'path' }) +export class PathPipe implements PipeTransform { + transform(value: string | undefined, command: PathCommand) { + if (!value) return value + + switch (command) { + case 'normalize': + return path.normalize(value) + case 'basename': + return path.basename(value) + case 'dirname': + return path.dirname(value) + case 'extname': + return path.extname(value) + case 'namespaced': + return path.toNamespacedPath(value) + default: + return value + } + } +} diff --git a/desktop/src/shared/pipes/skyObject.pipe.ts b/desktop/src/shared/pipes/skyObject.pipe.ts index a73df5489..c8b414f03 100644 --- a/desktop/src/shared/pipes/skyObject.pipe.ts +++ b/desktop/src/shared/pipes/skyObject.pipe.ts @@ -2,9 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core' import { AstronomicalObject } from '../types/atlas.types' import { Undefinable } from '../utils/types' -const SKY_OBJECT_PARTS = ['name', 'firstName'] as const - -export type SkyObjectPart = (typeof SKY_OBJECT_PARTS)[number] +export type SkyObjectPart = 'name' | 'firstName' @Pipe({ name: 'skyObject' }) export class SkyObjectPipe implements PipeTransform { @@ -13,7 +11,7 @@ export class SkyObjectPipe implements PipeTransform { case 'name': return value?.name.replaceAll('|', ' · ') case 'firstName': - return value?.name.split(/\[([^\]]+)\]/g).filter(Boolean)[0] + return value?.name.split(/\[([^\]]+)\]/g).find(Boolean) default: return `${value}` } diff --git a/desktop/src/shared/services/prime.service.ts b/desktop/src/shared/services/angular.service.ts similarity index 80% rename from desktop/src/shared/services/prime.service.ts rename to desktop/src/shared/services/angular.service.ts index 3d450f9e7..43e5e5ab1 100644 --- a/desktop/src/shared/services/prime.service.ts +++ b/desktop/src/shared/services/angular.service.ts @@ -5,14 +5,14 @@ import { ConfirmDialog } from '../dialogs/confirm/confirm.dialog' import { Undefinable } from '../utils/types' @Injectable({ providedIn: 'root' }) -export class PrimeService { +export class AngularService { constructor( - private readonly dialog: DialogService, - private readonly messager: MessageService, + private readonly dialogService: DialogService, + private readonly messageService: MessageService, ) {} open(componentType: Type, config: DynamicDialogConfig) { - const ref = this.dialog.open(componentType, { + const ref = this.dialogService.open(componentType, { ...config, duplicate: true, draggable: config.draggable ?? true, @@ -41,6 +41,6 @@ export class PrimeService { } message(text: string, severity: 'info' | 'warn' | 'error' | 'success' = 'success') { - this.messager.add({ severity, detail: text, life: 8500 }) + this.messageService.add({ severity, detail: text, life: 8500 }) } } diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 0c5d9f7d4..1278d9cab 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -1,35 +1,35 @@ import { Injectable } from '@angular/core' -import moment from 'moment' import { DARVStart, TPPAStart } from '../types/alignment.types' -import { Angle, BodyPosition, CloseApproach, ComputedLocation, Constellation, DeepSkyObject, MinorPlanet, Satellite, SatelliteGroupType, SkyObjectType, Twilight } from '../types/atlas.types' +import { extractDate, extractDateTime } from '../types/angular.types' +import { Angle, BodyPosition, CloseApproach, ComputedLocation, Constellation, DeepSkyObject, Location, MinorPlanet, Satellite, SatelliteGroupType, SkyObjectType, Twilight } from '../types/atlas.types' import { AutoFocusRequest } from '../types/autofocus.type' -import { CalibrationFrame, CalibrationFrameGroup } from '../types/calibration.types' +import { CalibrationFrame } from '../types/calibration.types' import { Camera, CameraStartCapture } from '../types/camera.types' import { Device, INDIProperty, INDISendProperty } from '../types/device.types' 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, Equipment } from '../types/home.types' -import { CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAnnotation, ImageInfo, ImageSaveDialog, ImageSolved, ImageTransformation } from '../types/image.types' -import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlType, SlewRate, TrackMode } from '../types/mount.types' +import { ConnectionStatus, ConnectionType } from '../types/home.types' +import { AnnotateImageRequest, CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAnnotation, ImageInfo, ImageMousePosition, ImageSaveDialog, ImageSolved, ImageTransformation } from '../types/image.types' +import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlProtocol, SlewRate, TrackMode } from '../types/mount.types' import { PlateSolverRequest } from '../types/platesolver.types' import { Rotator } from '../types/rotator.types' -import { SequencePlan } from '../types/sequencer.types' +import { SequencerPlan } from '../types/sequencer.types' import { AnalyzedTarget, StackingRequest } from '../types/stacker.types' import { StarDetectionRequest } from '../types/stardetector.types' -import { FilterWheel } from '../types/wheel.types' +import { Wheel } from '../types/wheel.types' import { Undefinable } from '../utils/types' import { HttpService } from './http.service' @Injectable({ providedIn: 'root' }) export class ApiService { - constructor(private readonly http: HttpService) {} - get baseUrl() { return this.http.baseUrl } + constructor(private readonly http: HttpService) {} + // CONNECTION connect(host: string, port: number, type: ConnectionType) { @@ -71,12 +71,6 @@ export class ApiService { return this.http.get(`cameras/${camera.id}/capturing`) } - cameraSnoop(camera: Camera, equipment: Equipment) { - const { mount, wheel, focuser, rotator } = equipment - const query = this.http.query({ mount: mount?.id, wheel: wheel?.id, focuser: focuser?.id, rotator: rotator?.id }) - return this.http.put(`cameras/${camera.id}/snoop?${query}`) - } - cameraCooler(camera: Camera, enabled: boolean) { return this.http.put(`cameras/${camera.id}/cooler?enabled=${enabled}`) } @@ -85,8 +79,7 @@ export class ApiService { return this.http.put(`cameras/${camera.id}/temperature/setpoint?temperature=${temperature}`) } - cameraStartCapture(camera: Camera, data: CameraStartCapture, equipment: Equipment) { - const { mount, wheel, focuser, rotator } = equipment + cameraStartCapture(camera: Camera, data: CameraStartCapture, mount?: Mount, wheel?: Wheel, focuser?: Focuser, rotator?: Rotator) { const query = this.http.query({ mount: mount?.id, wheel: wheel?.id, focuser: focuser?.id, rotator: rotator?.id }) return this.http.put(`cameras/${camera.id}/capture/start?${query}`, data) } @@ -181,13 +174,13 @@ export class ApiService { return this.http.get(`mounts/${mount.id}/location/${type}`) } - pointMountHere(mount: Mount, path: string, x: number, y: number) { - const query = this.http.query({ path, x, y }) + pointMountHere(mount: Mount, path: string, point: ImageMousePosition) { + const query = this.http.query({ path, ...point }) return this.http.put(`mounts/${mount.id}/point-here?${query}`) } - mountRemoteControlStart(mount: Mount, type: MountRemoteControlType, host: string, port: number) { - const query = this.http.query({ type, host, port }) + mountRemoteControlStart(mount: Mount, protocol: MountRemoteControlProtocol, host: string, port: number) { + const query = this.http.query({ protocol, host, port }) return this.http.put(`mounts/${mount.id}/remote-control/start?${query}`) } @@ -195,8 +188,8 @@ export class ApiService { return this.http.get(`mounts/${mount.id}/remote-control`) } - mountRemoteControlStop(mount: Mount, type: MountRemoteControlType) { - const query = this.http.query({ type }) + mountRemoteControlStop(mount: Mount, protocol: MountRemoteControlProtocol) { + const query = this.http.query({ protocol }) return this.http.put(`mounts/${mount.id}/remote-control/stop?${query}`) } @@ -249,30 +242,30 @@ export class ApiService { // FILTER WHEEL wheels() { - return this.http.get(`wheels`) + return this.http.get(`wheels`) } wheel(id: string) { - return this.http.get(`wheels/${id}`) + return this.http.get(`wheels/${id}`) } - wheelConnect(wheel: FilterWheel) { + wheelConnect(wheel: Wheel) { return this.http.put(`wheels/${wheel.id}/connect`) } - wheelDisconnect(wheel: FilterWheel) { + wheelDisconnect(wheel: Wheel) { return this.http.put(`wheels/${wheel.id}/disconnect`) } - wheelMoveTo(wheel: FilterWheel, position: number) { + wheelMoveTo(wheel: Wheel, position: number) { return this.http.put(`wheels/${wheel.id}/move-to?position=${position}`) } - wheelSync(wheel: FilterWheel, names: string[]) { + wheelSync(wheel: Wheel, names: string[]) { return this.http.put(`wheels/${wheel.id}/sync?names=${names.join(',')}`) } - wheelListen(wheel: FilterWheel) { + wheelListen(wheel: Wheel) { return this.http.put(`wheels/${wheel.id}/listen`) } @@ -388,14 +381,10 @@ export class ApiService { return this.http.put(`guiding/dither?${query}`) } - setGuidingSettle(settle: SettleInfo) { + guidingSettle(settle: SettleInfo) { return this.http.put(`guiding/settle`, settle) } - getGuidingSettle() { - return this.http.get(`guiding/settle`) - } - guidingStop() { return this.http.put(`guiding/stop`) } @@ -456,51 +445,51 @@ export class ApiService { // SKY ATLAS - positionOfSun(dateTime: Date, fast: boolean = false) { - const [date, time] = moment(dateTime).format('YYYY-MM-DD HH:mm').split(' ') - const query = this.http.query({ date, time, fast, hasLocation: true }) + positionOfSun(dateTime: Date, location?: Location, fast: boolean = false) { + const [date, time] = extractDateTime(dateTime) + const query = this.http.query({ date, time, fast, hasLocation: location?.id || true }) return this.http.get(`sky-atlas/sun/position?${query}`) } - altitudePointsOfSun(dateTime: Date, fast: boolean = false) { - const date = moment(dateTime).format('YYYY-MM-DD') - const query = this.http.query({ date, fast, hasLocation: true }) + altitudePointsOfSun(dateTime: Date, location?: Location, fast: boolean = false) { + const date = extractDate(dateTime) + const query = this.http.query({ date, fast, hasLocation: location?.id || true }) return this.http.get<[number, number][]>(`sky-atlas/sun/altitude-points?${query}`) } - positionOfMoon(dateTime: Date, fast: boolean = false) { - const [date, time] = moment(dateTime).format('YYYY-MM-DD HH:mm').split(' ') - const query = this.http.query({ date, time, fast, hasLocation: true }) + positionOfMoon(dateTime: Date, location?: Location, fast: boolean = false) { + const [date, time] = extractDateTime(dateTime) + const query = this.http.query({ date, time, fast, hasLocation: location?.id || true }) return this.http.get(`sky-atlas/moon/position?${query}`) } - altitudePointsOfMoon(dateTime: Date, fast: boolean = false) { - const date = moment(dateTime).format('YYYY-MM-DD') - const query = this.http.query({ date, fast, hasLocation: true }) + altitudePointsOfMoon(dateTime: Date, location?: Location, fast: boolean = false) { + const date = extractDate(dateTime) + const query = this.http.query({ date, fast, hasLocation: location?.id || true }) return this.http.get<[number, number][]>(`sky-atlas/moon/altitude-points?${query}`) } - positionOfPlanet(code: string, dateTime: Date, fast: boolean = false) { - const [date, time] = moment(dateTime).format('YYYY-MM-DD HH:mm').split(' ') - const query = this.http.query({ date, time, fast, hasLocation: true }) + positionOfPlanet(code: string, dateTime: Date, location?: Location, fast: boolean = false) { + const [date, time] = extractDateTime(dateTime) + const query = this.http.query({ date, time, fast, hasLocation: location?.id || true }) return this.http.get(`sky-atlas/planets/${encodeURIComponent(code)}/position?${query}`) } - altitudePointsOfPlanet(code: string, dateTime: Date, fast: boolean = false) { - const date = moment(dateTime).format('YYYY-MM-DD') - const query = this.http.query({ date, fast, hasLocation: true }) + altitudePointsOfPlanet(code: string, dateTime: Date, location?: Location, fast: boolean = false) { + const date = extractDate(dateTime) + const query = this.http.query({ date, fast, hasLocation: location?.id || true }) return this.http.get<[number, number][]>(`sky-atlas/planets/${encodeURIComponent(code)}/altitude-points?${query}`) } - positionOfSkyObject(simbad: DeepSkyObject, dateTime: Date) { - const [date, time] = moment(dateTime).format('YYYY-MM-DD HH:mm').split(' ') - const query = this.http.query({ date, time, hasLocation: true }) + positionOfSkyObject(simbad: DeepSkyObject, dateTime: Date, location?: Location) { + const [date, time] = extractDateTime(dateTime) + const query = this.http.query({ date, time, hasLocation: location?.id || true }) return this.http.get(`sky-atlas/sky-objects/${simbad.id}/position?${query}`) } - altitudePointsOfSkyObject(simbad: DeepSkyObject, dateTime: Date) { - const date = moment(dateTime).format('YYYY-MM-DD') - const query = this.http.query({ date, hasLocation: true }) + altitudePointsOfSkyObject(simbad: DeepSkyObject, dateTime: Date, location?: Location) { + const date = extractDate(dateTime) + const query = this.http.query({ date, hasLocation: location?.id || true }) return this.http.get<[number, number][]>(`sky-atlas/sky-objects/${simbad.id}/altitude-points?${query}`) } @@ -513,15 +502,15 @@ export class ApiService { return this.http.get(`sky-atlas/sky-objects/types`) } - positionOfSatellite(satellite: Satellite, dateTime: Date) { - const [date, time] = moment(dateTime).format('YYYY-MM-DD HH:mm').split(' ') - const query = this.http.query({ date, time, hasLocation: true }) + positionOfSatellite(satellite: Satellite, dateTime: Date, location?: Location) { + const [date, time] = extractDateTime(dateTime) + const query = this.http.query({ date, time, hasLocation: location?.id || true }) return this.http.get(`sky-atlas/satellites/${satellite.id}/position?${query}`) } - altitudePointsOfSatellite(satellite: Satellite, dateTime: Date) { - const date = moment(dateTime).format('YYYY-MM-DD') - const query = this.http.query({ date, hasLocation: true }) + altitudePointsOfSatellite(satellite: Satellite, dateTime: Date, location?: Location) { + const date = extractDate(dateTime) + const query = this.http.query({ date, hasLocation: location?.id || true }) return this.http.get<[number, number][]>(`sky-atlas/satellites/${satellite.id}/altitude-points?${query}`) } @@ -530,9 +519,9 @@ export class ApiService { return this.http.get(`sky-atlas/satellites?${query}`) } - twilight(dateTime: Date, fast: boolean = false) { - const date = moment(dateTime).format('YYYY-MM-DD') - const query = this.http.query({ date, fast, hasLocation: true }) + twilight(dateTime: Date, location?: Location, fast: boolean = false) { + const date = extractDate(dateTime) + const query = this.http.query({ date, fast, hasLocation: location?.id || true }) return this.http.get(`sky-atlas/twilight?${query}`) } @@ -541,15 +530,15 @@ export class ApiService { return this.http.get(`sky-atlas/minor-planets?${query}`) } - closeApproachesForMinorPlanets(days: number = 7, distance: number = 10, dateTime?: Date | string) { - const date = !dateTime || typeof dateTime === 'string' ? dateTime : moment(dateTime).format('YYYY-MM-DD') + closeApproachesOfMinorPlanets(days: number = 7, distance: number = 10, dateTime?: Date | string) { + const date = !dateTime || typeof dateTime === 'string' ? dateTime : extractDate(dateTime) const query = this.http.query({ days, distance, date }) return this.http.get(`sky-atlas/minor-planets/close-approaches?${query}`) } - annotationsOfImage(path: string, starsAndDSOs: boolean = true, minorPlanets: boolean = false, minorPlanetMagLimit: number = 12.0, includeMinorPlanetsWithoutMagnitude: boolean = false, useSimbad: boolean = false) { - const query = this.http.query({ path, starsAndDSOs, minorPlanets, minorPlanetMagLimit, includeMinorPlanetsWithoutMagnitude, useSimbad, hasLocation: true }) - return this.http.get(`image/annotations?${query}`) + annotationsOfImage(path: string, request: AnnotateImageRequest, location?: Location) { + const query = this.http.query({ path, hasLocation: location?.id || true }) + return this.http.put(`image/annotations?${query}`, request) } saveImageAs(path: string, save: ImageSaveDialog, camera?: Camera) { @@ -587,7 +576,7 @@ export class ApiService { } calibrationFrames(name: string) { - return this.http.get(`calibration-frames/${name}`) + return this.http.get(`calibration-frames/${name}`) } uploadCalibrationFrame(name: string, path: string) { @@ -595,9 +584,8 @@ export class ApiService { return this.http.put(`calibration-frames/${name}?${query}`) } - editCalibrationFrame(frame: CalibrationFrame) { - const query = this.http.query({ name: frame.name, enabled: frame.enabled }) - return this.http.patch(`calibration-frames/${frame.id}?${query}`) + updateCalibrationFrame(frame: CalibrationFrame) { + return this.http.post('calibration-frames', frame) } deleteCalibrationFrame(frame: CalibrationFrame) { @@ -645,9 +633,9 @@ export class ApiService { // SEQUENCER - sequencerStart(camera: Camera, plan: SequencePlan) { - const body: SequencePlan = { ...plan, mount: undefined, camera: undefined, wheel: undefined, focuser: undefined } - const query = this.http.query({ mount: plan.mount, focuser: plan.focuser, wheel: plan.wheel }) + sequencerStart(camera: Camera, plan: SequencerPlan) { + const body: SequencerPlan = { ...plan, mount: undefined, camera: undefined, wheel: undefined, focuser: undefined, rotator: undefined } + const query = this.http.query({ mount: plan.mount?.id, focuser: plan.focuser?.id, wheel: plan.wheel?.id, rotator: plan.rotator?.id }) return this.http.put(`sequencer/${camera.id}/start?${query}`, body) } @@ -675,8 +663,8 @@ export class ApiService { // SOLVER - solverStart(solver: PlateSolverRequest, path: string, blind: boolean, centerRA: Angle, centerDEC: Angle, radius: Angle) { - const query = this.http.query({ path, blind, centerRA, centerDEC, radius }) + solverStart(solver: PlateSolverRequest, path: string) { + const query = this.http.query({ path }) return this.http.put(`plate-solver/start?${query}`, solver) } diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 1cf09cac6..2ff5fa757 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -1,29 +1,29 @@ import { Injectable } from '@angular/core' -import { FramingData } from '../../app/framing/framing.component' import { OpenWindow, WindowPreference } from '../types/app.types' import { SkyAtlasInput } from '../types/atlas.types' import { Camera, CameraDialogInput, CameraStartCapture } from '../types/camera.types' import { Device } from '../types/device.types' import { Focuser } from '../types/focuser.types' +import { LoadFraming } from '../types/framing.types' import { ImageSource, OpenImage } from '../types/image.types' import { Mount } from '../types/mount.types' import { Rotator } from '../types/rotator.types' -import { FilterWheel, WheelDialogInput } from '../types/wheel.types' +import { Wheel, WheelDialogInput } from '../types/wheel.types' import { Undefinable } from '../utils/types' import { ElectronService } from './electron.service' @Injectable({ providedIn: 'root' }) export class BrowserWindowService { - constructor(private readonly electron: ElectronService) {} + constructor(private readonly electronService: ElectronService) {} openWindow(open: OpenWindow): Promise { open.preference.modal = false - return this.electron.ipcRenderer.invoke('WINDOW.OPEN', { ...open, windowId: window.id }) + return this.electronService.ipcRenderer.invoke('WINDOW.OPEN', { ...open, windowId: window.id }) } openModal(open: OpenWindow): Promise> { open.preference.modal = true - return this.electron.ipcRenderer.invoke('WINDOW.OPEN', { ...open, windowId: window.id }) + return this.electronService.ipcRenderer.invoke('WINDOW.OPEN', { ...open, windowId: window.id }) } openMount(data: Mount, preference: WindowPreference = {}) { @@ -32,7 +32,7 @@ export class BrowserWindowService { } openCamera(data: Camera, preference: WindowPreference = {}) { - Object.assign(preference, { icon: 'camera', width: 400, height: 467 }) + Object.assign(preference, { icon: 'camera', width: 400, height: 477 }) return this.openWindow({ preference, data, id: `camera.${data.name}`, path: 'camera' }) } @@ -51,7 +51,7 @@ export class BrowserWindowService { return this.openWindow({ preference, data, id: `focuser.${data.name}`, path: 'focuser' }) } - openWheel(data: FilterWheel, preference: WindowPreference = {}) { + openWheel(data: Wheel, preference: WindowPreference = {}) { Object.assign(preference, { icon: 'filter-wheel', width: 280, height: 195 }) return this.openWindow({ preference, data, id: `wheel.${data.name}`, path: 'wheel' }) } @@ -72,7 +72,7 @@ export class BrowserWindowService { } openGuider(preference: WindowPreference = {}) { - Object.assign(preference, { icon: 'guider', width: 440, height: 455 }) + Object.assign(preference, { icon: 'guider', width: 380, height: 444 }) return this.openWindow({ preference, id: 'guider', path: 'guider' }) } @@ -103,7 +103,7 @@ export class BrowserWindowService { return this.openWindow({ preference, data, id: 'atlas', path: 'atlas' }) } - openFraming(data?: FramingData, preference: WindowPreference = {}) { + openFraming(data?: LoadFraming, preference: WindowPreference = {}) { Object.assign(preference, { icon: 'framing', width: 280, height: 303 }) return this.openWindow({ preference, data, id: 'framing', path: 'framing' }) } @@ -139,7 +139,7 @@ export class BrowserWindowService { } openCalibration(preference: WindowPreference = {}) { - Object.assign(preference, { icon: 'photo-filter', width: 420, height: 400, minHeight: 400 }) + Object.assign(preference, { icon: 'calibration', width: 370, height: 442, minHeight: 400 }) return this.openWindow({ preference, id: 'calibration', path: 'calibration' }) } @@ -149,7 +149,7 @@ export class BrowserWindowService { } openAbout() { - const preference: WindowPreference = { icon: 'about', width: 430, height: 307, bringToFront: true } + const preference: WindowPreference = { icon: 'about', width: 430, height: 340, bringToFront: true } return this.openWindow({ preference, id: 'about', path: 'about' }) } } diff --git a/desktop/src/shared/services/confirmation.service.ts b/desktop/src/shared/services/confirmation.service.ts index 3f996eb4c..55b4c0b12 100644 --- a/desktop/src/shared/services/confirmation.service.ts +++ b/desktop/src/shared/services/confirmation.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@angular/core' import { ConfirmEventType } from 'primeng/api' import { ConfirmationEvent } from '../types/app.types' +import { AngularService } from './angular.service' import { ApiService } from './api.service' -import { PrimeService } from './prime.service' @Injectable({ providedIn: 'root' }) export class ConfirmationService { private readonly keys = new Map() constructor( - private readonly prime: PrimeService, + private readonly angularService: AngularService, private readonly api: ApiService, ) {} @@ -26,8 +26,7 @@ export class ConfirmationService { } async processConfirmationEvent(event: ConfirmationEvent) { - console.info('processing confirmation event', event) - const response = await this.prime.confirm(event.message) + const response = await this.angularService.confirm(event.message) await this.api.confirm(event.idempotencyKey, response === ConfirmEventType.ACCEPT) this.unregister(event.idempotencyKey) } diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 06d1637e7..bf88acc10 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -4,9 +4,9 @@ import { Injectable } from '@angular/core' // other than as TypeScript types, the resulting javascript file will // look as if you never imported the module at all. -import * as childProcess from 'child_process' -import { ipcRenderer, webFrame } from 'electron' -import * as fs from 'fs' +import type * as childProcess from 'child_process' +import type { ipcRenderer, webFrame } from 'electron' +import type * as fs from 'fs' import { DARVEvent, TPPAEvent } from '../types/alignment.types' import { DeviceMessageEvent } from '../types/api.types' import { CloseWindow, ConfirmationEvent, FullscreenWindow, JsonFile, NotificationEvent, OpenDirectory, OpenFile, ResizeWindow, SaveJson, WindowCommand } from '../types/app.types' @@ -22,15 +22,22 @@ import { ROISelected } from '../types/image.types' import { Mount } from '../types/mount.types' import { Rotator } from '../types/rotator.types' import { SequencerEvent } from '../types/sequencer.types' -import { FilterWheel, WheelRenamed } from '../types/wheel.types' +import { Wheel, WheelRenamed } from '../types/wheel.types' -export const IMAGE_FILE_FILTER: Electron.FileFilter[] = [ +export const OPEN_IMAGE_FILE_FILTER: Electron.FileFilter[] = [ { name: 'All', extensions: ['fits', 'fit', 'xisf'] }, { name: 'FITS', extensions: ['fits', 'fit'] }, { name: 'XISF', extensions: ['xisf'] }, ] -interface EventMappedType { +export const SAVE_IMAGE_FILE_FILTER: Electron.FileFilter[] = [ + { name: 'All', extensions: ['fits', 'fit', 'xisf', 'png', 'jpg', 'jpeg'] }, + { name: 'FITS', extensions: ['fits', 'fit'] }, + { name: 'XISF', extensions: ['xisf'] }, + { name: 'Image', extensions: ['png', 'jpg', 'jpeg'] }, +] + +export interface EventTypes { NOTIFICATION: NotificationEvent CONFIRMATION: ConfirmationEvent 'DEVICE.PROPERTY_CHANGED': INDIMessageEvent @@ -49,9 +56,9 @@ interface EventMappedType { 'ROTATOR.UPDATED': DeviceMessageEvent 'ROTATOR.ATTACHED': DeviceMessageEvent 'ROTATOR.DETACHED': DeviceMessageEvent - 'WHEEL.UPDATED': DeviceMessageEvent - 'WHEEL.ATTACHED': DeviceMessageEvent - 'WHEEL.DETACHED': DeviceMessageEvent + 'WHEEL.UPDATED': DeviceMessageEvent + 'WHEEL.ATTACHED': DeviceMessageEvent + 'WHEEL.DETACHED': DeviceMessageEvent 'GUIDE_OUTPUT.UPDATED': DeviceMessageEvent 'GUIDE_OUTPUT.ATTACHED': DeviceMessageEvent 'GUIDE_OUTPUT.DETACHED': DeviceMessageEvent @@ -87,10 +94,10 @@ interface EventMappedType { @Injectable({ providedIn: 'root' }) export class ElectronService { - ipcRenderer!: typeof ipcRenderer - webFrame!: typeof webFrame - childProcess!: typeof childProcess - fs!: typeof fs + readonly ipcRenderer!: typeof ipcRenderer + private readonly webFrame!: typeof webFrame + private readonly childProcess!: typeof childProcess + private readonly fs!: typeof fs constructor() { if (this.isElectron) { @@ -121,13 +128,11 @@ export class ElectronService { return !!(window && window.process?.type) } - send(channel: K, data?: EventMappedType[K]) { + send(channel: K, data?: EventTypes[K]) { return this.ipcRenderer.invoke(channel, data) } - on(channel: K, listener: (arg: EventMappedType[K]) => void) { - console.info('listening to channel: %s', channel) - + on(channel: K, listener: (arg: EventTypes[K]) => void) { this.ipcRenderer.on(channel, (_, arg) => { listener(arg) }) @@ -149,7 +154,7 @@ export class ElectronService { return this.openFile({ ...data, windowId: data?.windowId ?? window.id, - filters: IMAGE_FILE_FILTER, + filters: OPEN_IMAGE_FILE_FILTER, }) } @@ -157,7 +162,7 @@ export class ElectronService { return this.openFiles({ ...data, windowId: data?.windowId ?? window.id, - filters: IMAGE_FILE_FILTER, + filters: OPEN_IMAGE_FILE_FILTER, }) } @@ -165,12 +170,7 @@ export class ElectronService { return this.saveFile({ ...data, windowId: data?.windowId ?? window.id, - filters: [ - { name: 'All', extensions: ['fits', 'fit', 'xisf', 'png', 'jpg', 'jpeg'] }, - { name: 'FITS', extensions: ['fits', 'fit'] }, - { name: 'XISF', extensions: ['xisf'] }, - { name: 'Image', extensions: ['png', 'jpg', 'jpeg'] }, - ], + filters: SAVE_IMAGE_FILE_FILTER, }) } @@ -239,4 +239,8 @@ export class ElectronService { calibrationChanged() { return this.send('CALIBRATION.CHANGED') } + + locationChanged(location: Location) { + return this.send('LOCATION.CHANGED', location) + } } diff --git a/desktop/src/shared/services/http.service.ts b/desktop/src/shared/services/http.service.ts index d6310aaa7..e3bcfc3ec 100644 --- a/desktop/src/shared/services/http.service.ts +++ b/desktop/src/shared/services/http.service.ts @@ -7,11 +7,9 @@ export type QueryParamType = Nullable | QueryParamTyp @Injectable({ providedIn: 'root' }) export class HttpService { - constructor(private readonly http: HttpClient) {} + readonly baseUrl = `http://${window.apiHost}:${window.apiPort}` - get baseUrl() { - return `http://${window.apiHost}:${window.apiPort}` - } + constructor(private readonly http: HttpClient) {} get(path: string) { return firstValueFrom(this.http.get(`${this.baseUrl}/${path}`)) diff --git a/desktop/src/shared/services/pinger.service.ts b/desktop/src/shared/services/pinger.service.ts deleted file mode 100644 index dd4123a81..000000000 --- a/desktop/src/shared/services/pinger.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@angular/core' - -export interface Pingable { - ping(): void -} - -@Injectable({ providedIn: 'root' }) -export class Pinger { - private readonly pingables = new Map() - - isRegistered(pingable: Pingable) { - return this.pingables.has(pingable) - } - - register(pingable: Pingable, interval: number, initialDelay: number = 1000) { - this.unregister(pingable) - - if (interval > 0) { - if (initialDelay > 0 && initialDelay < interval - 1000) { - setTimeout(() => { - pingable.ping() - }, initialDelay) - } - - const ping = setInterval(() => { - pingable.ping() - }, interval) as unknown as number - - this.pingables.set(pingable, ping) - } - } - - unregister(pingable: Pingable) { - clearInterval(this.pingables.get(pingable)) - this.pingables.delete(pingable) - } -} diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index c410e4917..739bf6b3b 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -1,21 +1,21 @@ import { Injectable } from '@angular/core' -import { AlignmentPreference, EMPTY_ALIGNMENT_PREFERENCE } from '../types/alignment.types' -import { EMPTY_LOCATION, EMPTY_SKY_ATLAS_PREFERENCE, Location, SkyAtlasPreference } from '../types/atlas.types' -import { AutoFocusPreference, EMPTY_AUTO_FOCUS_PREFERENCE } from '../types/autofocus.type' -import { CalibrationPreference } from '../types/calibration.types' -import { Camera, CameraPreference, CameraStartCapture, EMPTY_CAMERA_PREFERENCE, EMPTY_LIVE_STACKING_REQUEST, LiveStackerType, LiveStackingRequest } from '../types/camera.types' -import { Device } from '../types/device.types' -import { Focuser, FocuserPreference } from '../types/focuser.types' -import { ConnectionDetails, Equipment, HomePreference } from '../types/home.types' -import { EMPTY_IMAGE_PREFERENCE, FOV, ImagePreference } from '../types/image.types' -import { EMPTY_MOUNT_PREFERENCE, Mount, MountPreference } from '../types/mount.types' -import { EMPTY_PLATE_SOLVER_REQUEST, PlateSolverRequest, PlateSolverType } from '../types/platesolver.types' -import { Rotator, RotatorPreference } from '../types/rotator.types' -import { EMPTY_SEQUENCER_PREFERENCE, SequencerPreference } from '../types/sequencer.types' -import { CameraCaptureNamingFormat, DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT } from '../types/settings.types' -import { EMPTY_STACKER_PREFERENCE, EMPTY_STACKING_REQUEST, StackerPreference, StackerType, StackingRequest } from '../types/stacker.types' -import { EMPTY_STAR_DETECTION_REQUEST, StarDetectionRequest, StarDetectorType } from '../types/stardetector.types' -import { FilterWheel, WheelPreference } from '../types/wheel.types' +import { AlignmentPreference, alignmentPreferenceWithDefault, DEFAULT_ALIGNMENT_PREFERENCE } from '../types/alignment.types' +import { DEFAULT_SKY_ATLAS_PREFERENCE, SkyAtlasPreference, skyAtlasPreferenceWithDefault } from '../types/atlas.types' +import { AutoFocusPreference, autoFocusPreferenceWithDefault, DEFAULT_AUTO_FOCUS_PREFERENCE } from '../types/autofocus.type' +import { CalibrationPreference, calibrationPreferenceWithDefault, DEFAULT_CALIBRATION_PREFERENCE } from '../types/calibration.types' +import { Camera, CameraPreference, cameraPreferenceWithDefault, DEFAULT_CAMERA_PREFERENCE } from '../types/camera.types' +import { DEFAULT_FLAT_WIZARD_PREFERENCE, FlatWizardPreference, flatWizardPreferenceWithDefault } from '../types/flat-wizard.types' +import { DEFAULT_FOCUSER_PREFERENCE, Focuser, FocuserPreference, focuserPreferenceWithDefault } from '../types/focuser.types' +import { DEFAULT_FRAMING_PREFERENCE, FramingPreference, framingPreferenceWithDefault } from '../types/framing.types' +import { DEFAULT_GUIDER_PREFERENCE, GuiderPreference, guiderPreferenceWithDefault } from '../types/guider.types' +import { DEFAULT_HOME_PREFERENCE, HomePreference, homePreferenceWithDefault } from '../types/home.types' +import { DEFAULT_IMAGE_PREFERENCE, ImagePreference, imagePreferenceWithDefault } from '../types/image.types' +import { DEFAULT_MOUNT_PREFERENCE, Mount, MountPreference, mountPreferenceWithDefault } from '../types/mount.types' +import { DEFAULT_ROTATOR_PREFERENCE, Rotator, RotatorPreference, rotatorPreferenceWithDefault } from '../types/rotator.types' +import { DEFAULT_SEQUENCER_PREFERENCE, SequencerPreference, sequencerPreferenceWithDefault } from '../types/sequencer.types' +import { DEFAULT_SETTINGS_PREFERENCE, SettingsPreference, settingsPreferenceWithDefault } from '../types/settings.types' +import { DEFAULT_STACKER_PREFERENCE, StackerPreference, stackerPreferenceWithDefault } from '../types/stacker.types' +import { DEFAULT_WHEEL_PREFERENCE, Wheel, WheelPreference, wheelPreferenceWithDefault } from '../types/wheel.types' import { Undefinable } from '../utils/types' import { LocalStorageService } from './local-storage.service' @@ -24,6 +24,7 @@ export class PreferenceData { private readonly storage: LocalStorageService, private readonly key: string, private readonly defaultValue: T | (() => T), + private readonly withDefault?: (value: T) => T, ) {} has() { @@ -31,7 +32,8 @@ export class PreferenceData { } get(defaultValue?: T | (() => T)): T { - return this.storage.get(this.key, defaultValue ?? this.defaultValue) + const value = this.storage.get(this.key, defaultValue ?? this.defaultValue) + return this.withDefault?.(value) ?? value } set(value: Undefinable) { @@ -47,77 +49,46 @@ export class PreferenceData { export class PreferenceService { constructor(private readonly storage: LocalStorageService) {} - wheelPreference(wheel: FilterWheel) { - return new PreferenceData(this.storage, `wheel.${wheel.name}`, {}) + wheel(wheel: Wheel) { + return new PreferenceData(this.storage, `wheel.${wheel.name}`, () => structuredClone(DEFAULT_WHEEL_PREFERENCE), wheelPreferenceWithDefault) } - cameraPreference(camera: Camera) { - return new PreferenceData(this.storage, `camera.${camera.name}`, () => structuredClone(EMPTY_CAMERA_PREFERENCE)) + camera(camera: Camera) { + return new PreferenceData(this.storage, `camera.${camera.name}`, () => structuredClone(DEFAULT_CAMERA_PREFERENCE), cameraPreferenceWithDefault) } - cameraStartCaptureForFlatWizard(camera: Camera) { - return new PreferenceData(this.storage, `camera.${camera.name}.flatWizard`, () => this.cameraPreference(camera).get()) + mount(mount: Mount) { + return new PreferenceData(this.storage, `mount.${mount.name}`, () => structuredClone(DEFAULT_MOUNT_PREFERENCE), mountPreferenceWithDefault) } - cameraStartCaptureForDARV(camera: Camera) { - return new PreferenceData(this.storage, `camera.${camera.name}.darv`, () => this.cameraPreference(camera).get()) - } - - cameraStartCaptureForTPPA(camera: Camera) { - return new PreferenceData(this.storage, `camera.${camera.name}.tppa`, () => this.cameraPreference(camera).get()) - } - - cameraStartCaptureForAutoFocus(camera: Camera) { - return new PreferenceData(this.storage, `camera.${camera.name}.autoFocus`, () => this.cameraPreference(camera).get()) - } - - mountPreference(mount: Mount) { - return new PreferenceData(this.storage, `mount.${mount.name}`, () => structuredClone(EMPTY_MOUNT_PREFERENCE)) - } - - plateSolverRequest(type: PlateSolverType) { - return new PreferenceData(this.storage, `plateSolver.${type}`, () => ({ ...EMPTY_PLATE_SOLVER_REQUEST, type }) as PlateSolverRequest) - } - - starDetectionRequest(type: StarDetectorType) { - return new PreferenceData(this.storage, `starDetection.${type}`, () => ({ ...EMPTY_STAR_DETECTION_REQUEST, type }) as StarDetectionRequest) - } - - liveStackingRequest(type: LiveStackerType) { - return new PreferenceData(this.storage, `liveStacking.${type}`, () => ({ ...EMPTY_LIVE_STACKING_REQUEST, type }) as LiveStackingRequest) - } - - stackingRequest(type: StackerType) { - return new PreferenceData(this.storage, `stacking.${type}`, () => ({ ...EMPTY_STACKING_REQUEST, type }) as StackingRequest) + focusOffsets(wheel: Wheel, focuser: Focuser) { + return new PreferenceData(this.storage, `focusOffsets.${wheel.name}.${focuser.name}`, () => new Array(wheel.count).fill(0)) } - equipmentForDevice(device: Device) { - return new PreferenceData(this.storage, `equipment.${device.name}`, () => ({}) as Equipment) + focuser(focuser: Focuser) { + return new PreferenceData(this.storage, `focuser.${focuser.name}`, () => structuredClone(DEFAULT_FOCUSER_PREFERENCE), focuserPreferenceWithDefault) } - focusOffsets(wheel: FilterWheel, focuser: Focuser) { - return new PreferenceData(this.storage, `focusOffsets.${wheel.name}.${focuser.name}`, () => new Array(wheel.count).fill(0)) + rotator(rotator: Rotator) { + return new PreferenceData(this.storage, `rotator.${rotator.name}`, () => structuredClone(DEFAULT_ROTATOR_PREFERENCE), rotatorPreferenceWithDefault) } - focuserPreference(focuser: Focuser) { - return new PreferenceData(this.storage, `focuser.${focuser.name}`, {}) + flatWizard(camera: Camera) { + return new PreferenceData(this.storage, `flatWizard.${camera.name}`, () => structuredClone(DEFAULT_FLAT_WIZARD_PREFERENCE), flatWizardPreferenceWithDefault) } - rotatorPreference(rotator: Rotator) { - return new PreferenceData(this.storage, `rotator.${rotator.name}`, {}) + autoFocus(camera: Camera) { + return new PreferenceData(this.storage, `autoFocus.${camera.name}`, () => structuredClone(DEFAULT_AUTO_FOCUS_PREFERENCE), autoFocusPreferenceWithDefault) } - readonly connections = new PreferenceData(this.storage, 'home.connections', () => []) - readonly locations = new PreferenceData(this.storage, 'locations', () => [structuredClone(EMPTY_LOCATION)]) - readonly selectedLocation = new PreferenceData(this.storage, 'locations.selected', () => structuredClone(EMPTY_LOCATION)) - readonly homePreference = new PreferenceData(this.storage, 'home', () => ({}) as HomePreference) - readonly imagePreference = new PreferenceData(this.storage, 'image', () => structuredClone(EMPTY_IMAGE_PREFERENCE)) - readonly skyAtlasPreference = new PreferenceData(this.storage, 'atlas', () => structuredClone(EMPTY_SKY_ATLAS_PREFERENCE)) - readonly alignmentPreference = new PreferenceData(this.storage, 'alignment', () => structuredClone(EMPTY_ALIGNMENT_PREFERENCE)) - readonly imageFOVs = new PreferenceData(this.storage, 'image.fovs', () => []) - readonly calibrationPreference = new PreferenceData(this.storage, 'calibration', () => ({}) as CalibrationPreference) - readonly autoFocusPreference = new PreferenceData(this.storage, 'autoFocus', () => structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE)) - readonly sequencerPreference = new PreferenceData(this.storage, 'sequencer', () => structuredClone(EMPTY_SEQUENCER_PREFERENCE)) - readonly cameraCaptureNamingFormatPreference = new PreferenceData(this.storage, 'camera.namingFormat', () => structuredClone(DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT)) - readonly stackerPreference = new PreferenceData(this.storage, 'stacker', () => structuredClone(EMPTY_STACKER_PREFERENCE)) + readonly home = new PreferenceData(this.storage, 'home', () => structuredClone(DEFAULT_HOME_PREFERENCE), homePreferenceWithDefault) + readonly imagePreference = new PreferenceData(this.storage, 'image', () => structuredClone(DEFAULT_IMAGE_PREFERENCE), imagePreferenceWithDefault) + readonly skyAtlasPreference = new PreferenceData(this.storage, 'atlas', () => structuredClone(DEFAULT_SKY_ATLAS_PREFERENCE), skyAtlasPreferenceWithDefault) + readonly alignment = new PreferenceData(this.storage, 'alignment', () => structuredClone(DEFAULT_ALIGNMENT_PREFERENCE), alignmentPreferenceWithDefault) + readonly calibrationPreference = new PreferenceData(this.storage, 'calibration', () => structuredClone(DEFAULT_CALIBRATION_PREFERENCE), calibrationPreferenceWithDefault) + readonly sequencerPreference = new PreferenceData(this.storage, 'sequencer', () => structuredClone(DEFAULT_SEQUENCER_PREFERENCE), sequencerPreferenceWithDefault) + readonly stacker = new PreferenceData(this.storage, 'stacker', () => structuredClone(DEFAULT_STACKER_PREFERENCE), stackerPreferenceWithDefault) + readonly guider = new PreferenceData(this.storage, 'guider', () => structuredClone(DEFAULT_GUIDER_PREFERENCE), guiderPreferenceWithDefault) + readonly framing = new PreferenceData(this.storage, 'framing', () => structuredClone(DEFAULT_FRAMING_PREFERENCE), framingPreferenceWithDefault) + readonly settings = new PreferenceData(this.storage, 'settings', () => structuredClone(DEFAULT_SETTINGS_PREFERENCE), settingsPreferenceWithDefault) } diff --git a/desktop/src/shared/services/remote-storage.service.ts b/desktop/src/shared/services/remote-storage.service.ts deleted file mode 100644 index ade4eba8f..000000000 --- a/desktop/src/shared/services/remote-storage.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@angular/core' -import { Undefinable } from '../utils/types' -import { ApiService } from './api.service' -import { StorageService } from './storage.service' - -@Injectable({ providedIn: 'root' }) -export class RemoteStorageService implements StorageService { - constructor(private readonly 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: unknown) { - if (value === null || value === undefined) return this.delete(key) - else return this.api.setPreference(key, value) - } -} diff --git a/desktop/src/shared/services/ticker.service.ts b/desktop/src/shared/services/ticker.service.ts new file mode 100644 index 000000000..04ddd9afc --- /dev/null +++ b/desktop/src/shared/services/ticker.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core' + +export interface Tickable { + tick(): void +} + +@Injectable({ providedIn: 'root' }) +export class Ticker { + private readonly tickables = new Map() + + isRegistered(tickable: Tickable) { + return this.tickables.has(tickable) + } + + register(tickable: Tickable, interval: number, initialDelay: number = 1000) { + this.unregister(tickable) + + if (interval > 0) { + if (initialDelay > 0 && initialDelay < interval - 1000) { + setTimeout(() => { + tickable.tick() + }, initialDelay) + } + + const ping = setInterval(() => { + tickable.tick() + }, interval) as unknown as number + + this.tickables.set(tickable, ping) + } + } + + unregister(tickable: Tickable) { + clearInterval(this.tickables.get(tickable)) + this.tickables.delete(tickable) + } +} diff --git a/desktop/src/shared/types/about.types.ts b/desktop/src/shared/types/about.types.ts new file mode 100644 index 000000000..23ef5a80f --- /dev/null +++ b/desktop/src/shared/types/about.types.ts @@ -0,0 +1,13 @@ +export interface IconItem { + name: string + author: string + link: string +} + +export interface DependencyItem { + name: string + version: string + link: string +} + +export const FLAT_ICON_URL = 'https://www.flaticon.com/free-icon' diff --git a/desktop/src/shared/types/alignment.types.ts b/desktop/src/shared/types/alignment.types.ts index 3752c29ab..9bb245529 100644 --- a/desktop/src/shared/types/alignment.types.ts +++ b/desktop/src/shared/types/alignment.types.ts @@ -1,7 +1,7 @@ import type { Angle } from './atlas.types' -import type { Camera, CameraCaptureEvent, CameraStartCapture } from './camera.types' +import { cameraStartCaptureWithDefault, DEFAULT_CAMERA_START_CAPTURE, type Camera, type CameraCaptureEvent, type CameraStartCapture } from './camera.types' import type { GuideDirection } from './guider.types' -import type { PlateSolverRequest, PlateSolverType } from './platesolver.types' +import { DEFAULT_PLATE_SOLVER_REQUEST, plateSolverRequestWithDefault, type PlateSolverRequest } from './platesolver.types' export type Hemisphere = 'NORTHERN' | 'SOUTHERN' @@ -12,33 +12,13 @@ export type TPPAState = 'IDLE' | 'SLEWING' | 'SLEWED' | 'SETTLING' | 'EXPOSURING export type AlignmentMethod = 'DARV' | 'TPPA' export interface AlignmentPreference { - darvInitialPause: number - darvExposureTime: number darvHemisphere: Hemisphere - tppaStartFromCurrentPosition: boolean - tppaStepDirection: GuideDirection - tppaCompensateRefraction: boolean - tppaStopTrackingWhenDone: boolean - tppaStepDuration: number - tppaPlateSolverType: PlateSolverType -} - -export const EMPTY_ALIGNMENT_PREFERENCE: AlignmentPreference = { - darvInitialPause: 5, - darvExposureTime: 30, - darvHemisphere: 'NORTHERN', - tppaStartFromCurrentPosition: true, - tppaStepDirection: 'EAST', - tppaCompensateRefraction: true, - tppaStopTrackingWhenDone: true, - tppaStepDuration: 5, - tppaPlateSolverType: 'ASTAP', + darvRequest: DARVStart + tppaRequest: TPPAStart } export interface DARVStart { capture: CameraStartCapture - exposureTime: number - initialPause: number direction: GuideDirection reversed: boolean } @@ -61,6 +41,17 @@ export interface TPPAStart { stepSpeed?: string } +export interface TPPAResult { + failed: boolean + rightAscension: Angle + declination: Angle + azimuthError: Angle + azimuthErrorDirection: string + altitudeError: Angle + altitudeErrorDirection: string + totalError: Angle +} + export interface TPPAEvent extends MessageEvent { camera: Camera state: TPPAState @@ -73,3 +64,80 @@ export interface TPPAEvent extends MessageEvent { altitudeErrorDirection: string capture?: CameraCaptureEvent } + +export interface DARVResult { + direction?: GuideDirection +} + +export const DEFAULT_CAMERA_START_CAPTURE_TPPA: CameraStartCapture = { + ...DEFAULT_CAMERA_START_CAPTURE, +} + +export const DEFAULT_TPPA_START: TPPAStart = { + capture: DEFAULT_CAMERA_START_CAPTURE_TPPA, + plateSolver: DEFAULT_PLATE_SOLVER_REQUEST, + startFromCurrentPosition: true, + stepDirection: 'EAST', + compensateRefraction: true, + stopTrackingWhenDone: true, + stepDuration: 5, +} + +export const DEFAULT_TPPA_RESULT: TPPAResult = { + failed: false, + rightAscension: `00h00m00s`, + declination: `00°00'00"`, + azimuthError: `00°00'00"`, + azimuthErrorDirection: '', + altitudeError: `00°00'00"`, + altitudeErrorDirection: '', + totalError: `00°00'00"`, +} + +export const DEFAULT_CAMERA_START_CAPTURE_DARV: CameraStartCapture = { + ...DEFAULT_CAMERA_START_CAPTURE, + exposureDelay: 5, + exposureTime: 30000000, +} + +export const DEFAULT_DARV_START: DARVStart = { + capture: DEFAULT_CAMERA_START_CAPTURE_DARV, + direction: 'NORTH', + reversed: false, +} + +export const DEFAULT_DARV_RESULT: DARVResult = {} + +export const DEFAULT_ALIGNMENT_PREFERENCE: AlignmentPreference = { + darvHemisphere: 'NORTHERN', + darvRequest: DEFAULT_DARV_START, + tppaRequest: DEFAULT_TPPA_START, +} + +export function darvStartWithDefault(request?: Partial, source: DARVStart = DEFAULT_DARV_START) { + if (!request) return structuredClone(source) + request.capture = cameraStartCaptureWithDefault(request.capture, source.capture) + request.direction ||= source.direction + request.reversed ??= source.reversed + return request as DARVStart +} + +export function tppaStartWithDefault(request?: Partial, source: TPPAStart = DEFAULT_TPPA_START) { + if (!request) return structuredClone(source) + request.capture = cameraStartCaptureWithDefault(request.capture, source.capture) + request.plateSolver = plateSolverRequestWithDefault(request.plateSolver, source.plateSolver) + request.startFromCurrentPosition ??= source.startFromCurrentPosition + request.stepDirection ||= source.stepDirection + request.compensateRefraction ??= source.compensateRefraction + request.stopTrackingWhenDone ??= source.stopTrackingWhenDone + request.stepDuration ??= source.stepDuration + return request as TPPAStart +} + +export function alignmentPreferenceWithDefault(preference?: Partial, source: AlignmentPreference = DEFAULT_ALIGNMENT_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.darvHemisphere ||= source.darvHemisphere + preference.darvRequest = darvStartWithDefault(preference.darvRequest, source.darvRequest) + preference.tppaRequest = tppaStartWithDefault(preference.tppaRequest, source.tppaRequest) + return preference as AlignmentPreference +} diff --git a/desktop/src/shared/types/angular.types.ts b/desktop/src/shared/types/angular.types.ts new file mode 100644 index 000000000..7abbd0d26 --- /dev/null +++ b/desktop/src/shared/types/angular.types.ts @@ -0,0 +1,30 @@ +export type Severity = 'success' | 'info' | 'warning' | 'danger' + +export type TooltipPosition = 'right' | 'left' | 'top' | 'bottom' + +export interface DropdownItem { + label: string + value: T +} + +export function extractDateTime(date: Date) { + return [extractDate(date), extractTime(date)] +} + +function padNumber(value: number) { + return value <= 9 ? `0${value}` : `${value}` +} + +export function extractDate(date: Date) { + return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}` +} + +export function extractTime(date: Date, hasSeconds: boolean = true) { + const time = `${padNumber(date.getHours())}:${padNumber(date.getMinutes())}` + + if (hasSeconds) { + return `${time}:${padNumber(date.getSeconds())}` + } else { + return time + } +} diff --git a/desktop/src/shared/types/api.types.ts b/desktop/src/shared/types/api.types.ts index a1edbff8e..aea0aa242 100644 --- a/desktop/src/shared/types/api.types.ts +++ b/desktop/src/shared/types/api.types.ts @@ -1,5 +1,7 @@ import type { Device } from './device.types' +export type ApiEventType = (typeof API_EVENT_TYPES)[number] + export interface MessageEvent { eventName: string } @@ -53,5 +55,3 @@ export const API_EVENT_TYPES = [ // Auto Focus. 'AUTO_FOCUS.ELAPSED', ] as const - -export type ApiEventType = (typeof API_EVENT_TYPES)[number] diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts index 5c95403fe..742b3181f 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -1,8 +1,9 @@ +import type { Severity } from './angular.types' import type { MessageEvent } from './api.types' -export type Severity = 'success' | 'info' | 'warning' | 'danger' +export type InternalEventType = (typeof INTERNAL_EVENT_TYPES)[number] -export type TooltipPosition = 'right' | 'left' | 'top' | 'bottom' +export type SaveJson = OpenFile & JsonFile export interface NotificationEvent extends MessageEvent { target?: string @@ -16,28 +17,6 @@ export interface ConfirmationEvent extends MessageEvent { idempotencyKey: string } -export const INTERNAL_EVENT_TYPES = [ - 'DIRECTORY.OPEN', - 'FILE.OPEN', - 'FILE.SAVE', - 'WINDOW.OPEN', - 'WINDOW.CLOSE', - 'WINDOW.PIN', - 'WINDOW.UNPIN', - 'WINDOW.MINIMIZE', - 'WINDOW.MAXIMIZE', - 'WINDOW.RESIZE', - 'WHEEL.RENAMED', - 'LOCATION.CHANGED', - 'JSON.WRITE', - 'JSON.READ', - 'CALIBRATION.CHANGED', - 'WINDOW.FULLSCREEN', - 'ROI.SELECTED', -] as const - -export type InternalEventType = (typeof INTERNAL_EVENT_TYPES)[number] - export interface WindowPreference { modal?: boolean autoResizable?: boolean @@ -88,15 +67,22 @@ export interface JsonFile { json: T } -export interface SaveJson extends OpenFile, JsonFile {} - -export type StoredWindowDataKey = `window.${string}` - -export interface StoredWindowDataValue { - x: number - y: number - width: number - height: number -} - -export type StoredWindowData = Record +export const INTERNAL_EVENT_TYPES = [ + 'DIRECTORY.OPEN', + 'FILE.OPEN', + 'FILE.SAVE', + 'WINDOW.OPEN', + 'WINDOW.CLOSE', + 'WINDOW.PIN', + 'WINDOW.UNPIN', + 'WINDOW.MINIMIZE', + 'WINDOW.MAXIMIZE', + 'WINDOW.RESIZE', + 'WHEEL.RENAMED', + 'LOCATION.CHANGED', + 'JSON.WRITE', + 'JSON.READ', + 'CALIBRATION.CHANGED', + 'WINDOW.FULLSCREEN', + 'ROI.SELECTED', +] as const diff --git a/desktop/src/shared/types/atlas.types.ts b/desktop/src/shared/types/atlas.types.ts index 8ef9bca6b..4071a0d87 100644 --- a/desktop/src/shared/types/atlas.types.ts +++ b/desktop/src/shared/types/atlas.types.ts @@ -1,14 +1,121 @@ +import type { Subscription } from 'rxjs' +import type { Severity } from './angular.types' import type { PierSide } from './mount.types' export type Angle = string | number -export interface PlanetTableItem { +export type Constellation = (typeof CONSTELLATIONS)[number] + +export type ClassificationType = (typeof CLASSIFICATION_TYPES)[number] + +export type SkyObjectType = (typeof SKY_OBJECT_TYPES)[number] + +export type MinorPlanetKind = 'ASTEROID' | 'COMET' + +export type Star = DeepSkyObject & SpectralSkyObject + +export type SatelliteGroupType = (typeof SATELLITE_GROUPS)[number] + +export type PlanetType = 'PLANET' | 'DWARF_PLANET' | 'MOON_OF_MARS' | 'MOON_OF_JUPITER' | 'MOON_OF_SATURN' | 'MOON_OF_URANUS' | 'MOON_OF_NEPTUNE' | 'MOON_OF_PLUTO' | 'ASTEROID' + +export type AltitudeDataPoint = [number, number] + +export type SatelliteSearchGroups = Record + +export enum BodyTabType { + SUN, + MOON, + PLANET, + MINOR_PLANET, + SKY_OBJECT, + SATELLITE, +} + +export interface BodyTag { + label: string + severity: Severity +} + +export interface BodyTab { + position: BodyPosition name: string - type: string + tags: BodyTag[] +} + +export interface SunTab extends BodyTab { + image: string +} + +export type MoonTab = BodyTab + +export interface PlanetItem { + name: string + type: PlanetType code: string } -export interface SearchFilter { +export interface PlanetTab extends BodyTab { + selected?: PlanetItem + readonly planets: PlanetItem[] +} + +export interface OrbitalPhysicalParameter { + name: string + description: string + value: string +} + +export interface MinorPlanetListItem { + name: string + pdes: string +} + +export interface MinorPlanet { + found: boolean + name: string + spkId: number + kind?: MinorPlanetKind + pha: boolean + neo: boolean + orbitType: string + parameters: OrbitalPhysicalParameter[] + list: MinorPlanetListItem[] +} + +export interface CloseApproach { + name: string + designation: string + dateTime: number + distance: number + absoluteMagnitude: number +} + +export interface MinorPlanetTab extends BodyTab { + tab: number + search: { + text: string + result?: MinorPlanet + } + closeApproach: { + days: number + lunarDistance: number + result: CloseApproach[] + selected?: CloseApproach + } + list: { + items: MinorPlanetListItem[] + showDialog: boolean + } +} + +export interface SkyObjectTab extends BodyTab { + search: SkyObjectSearchDialog & { + result: DeepSkyObject[] + selected?: DeepSkyObject + } +} + +export interface SkyObjectSearchFilter { text: string rightAscension: Angle declination: Angle @@ -16,10 +123,234 @@ export interface SearchFilter { constellation: Constellation | 'ALL' magnitude: [number, number] type: SkyObjectType | 'ALL' - types: (SkyObjectType | 'ALL')[] } -export const EMPTY_SEARCH_FILTER: SearchFilter = { +export interface SkyObjectSearchDialog { + showDialog: boolean + filter: SkyObjectSearchFilter +} + +export interface SatelliteSearchFilter { + text: string + groups: SatelliteSearchGroups +} + +export interface SatelliteSearchDialog { + showDialog: boolean + filter: SatelliteSearchFilter +} + +export interface Satellite { + id: number + name: string + tle: string + groups: SatelliteGroupType[] +} + +export interface SatelliteTab extends BodyTab { + search: SatelliteSearchDialog & { + result: Satellite[] + selected?: Satellite + } +} + +export interface BodyTabRefresh { + count: number + timer?: Subscription + position: boolean + chart: boolean +} + +export interface DateTimeAndLocation { + manual: boolean + dateTime: Date + location: Location +} + +export interface Location { + id: number + name: string + latitude: number + longitude: number + elevation: number + offsetInMinutes: number +} + +export interface SkyAtlasPreference { + satellites: SatelliteSearchGroups + location: Location + fast: boolean +} + +export interface SkyAtlasInput { + tab: BodyTabType + filter?: Partial> +} + +export interface EquatorialCoordinate { + rightAscension: Angle + declination: Angle +} + +export interface EquatorialCoordinateJ2000 { + rightAscensionJ2000: Angle + declinationJ2000: Angle +} + +export interface HorizontalCoordinate { + azimuth: Angle + altitude: Angle +} + +export interface BodyPosition extends EquatorialCoordinate, EquatorialCoordinateJ2000, HorizontalCoordinate { + magnitude: number + constellation: Constellation + distance: number + distanceUnit: string + illuminated: number + elongation: number + leading: boolean +} + +export interface Twilight { + civilDusk: number[] + nauticalDusk: number[] + astronomicalDusk: number[] + night: number[] + astronomicalDawn: number[] + nauticalDawn: number[] + civilDawn: number[] +} + +export interface AstronomicalObject extends EquatorialCoordinateJ2000 { + id: number + name: string + magnitude: number +} + +export interface SpectralSkyObject { + spType: string +} + +export interface OrientedSkyObject { + majorAxis: number + minorAxis: number + orientation: number +} + +export interface DeepSkyObject extends AstronomicalObject { + type: SkyObjectType + redshift: number + parallax: number + radialVelocity: number + distance: number + pmRA: number + pmDEC: number + constellation: Constellation +} + +export interface ComputedLocation extends EquatorialCoordinate, EquatorialCoordinateJ2000, HorizontalCoordinate { + constellation: Constellation + meridianAt: string + timeLeftToMeridianFlip: string + lst: string + pierSide: PierSide +} + +export const DEFAULT_BODY_POSITION: BodyPosition = { + rightAscensionJ2000: '00h00m00s', + declinationJ2000: `+000°00'00"`, + rightAscension: '00h00m00s', + declination: `+000°00'00"`, + azimuth: `000°00'00"`, + altitude: `+00°00'00"`, + magnitude: 0, + constellation: 'AND', + distance: 0, + distanceUnit: 'ly', + illuminated: 0, + elongation: 0, + leading: false, +} + +export const DEFAULT_SUN: SunTab = { + name: 'Sun', + position: DEFAULT_BODY_POSITION, + tags: [], + image: '', +} + +export const DEFAULT_MOON: MoonTab = { + name: 'Moon', + position: DEFAULT_BODY_POSITION, + tags: [], +} + +export const DEFAULT_PLANET_ITEMS: PlanetItem[] = [ + { name: 'Mercury', type: 'PLANET', code: '199' }, + { name: 'Venus', type: 'PLANET', code: '299' }, + { name: 'Mars', type: 'PLANET', code: '499' }, + { name: 'Jupiter', type: 'PLANET', code: '599' }, + { name: 'Saturn', type: 'PLANET', code: '699' }, + { name: 'Uranus', type: 'PLANET', code: '799' }, + { name: 'Neptune', type: 'PLANET', code: '899' }, + { name: 'Pluto', type: 'DWARF_PLANET', code: '999' }, + { name: 'Phobos', type: 'MOON_OF_MARS', code: '401' }, + { name: 'Deimos', type: 'MOON_OF_MARS', code: '402' }, + { name: 'Io', type: 'MOON_OF_JUPITER', code: '501' }, + { name: 'Europa', type: 'MOON_OF_JUPITER', code: '402' }, + { name: 'Ganymede', type: 'MOON_OF_JUPITER', code: '403' }, + { name: 'Callisto', type: 'MOON_OF_JUPITER', code: '504' }, + { name: 'Mimas', type: 'MOON_OF_SATURN', code: '601' }, + { name: 'Enceladus', type: 'MOON_OF_SATURN', code: '602' }, + { name: 'Tethys', type: 'MOON_OF_SATURN', code: '603' }, + { name: 'Dione', type: 'MOON_OF_SATURN', code: '604' }, + { name: 'Rhea', type: 'MOON_OF_SATURN', code: '605' }, + { name: 'Titan', type: 'MOON_OF_SATURN', code: '606' }, + { name: 'Hyperion', type: 'MOON_OF_SATURN', code: '607' }, + { name: 'Iapetus', type: 'MOON_OF_SATURN', code: '608' }, + { name: 'Ariel', type: 'MOON_OF_URANUS', code: '701' }, + { name: 'Umbriel', type: 'MOON_OF_URANUS', code: '702' }, + { name: 'Titania', type: 'MOON_OF_URANUS', code: '703' }, + { name: 'Oberon', type: 'MOON_OF_URANUS', code: '704' }, + { name: 'Miranda', type: 'MOON_OF_URANUS', code: '705' }, + { name: 'Triton', type: 'MOON_OF_NEPTUNE', code: '801' }, + { name: 'Charon', type: 'MOON_OF_PLUTO', code: '901' }, + { name: '1 Ceres', type: 'DWARF_PLANET', code: '1;' }, + { name: '90377 Sedna', type: 'DWARF_PLANET', code: '90377;' }, + { name: '136199 Eris', type: 'DWARF_PLANET', code: '136199;' }, + { name: '2 Pallas', type: 'ASTEROID', code: '2;' }, + { name: '3 Juno', type: 'ASTEROID', code: '3;' }, + { name: '4 Vesta', type: 'ASTEROID', code: '4;' }, +] + +export const DEFAULT_PLANET: PlanetTab = { + name: '', + position: DEFAULT_BODY_POSITION, + tags: [], + planets: DEFAULT_PLANET_ITEMS, +} + +export const DEFAULT_MINOR_PLANET: MinorPlanetTab = { + tab: 0, + name: '', + position: DEFAULT_BODY_POSITION, + tags: [], + search: { + text: '', + }, + closeApproach: { + days: 7, + lunarDistance: 10, + result: [], + }, + list: { + showDialog: false, + items: [], + }, +} + +export const DEFAULT_SKY_OBJECT_SEARCH_FILTER: SkyObjectSearchFilter = { text: '', rightAscension: '00h00m00s', declination: `+000°00'00"`, @@ -27,42 +358,141 @@ export const EMPTY_SEARCH_FILTER: SearchFilter = { constellation: 'ALL', magnitude: [-30, 30], type: 'ALL', - types: ['ALL'], } -export interface SatelliteGroupFilterItem { - group: SatelliteGroupType - enabled: boolean +export const DEFAULT_SKY_OBJECT_SEARCH_DIALOG: SkyObjectSearchDialog = { + showDialog: false, + filter: DEFAULT_SKY_OBJECT_SEARCH_FILTER, } -export interface SkyAtlasPreference { - satellites: SatelliteGroupFilterItem[] - fast: boolean +export const DEFAULT_SKY_OBJECT: SkyObjectTab = { + name: '', + search: { + ...DEFAULT_SKY_OBJECT_SEARCH_DIALOG, + result: [], + }, + position: DEFAULT_BODY_POSITION, + tags: [], } -export const EMPTY_SKY_ATLAS_PREFERENCE: SkyAtlasPreference = { - satellites: [], - fast: false, +export const DEFAULT_SATELLITE_SEARCH_GROUPS: SatelliteSearchGroups = { + ACTIVE: false, + AMATEUR: true, + ANALYST: false, + ARGOS: false, + BEIDOU: true, + COSMOS_1408_DEBRIS: false, + COSMOS_2251_DEBRIS: false, + CUBESAT: false, + DMC: false, + EDUCATION: false, + ENGINEERING: false, + FENGYUN_1C_DEBRIS: false, + GALILEO: true, + GEO: false, + GEODETIC: false, + GLO_OPS: true, + GLOBALSTAR: false, + GNSS: true, + GOES: false, + GORIZONT: false, + GPS_OPS: true, + INTELSAT: false, + IRIDIUM_33_DEBRIS: false, + IRIDIUM_NEXT: false, + IRIDIUM: false, + LAST_30_DAYS: false, + MILITARY: false, + MOLNIYA: false, + MUSSON: false, + NNSS: false, + NOAA: false, + ONEWEB: true, + ORBCOMM: false, + OTHER_COMM: false, + OTHER: false, + PLANET: false, + RADAR: false, + RADUGA: false, + RESOURCE: false, + SARSAT: false, + SATNOGS: false, + SBAS: false, + SCIENCE: true, + SES: false, + SPIRE: false, + STARLINK: true, + STATIONS: true, + SWARM: false, + TDRSS: false, + VISUAL: true, + WEATHER: false, + X_COMM: false, } -export enum SkyAtlasTab { - SUN, - MOON, - PLANET, - MINOR_PLANET, - SKY_OBJECT, - SATELLITE, +export const DEFAULT_SATELLITE_SEARCH_FILTER: SatelliteSearchFilter = { + text: '', + groups: DEFAULT_SATELLITE_SEARCH_GROUPS, } -export interface SkyAtlasInput { - tab: SkyAtlasTab - filter?: Partial> +export const DEFAULT_SATELLITE_SEARCH_DIALOG: SatelliteSearchDialog = { + showDialog: false, + filter: DEFAULT_SATELLITE_SEARCH_FILTER, } -export interface SettingsDialog { - showDialog: boolean +export const DEFAULT_SATELLITE: SatelliteTab = { + name: '', + search: { + ...DEFAULT_SATELLITE_SEARCH_DIALOG, + result: [], + }, + position: DEFAULT_BODY_POSITION, + tags: [], +} + +export const DEFAULT_COMPUTED_LOCATION: ComputedLocation = { + constellation: 'AND', + meridianAt: '00:00', + timeLeftToMeridianFlip: '00:00', + lst: '00:00', + pierSide: 'NEITHER', + rightAscensionJ2000: '00h00m00s', + declinationJ2000: `+000°00'00"`, + rightAscension: '00h00m00s', + declination: `+000°00'00"`, + azimuth: `000°00'00"`, + altitude: `+00°00'00"`, } +export const DEFAULT_LOCATION: Location = { + id: 0, + name: 'Null Island', + latitude: 0, + longitude: 0, + elevation: 0, + offsetInMinutes: 0, +} + +export const DEFAULT_BODY_TAB_REFRESH: BodyTabRefresh = { + count: 0, + position: false, + chart: false, +} + +export const DEFAULT_DATE_TIME_AND_LOCATION: DateTimeAndLocation = { + manual: false, + dateTime: new Date(), + location: DEFAULT_LOCATION, +} + +export const DEFAULT_SKY_ATLAS_PREFERENCE: SkyAtlasPreference = { + satellites: DEFAULT_SATELLITE_SEARCH_GROUPS, + location: DEFAULT_DATE_TIME_AND_LOCATION.location, + fast: false, +} + +export const CLASSIFICATION_TYPES = ['STAR', 'SET_OF_STARS', 'INTERSTELLAR_MEDIUM', 'GALAXY', 'SET_OF_GALAXIES', 'GRAVITATION', 'SPECTRAL', 'OTHER'] as const + export const CONSTELLATIONS = [ 'AND', 'ANT', @@ -154,12 +584,6 @@ export const CONSTELLATIONS = [ 'VUL', ] as const -export type Constellation = (typeof CONSTELLATIONS)[number] - -export const CLASSIFICATION_TYPES = ['STAR', 'SET_OF_STARS', 'INTERSTELLAR_MEDIUM', 'GALAXY', 'SET_OF_GALAXIES', 'GRAVITATION', 'SPECTRAL', 'OTHER'] as const - -export type ClassificationType = (typeof CLASSIFICATION_TYPES)[number] - export const SKY_OBJECT_TYPES = [ 'ACTIVE_GALAXY_NUCLEUS', 'ALPHA2_CVN_VARIABLE', @@ -315,143 +739,6 @@ export const SKY_OBJECT_TYPES = [ 'YOUNG_STELLAR_OBJECT', ] as const -export type SkyObjectType = (typeof SKY_OBJECT_TYPES)[number] - -export interface EquatorialCoordinate { - rightAscension: Angle - declination: Angle -} - -export interface EquatorialCoordinateJ2000 { - rightAscensionJ2000: Angle - declinationJ2000: Angle -} - -export interface HorizontalCoordinate { - azimuth: Angle - altitude: Angle -} - -export interface BodyPosition extends EquatorialCoordinate, EquatorialCoordinateJ2000, HorizontalCoordinate { - magnitude: number - constellation: Constellation - distance: number - distanceUnit: string - illuminated: number - elongation: number - leading: boolean -} - -export const EMPTY_BODY_POSITION: BodyPosition = { - rightAscensionJ2000: '00h00m00s', - declinationJ2000: `+000°00'00"`, - rightAscension: '00h00m00s', - declination: `+000°00'00"`, - azimuth: `000°00'00"`, - altitude: `+00°00'00"`, - magnitude: 0, - constellation: 'AND', - distance: 0, - distanceUnit: 'ly', - illuminated: 0, - elongation: 0, - leading: false, -} - -export interface Twilight { - civilDusk: number[] - nauticalDusk: number[] - astronomicalDusk: number[] - night: number[] - astronomicalDawn: number[] - nauticalDawn: number[] - civilDawn: number[] -} - -export type MinorPlanetKind = 'ASTEROID' | 'COMET' - -export interface MinorPlanetSearchItem { - name: string - pdes: string -} - -export interface MinorPlanet { - found: boolean - name: string - spkId: number - kind?: MinorPlanetKind - pha: boolean - neo: boolean - orbitType: string - parameters: OrbitalPhysicalParameter[] - searchItems: MinorPlanetSearchItem[] -} - -export interface OrbitalPhysicalParameter { - name: string - description: string - value: string -} - -export interface CloseApproach { - name: string - designation: string - dateTime: number - distance: number - absoluteMagnitude: number -} - -export interface AstronomicalObject extends EquatorialCoordinateJ2000 { - id: number - name: string - magnitude: number -} - -export interface SpectralSkyObject { - spType: string -} - -export type Star = DeepSkyObject & SpectralSkyObject - -export interface OrientedSkyObject { - majorAxis: number - minorAxis: number - orientation: number -} - -export interface DeepSkyObject extends AstronomicalObject { - type: SkyObjectType - redshift: number - parallax: number - radialVelocity: number - distance: number - pmRA: number - pmDEC: number - constellation: Constellation -} - -export interface ComputedLocation extends EquatorialCoordinate, EquatorialCoordinateJ2000, HorizontalCoordinate { - constellation: Constellation - meridianAt: string - timeLeftToMeridianFlip: string - lst: string - pierSide: PierSide -} - -export const EMPTY_COMPUTED_LOCATION: ComputedLocation = { - constellation: 'AND', - meridianAt: '00:00', - timeLeftToMeridianFlip: '00:00', - lst: '00:00', - pierSide: 'NEITHER', - rightAscensionJ2000: '00h00m00s', - declinationJ2000: `+000°00'00"`, - rightAscension: '00h00m00s', - declination: `+000°00'00"`, - azimuth: `000°00'00"`, - altitude: `+00°00'00"`, -} - export const SATELLITE_GROUPS = [ 'LAST_30_DAYS', 'STATIONS', @@ -507,29 +794,54 @@ export const SATELLITE_GROUPS = [ 'OTHER', ] as const -export type SatelliteGroupType = (typeof SATELLITE_GROUPS)[number] +export function searchFilterWithDefault(filter?: Partial, source: SkyObjectSearchFilter = DEFAULT_SKY_OBJECT_SEARCH_FILTER) { + if (!filter) return structuredClone(source) + filter.rightAscension ??= source.rightAscension + filter.declination ??= source.declination + filter.radius ||= source.radius + filter.constellation ??= source.constellation + filter.magnitude ??= source.magnitude + filter.type ??= source.type + return filter as SkyObjectSearchFilter +} -export interface Satellite { - id: number - name: string - tle: string - groups: SatelliteGroupType[] +export function satelliteSearchGroupsWithDefault(groups?: Partial, source: SatelliteSearchGroups = DEFAULT_SATELLITE_SEARCH_GROUPS) { + if (!groups) return structuredClone(source) + + if ('ACTIVE' in groups) { + for (const entry of Object.entries(source)) { + const key = entry[0] as SatelliteGroupType + groups[key] ??= source[key] + } + + return groups as SatelliteSearchGroups + } else { + return structuredClone(source) + } } -export interface Location { - id: number - name: string - latitude: number - longitude: number - elevation: number - offsetInMinutes: number +export function resetSatelliteSearchGroup(groups: SatelliteSearchGroups, source: SatelliteSearchGroups = DEFAULT_SATELLITE_SEARCH_GROUPS) { + for (const entry of Object.entries(source)) { + const key = entry[0] as SatelliteGroupType + groups[key] = source[key] + } } -export const EMPTY_LOCATION: Location = { - id: 0, - name: 'Null Island', - latitude: 0, - longitude: 0, - elevation: 0, - offsetInMinutes: 0, +export function locationWithDefault(location?: Partial, source: Location = DEFAULT_LOCATION) { + if (!location) return structuredClone(source) + location.id ??= source.id + location.name ||= source.name + location.latitude ??= source.latitude + location.longitude ??= source.longitude + location.elevation ??= source.elevation + location.offsetInMinutes ??= source.offsetInMinutes + return location as Location +} + +export function skyAtlasPreferenceWithDefault(preference?: Partial, source: SkyAtlasPreference = DEFAULT_SKY_ATLAS_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.satellites = satelliteSearchGroupsWithDefault(preference.satellites, source.satellites) + preference.location = locationWithDefault(preference.location, source.location) + preference.fast ??= source.fast + return preference as SkyAtlasPreference } diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts index 2e1357061..874f452e5 100644 --- a/desktop/src/shared/types/autofocus.type.ts +++ b/desktop/src/shared/types/autofocus.type.ts @@ -1,7 +1,7 @@ import type { Point } from 'electron' -import type { CameraCaptureEvent, CameraStartCapture } from './camera.types' +import { cameraStartCaptureWithDefault, DEFAULT_CAMERA_START_CAPTURE, type CameraCaptureEvent, type CameraStartCapture } from './camera.types' import type { StarDetectionRequest } from './stardetector.types' -import { EMPTY_STAR_DETECTION_REQUEST } from './stardetector.types' +import { DEFAULT_STAR_DETECTION_REQUEST, starDetectionRequestWithDefault } from './stardetector.types' export type AutoFocusState = 'IDLE' | 'MOVING' | 'EXPOSURING' | 'EXPOSURED' | 'ANALYSING' | 'ANALYSED' | 'CURVE_FITTED' | 'FAILED' | 'FINISHED' @@ -26,20 +26,8 @@ export interface AutoFocusRequest { starDetector: StarDetectionRequest } -export type AutoFocusPreference = Omit - -export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { - fittingMode: 'HYPERBOLIC', - rSquaredThreshold: 0.5, - initialOffsetSteps: 4, - stepSize: 100, - totalNumberOfAttempts: 1, - backlashCompensation: { - mode: 'NONE', - backlashIn: 0, - backlashOut: 0, - }, - starDetector: EMPTY_STAR_DETECTION_REQUEST, +export interface AutoFocusPreference { + request: AutoFocusRequest } export interface Curve { @@ -91,3 +79,55 @@ export interface AutoFocusEvent { chart?: AutoFocusChart capture?: CameraCaptureEvent } + +export const DEFAULT_CAMERA_START_CAPTURE_AUTO_FOCUS: CameraStartCapture = { + ...DEFAULT_CAMERA_START_CAPTURE, +} + +export const DEFAULT_BACKLASH_COMPENSATION: BacklashCompensation = { + mode: 'NONE', + backlashIn: 0, + backlashOut: 0, +} + +export const DEFAULT_AUTO_FOCUS_REQUEST: AutoFocusRequest = { + capture: DEFAULT_CAMERA_START_CAPTURE_AUTO_FOCUS, + fittingMode: 'HYPERBOLIC', + rSquaredThreshold: 0.5, + initialOffsetSteps: 4, + stepSize: 100, + totalNumberOfAttempts: 1, + backlashCompensation: DEFAULT_BACKLASH_COMPENSATION, + starDetector: DEFAULT_STAR_DETECTION_REQUEST, +} + +export const DEFAULT_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = { + request: DEFAULT_AUTO_FOCUS_REQUEST, +} + +export function backlashCompensationWithDefault(compensation?: Partial, source: BacklashCompensation = DEFAULT_BACKLASH_COMPENSATION) { + if (!compensation) return structuredClone(source) + compensation.mode ||= source.mode + compensation.backlashIn ??= source.backlashIn + compensation.backlashOut ??= source.backlashOut + return compensation as BacklashCompensation +} + +export function autoFocusRequestWithDefault(request?: Partial, source: AutoFocusRequest = DEFAULT_AUTO_FOCUS_REQUEST) { + if (!request) return structuredClone(source) + request.capture = cameraStartCaptureWithDefault(request.capture, source.capture) + request.fittingMode ??= source.fittingMode + request.rSquaredThreshold ??= source.rSquaredThreshold + request.initialOffsetSteps ??= source.initialOffsetSteps + request.stepSize ??= source.stepSize + request.totalNumberOfAttempts ??= source.totalNumberOfAttempts + request.backlashCompensation = backlashCompensationWithDefault(request.backlashCompensation, source.backlashCompensation) + request.starDetector = starDetectionRequestWithDefault(request.starDetector, source.starDetector) + return request as AutoFocusRequest +} + +export function autoFocusPreferenceWithDefault(preference?: Partial, source: AutoFocusPreference = DEFAULT_AUTO_FOCUS_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.request = autoFocusRequestWithDefault(preference.request, source.request) + return preference as AutoFocusPreference +} diff --git a/desktop/src/shared/types/calibration.types.ts b/desktop/src/shared/types/calibration.types.ts index 51b3c445f..e5b798708 100644 --- a/desktop/src/shared/types/calibration.types.ts +++ b/desktop/src/shared/types/calibration.types.ts @@ -1,28 +1,33 @@ -import type { FrameType } from './camera.types' +import type { Image } from './image.types' -export interface CalibrationFrame { +export interface CalibrationFrame extends Image { id: number - type: FrameType - name: string - filter?: string - exposureTime: number - temperature: number - width: number - height: number - binX: number - binY: number - gain: number + group: string path: string enabled: boolean } -export interface CalibrationFrameGroup { - id: number - name: string - key: Omit - frames: CalibrationFrame[] +export interface CalibrationGroupDialog { + showDialog: boolean + group: string + save?: () => Promise | void } export interface CalibrationPreference { - openPath?: string + filePath?: string + directoryPath?: string +} + +export const DEFAULT_CALIBRATION_GROUP_DIALOG: CalibrationGroupDialog = { + showDialog: false, + group: '', +} + +export const DEFAULT_CALIBRATION_PREFERENCE: CalibrationPreference = {} + +export function calibrationPreferenceWithDefault(preference?: Partial, source: CalibrationPreference = DEFAULT_CALIBRATION_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.filePath ||= source.filePath + preference.directoryPath ||= source.directoryPath + return preference as CalibrationPreference } diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 4948c7139..2bdebb1c8 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -2,10 +2,13 @@ import type { MessageEvent } from './api.types' import type { Thermometer } from './auxiliary.types' import type { CompanionDevice, Device, PropertyState } from './device.types' import { isCompanionDevice } from './device.types' +import type { Focuser } from './focuser.types' import type { GuideOutput } from './guider.types' -import { type CameraCaptureNamingFormat } from './settings.types' +import type { Mount } from './mount.types' +import type { Rotator } from './rotator.types' +import type { Wheel } from './wheel.types' -export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' | 'AUTO_FOCUS' +export type CameraMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' | 'AUTO_FOCUS' export type FrameType = 'LIGHT' | 'DARK' | 'FLAT' | 'BIAS' @@ -17,73 +20,182 @@ export type ExposureMode = 'SINGLE' | 'FIXED' | 'LOOP' export type LiveStackerType = 'SIRIL' | 'PIXINSIGHT' -export enum ExposureTimeUnit { - MINUTE = 'm', - SECOND = 's', - MILLISECOND = 'ms', - MICROSECOND = 'µs', +export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' | 'EXPOSURING' | 'WAITING' | 'SETTLING' | 'DITHERING' | 'STACKING' | 'PAUSING' | 'PAUSED' | 'EXPOSURE_FINISHED' | 'CAPTURE_FINISHED' + +export type ExposureTimeUnit = 'MINUTE' | 'SECOND' | 'MILLISECOND' | 'MICROSECOND' + +export interface Camera extends GuideOutput, Thermometer { + readonly exposuring: boolean + readonly hasCoolerControl: boolean + readonly coolerPower: number + readonly cooler: boolean + readonly hasDewHeater: boolean + readonly dewHeater: boolean + readonly frameFormats: string[] + readonly canAbort: boolean + readonly cfaOffsetX: number + readonly cfaOffsetY: number + readonly cfaType: CfaPattern + readonly exposureMin: number + readonly exposureMax: number + readonly exposureState: PropertyState + readonly exposureTime: number + readonly hasCooler: boolean + readonly canSetTemperature: boolean + readonly canSubFrame: boolean + readonly x: number + readonly minX: number + readonly maxX: number + readonly y: number + readonly minY: number + readonly maxY: number + readonly width: number + readonly minWidth: number + readonly maxWidth: number + readonly height: number + readonly minHeight: number + readonly maxHeight: number + readonly canBin: boolean + readonly maxBinX: number + readonly maxBinY: number + readonly binX: number + readonly binY: number + readonly gain: number + readonly gainMin: number + readonly gainMax: number + readonly offset: number + readonly offsetMin: number + readonly offsetMax: number + readonly hasGuideHead: boolean + readonly pixelSizeX: number + readonly pixelSizeY: number + readonly capturesPath: string + readonly guideHead?: Device } -export function isCamera(device?: Device): device is Camera { - return !!device && 'exposuring' in device +export interface GuideHead extends Camera, CompanionDevice {} + +export interface Dither { + enabled: boolean + amount: number + raOnly: boolean + afterExposures: number } -export function isGuideHead(device?: Device): device is GuideHead { - return isCamera(device) && isCompanionDevice(device) && !!device.main +export interface CameraCaptureNamingFormat { + light?: string + dark?: string + flat?: string + bias?: string } -export interface Camera extends GuideOutput, Thermometer { - exposuring: boolean - hasCoolerControl: boolean - coolerPower: number - cooler: boolean - hasDewHeater: boolean - dewHeater: boolean - frameFormats: string[] - canAbort: boolean - cfaOffsetX: number - cfaOffsetY: number - cfaType: CfaPattern - exposureMin: number - exposureMax: number - exposureState: PropertyState +export interface CameraStartCapture { + enabled: boolean exposureTime: number - hasCooler: boolean - canSetTemperature: boolean - canSubFrame: boolean + exposureAmount: number + exposureDelay: number x: number - minX: number - maxX: number y: number - minY: number - maxY: number width: number - minWidth: number - maxWidth: number height: number - minHeight: number - maxHeight: number - canBin: boolean - maxBinX: number - maxBinY: number + frameFormat?: string + frameType: FrameType binX: number binY: number gain: number - gainMin: number - gainMax: number offset: number - offsetMin: number - offsetMax: number - hasGuideHead: boolean - pixelSizeX: number - pixelSizeY: number - capturesPath: string - guideHead?: Device + autoSave: boolean + savePath?: string + autoSubFolderMode: AutoSubFolderMode + dither: Dither + filterPosition: number + shutterPosition: number + focusOffset: number + calibrationGroup?: string + liveStacking: LiveStackingRequest + namingFormat: CameraCaptureNamingFormat } -export interface GuideHead extends Camera, CompanionDevice {} +export interface CameraCaptureEvent extends MessageEvent { + camera: Camera + exposureAmount: number + exposureCount: number + captureElapsedTime: number + captureProgress: number + captureRemainingTime: number + stepElapsedTime: number + stepProgress: number + stepRemainingTime: number + savedPath?: string + liveStackedPath?: string + state: CameraCaptureState + capture?: CameraStartCapture +} + +export interface CameraDialogInput { + mode: CameraMode + camera: Camera + request: CameraStartCapture +} -export const EMPTY_CAMERA: Camera = { +export interface CameraPreference { + request: CameraStartCapture + setpointTemperature: number + exposureTimeUnit: ExposureTimeUnit + exposureMode: ExposureMode + subFrame: boolean + mount?: Mount + focuser?: Focuser + wheel?: Wheel + rotator?: Rotator +} + +export interface CameraStepInfo { + remainingTime: number + progress: number + elapsedTime: number +} + +export interface CameraCaptureInfo { + looping: boolean + amount: number + remainingTime: number + elapsedTime: number + progress: number + count: number +} + +export interface LiveStackerSettings { + executablePath: string + slot: number +} + +export interface LiveStackingRequest extends LiveStackerSettings { + enabled: boolean + type: LiveStackerType + darkPath?: string + flatPath?: string + biasPath?: string + use32Bits: boolean +} + +export interface CameraDitherDialog { + showDialog: boolean + request: Dither +} + +export interface CameraLiveStackingDialog { + showDialog: boolean + request: LiveStackingRequest +} + +export interface CameraNamingFormatDialog { + showDialog: boolean + format: CameraCaptureNamingFormat +} + +export const DEFAULT_CAMERA: Camera = { + type: 'CAMERA', sender: '', id: '', exposuring: false, @@ -139,41 +251,50 @@ export const EMPTY_CAMERA: Camera = { temperature: 0, } -export interface Dither { - enabled: boolean - amount: number - raOnly: boolean - afterExposures: number +export const DEFAULT_CAMERA_CAPTURE_INFO: CameraCaptureInfo = { + looping: false, + amount: 0, + remainingTime: 0, + elapsedTime: 0, + progress: 0, + count: 0, } -export interface CameraStartCapture { - enabled?: boolean - exposureTime: number - exposureAmount: number - exposureDelay: number - x: number - y: number - width: number - height: number - frameFormat?: string - frameType: FrameType - binX: number - binY: number - gain: number - offset: number - autoSave: boolean - savePath?: string - autoSubFolderMode: AutoSubFolderMode - dither: Dither - filterPosition?: number - shutterPosition?: number - focusOffset?: number - calibrationGroup?: string - liveStacking: LiveStackingRequest - namingFormat: CameraCaptureNamingFormat +export const DEFAULT_LIVE_STACKER_SETTINGS: LiveStackerSettings = { + executablePath: '', + slot: 0, +} + +export const DEFAULT_LIVE_STACKING_REQUEST: LiveStackingRequest = { + ...DEFAULT_LIVE_STACKER_SETTINGS, + enabled: false, + type: 'SIRIL', + use32Bits: false, +} + +export const CAMERA_CAPTURE_NAMING_FORMAT_LIGHT = '[camera]_[type]_[year:2][month][day][hour][min][sec][ms]_[filter]_[width]_[height]_[exp]_[bin]_[gain]' +export const CAMERA_CAPTURE_NAMING_FORMAT_DARK = '[camera]_[type]_[width]_[height]_[exp]_[bin]_[gain]' +export const CAMERA_CAPTURE_NAMING_FORMAT_FLAT = '[camera]_[type]_[filter]_[width]_[height]_[bin]' +export const CAMERA_CAPTURE_NAMING_FORMAT_BIAS = '[camera]_[type]_[width]_[height]_[bin]_[gain]' + +export const EMPTY_CAMERA_CAPTURE_NAMING_FORMAT: CameraCaptureNamingFormat = {} + +export const DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT: CameraCaptureNamingFormat = { + light: CAMERA_CAPTURE_NAMING_FORMAT_LIGHT, + dark: CAMERA_CAPTURE_NAMING_FORMAT_DARK, + flat: CAMERA_CAPTURE_NAMING_FORMAT_FLAT, + bias: CAMERA_CAPTURE_NAMING_FORMAT_BIAS, } -export const EMPTY_CAMERA_START_CAPTURE: CameraStartCapture = { +export const DEFAULT_DITHER: Dither = { + enabled: false, + amount: 1.5, + raOnly: false, + afterExposures: 5, +} + +export const DEFAULT_CAMERA_START_CAPTURE: CameraStartCapture = { + enabled: true, exposureTime: 1, exposureAmount: 1, exposureDelay: 0, @@ -188,22 +309,57 @@ export const EMPTY_CAMERA_START_CAPTURE: CameraStartCapture = { offset: 0, autoSave: false, autoSubFolderMode: 'OFF', - dither: { - enabled: false, - afterExposures: 1, - amount: 1.5, - raOnly: false, - }, - liveStacking: { - enabled: false, - type: 'SIRIL', - executablePath: '', - use32Bits: false, - slot: 1, - }, + filterPosition: 0, + shutterPosition: 0, + focusOffset: 0, + dither: DEFAULT_DITHER, + liveStacking: DEFAULT_LIVE_STACKING_REQUEST, namingFormat: {}, } +export const DEFAULT_CAMERA_PREFERENCE: CameraPreference = { + request: DEFAULT_CAMERA_START_CAPTURE, + setpointTemperature: 0, + exposureTimeUnit: 'MICROSECOND', + exposureMode: 'SINGLE', + subFrame: false, +} + +export const DEFAULT_CAMERA_STEP_INFO: CameraStepInfo = { + remainingTime: 0, + progress: 0, + elapsedTime: 0, +} + +export function cameraStartCaptureWithDefault(request?: Partial, source: CameraStartCapture = DEFAULT_CAMERA_START_CAPTURE) { + if (!request) return structuredClone(source) + request.enabled ??= source.enabled + request.exposureTime ??= source.exposureTime + request.exposureAmount ??= source.exposureAmount + request.exposureDelay ??= source.exposureDelay + request.x ??= source.x + request.y ??= source.y + request.width ??= source.width + request.height ??= source.height + request.frameFormat ||= source.frameFormat + request.frameType ||= source.frameType + request.binX ??= source.binX + request.binY ??= source.binY + request.gain ??= source.gain + request.offset ??= source.offset + request.autoSave ??= source.autoSave + request.savePath ||= source.savePath + request.autoSubFolderMode ||= source.autoSubFolderMode + request.filterPosition ??= source.filterPosition + request.shutterPosition ??= source.shutterPosition + request.focusOffset ??= source.focusOffset + request.calibrationGroup ||= source.calibrationGroup + request.dither = ditherWithDefault(request.dither, source.dither) + request.liveStacking = liveStackingRequestWithDefault(request.liveStacking, source.liveStacking) + request.namingFormat = cameraCaptureNamingFormatWithDefault(request.namingFormat, source.namingFormat) + return request as CameraStartCapture +} + 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)) @@ -218,107 +374,57 @@ export function updateCameraStartCaptureFromCamera(request: CameraStartCapture, 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 (camera.frameFormats.length && (!request.frameFormat || !camera.frameFormats.includes(request.frameFormat))) request.frameFormat = camera.frameFormats[0] + if (camera.exposureMin > 1 && camera.exposureMax > camera.exposureMin) request.exposureTime = Math.max(camera.exposureMin, Math.min(request.exposureTime, camera.exposureMax)) } -export interface CameraCaptureEvent extends MessageEvent { - camera: Camera - exposureAmount: number - exposureCount: number - captureElapsedTime: number - captureProgress: number - captureRemainingTime: number - stepElapsedTime: number - stepProgress: number - stepRemainingTime: number - savedPath?: string - liveStackedPath?: string - state: CameraCaptureState - capture?: CameraStartCapture -} - -export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' | 'EXPOSURING' | 'WAITING' | 'SETTLING' | 'DITHERING' | 'STACKING' | 'PAUSING' | 'PAUSED' | 'EXPOSURE_FINISHED' | 'CAPTURE_FINISHED' - -export interface CameraDialogInput { - mode: CameraDialogMode - camera: Camera - request: CameraStartCapture -} - -export interface CameraPreference extends CameraStartCapture { - setpointTemperature: number - exposureTimeUnit: ExposureTimeUnit - exposureMode: ExposureMode - subFrame: boolean +export function cameraPreferenceWithDefault(preference?: Partial, source: CameraPreference = DEFAULT_CAMERA_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.request = cameraStartCaptureWithDefault(preference.request, source.request) + preference.setpointTemperature ??= source.setpointTemperature + preference.exposureTimeUnit ??= source.exposureTimeUnit + preference.exposureMode ||= source.exposureMode + preference.subFrame ??= source.subFrame + return preference as CameraPreference } -export const EMPTY_CAMERA_PREFERENCE: CameraPreference = { - ...EMPTY_CAMERA_START_CAPTURE, - setpointTemperature: 0, - exposureTimeUnit: ExposureTimeUnit.MICROSECOND, - exposureMode: 'SINGLE', - subFrame: false, +export function liveStackerSettingsWithDefault(settings?: Partial, source: LiveStackerSettings = DEFAULT_LIVE_STACKER_SETTINGS) { + if (!settings) return structuredClone(source) + settings.executablePath ||= source.executablePath + settings.slot ??= source.slot + return settings as LiveStackerSettings } -export interface CameraStepInfo { - remainingTime: number - progress: number - elapsedTime: number -} - -export const EMPTY_CAMERA_STEP_INFO: CameraStepInfo = { - remainingTime: 0, - progress: 0, - elapsedTime: 0, +export function liveStackingRequestWithDefault(request?: Partial, source: LiveStackingRequest = DEFAULT_LIVE_STACKING_REQUEST) { + if (!request) return structuredClone(source) + liveStackerSettingsWithDefault(request, source) + request.enabled ??= source.enabled + request.type ??= source.type + request.use32Bits ??= source.use32Bits + return request as LiveStackingRequest } -export interface CameraCaptureInfo { - looping: boolean - amount: number - remainingTime: number - elapsedTime: number - progress: number - count: number +export function cameraCaptureNamingFormatWithDefault(format?: Partial, source: CameraCaptureNamingFormat = DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT) { + if (!format) return structuredClone(source) + format.light ||= source.light + format.dark ||= source.dark + format.flat ||= source.flat + format.bias ||= source.bias + return format as CameraCaptureNamingFormat } -export const EMPTY_CAMERA_CAPTURE_INFO: CameraCaptureInfo = { - looping: false, - amount: 0, - remainingTime: 0, - elapsedTime: 0, - progress: 0, - count: 0, +export function ditherWithDefault(dither?: Partial, source: Dither = DEFAULT_DITHER) { + if (!dither) return structuredClone(source) + dither.enabled ??= source.enabled + dither.amount ??= source.amount + dither.raOnly ??= source.raOnly + dither.afterExposures ??= source.afterExposures + return dither as Dither } -export interface LiveStackingRequest { - enabled: boolean - type: LiveStackerType - executablePath: string - darkPath?: string - flatPath?: string - biasPath?: string - use32Bits: boolean - slot: number -} - -export const EMPTY_LIVE_STACKING_REQUEST: LiveStackingRequest = { - enabled: false, - type: 'SIRIL', - executablePath: '', - use32Bits: false, - slot: 1, -} - -export interface CameraDitherDialog { - showDialog: boolean - request: Dither -} - -export interface CameraLiveStackingDialog { - showDialog: boolean - request: LiveStackingRequest +export function isCamera(device?: Device): device is Camera { + return !!device && device.type === 'CAMERA' } -export interface CameraNamingFormatDialog { - showDialog: boolean - format: CameraCaptureNamingFormat +export function isGuideHead(device?: Device): device is GuideHead { + return isCamera(device) && isCompanionDevice(device) && !!device.main } diff --git a/desktop/src/shared/types/device.types.ts b/desktop/src/shared/types/device.types.ts index 63729dac7..90e2f2c44 100644 --- a/desktop/src/shared/types/device.types.ts +++ b/desktop/src/shared/types/device.types.ts @@ -11,6 +11,7 @@ export type SwitchRule = 'ONE_OF_MANY' | 'AT_MOST_ONE' | 'ANY_OF_MANY' export type DeviceType = 'CAMERA' | 'MOUNT' | 'WHEEL' | 'FOCUSER' | 'ROTATOR' | 'GPS' | 'DOME' | 'SWITCH' export interface Device { + readonly type: DeviceType readonly sender: string readonly id: string readonly name: string diff --git a/desktop/src/shared/types/flat-wizard.types.ts b/desktop/src/shared/types/flat-wizard.types.ts index d2f9aa9e8..d7565dfe5 100644 --- a/desktop/src/shared/types/flat-wizard.types.ts +++ b/desktop/src/shared/types/flat-wizard.types.ts @@ -1,4 +1,10 @@ -import type { CameraCaptureEvent, CameraStartCapture } from './camera.types' +import { cameraStartCaptureWithDefault, DEFAULT_CAMERA_START_CAPTURE, type CameraCaptureEvent, type CameraStartCapture } from './camera.types' + +export type FlatWizardState = 'EXPOSURING' | 'CAPTURED' | 'FAILED' + +export interface FlatWizardPreference { + request: FlatWizardRequest +} export interface FlatWizardRequest { capture: CameraStartCapture @@ -8,11 +14,42 @@ export interface FlatWizardRequest { meanTolerance: number } -export type FlatWizardState = 'EXPOSURING' | 'CAPTURED' | 'FAILED' - export interface FlatWizardEvent { state: FlatWizardState exposureTime: number capture?: CameraCaptureEvent savedPath?: string } + +export const DEFAULT_CAMERA_START_CAPTURE_FLAT_WIZARD: CameraStartCapture = { + ...DEFAULT_CAMERA_START_CAPTURE, + frameType: 'FLAT', +} + +export const DEFAULT_FLAT_WIZARD_REQUEST: FlatWizardRequest = { + capture: DEFAULT_CAMERA_START_CAPTURE_FLAT_WIZARD, + exposureMin: 1, + exposureMax: 2000, + meanTarget: 32768, + meanTolerance: 10, +} + +export const DEFAULT_FLAT_WIZARD_PREFERENCE: FlatWizardPreference = { + request: DEFAULT_FLAT_WIZARD_REQUEST, +} + +export function flatWizardRequestWithDefault(request?: Partial, source: FlatWizardRequest = DEFAULT_FLAT_WIZARD_REQUEST) { + if (!request) return structuredClone(source) + request.capture = cameraStartCaptureWithDefault(request.capture, source.capture) + request.exposureMin ??= source.exposureMin + request.exposureMax ??= source.exposureMax + request.meanTarget ??= source.meanTarget + request.meanTolerance ??= source.meanTolerance + return request as FlatWizardRequest +} + +export function flatWizardPreferenceWithDefault(preference?: Partial, source: FlatWizardPreference = DEFAULT_FLAT_WIZARD_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.request = flatWizardRequestWithDefault(preference.request, source.request) + return preference as FlatWizardPreference +} diff --git a/desktop/src/shared/types/focuser.types.ts b/desktop/src/shared/types/focuser.types.ts index b2263cc71..b73e9f815 100644 --- a/desktop/src/shared/types/focuser.types.ts +++ b/desktop/src/shared/types/focuser.types.ts @@ -14,7 +14,13 @@ export interface Focuser extends Device, Thermometer { maxPosition: number } -export const EMPTY_FOCUSER: Focuser = { +export interface FocuserPreference { + stepsRelative: number + stepsAbsolute: number +} + +export const DEFAULT_FOCUSER: Focuser = { + type: 'FOCUSER', sender: '', id: '', moving: false, @@ -33,11 +39,18 @@ export const EMPTY_FOCUSER: Focuser = { temperature: 0, } -export interface FocuserPreference { - stepsRelative?: number - stepsAbsolute?: number +export const DEFAULT_FOCUSER_PREFERENCE: FocuserPreference = { + stepsRelative: 100, + stepsAbsolute: 0, } export function isFocuser(device?: Device): device is Focuser { - return !!device && 'maxPosition' in device + return !!device && device.type === 'FOCUSER' +} + +export function focuserPreferenceWithDefault(preference?: Partial, source: FocuserPreference = DEFAULT_FOCUSER_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.stepsAbsolute ??= source.stepsAbsolute + preference.stepsAbsolute ??= source.stepsAbsolute + return preference as FocuserPreference } diff --git a/desktop/src/shared/types/framing.types.ts b/desktop/src/shared/types/framing.types.ts index a4952b463..3791856f7 100644 --- a/desktop/src/shared/types/framing.types.ts +++ b/desktop/src/shared/types/framing.types.ts @@ -1,3 +1,5 @@ +import type { Angle } from './atlas.types' + export interface HipsSurvey { id: string category: string @@ -7,3 +9,43 @@ export interface HipsSurvey { pixelScale: number skyFraction: number } + +export interface FramingPreference { + rightAscension: Angle + declination: Angle + width: number + height: number + fov: number + rotation: number + hipsSurvey?: HipsSurvey +} + +export interface LoadFraming { + rightAscension: Angle + declination: Angle + width?: number + height?: number + fov?: number + rotation?: number +} + +export const DEFAULT_FRAMING_PREFERENCE: FramingPreference = { + rightAscension: '00h00m00s', + declination: `000°00'00"`, + width: 1024, + height: 720, + fov: 1, + rotation: 0, +} + +export function framingPreferenceWithDefault(preference?: Partial, source: FramingPreference = DEFAULT_FRAMING_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.rightAscension ??= source.rightAscension + preference.declination ??= source.declination + preference.width ??= source.width + preference.height ??= source.height + preference.fov ??= source.fov + preference.rotation ??= source.rotation + preference.hipsSurvey ??= source.hipsSurvey + return preference as FramingPreference +} diff --git a/desktop/src/shared/types/guider.types.ts b/desktop/src/shared/types/guider.types.ts index 4ba757fcf..8a55d2327 100644 --- a/desktop/src/shared/types/guider.types.ts +++ b/desktop/src/shared/types/guider.types.ts @@ -6,16 +6,16 @@ export type GuideDirection = | 'WEST' // RA+ | 'EAST' // RA- -export const GUIDE_STATES = ['STOPPED', 'SELECTED', 'CALIBRATING', 'GUIDING', 'LOST_LOCK', 'PAUSED', 'LOOPING'] as const -export type GuideState = (typeof GUIDE_STATES)[number] +export type GuideState = 'STOPPED' | 'SELECTED' | 'CALIBRATING' | 'GUIDING' | 'LOST_LOCK' | 'PAUSED' | 'LOOPING' -export const GUIDER_TYPES = ['PHD2'] as const -export type GuiderType = (typeof GUIDER_TYPES)[number] +export type GuiderType = 'PHD2' export type GuiderPlotMode = 'RA/DEC' | 'DX/DY' export type GuiderYAxisUnit = 'ARCSEC' | 'PIXEL' +export type GuidePulseDurations = Record, number> + export interface GuidePoint { x: number y: number @@ -61,7 +61,54 @@ export interface GuideOutput extends Device { pulseGuiding: boolean } -export const EMPTY_GUIDE_OUTPUT: GuideOutput = { +export interface Guider { + connected: boolean + state: GuideState + settling: boolean + pixelScale: number +} + +export interface SettleInfo { + amount: number + time: number + timeout: number +} + +export interface GuiderMessageEvent extends MessageEvent { + data: T +} + +export interface GuiderPreference { + host: string + port: number + plotMode: GuiderPlotMode + yAxisUnit: GuiderYAxisUnit + settle: SettleInfo + pulseDuration: GuidePulseDurations +} + +export interface GuiderPHD2 { + connected: boolean + state: GuideState + step?: GuideStep + message: string +} + +export interface GuiderPulse { + connected: boolean + pulsing: boolean +} + +export interface GuiderChartInfo { + pixelScale: number + rmsRA: number + rmsDEC: number + rmsTotal: number + durationScale: number +} + +export const DEFAULT_GUIDE_OUTPUT: GuideOutput = { + type: 'CAMERA', sender: '', id: '', canPulseGuide: false, @@ -70,11 +117,45 @@ export const EMPTY_GUIDE_OUTPUT: GuideOutput = { connected: false, } -export interface Guider { - connected: boolean - state: GuideState - settling: boolean - pixelScale: number +export const DEFAULT_SETTLE: SettleInfo = { + amount: 1.5, + time: 10, + timeout: 30, +} + +export const DEFAULT_GUIDE_PULSE_DURATIONS: GuidePulseDurations = { + north: 1000, + south: 1000, + east: 1000, + west: 1000, +} + +export const DEFAULT_GUIDER_PHD2: GuiderPHD2 = { + connected: false, + state: 'STOPPED', + message: '', +} + +export const DEFAULT_GUIDER_PULSE: GuiderPulse = { + connected: false, + pulsing: false, +} + +export const DEFAULT_GUIDER_PREFERENCE: GuiderPreference = { + host: 'localhost', + port: 4400, + plotMode: 'RA/DEC', + yAxisUnit: 'ARCSEC', + settle: DEFAULT_SETTLE, + pulseDuration: DEFAULT_GUIDE_PULSE_DURATIONS, +} + +export const DEFAULT_GUIDER_CHART_INFO: GuiderChartInfo = { + pixelScale: 1.0, + rmsRA: 0.0, + rmsDEC: 0.0, + rmsTotal: 0.0, + durationScale: 1.0, } export function reverseGuideDirection(direction: GuideDirection): GuideDirection { @@ -92,12 +173,30 @@ export function reverseGuideDirection(direction: GuideDirection): GuideDirection } } -export interface SettleInfo { - amount: number - time: number - timeout: number +export function settleWithDefault(settle?: Partial, source: SettleInfo = DEFAULT_SETTLE) { + if (!settle) return structuredClone(source) + settle.amount ??= source.amount + settle.time ??= source.time + settle.timeout ??= source.timeout + return settle as SettleInfo } -export interface GuiderMessageEvent extends MessageEvent { - data: T +export function pulseDurationWithDefault(duration?: Partial, source: GuidePulseDurations = DEFAULT_GUIDE_PULSE_DURATIONS) { + if (!duration) return structuredClone(source) + duration.north ??= source.north + duration.south ??= source.south + duration.east ??= source.east + duration.west ??= source.west + return duration as GuidePulseDurations +} + +export function guiderPreferenceWithDefault(preference?: Partial, source: GuiderPreference = DEFAULT_GUIDER_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.host ||= source.host + preference.port ??= source.port + preference.plotMode ||= source.plotMode + preference.yAxisUnit ||= source.yAxisUnit + preference.settle = settleWithDefault(preference.settle, source.settle) + preference.pulseDuration = pulseDurationWithDefault(preference.pulseDuration, source.pulseDuration) + return preference as GuiderPreference } diff --git a/desktop/src/shared/types/home.types.ts b/desktop/src/shared/types/home.types.ts index 14a1c01f3..5004688fb 100644 --- a/desktop/src/shared/types/home.types.ts +++ b/desktop/src/shared/types/home.types.ts @@ -1,15 +1,10 @@ -import type { Camera } from './camera.types' import type { DeviceType } from './device.types' -import type { Focuser } from './focuser.types' -import type { Mount } from './mount.types' -import type { Rotator } from './rotator.types' -import type { FilterWheel } from './wheel.types' -export type HomeWindowType = DeviceType | 'GUIDER' | 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | 'AUTO_FOCUS' | 'STACKER' +export type HomeWindowType = DeviceType | 'GUIDER' | 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | 'AUTO_FOCUS' | 'STACKER' | 'CALIBRATION' -export const CONNECTION_TYPES = ['INDI', 'ALPACA'] as const +export type ConnectionType = 'INDI' | 'ALPACA' -export type ConnectionType = (typeof CONNECTION_TYPES)[number] +export type ConnectionStatus = Omit, 'connected' | 'name' | 'connectedAt'> export interface ConnectionDetails { name: string @@ -22,29 +17,45 @@ export interface ConnectionDetails { id?: string } -export type ConnectionStatus = Omit, 'connected' | 'name' | 'connectedAt'> +export interface ConnectionClosed { + id: string +} + +export interface HomePreference { + connections: ConnectionDetails[] + imagePath?: string +} + +export interface HomeConnectionDialog { + showDialog: boolean + connection: ConnectionDetails + edited: boolean +} + +export const DEFAULT_CONNECTION_HOST: string = 'localhost' +export const DEFAULT_CONNECTION_PORT: number = 7624 -export const EMPTY_CONNECTION_DETAILS: ConnectionDetails = { - name: '', - host: 'localhost', - port: 7624, +export const DEFAULT_CONNECTION_DETAILS: ConnectionDetails = { + name: 'Local', + host: DEFAULT_CONNECTION_HOST, + port: DEFAULT_CONNECTION_PORT, type: 'INDI', connected: false, } -export interface ConnectionClosed { - id: string +export const DEFAULT_HOME_PREFERENCE: HomePreference = { + connections: [], } -export interface HomePreference { - imagePath?: string +export const DEFAULT_HOME_CONNECTION_DIALOG: HomeConnectionDialog = { + showDialog: false, + edited: false, + connection: DEFAULT_CONNECTION_DETAILS, } -export interface Equipment { - camera?: Camera - guider?: Camera - mount?: Mount - focuser?: Focuser - wheel?: FilterWheel - rotator?: Rotator +export function homePreferenceWithDefault(preference?: Partial, source: HomePreference = DEFAULT_HOME_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.connections ??= source.connections + preference.imagePath ??= source.imagePath + return preference as HomePreference } diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index bac08fd39..5fbf79c7e 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -1,8 +1,10 @@ import type { Point, Size } from 'electron' +import type { PanZoom } from 'panzoom' +import type { CoordinateInterpolator, InterpolatedCoordinate } from '../utils/coordinate-interpolation' import type { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from './atlas.types' -import type { Camera, CameraStartCapture } from './camera.types' -import type { PlateSolverRequest } from './platesolver.types' -import type { StarDetectionRequest } from './stardetector.types' +import type { Camera, CameraStartCapture, FrameType } from './camera.types' +import { DEFAULT_PLATE_SOLVER_REQUEST, plateSolverRequestWithDefault, type PlateSolverRequest } from './platesolver.types' +import { DEFAULT_STAR_DETECTION_REQUEST, starDetectionRequestWithDefault, type StarDetectionRequest } from './stardetector.types' export type ImageChannel = 'RED' | 'GREEN' | 'BLUE' | 'GRAY' @@ -16,7 +18,34 @@ export type Bitpix = 'BYTE' | 'SHORT' | 'INTEGER' | 'LONG' | 'FLOAT' | 'DOUBLE' export type LiveStackingMode = 'NONE' | 'RAW' | 'STACKED' -export interface FITSHeaderItem { +export type ImageCalibrationSource = 'CAMERA' | 'MENU' + +export type ImageMousePosition = Point + +export interface Image { + type: FrameType + width: number + height: number + binX: number + binY: number + exposureTime: number + temperature?: number + gain: number + filter?: string +} + +export interface ImagePreference { + savePath?: string + crossHair: boolean + transformation: ImageTransformation + solver: PlateSolverRequest + starDetector: StarDetectionRequest + annotation: AnnotateImageRequest + fovs: FOV[] + pixelated: boolean +} + +export interface ImageHeaderItem { name: string value: string } @@ -27,13 +56,11 @@ export interface ImageInfo { width: number height: number mono: boolean - stretchShadow: number - stretchHighlight: number - stretchMidtone: number + stretch: ImageStretch rightAscension?: Angle declination?: Angle solved?: ImageSolved - headers: FITSHeaderItem[] + headers: ImageHeaderItem[] bitpix: Bitpix statistics: ImageStatistics } @@ -55,17 +82,6 @@ export interface ImageSolved extends EquatorialCoordinateJ2000 { radius: number } -export const EMPTY_IMAGE_SOLVED: ImageSolved = { - solved: false, - orientation: 0, - scale: 0, - width: 0, - height: 0, - radius: 0, - rightAscensionJ2000: '00h00m00s', - declinationJ2000: '+000°00\'00"', -} - export interface CoordinateInterpolation { ma: number[] md: number[] @@ -99,16 +115,6 @@ export interface ImageStatisticsBitOption { bitLength: number } -export const IMAGE_STATISTICS_BIT_OPTIONS: ImageStatisticsBitOption[] = [ - { name: 'Normalized: [0, 1]', rangeMax: 1, bitLength: 16 }, - { name: '8-bit: [0, 255]', rangeMax: 255, bitLength: 8 }, - { name: '9-bit: [0, 511]', rangeMax: 511, bitLength: 9 }, - { name: '10-bit: [0, 1023]', rangeMax: 1023, bitLength: 10 }, - { name: '12-bit: [0, 4095]', rangeMax: 4095, bitLength: 12 }, - { name: '14-bit: [0, 16383]', rangeMax: 16383, bitLength: 14 }, - { name: '16-bit: [0, 65535]', rangeMax: 65535, bitLength: 16 }, -] as const - export interface ImageStatistics { count: number maxCount: number @@ -122,34 +128,6 @@ export interface ImageStatistics { maximum: number } -export type StarDetectionImagePreference = Pick - -export interface PlateSolverImagePreference extends Pick { - radius: number - focalLength: number - pixelSize: number -} - -export interface ImagePreference { - savePath?: string - solver?: PlateSolverImagePreference - starDetection?: StarDetectionImagePreference -} - -export const EMPTY_IMAGE_PREFERENCE: ImagePreference = { - solver: { - type: 'ASTAP', - radius: 4, - focalLength: 0, - pixelSize: 0, - }, - starDetection: { - type: 'ASTAP', - minSNR: 0, - maxStars: 0, - }, -} - export interface OpenImage { camera?: Camera path: string @@ -162,11 +140,10 @@ export interface OpenImage { export interface ImageData { camera?: Camera path?: string - liveStackedPath?: string - source?: ImageSource + source: ImageSource title?: string capture?: CameraStartCapture - exposureCount?: number + exposureCount: number } export interface FOV { @@ -187,24 +164,6 @@ export interface FOV { } } -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', -} - export interface FOVEquipment { id: number name: string @@ -222,39 +181,45 @@ export interface FOVTelescope extends FOVEquipment { focalLength: number } -export interface ImageSCNRDialog { - showDialog: boolean +export interface ImageSCNR { channel?: ImageChannel amount: number method: SCNRProtectionMethod } -export interface ImageFITSHeadersDialog { +export interface ImageSCNRDialog { showDialog: boolean - headers: FITSHeaderItem[] + transformation: ImageSCNR } -export interface ImageStretchDialog { +export interface ImageHeadersDialog { showDialog: boolean + headers: ImageHeaderItem[] +} + +export interface ImageStretch { auto: boolean shadow: number highlight: number midtone: number } -export interface ImageSolverDialog extends PlateSolverImagePreference { +export interface ImageStretchDialog { + showDialog: boolean + transformation: ImageStretch +} + +export interface ImageSolverDialog { showDialog: boolean running: boolean - blind: boolean - centerRA: Angle - centerDEC: Angle + request: PlateSolverRequest readonly solved: ImageSolved } -export interface ImageFOVDialog extends FOV { +export interface ImageFOVDialog { showDialog: boolean + selected: FOV fovs: FOV[] - edited?: FOV showCameraDialog: boolean cameras: FOVCamera[] camera?: FOVCamera @@ -263,12 +228,8 @@ export interface ImageFOVDialog extends FOV { telescope?: FOVTelescope } -export interface ImageROI { +export interface ImageROI extends Size, Point { show: boolean - x: number - y: number - width: number - height: number } export interface ImageSaveDialog { @@ -284,22 +245,27 @@ export interface ImageTransformation { force: boolean calibrationGroup?: string debayer: boolean - stretch: Omit + stretch: ImageStretch mirrorHorizontal: boolean mirrorVertical: boolean invert: boolean - scnr: Pick + scnr: ImageSCNR + useJPEG: boolean +} + +export interface AnnotateImageRequest { + starsAndDSOs: boolean + minorPlanets: boolean + minorPlanetsMagLimit: number + includeMinorPlanetsWithoutMagnitude: boolean + useSimbad: boolean } export interface ImageAnnotationDialog { showDialog: boolean running: boolean visible: boolean - useStarsAndDSOs: boolean - useMinorPlanets: boolean - minorPlanetsMagLimit: number - includeMinorPlanetsWithoutMagnitude: boolean - useSimbad: boolean + request: AnnotateImageRequest data: ImageAnnotation[] } @@ -311,16 +277,317 @@ export interface ROISelected { height: number } -export interface StarDetectionDialog extends StarDetectionImagePreference { +export interface StarDetectorDialog { showDialog: boolean running: boolean visible: boolean stars: DetectedStar[] computed: ComputedDetectedStars selected: DetectedStar + request: StarDetectionRequest } -export interface AnnotationInfoDialog { +export interface AstronomicalObjectDialog { showDialog: boolean info?: AstronomicalObject & Partial } + +export interface ImageStatisticsDialog { + showDialog: boolean + statistics: ImageStatistics + bitOption: ImageStatisticsBitOption +} + +export interface ImageMouseCoordinates extends InterpolatedCoordinate, ImageMousePosition { + show: boolean + interpolator?: CoordinateInterpolator +} + +export interface ImageCalibration { + source: ImageCalibrationSource +} + +export interface ImageLiveStacking { + mode: LiveStackingMode + path?: string +} + +export interface ImageZoom { + scale: number + panZoom?: PanZoom +} + +export interface ImageSettingsDialog { + showDialog: boolean + preference: ImagePreference +} + +export const DEFAULT_IMAGE_SOLVED: ImageSolved = { + solved: false, + orientation: 0, + scale: 0, + width: 0, + height: 0, + radius: 0, + rightAscensionJ2000: '00h00m00s', + declinationJ2000: '+000°00\'00"', +} + +export const DEFAULT_IMAGE_STRETCH: ImageStretch = { + auto: true, + shadow: 0, + highlight: 1, + midtone: 0.5, +} + +export const DEFAULT_IMAGE_STRETCH_DIALOG: ImageStretchDialog = { + showDialog: false, + transformation: DEFAULT_IMAGE_STRETCH, +} + +export const DEFAULT_IMAGE_SCNR: ImageSCNR = { + amount: 0.5, + method: 'AVERAGE_NEUTRAL', +} + +export const DEFAULT_IMAGE_SCNR_DIALOG: ImageSCNRDialog = { + showDialog: false, + transformation: DEFAULT_IMAGE_SCNR, +} + +export const DEFAULT_IMAGE_TRANSFORMATION: ImageTransformation = { + force: false, + debayer: true, + stretch: DEFAULT_IMAGE_STRETCH, + mirrorHorizontal: false, + mirrorVertical: false, + invert: false, + scnr: DEFAULT_IMAGE_SCNR, + useJPEG: true, +} + +export const DEFAULT_IMAGE_SOLVER_DIALOG: ImageSolverDialog = { + showDialog: false, + running: false, + request: DEFAULT_PLATE_SOLVER_REQUEST, + solved: DEFAULT_IMAGE_SOLVED, +} + +export const IMAGE_STATISTICS_BIT_OPTIONS: ImageStatisticsBitOption[] = [ + { name: 'Normalized: [0, 1]', rangeMax: 1, bitLength: 16 }, + { name: '8-bit: [0, 255]', rangeMax: 255, bitLength: 8 }, + { name: '9-bit: [0, 511]', rangeMax: 511, bitLength: 9 }, + { name: '10-bit: [0, 1023]', rangeMax: 1023, bitLength: 10 }, + { name: '12-bit: [0, 4095]', rangeMax: 4095, bitLength: 12 }, + { name: '14-bit: [0, 16383]', rangeMax: 16383, bitLength: 14 }, + { name: '16-bit: [0, 65535]', rangeMax: 65535, bitLength: 16 }, +] as const + +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', +} + +export const DEFAULT_IMAGE_FOV_DIALOG: ImageFOVDialog = { + selected: DEFAULT_FOV, + showDialog: false, + fovs: [], + showCameraDialog: false, + cameras: [], + showTelescopeDialog: false, + telescopes: [], +} + +export const DEFAULT_COMPUTED_DETECTED_STARS: ComputedDetectedStars = { + hfd: 0, + snr: 0, + stdDev: 0, + fluxMax: 0, + fluxMin: 0, +} + +export const DEFAULT_DETECTED_STAR: DetectedStar = { + x: 0, + y: 0, + snr: 0, + hfd: 0, + flux: 0, +} + +export const DEFAULT_STAR_DETECTOR_DIALOG: StarDetectorDialog = { + showDialog: false, + running: false, + visible: false, + stars: [], + computed: DEFAULT_COMPUTED_DETECTED_STARS, + selected: DEFAULT_DETECTED_STAR, + request: DEFAULT_STAR_DETECTION_REQUEST, +} + +export const DEFAULT_ANNOTATE_IMAGE_REQUEST: AnnotateImageRequest = { + starsAndDSOs: true, + minorPlanets: false, + minorPlanetsMagLimit: 15.0, + includeMinorPlanetsWithoutMagnitude: false, + useSimbad: false, +} + +export const DEFAULT_IMAGE_ANNOTATION_DIALOG: ImageAnnotationDialog = { + showDialog: false, + running: false, + visible: false, + data: [], + request: DEFAULT_ANNOTATE_IMAGE_REQUEST, +} + +export const DEFAULT_IMAGE_ROI: ImageROI = { + show: false, + x: 0, + y: 0, + width: 0, + height: 0, +} + +export const DEFAULT_IMAGE_SAVE_DIALOG: ImageSaveDialog = { + showDialog: false, + format: 'FITS', + bitpix: 'BYTE', + path: '', + shouldBeTransformed: true, + transformation: DEFAULT_IMAGE_TRANSFORMATION, +} + +export const DEFAULT_IMAGE_STATISTICS: ImageStatistics = { + count: 0, + maxCount: 0, + mean: 0, + sumOfSquares: 0, + median: 0, + variance: 0, + stdDev: 0, + avgDev: 0, + minimum: 0, + maximum: 0, +} + +export const DEFAULT_IMAGE_STATISTICS_DIALOG: ImageStatisticsDialog = { + showDialog: false, + statistics: DEFAULT_IMAGE_STATISTICS, + bitOption: IMAGE_STATISTICS_BIT_OPTIONS[0], +} + +export const DEFAULT_IMAGE_DATA: ImageData = { + source: 'PATH', + exposureCount: 0, +} + +export const DEFAULT_IMAGE_MOUSE_POSITION: ImageMousePosition = { + x: 0, + y: 0, +} + +export const DEFAULT_IMAGE_MOUSE_COORDINATES: ImageMouseCoordinates = { + show: false, + ...DEFAULT_IMAGE_MOUSE_POSITION, + alpha: '', + delta: '', + rightAscensionJ2000: '', + declinationJ2000: '', +} + +export const DEFAULT_IMAGE_CALIBRATION: ImageCalibration = { + source: 'CAMERA', +} + +export const DEFAULT_IMAGE_LIVE_STACKING: ImageLiveStacking = { + mode: 'NONE', +} + +export const DEFAULT_IMAGE_ZOOM: ImageZoom = { + scale: 1, +} + +export const DEFAULT_IMAGE_PREFERENCE: ImagePreference = { + crossHair: false, + transformation: DEFAULT_IMAGE_TRANSFORMATION, + solver: DEFAULT_PLATE_SOLVER_REQUEST, + starDetector: DEFAULT_STAR_DETECTION_REQUEST, + annotation: DEFAULT_ANNOTATE_IMAGE_REQUEST, + fovs: [], + pixelated: true, +} + +export const DEFAULT_IMAGE_SETTINGS_DIALOG: ImageSettingsDialog = { + showDialog: false, + preference: DEFAULT_IMAGE_PREFERENCE, +} + +export function imageFormatFromExtension(extension: string): ImageFormat { + return ( + extension === '.xisf' ? 'XISF' + : extension === '.png' ? 'PNG' + : extension === '.jpg' ? 'JPG' + : 'FITS' + ) +} + +export function imageStretchWithDefault(stretch?: Partial, source: ImageStretch = DEFAULT_IMAGE_STRETCH) { + if (!stretch) return structuredClone(source) + stretch.auto ??= source.auto + stretch.shadow ??= source.shadow + stretch.highlight ??= source.highlight + stretch.midtone ??= source.midtone + return stretch as ImageStretch +} + +export function annotateImageRequestWithDefault(request?: Partial, source: AnnotateImageRequest = DEFAULT_ANNOTATE_IMAGE_REQUEST) { + if (!request) return structuredClone(source) + request.starsAndDSOs ??= source.starsAndDSOs + request.minorPlanets ??= source.minorPlanets + request.minorPlanetsMagLimit ??= source.minorPlanetsMagLimit + request.includeMinorPlanetsWithoutMagnitude ??= source.includeMinorPlanetsWithoutMagnitude + request.useSimbad ??= source.useSimbad + return request as AnnotateImageRequest +} + +export function imageTransformationWithDefault(transformation?: Partial, source: ImageTransformation = DEFAULT_IMAGE_TRANSFORMATION) { + if (!transformation) return structuredClone(source) + transformation.force ??= source.force + transformation.calibrationGroup ||= source.calibrationGroup + transformation.debayer ??= source.debayer + transformation.stretch = imageStretchWithDefault(transformation.stretch, source.stretch) + transformation.mirrorHorizontal ??= source.mirrorHorizontal + transformation.mirrorVertical ??= source.mirrorVertical + transformation.invert ??= source.invert + transformation.scnr ??= source.scnr + transformation.useJPEG ??= source.useJPEG + return transformation as ImageTransformation +} + +export function imagePreferenceWithDefault(preference?: Partial, source: ImagePreference = DEFAULT_IMAGE_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.savePath ||= source.savePath + preference.crossHair ??= source.crossHair + preference.transformation = imageTransformationWithDefault(preference.transformation, source.transformation) + preference.solver = plateSolverRequestWithDefault(preference.solver, source.solver) + preference.starDetector = starDetectionRequestWithDefault(preference.starDetector, source.starDetector) + preference.annotation = annotateImageRequestWithDefault(preference.annotation, source.annotation) + preference.fovs ??= structuredClone(source.fovs) + preference.pixelated ??= source.pixelated + preference.fovs.forEach((e) => (e.enabled = false)) + preference.fovs.forEach((e) => (e.computed = undefined)) + return preference as ImagePreference +} diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts index 3f9c409f6..f88e21e4b 100644 --- a/desktop/src/shared/types/mount.types.ts +++ b/desktop/src/shared/types/mount.types.ts @@ -11,9 +11,13 @@ export type TrackMode = 'SIDEREAL' | ' LUNAR' | 'SOLAR' | 'KING' | 'CUSTOM' export type CelestialLocationType = 'ZENITH' | 'NORTH_POLE' | 'SOUTH_POLE' | 'GALACTIC_CENTER' | 'MERIDIAN_EQUATOR' | 'MERIDIAN_ECLIPTIC' | 'EQUATOR_ECLIPTIC' -export type MountRemoteControlType = 'LX200' | 'STELLARIUM' +export type MountRemoteControlProtocol = 'LX200' | 'STELLARIUM' -export type MoveDirectionType = 'N' | 'S' | 'W' | 'E' | 'NW' | 'NE' | 'SW' | 'SE' +export type CardinalDirection = 'N' | 'S' | 'W' | 'E' + +export type OrdinalDirection = 'NW' | 'NE' | 'SW' | 'SE' + +export type MountSlewDirection = CardinalDirection | OrdinalDirection export interface SlewRate { name: string @@ -42,7 +46,38 @@ export interface Mount extends EquatorialCoordinate, GPS, GuideOutput, Parkable guideRateNS: number } -export const EMPTY_MOUNT: Mount = { +export interface MountRemoteControl { + protocol: MountRemoteControlProtocol + mount: Mount + running: boolean + rightAscension: Angle + declination: Angle + latitude: Angle + longitude: Angle + slewing: boolean + tracking: boolean + parked: boolean + host: string + port: number +} + +export interface MountRemoteControlDialog { + showDialog: boolean + protocol: MountRemoteControlProtocol + host: string + port: number + controls: MountRemoteControl[] +} + +export interface MountPreference { + targetCoordinateType: TargetCoordinateType + targetRightAscension: Angle + targetDeclination: Angle + targetCoordinateCommand: number +} + +export const DEFAULT_MOUNT: Mount = { + type: 'MOUNT', sender: '', id: '', slewing: false, @@ -74,41 +109,30 @@ export const EMPTY_MOUNT: Mount = { parked: false, } -export interface MountRemoteControl { - type: MountRemoteControlType - mount: Mount - running: boolean - rightAscension: Angle - declination: Angle - latitude: Angle - longitude: Angle - slewing: boolean - tracking: boolean - parked: boolean - host: string - port: number +export const DEFAULT_MOUNT_REMOTE_CONTROL_DIALOG: MountRemoteControlDialog = { + showDialog: false, + protocol: 'LX200', + host: '0.0.0.0', + port: 10001, + controls: [], } -export interface MountRemoteControlDialog { - showDialog: boolean - type: MountRemoteControlType - host: string - port: number - data: MountRemoteControl[] -} - -export interface MountPreference { - targetCoordinateType: TargetCoordinateType - targetRightAscension: Angle - targetDeclination: Angle -} - -export const EMPTY_MOUNT_PREFERENCE: MountPreference = { +export const DEFAULT_MOUNT_PREFERENCE: MountPreference = { targetCoordinateType: 'JNOW', - targetRightAscension: '', - targetDeclination: '', + targetRightAscension: '00h00m00s', + targetDeclination: `000°00'00"`, + targetCoordinateCommand: 0, } export function isMount(device?: Device): device is Mount { - return !!device && 'tracking' in device + return !!device && device.type === 'MOUNT' +} + +export function mountPreferenceWithDefault(preference?: Partial, source: MountPreference = DEFAULT_MOUNT_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.targetCoordinateType ||= source.targetCoordinateType + preference.targetRightAscension ??= source.targetRightAscension + preference.targetDeclination ??= source.targetDeclination + preference.targetCoordinateCommand ??= source.targetCoordinateCommand + return preference as MountPreference } diff --git a/desktop/src/shared/types/platesolver.types.ts b/desktop/src/shared/types/platesolver.types.ts index e803b1a39..b79bc1f52 100644 --- a/desktop/src/shared/types/platesolver.types.ts +++ b/desktop/src/shared/types/platesolver.types.ts @@ -1,21 +1,29 @@ +import type { Angle } from './atlas.types' + export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP' | 'SIRIL' | 'PIXINSIGHT' -export interface PlateSolverRequest { - type: PlateSolverType +export interface PlateSolverSettings { executablePath: string downsampleFactor: number apiUrl: string apiKey: string timeout: number slot: number - pixelSize?: number - focalLength?: number +} + +export interface PlateSolverRequest extends PlateSolverSettings { + type: PlateSolverType + blind: boolean + centerRA: Angle + centerDEC: Angle + radius: Angle + pixelSize: number + focalLength: number } export const NOVA_ASTROMETRY_NET_URL = 'https://nova.astrometry.net/' -export const EMPTY_PLATE_SOLVER_REQUEST: PlateSolverRequest = { - type: 'ASTAP', +export const DEFAULT_PLATE_SOLVER_SETTINGS: PlateSolverSettings = { executablePath: '', downsampleFactor: 0, apiUrl: NOVA_ASTROMETRY_NET_URL, @@ -23,3 +31,34 @@ export const EMPTY_PLATE_SOLVER_REQUEST: PlateSolverRequest = { timeout: 300, slot: 1, } + +export const DEFAULT_PLATE_SOLVER_REQUEST: PlateSolverRequest = { + ...DEFAULT_PLATE_SOLVER_SETTINGS, + type: 'ASTAP', + blind: true, + centerRA: 0, + centerDEC: 0, + radius: 4, + focalLength: 0, + pixelSize: 0, +} + +export function plateSolverSettingsWithDefault(settings?: Partial, source: PlateSolverSettings = DEFAULT_PLATE_SOLVER_SETTINGS) { + if (!settings) return structuredClone(source) + settings.executablePath ||= source.executablePath + settings.downsampleFactor ??= source.downsampleFactor + settings.apiUrl ||= source.apiUrl + settings.apiKey ||= source.apiKey + settings.timeout ??= source.timeout + settings.slot ??= source.slot + return settings as PlateSolverSettings +} + +export function plateSolverRequestWithDefault(request?: Partial, source: PlateSolverRequest = DEFAULT_PLATE_SOLVER_REQUEST) { + if (!request) return structuredClone(source) + plateSolverSettingsWithDefault(request, source) + request.type ??= source.type + request.pixelSize ??= source.pixelSize + request.focalLength ??= source.focalLength + return request as PlateSolverRequest +} diff --git a/desktop/src/shared/types/rotator.types.ts b/desktop/src/shared/types/rotator.types.ts index 2a73995e9..0b003d434 100644 --- a/desktop/src/shared/types/rotator.types.ts +++ b/desktop/src/shared/types/rotator.types.ts @@ -13,7 +13,12 @@ export interface Rotator extends Device { maxAngle: number } -export const EMPTY_ROTATOR: Rotator = { +export interface RotatorPreference { + angle: number +} + +export const DEFAULT_ROTATOR: Rotator = { + type: 'ROTATOR', sender: '', id: '', name: '', @@ -30,10 +35,16 @@ export const EMPTY_ROTATOR: Rotator = { connected: false, } -export interface RotatorPreference { - angle?: number +export const DEFAULT_ROTATOR_PREFERENCE: RotatorPreference = { + angle: 0, } export function isRotator(device?: Device): device is Rotator { - return !!device && 'angle' in device + return !!device && device.type === 'ROTATOR' +} + +export function rotatorPreferenceWithDefault(preference?: Partial, source: RotatorPreference = DEFAULT_ROTATOR_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.angle ??= source.angle + return preference as RotatorPreference } diff --git a/desktop/src/shared/types/sequencer.types.ts b/desktop/src/shared/types/sequencer.types.ts index fe102cb26..c92cd986b 100644 --- a/desktop/src/shared/types/sequencer.types.ts +++ b/desktop/src/shared/types/sequencer.types.ts @@ -1,13 +1,31 @@ -import type { AutoSubFolderMode, CameraCaptureEvent, CameraStartCapture, Dither } from './camera.types' -import type { CameraCaptureNamingFormat } from './settings.types' +import type { Camera } from './camera.types' +import { + cameraCaptureNamingFormatWithDefault, + DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, + DEFAULT_CAMERA_START_CAPTURE, + DEFAULT_DITHER, + ditherWithDefault, + EMPTY_CAMERA_CAPTURE_NAMING_FORMAT, + type AutoSubFolderMode, + type CameraCaptureEvent, + type CameraCaptureNamingFormat, + type CameraStartCapture, + type Dither, +} from './camera.types' +import type { Focuser } from './focuser.types' +import type { Mount } from './mount.types' +import type { Rotator } from './rotator.types' +import type { Wheel } from './wheel.types' -export type SequenceCaptureMode = 'FULLY' | 'INTERLEAVED' +export type Sequence = CameraStartCapture + +export type SequencerCaptureMode = 'FULLY' | 'INTERLEAVED' export type SequencerState = 'IDLE' | 'PAUSING' | 'PAUSED' | 'RUNNING' -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 SequenceProperty = 'EXPOSURE_TIME' | 'EXPOSURE_AMOUNT' | 'EXPOSURE_DELAY' | 'FRAME_TYPE' | 'X' | 'Y' | 'WIDTH' | 'HEIGHT' | 'BIN' | 'FRAME_FORMAT' | 'GAIN' | 'OFFSET' -export type SequenceEntryProperty = (typeof SEQUENCE_ENTRY_PROPERTIES)[number] +export type SequenceProperties = Record export interface AutoFocusAfterConditions { enabled: boolean @@ -23,47 +41,20 @@ export interface AutoFocusAfterConditions { afterHFDIncreaseEnabled: boolean } -export interface SequencePlan { +export interface SequencerPlan { initialDelay: number - captureMode: SequenceCaptureMode + captureMode: SequencerCaptureMode autoSubFolderMode: AutoSubFolderMode savePath?: string - entries: CameraStartCapture[] + sequences: Sequence[] dither: Dither autoFocus: AutoFocusAfterConditions namingFormat: CameraCaptureNamingFormat - camera?: string - mount?: string - wheel?: string - focuser?: string - rotator?: string -} - -export const EMPTY_SEQUENCE_PLAN: SequencePlan = { - initialDelay: 0, - captureMode: 'FULLY', - autoSubFolderMode: 'OFF', - entries: [], - dither: { - enabled: false, - amount: 1.5, - raOnly: false, - afterExposures: 1, - }, - autoFocus: { - enabled: false, - onStart: false, - onFilterChange: false, - afterElapsedTime: 1800, // 30 min - afterExposures: 10, - afterTemperatureChange: 5, - afterHFDIncrease: 10, - afterElapsedTimeEnabled: false, - afterExposuresEnabled: false, - afterTemperatureChangeEnabled: false, - afterHFDIncreaseEnabled: false, - }, - namingFormat: {}, + camera?: Camera + mount?: Mount + wheel?: Wheel + focuser?: Focuser + rotator?: Rotator } export interface SequencerEvent extends MessageEvent { @@ -75,11 +66,123 @@ export interface SequencerEvent extends MessageEvent { state: SequencerState } +export interface SequencePropertyDialog { + showDialog: boolean + sequence?: Sequence + count: [number, number] + properties: SequenceProperties +} + export interface SequencerPreference { - savedPath?: string - plan: SequencePlan + loadPath?: string + plan: SequencerPlan + properties: SequenceProperties +} + +export const DEFAULT_AUTO_FOCUS_AFTER_CONDITIONS: AutoFocusAfterConditions = { + enabled: false, + onStart: false, + onFilterChange: false, + afterElapsedTime: 1800, // 30 min + afterExposures: 10, + afterTemperatureChange: 5, + afterHFDIncrease: 10, + afterElapsedTimeEnabled: false, + afterExposuresEnabled: false, + afterTemperatureChangeEnabled: false, + afterHFDIncreaseEnabled: false, +} + +export const DEFAULT_SEQUENCE: Sequence = { + ...DEFAULT_CAMERA_START_CAPTURE, + autoSave: true, + autoSubFolderMode: 'OFF', + filterPosition: 0, + shutterPosition: 0, + focusOffset: 0, + namingFormat: EMPTY_CAMERA_CAPTURE_NAMING_FORMAT, +} + +export const DEFAULT_SEQUENCER_PLAN: SequencerPlan = { + initialDelay: 0, + captureMode: 'FULLY', + autoSubFolderMode: 'OFF', + dither: DEFAULT_DITHER, + autoFocus: DEFAULT_AUTO_FOCUS_AFTER_CONDITIONS, + namingFormat: DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, + sequences: [], +} + +export const DEFAULT_SEQUENCE_PROPERTIES: SequenceProperties = { + EXPOSURE_TIME: true, + EXPOSURE_AMOUNT: true, + EXPOSURE_DELAY: true, + FRAME_TYPE: true, + X: true, + Y: true, + WIDTH: true, + HEIGHT: true, + BIN: true, + FRAME_FORMAT: true, + GAIN: true, + OFFSET: true, +} + +export const DEFAULT_SEQUENCE_PROPERTY_DIALOG: SequencePropertyDialog = { + showDialog: false, + count: [0, 0], + properties: DEFAULT_SEQUENCE_PROPERTIES, +} + +export const DEFAULT_SEQUENCER_PREFERENCE: SequencerPreference = { + plan: DEFAULT_SEQUENCER_PLAN, + properties: DEFAULT_SEQUENCE_PROPERTY_DIALOG.properties, +} + +export function autoFocusAfterConditionsWithDefault(conditions?: Partial, source: AutoFocusAfterConditions = DEFAULT_AUTO_FOCUS_AFTER_CONDITIONS) { + if (!conditions) return structuredClone(source) + conditions.enabled ??= source.enabled + conditions.onStart ??= source.onStart + conditions.onFilterChange ??= source.onFilterChange + conditions.afterElapsedTime ??= source.afterElapsedTime + conditions.afterElapsedTimeEnabled ??= source.afterElapsedTimeEnabled + conditions.afterExposures ??= source.afterExposures + conditions.afterExposuresEnabled ??= source.afterExposuresEnabled + conditions.afterTemperatureChange ??= source.afterTemperatureChange + conditions.afterTemperatureChangeEnabled ??= source.afterTemperatureChangeEnabled + conditions.afterHFDIncrease ??= source.afterHFDIncrease + conditions.afterHFDIncreaseEnabled ??= source.afterHFDIncreaseEnabled + return conditions as AutoFocusAfterConditions +} + +export function sequencePropertiesWithDefault(properties?: Partial, source: SequenceProperties = DEFAULT_SEQUENCE_PROPERTIES) { + if (!properties) return structuredClone(source) + + for (const entry of Object.entries(source)) { + const key = entry[0] as SequenceProperty + properties[key] ??= source[key] + } + + return properties as SequenceProperties +} + +export function sequencerPlanWithDefault(plan?: Partial, source: SequencerPlan = DEFAULT_SEQUENCER_PLAN) { + if (!plan) return structuredClone(source) + plan.initialDelay ??= source.initialDelay + plan.captureMode ||= source.captureMode + plan.autoSubFolderMode ||= source.autoSubFolderMode + plan.savePath ||= source.savePath + plan.sequences ??= source.sequences + plan.dither = ditherWithDefault(plan.dither, source.dither) + plan.autoFocus = autoFocusAfterConditionsWithDefault(plan.autoFocus, source.autoFocus) + plan.namingFormat = cameraCaptureNamingFormatWithDefault(plan.namingFormat, source.namingFormat) + return plan as SequencerPlan } -export const EMPTY_SEQUENCER_PREFERENCE: SequencerPreference = { - plan: structuredClone(EMPTY_SEQUENCE_PLAN), +export function sequencerPreferenceWithDefault(preference?: Partial, source: SequencerPreference = DEFAULT_SEQUENCER_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.loadPath ||= source.loadPath + preference.plan = sequencerPlanWithDefault(preference.plan, source.plan) + preference.properties = sequencePropertiesWithDefault(preference.properties, source.properties) + return preference as SequencerPreference } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index 402676bb3..f7956d1a9 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -1,39 +1,92 @@ -import type { FrameType } from './camera.types' - -export interface CameraCaptureNamingFormat { - light?: string - dark?: string - flat?: string - bias?: string -} +import type { Location } from './atlas.types' +import { DEFAULT_LOCATION, locationWithDefault } from './atlas.types' +import type { LiveStackerSettings, LiveStackerType } from './camera.types' +import { cameraCaptureNamingFormatWithDefault, DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, DEFAULT_LIVE_STACKER_SETTINGS, liveStackerSettingsWithDefault, type CameraCaptureNamingFormat, type FrameType } from './camera.types' +import { DEFAULT_PLATE_SOLVER_SETTINGS, plateSolverSettingsWithDefault, type PlateSolverSettings, type PlateSolverType } from './platesolver.types' +import { DEFAULT_STACKER_SETTINGS, stackerSettingsWithDefault, type StackerSettings, type StackerType } from './stacker.types' +import { DEFAULT_STAR_DETECTOR_SETTINGS, starDetectorSettingsWithDefault, type StarDetectorSettings, type StarDetectorType } from './stardetector.types' export type SettingsTabKey = 'LOCATION' | 'PLATE_SOLVER' | 'STAR_DETECTOR' | 'LIVE_STACKER' | 'STACKER' | 'CAPTURE_NAMING_FORMAT' -export interface SettingsTab { - id: SettingsTabKey - name: string +export interface SettingsPreference { + plateSolver: Record + starDetector: Record + liveStacker: Record + stacker: Record + namingFormat: CameraCaptureNamingFormat + locations: Location[] + location: Location +} + +export const DEFAULT_SETTINGS_PREFERENCE: SettingsPreference = { + plateSolver: { + ASTROMETRY_NET: structuredClone(DEFAULT_PLATE_SOLVER_SETTINGS), + ASTROMETRY_NET_ONLINE: structuredClone(DEFAULT_PLATE_SOLVER_SETTINGS), + ASTAP: structuredClone(DEFAULT_PLATE_SOLVER_SETTINGS), + SIRIL: structuredClone(DEFAULT_PLATE_SOLVER_SETTINGS), + PIXINSIGHT: structuredClone(DEFAULT_PLATE_SOLVER_SETTINGS), + }, + starDetector: { + ASTAP: structuredClone(DEFAULT_STAR_DETECTOR_SETTINGS), + SIRIL: structuredClone(DEFAULT_STAR_DETECTOR_SETTINGS), + PIXINSIGHT: structuredClone(DEFAULT_STAR_DETECTOR_SETTINGS), + }, + liveStacker: { + SIRIL: structuredClone(DEFAULT_LIVE_STACKER_SETTINGS), + PIXINSIGHT: structuredClone(DEFAULT_LIVE_STACKER_SETTINGS), + }, + stacker: { + PIXINSIGHT: structuredClone(DEFAULT_STACKER_SETTINGS), + }, + namingFormat: DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, + locations: [DEFAULT_LOCATION], + location: DEFAULT_LOCATION, } -export const DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT: CameraCaptureNamingFormat = { - light: '[camera]_[type]_[year:2][month][day][hour][min][sec][ms]_[filter]_[width]_[height]_[exp]_[bin]_[gain]', - dark: '[camera]_[type]_[width]_[height]_[exp]_[bin]_[gain]', - flat: '[camera]_[type]_[filter]_[width]_[height]_[bin]', - bias: '[camera]_[type]_[width]_[height]_[bin]_[gain]', +export function settingsPreferenceWithDefault(preference?: Partial, source: SettingsPreference = DEFAULT_SETTINGS_PREFERENCE) { + if (!preference) return structuredClone(source) + + preference.plateSolver ??= structuredClone(source.plateSolver) + preference.starDetector ??= structuredClone(source.starDetector) + preference.liveStacker ??= structuredClone(source.liveStacker) + preference.stacker ??= structuredClone(source.stacker) + + for (const [key, value] of Object.entries(preference.plateSolver)) { + plateSolverSettingsWithDefault(value, source.plateSolver[key as never]) + } + for (const [key, value] of Object.entries(preference.starDetector)) { + starDetectorSettingsWithDefault(value, source.starDetector[key as never]) + } + for (const [key, value] of Object.entries(preference.liveStacker)) { + liveStackerSettingsWithDefault(value, source.liveStacker[key as never]) + } + for (const [key, value] of Object.entries(preference.stacker)) { + stackerSettingsWithDefault(value, source.stacker[key as never]) + } + + preference.namingFormat = cameraCaptureNamingFormatWithDefault(preference.namingFormat, source.namingFormat) + preference.location = locationWithDefault(preference.location, source.location) + + if (!preference.locations?.length) { + preference.locations = structuredClone(source.locations) + } + + return preference as SettingsPreference } export function resetCameraCaptureNamingFormat(type: FrameType, format: CameraCaptureNamingFormat, defaultValue?: CameraCaptureNamingFormat) { switch (type) { case 'LIGHT': - format.light = defaultValue?.light ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.light + format.light = defaultValue?.light || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.light break case 'DARK': - format.dark = defaultValue?.dark ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.dark + format.dark = defaultValue?.dark || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.dark break case 'FLAT': - format.flat = defaultValue?.flat ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.flat + format.flat = defaultValue?.flat || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.flat break case 'BIAS': - format.bias = defaultValue?.bias ?? DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.bias + format.bias = defaultValue?.bias || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.bias break } } diff --git a/desktop/src/shared/types/stacker.types.ts b/desktop/src/shared/types/stacker.types.ts index f2971eeff..c7250dd8c 100644 --- a/desktop/src/shared/types/stacker.types.ts +++ b/desktop/src/shared/types/stacker.types.ts @@ -4,10 +4,14 @@ export type StackerType = 'PIXINSIGHT' export type StackerGroupType = 'LUMINANCE' | 'RED' | 'GREEN' | 'BLUE' | 'MONO' | 'RGB' -export interface StackingRequest { +export interface StackerSettings { + executablePath: string + slot: number +} + +export interface StackingRequest extends StackerSettings { outputDirectory: string type: StackerType - executablePath: string darkPath?: string darkEnabled: boolean flatPath?: string @@ -15,24 +19,10 @@ export interface StackingRequest { biasPath?: string biasEnabled: boolean use32Bits: boolean - slot: number referencePath: string targets: StackingTarget[] } -export const EMPTY_STACKING_REQUEST: StackingRequest = { - outputDirectory: '', - type: 'PIXINSIGHT', - executablePath: '', - use32Bits: false, - slot: 1, - referencePath: '', - targets: [], - darkEnabled: false, - flatEnabled: false, - biasEnabled: false, -} - export interface StackingTarget { enabled: boolean path: string @@ -54,19 +44,58 @@ export interface AnalyzedTarget { } export interface StackerPreference { - type?: StackerType - outputDirectory?: string + request: StackingRequest defaultPath?: string - darkPath?: string - darkEnabled?: boolean - flatPath?: string - flatEnabled?: boolean - biasPath?: string - biasEnabled?: boolean } -export const EMPTY_STACKER_PREFERENCE: StackerPreference = { +export const DEFAULT_STACKER_SETTINGS: StackerSettings = { + executablePath: '', + slot: 0, +} + +export const DEFAULT_STACKING_REQUEST: StackingRequest = { + ...DEFAULT_STACKER_SETTINGS, + outputDirectory: '', + type: 'PIXINSIGHT', + use32Bits: false, + referencePath: '', + targets: [], darkEnabled: false, flatEnabled: false, biasEnabled: false, } + +export const DEFAULT_STACKER_PREFERENCE: StackerPreference = { + request: DEFAULT_STACKING_REQUEST, +} + +export function stackerSettingsWithDefault(preference?: Partial, source: StackerSettings = DEFAULT_STACKER_SETTINGS) { + if (!preference) return structuredClone(source) + preference.executablePath ||= source.executablePath + preference.slot ??= source.slot + return preference as StackerSettings +} + +export function stackingRequestWithDefault(request?: Partial, source: StackingRequest = DEFAULT_STACKING_REQUEST) { + if (!request) return structuredClone(source) + stackerSettingsWithDefault(request, source) + request.outputDirectory ||= source.outputDirectory + request.type ||= source.type + request.darkPath ||= source.darkPath + request.darkEnabled ??= source.darkEnabled + request.flatPath ||= source.flatPath + request.flatEnabled ??= source.flatEnabled + request.biasPath ||= source.biasPath + request.biasEnabled ??= source.biasEnabled + request.use32Bits ??= source.use32Bits + request.referencePath ||= source.referencePath + request.targets ??= source.targets + return request as StackingRequest +} + +export function stackerPreferenceWithDefault(preference?: Partial, source: StackerPreference = DEFAULT_STACKER_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.request = stackingRequestWithDefault(preference.request, source.request) + preference.defaultPath ??= source.defaultPath + return preference as StackerPreference +} diff --git a/desktop/src/shared/types/stardetector.types.ts b/desktop/src/shared/types/stardetector.types.ts index d0d344c41..5093e3ccf 100644 --- a/desktop/src/shared/types/stardetector.types.ts +++ b/desktop/src/shared/types/stardetector.types.ts @@ -1,19 +1,43 @@ export type StarDetectorType = 'ASTAP' | 'PIXINSIGHT' | 'SIRIL' -export interface StarDetectionRequest { - type: StarDetectorType +export interface StarDetectorSettings { executablePath: string timeout: number + slot: number +} + +export interface StarDetectionRequest extends StarDetectorSettings { + type: StarDetectorType minSNR?: number maxStars?: number - slot: number } -export const EMPTY_STAR_DETECTION_REQUEST: StarDetectionRequest = { - type: 'ASTAP', +export const DEFAULT_STAR_DETECTOR_SETTINGS: StarDetectorSettings = { executablePath: '', timeout: 300, + slot: 0, +} + +export const DEFAULT_STAR_DETECTION_REQUEST: StarDetectionRequest = { + ...DEFAULT_STAR_DETECTOR_SETTINGS, + type: 'ASTAP', minSNR: 0, maxStars: 0, - slot: 1, +} + +export function starDetectorSettingsWithDefault(settings?: Partial, source: StarDetectorSettings = DEFAULT_STAR_DETECTOR_SETTINGS) { + if (!settings) return structuredClone(source) + settings.executablePath ||= source.executablePath + settings.timeout ??= source.timeout + settings.slot ??= source.slot + return settings as StarDetectorSettings +} + +export function starDetectionRequestWithDefault(request?: Partial, source: StarDetectionRequest = DEFAULT_STAR_DETECTION_REQUEST) { + if (!request) return structuredClone(source) + starDetectorSettingsWithDefault(request, source) + request.type ||= source.type + request.minSNR ??= source.minSNR + request.maxStars ??= source.maxStars + return request as StarDetectionRequest } diff --git a/desktop/src/shared/types/wheel.types.ts b/desktop/src/shared/types/wheel.types.ts index bb2c7168f..2ebb46491 100644 --- a/desktop/src/shared/types/wheel.types.ts +++ b/desktop/src/shared/types/wheel.types.ts @@ -1,52 +1,41 @@ import type { CameraStartCapture } from './camera.types' import type { Device } from './device.types' -export type WheelDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' +export type WheelMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' -export interface FilterWheel extends Device { +export interface Wheel 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, - names: [], -} - export interface WheelDialogInput { - mode: WheelDialogMode - wheel: FilterWheel + mode: WheelMode + wheel: Wheel request: CameraStartCapture } export interface WheelPreference { - shutterPosition?: number + shutterPosition: number } -export interface FilterSlot { +export interface Filter { position: number name: string dark: boolean } export interface WheelRenamed { - wheel: FilterWheel - filter: FilterSlot + wheel: Wheel + filter: Filter } -export function makeFilterSlots(wheel: FilterWheel, filters: FilterSlot[], shutterPosition: number = 0) { +export function makeFilter(wheel: Wheel, filters: Filter[], shutterPosition: number = 0) { if (wheel.count <= 0) { filters = [] } else if (wheel.count !== filters.length) { - filters = new Array(wheel.count) + filters = new Array(wheel.count) } if (filters.length) { @@ -66,6 +55,28 @@ export function makeFilterSlots(wheel: FilterWheel, filters: FilterSlot[], shutt return filters } -export function isFilterWheel(device?: Device): device is FilterWheel { - return !!device && 'count' in device +export const DEFAULT_WHEEL: Wheel = { + type: 'WHEEL', + sender: '', + id: '', + count: 0, + position: 0, + moving: false, + name: '', + connected: false, + names: [], +} + +export const DEFAULT_WHEEL_PREFERENCE: WheelPreference = { + shutterPosition: 0, +} + +export function isWheel(device?: Device): device is Wheel { + return !!device && device.type === 'WHEEL' +} + +export function wheelPreferenceWithDefault(preference?: Partial, source: WheelPreference = DEFAULT_WHEEL_PREFERENCE) { + if (!preference) return structuredClone(source) + preference.shutterPosition ??= source.shutterPosition + return preference as WheelPreference } diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index 4449da541..73efde931 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -249,8 +249,8 @@ i.mdi { .p-menuitem-link { &.p-menuitem-selected { - background-color: $successButtonBg; - color: #212121 !important; + background-color: rgb(0 255 0 / 10%); + border-radius: 4px; } } @@ -318,6 +318,10 @@ p-tieredmenu *, } } +.p-listbox-header { + border-bottom: 0px; +} + .pixelated { image-rendering: pixelated; } @@ -334,6 +338,14 @@ p-tieredmenu *, gap: 1px; } +.mb-1px { + margin-bottom: 1px; +} + +.p-2-4 { + padding: 2px 4px; +} + .min-w-0 { min-width: 0px !important; } @@ -479,8 +491,8 @@ p-tieredmenu *, } ::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 4px; + height: 4px; } ::-webkit-scrollbar-thumb { diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 69c0debbb..10bca42d5 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "strict": true, "outDir": "./dist/out-tsc", - "module": "ES2022", + "module": "ESNext", "sourceMap": true, "declaration": false, "moduleResolution": "node", @@ -12,7 +12,7 @@ "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "noUncheckedIndexedAccess": false, - "noUnusedLocals": true, + "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noImplicitThis": true, diff --git a/desktop/tsconfig.serve.json b/desktop/tsconfig.serve.json index 1c64234ca..06b36ba89 100644 --- a/desktop/tsconfig.serve.json +++ b/desktop/tsconfig.serve.json @@ -2,18 +2,18 @@ "compilerOptions": { "sourceMap": true, "declaration": false, - "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "resolveJsonModule": true, - "module": "CommonJS", + "module": "NodeNext", "target": "ES2022", "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, "incremental": true, "types": ["node"], "lib": ["es2022", "es2018", "es2017", "es2016", "es2015", "dom"] }, - "files": ["app/main.ts", "app/preload.ts", "app/argument.parser.ts", "app/local.storage.ts", "app/window.manager.ts"], + "files": ["app/main.ts", "app/preload.ts", "app/argument.parser.ts", "app/window.manager.ts"], "include": ["src/shared/types/*.ts", "src/typings.d.ts"], "exclude": ["node_modules", "**/*.spec.ts"] } diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt index fef1fce0e..2e5f3c609 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt @@ -71,11 +71,8 @@ data object FitsFormat : ImageFormat { val numberOfChannels = header.numberOfChannels val bitpix = header.bitpix val position = source.position - val rangeMin = header.getFloat(FitsKeyword.DATAMIN, 0f) - val rangeMax = header.getFloat(FitsKeyword.DATAMAX, 1f) - val range = rangeMin..rangeMax - val data = SeekableSourceImageData(source, position, width, height, numberOfChannels, bitpix, range) + val data = SeekableSourceImageData(source, position, width, height, numberOfChannels, bitpix) val skipBytes = computeRemainingBytesToSkip(data.totalSizeInBytes) if (skipBytes > 0L) source.seek(position + data.totalSizeInBytes + skipBytes) diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt index 8733dde85..c514726a9 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/SeekableSourceImageData.kt @@ -7,7 +7,6 @@ import nebulosa.io.SeekableSource import nebulosa.log.loggerFor import okio.Buffer import okio.Sink -import kotlin.math.max import kotlin.math.min @Suppress("NOTHING_TO_INLINE") @@ -18,7 +17,6 @@ internal data class SeekableSourceImageData( override val height: Int, override val numberOfChannels: Int, private val bitpix: Bitpix, - private val range: ClosedFloatingPointRange, ) : ImageData { @JvmField internal val channelSizeInBytes = (numberOfPixels * bitpix.byteLength).toLong() @@ -100,14 +98,12 @@ internal data class SeekableSourceImageData( } if (min < 0f || max > 1f) { - val rangeMin = min(range.start, min) - val rangeMax = max(range.endInclusive, max) - val rangeDelta = rangeMax - rangeMin + val rangeDelta = max - min - LOG.info("rescaling [{}, {}] to [0, 1]. channel={}, delta={}", rangeMin, rangeMax, channel, rangeDelta) + LOG.info("rescaling [{}, {}] to [0, 1]. channel={}, delta={}", min, max, channel, rangeDelta) for (i in output.indices) { - output[i] = (output[i] - rangeMin) / rangeDelta + output[i] = (output[i] - min) / rangeDelta } } } diff --git a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt index 888c00588..87deb6d54 100644 --- a/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt +++ b/nebulosa-hips2fits/src/main/kotlin/nebulosa/hips2fits/Hips2FitsService.kt @@ -35,6 +35,7 @@ class Hips2FitsService( coordSystem.name.lowercase(), rotation.toDegrees, format.name.lowercase(), ) + // https://alasky.cds.unistra.fr/MocServer/query?get=record&fmt=json&expr=ID%3DCDS*%20%26%26%20hips_service_url*%3D*alasky*%20%26%26%20dataproduct_type%3Dimage%20%26%26%20moc_sky_fraction%20%3E%3D%200.99%20%26%26%20obs_regime%3DOptical%2CInfrared%2CUV%2CRadio%2CX-ray%2CGamma-ray 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") diff --git a/nebulosa-image/src/main/kotlin/nebulosa/image/algorithms/transformation/ScreenTransformFunction.kt b/nebulosa-image/src/main/kotlin/nebulosa/image/algorithms/transformation/ScreenTransformFunction.kt index 8c9737a48..87c17d9bc 100644 --- a/nebulosa-image/src/main/kotlin/nebulosa/image/algorithms/transformation/ScreenTransformFunction.kt +++ b/nebulosa-image/src/main/kotlin/nebulosa/image/algorithms/transformation/ScreenTransformFunction.kt @@ -16,11 +16,13 @@ data class ScreenTransformFunction( ) : TransformAlgorithm { data class Parameters( - val midtone: Float = 0.5f, - val shadow: Float = 0f, - val highlight: Float = 1f, + @JvmField val midtone: Float = 0.5f, + @JvmField val shadow: Float = 0f, + @JvmField val highlight: Float = 1f, ) { + constructor(midtone: Int, shadow: Int, highlight: Int) : this(midtone / 65536f, shadow / 65536f, highlight / 65536f) + companion object { @JvmStatic val DEFAULT = Parameters() diff --git a/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt b/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt index fba695c05..629b6512d 100644 --- a/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt +++ b/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt @@ -15,373 +15,371 @@ import org.junit.jupiter.api.Test class FitsTransformAlgorithmTest { - init { - @Test - fun monoRaw() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.save("fits-mono-raw").second shouldBe "e17cfc29c3b343409cd8617b6913330e" - } - - @Test - fun monoVerticalFlip() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(VerticalFlip) - mImage.save("fits-mono-vertical-flip").second shouldBe "262260dfe719726c0e7829a088279a21" - } - - @Test - fun monoHorizontalFlip() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(HorizontalFlip) - mImage.save("fits-mono-horizontal-flip").second shouldBe "daf0f05db5de3750962f338527564b27" - } - - @Test - fun monoVerticalAndHorizontalFlip() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(VerticalFlip, HorizontalFlip) - mImage.save("fits-mono-vertical-horizontal-flip").second shouldBe "3bc81f579a0e34ce9312c3b242209166" - } - - @Test - fun monoSubframe() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - val nImage = mImage.transform(SubFrame(45, 70, 16, 16)) - nImage.width shouldBeExactly 16 - nImage.height shouldBeExactly 16 - nImage.mono.shouldBeTrue() - nImage.save("fits-mono-subframe").second shouldBe "4d9984e778f82dde10b9aeeee7a29fe0" - } - - @Test - fun monoSharpen() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(Sharpen) - mImage.save("fits-mono-sharpen").second shouldBe "0b162242a4e673f6480b5206cf49ca50" - } - - @Test - fun monoMean() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(Mean) - mImage.save("fits-mono-mean").second shouldBe "cf866292f657c379ae3965931dd8eeea" - } - - @Test - fun monoInvert() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(Invert) - mImage.save("fits-mono-invert").second shouldBe "6e94463bb5b9561de1f0ee0a154db53e" - } - - @Test - fun monoEmboss() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(Emboss) - mImage.save("fits-mono-emboss").second shouldBe "94a8ef5e4573e392d087cf10c905ba12" - } - - @Test - fun monoEdges() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(Edges) - mImage.save("fits-mono-edges").second shouldBe "27ccd5f5e6098d0cae27e7495e18dd72" - } - - @Test - fun monoBlur() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(Blur) - mImage.save("fits-mono-blur").second shouldBe "f2c5466dccf71b5c4bee86c5fbbb95fc" - } - - @Test - fun monoGaussianBlur() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(GaussianBlur(sigma = 5.0, size = 9)) - mImage.save("fits-mono-gaussian-blur").second shouldBe "69057b0c4461fb0d55b779da9e72fd69" - } - - @Test - fun monoStfMidtone01Shadow00Highlight10() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f)) - mImage.save("fits-mono-stf-01-00-10").second shouldBe "22c0bd985e70a01330722d912869d6ee" - } - - @Test - fun monoStfMidtone09Shadow00Highlight10() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f)) - mImage.save("fits-mono-stf-09-00-10").second shouldBe "553ccb7546dce3a8f742d5e8f7c58a3f" - } - - @Test - fun monoStfMidtone01Shadow05Highlight10() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f, shadow = 0.5f)) - mImage.save("fits-mono-stf-01-05-10").second shouldBe "f31db854fab72033dce2f8c572ec6783" - } - - @Test - fun monoStfMidtone09Shadow05Highlight10() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f, shadow = 0.5f)) - mImage.save("fits-mono-stf-09-05-10").second shouldBe "633b49c4a1dbb5ad8e6a9d74f330636d" - } - - @Test - fun monoStfMidtone01Shadow00Highlight05() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f, highlight = 0.5f)) - mImage.save("fits-mono-stf-01-00-05").second shouldBe "26036937eb3e5f99cd6129f709ce4b31" - } - - @Test - fun monoStfMidtone09Shadow00Highlight05() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f, highlight = 0.5f)) - mImage.save("fits-mono-stf-09-00-05").second shouldBe "e8f694dae666ac15ce2f8a169eb84024" - } - - @Test - fun monoStfMidtone01Shadow04Highlight06() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f, 0.4f, 0.6f)) - mImage.save("fits-mono-stf-01-04-06").second shouldBe "5226aba21669a24f985703b3e7220568" - } - - @Test - fun monoStfMidtone09Shadow04Highlight06() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f, 0.4f, 0.6f)) - mImage.save("fits-mono-stf-09-04-06").second shouldBe "c2acb25ef7be92a51f63e673ec9a850f" - } - - @Test - fun monoAutoStf() { - val mImage = NGC3344_MONO_8_FITS.fits().asImage() - mImage.transform(AutoScreenTransformFunction) - mImage.save("fits-mono-auto-stf").second shouldBe "e17cfc29c3b343409cd8617b6913330e" - } - - @Test - fun colorRaw() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.save("fits-color-raw").second shouldBe "18fb83e240bc7a4cbafbc1aba2741db6" - } - - @Test - fun colorVerticalFlip() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(VerticalFlip) - mImage.save("fits-color-vertical-flip").second shouldBe "b717ecda5c5bba50cfa06304ef2bca88" - } - - @Test - fun colorHorizontalFlip() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(HorizontalFlip) - mImage.save("fits-color-horizontal-flip").second shouldBe "f70228600c77551473008ed4b9986439" - } - - @Test - fun colorVerticalAndHorizontalFlip() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(VerticalFlip, HorizontalFlip) - mImage.save("fits-color-vertical-horizontal-flip").second shouldBe "1237314044f20307b76203148af855e3" - } - - @Test - fun colorSubframe() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - val nImage = mImage.transform(SubFrame(45, 70, 16, 16)) - nImage.width shouldBeExactly 16 - nImage.height shouldBeExactly 16 - nImage.mono.shouldBeFalse() - nImage.save("fits-color-subframe").second shouldBe "282fc4fdf9142fcb4b18e1df1eef4caa" - } - - @Test - fun colorSharpen() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(Sharpen) - mImage.save("fits-color-sharpen").second shouldBe "e562282bdafdeba6ce88981bb9c3ba61" - } - - @Test - fun colorMean() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(Mean) - mImage.save("fits-color-mean").second shouldBe "a8380d928aaa756e202ba43bd3a2f207" - } - - @Test - fun colorInvert() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(Invert) - mImage.save("fits-color-invert").second shouldBe "decad269ec26450aebeaf7546867b5f8" - } - - @Test - fun colorEmboss() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(Emboss) - mImage.save("fits-color-emboss").second shouldBe "58d69250f1233055aa33f9ec7ca40af1" - } - - @Test - fun colorEdges() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(Edges) - mImage.save("fits-color-edges").second shouldBe "091f2955740a8edcd2401dc416d19d51" - } - - @Test - fun colorBlur() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(Blur) - mImage.save("fits-color-blur").second shouldBe "0fca440b763de5380fa29de736f3c792" - } - - @Test - fun colorGaussianBlur() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(GaussianBlur(sigma = 5.0, size = 9)) - mImage.save("fits-color-gaussian-blur").second shouldBe "394d1a4f136f15c802dd73004c421d64" - } - - @Test - fun colorStfMidtone01Shadow00Highlight10() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f)) - mImage.save("fits-color-stf-01-00-10").second shouldBe "e952bd263df6fd275b9a80aca554cb4b" - } - - @Test - fun colorStfMidtone09Shadow00Highlight10() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f)) - mImage.save("fits-color-stf-09-00-10").second shouldBe "038809d7612018e2e5c19d5e1f551abd" - } - - @Test - fun colorStfMidtone01Shadow05Highlight10() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f, shadow = 0.5f)) - mImage.save("fits-color-stf-01-05-10").second shouldBe "70e812260f56f8621002327575611f31" - } - - @Test - fun colorStfMidtone09Shadow05Highlight10() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f, shadow = 0.5f)) - mImage.save("fits-color-stf-09-05-10").second shouldBe "6ca400f617f466a9eb02a3a6f2985d99" - } - - @Test - fun colorStfMidtone01Shadow00Highlight05() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f, highlight = 0.5f)) - mImage.save("fits-color-stf-01-00-05").second shouldBe "3cd98ee9a8949d5100295acccd77010b" - } - - @Test - fun colorStfMidtone09Shadow00Highlight05() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f, highlight = 0.5f)) - mImage.save("fits-color-stf-09-00-05").second shouldBe "2cfeffc88c893cc5883d8a2221f29b91" - } - - @Test - fun colorStfMidtone01Shadow04Highlight06() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.1f, 0.4f, 0.6f)) - mImage.save("fits-color-stf-01-04-06").second shouldBe "532a07a1a166eb007c2e40651aec2097" - } - - @Test - fun colorStfMidtone09Shadow04Highlight06() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(ScreenTransformFunction(0.9f, 0.4f, 0.6f)) - mImage.save("fits-color-stf-09-04-06").second shouldBe "eb3d940d9fd2c8814e930715e89897c4" - } - - @Test - fun colorAutoStf() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(AutoScreenTransformFunction) - mImage.save("fits-color-auto-stf").second shouldBe "a9c3657d8597b927607eb438e666d3a0" - } - - @Test - fun colorScnrMaximumMask() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.MAXIMUM_MASK)) - mImage.save("fits-color-scnr-maximum-mask").second shouldBe "e7d2155e18ff1e3172f4e849ae983145" - } - - @Test - fun colorScnrAdditiveMask() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.ADDITIVE_MASK)) - mImage.save("fits-color-scnr-additive-mask").second shouldBe "a458c44cedcda704de16d80053fd87eb" - } - - @Test - fun colorScnrAverageNeutral() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.AVERAGE_NEUTRAL)) - mImage.save("fits-color-scnr-average-neutral").second shouldBe "e07345ffc4982a62301c95c76d3efb35" - } - - @Test - fun colorScnrMaximumNeutral() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.MAXIMUM_NEUTRAL)) - mImage.save("fits-color-scnr-maximum-neutral").second shouldBe "a1d4b04f57b001ba4a996bab0407fd7e" - } - - @Test - fun colorScnrMinimumNeutral() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.MINIMUM_NEUTRAL)) - mImage.save("fits-color-scnr-minimum-neutral").second shouldBe "8b7be57ff38da9c97b35d7888047c0f9" - } - - @Test - fun colorGrayscaleBt709() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - val nImage = mImage.transform(Grayscale.BT709) - nImage.save("fits-color-grayscale-bt709").second shouldBe "cab675aa35390a2d58cd48555d91054f" - } - - @Test - fun colorGrayscaleRmy() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - val nImage = mImage.transform(Grayscale.RMY) - nImage.save("fits-color-grayscale-rmy").second shouldBe "e113627002a4178d1010a2f6246e325f" - } - - @Test - fun colorGrayscaleY() { - val mImage = NGC3344_COLOR_32_FITS.fits().asImage() - val nImage = mImage.transform(Grayscale.Y) - nImage.save("fits-color-grayscale-y").second shouldBe "24dd4a7e0fa9e4be34c53c924a78a940" - } - - @Test - fun colorDebayer() { - val mImage = DEBAYER_FITS.fits().asImage() - val nImage = mImage.transform(AutoScreenTransformFunction) - nImage.save("fits-color-debayer").second shouldBe "86b5bdd67dfd6bbf5495afae4bf2bc04" - } - - @Test - fun colorNoDebayer() { - val mImage = DEBAYER_FITS.fits().asImage(false) - val nImage = mImage.transform(AutoScreenTransformFunction) - nImage.save("fits-color-no-debayer").second shouldBe "958ccea020deec1f0c075042a9ba37c3" - } + @Test + fun monoRaw() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.save("fits-mono-raw").second shouldBe "e17cfc29c3b343409cd8617b6913330e" + } + + @Test + fun monoVerticalFlip() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(VerticalFlip) + mImage.save("fits-mono-vertical-flip").second shouldBe "262260dfe719726c0e7829a088279a21" + } + + @Test + fun monoHorizontalFlip() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(HorizontalFlip) + mImage.save("fits-mono-horizontal-flip").second shouldBe "daf0f05db5de3750962f338527564b27" + } + + @Test + fun monoVerticalAndHorizontalFlip() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(VerticalFlip, HorizontalFlip) + mImage.save("fits-mono-vertical-horizontal-flip").second shouldBe "3bc81f579a0e34ce9312c3b242209166" + } + + @Test + fun monoSubframe() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + val nImage = mImage.transform(SubFrame(45, 70, 16, 16)) + nImage.width shouldBeExactly 16 + nImage.height shouldBeExactly 16 + nImage.mono.shouldBeTrue() + nImage.save("fits-mono-subframe").second shouldBe "4d9984e778f82dde10b9aeeee7a29fe0" + } + + @Test + fun monoSharpen() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(Sharpen) + mImage.save("fits-mono-sharpen").second shouldBe "0b162242a4e673f6480b5206cf49ca50" + } + + @Test + fun monoMean() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(Mean) + mImage.save("fits-mono-mean").second shouldBe "cf866292f657c379ae3965931dd8eeea" + } + + @Test + fun monoInvert() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(Invert) + mImage.save("fits-mono-invert").second shouldBe "6e94463bb5b9561de1f0ee0a154db53e" + } + + @Test + fun monoEmboss() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(Emboss) + mImage.save("fits-mono-emboss").second shouldBe "94a8ef5e4573e392d087cf10c905ba12" + } + + @Test + fun monoEdges() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(Edges) + mImage.save("fits-mono-edges").second shouldBe "27ccd5f5e6098d0cae27e7495e18dd72" + } + + @Test + fun monoBlur() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(Blur) + mImage.save("fits-mono-blur").second shouldBe "f2c5466dccf71b5c4bee86c5fbbb95fc" + } + + @Test + fun monoGaussianBlur() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(GaussianBlur(sigma = 5.0, size = 9)) + mImage.save("fits-mono-gaussian-blur").second shouldBe "69057b0c4461fb0d55b779da9e72fd69" + } + + @Test + fun monoStfMidtone01Shadow00Highlight10() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f)) + mImage.save("fits-mono-stf-01-00-10").second shouldBe "22c0bd985e70a01330722d912869d6ee" + } + + @Test + fun monoStfMidtone09Shadow00Highlight10() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f)) + mImage.save("fits-mono-stf-09-00-10").second shouldBe "553ccb7546dce3a8f742d5e8f7c58a3f" + } + + @Test + fun monoStfMidtone01Shadow05Highlight10() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f, shadow = 0.5f)) + mImage.save("fits-mono-stf-01-05-10").second shouldBe "f31db854fab72033dce2f8c572ec6783" + } + + @Test + fun monoStfMidtone09Shadow05Highlight10() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f, shadow = 0.5f)) + mImage.save("fits-mono-stf-09-05-10").second shouldBe "633b49c4a1dbb5ad8e6a9d74f330636d" + } + + @Test + fun monoStfMidtone01Shadow00Highlight05() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f, highlight = 0.5f)) + mImage.save("fits-mono-stf-01-00-05").second shouldBe "26036937eb3e5f99cd6129f709ce4b31" + } + + @Test + fun monoStfMidtone09Shadow00Highlight05() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f, highlight = 0.5f)) + mImage.save("fits-mono-stf-09-00-05").second shouldBe "e8f694dae666ac15ce2f8a169eb84024" + } + + @Test + fun monoStfMidtone01Shadow04Highlight06() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f, 0.4f, 0.6f)) + mImage.save("fits-mono-stf-01-04-06").second shouldBe "5226aba21669a24f985703b3e7220568" + } + + @Test + fun monoStfMidtone09Shadow04Highlight06() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f, 0.4f, 0.6f)) + mImage.save("fits-mono-stf-09-04-06").second shouldBe "c2acb25ef7be92a51f63e673ec9a850f" + } + + @Test + fun monoAutoStf() { + val mImage = NGC3344_MONO_8_FITS.fits().asImage() + mImage.transform(AutoScreenTransformFunction) + mImage.save("fits-mono-auto-stf").second shouldBe "e17cfc29c3b343409cd8617b6913330e" + } + + @Test + fun colorRaw() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.save("fits-color-raw").second shouldBe "18fb83e240bc7a4cbafbc1aba2741db6" + } + + @Test + fun colorVerticalFlip() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(VerticalFlip) + mImage.save("fits-color-vertical-flip").second shouldBe "b717ecda5c5bba50cfa06304ef2bca88" + } + + @Test + fun colorHorizontalFlip() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(HorizontalFlip) + mImage.save("fits-color-horizontal-flip").second shouldBe "f70228600c77551473008ed4b9986439" + } + + @Test + fun colorVerticalAndHorizontalFlip() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(VerticalFlip, HorizontalFlip) + mImage.save("fits-color-vertical-horizontal-flip").second shouldBe "1237314044f20307b76203148af855e3" + } + + @Test + fun colorSubframe() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + val nImage = mImage.transform(SubFrame(45, 70, 16, 16)) + nImage.width shouldBeExactly 16 + nImage.height shouldBeExactly 16 + nImage.mono.shouldBeFalse() + nImage.save("fits-color-subframe").second shouldBe "282fc4fdf9142fcb4b18e1df1eef4caa" + } + + @Test + fun colorSharpen() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(Sharpen) + mImage.save("fits-color-sharpen").second shouldBe "e562282bdafdeba6ce88981bb9c3ba61" + } + + @Test + fun colorMean() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(Mean) + mImage.save("fits-color-mean").second shouldBe "a8380d928aaa756e202ba43bd3a2f207" + } + + @Test + fun colorInvert() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(Invert) + mImage.save("fits-color-invert").second shouldBe "decad269ec26450aebeaf7546867b5f8" + } + + @Test + fun colorEmboss() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(Emboss) + mImage.save("fits-color-emboss").second shouldBe "58d69250f1233055aa33f9ec7ca40af1" + } + + @Test + fun colorEdges() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(Edges) + mImage.save("fits-color-edges").second shouldBe "091f2955740a8edcd2401dc416d19d51" + } + + @Test + fun colorBlur() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(Blur) + mImage.save("fits-color-blur").second shouldBe "0fca440b763de5380fa29de736f3c792" + } + + @Test + fun colorGaussianBlur() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(GaussianBlur(sigma = 5.0, size = 9)) + mImage.save("fits-color-gaussian-blur").second shouldBe "394d1a4f136f15c802dd73004c421d64" + } + + @Test + fun colorStfMidtone01Shadow00Highlight10() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f)) + mImage.save("fits-color-stf-01-00-10").second shouldBe "e952bd263df6fd275b9a80aca554cb4b" + } + + @Test + fun colorStfMidtone09Shadow00Highlight10() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f)) + mImage.save("fits-color-stf-09-00-10").second shouldBe "038809d7612018e2e5c19d5e1f551abd" + } + + @Test + fun colorStfMidtone01Shadow05Highlight10() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f, shadow = 0.5f)) + mImage.save("fits-color-stf-01-05-10").second shouldBe "70e812260f56f8621002327575611f31" + } + + @Test + fun colorStfMidtone09Shadow05Highlight10() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f, shadow = 0.5f)) + mImage.save("fits-color-stf-09-05-10").second shouldBe "6ca400f617f466a9eb02a3a6f2985d99" + } + + @Test + fun colorStfMidtone01Shadow00Highlight05() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f, highlight = 0.5f)) + mImage.save("fits-color-stf-01-00-05").second shouldBe "3cd98ee9a8949d5100295acccd77010b" + } + + @Test + fun colorStfMidtone09Shadow00Highlight05() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f, highlight = 0.5f)) + mImage.save("fits-color-stf-09-00-05").second shouldBe "2cfeffc88c893cc5883d8a2221f29b91" + } + + @Test + fun colorStfMidtone01Shadow04Highlight06() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.1f, 0.4f, 0.6f)) + mImage.save("fits-color-stf-01-04-06").second shouldBe "532a07a1a166eb007c2e40651aec2097" + } + + @Test + fun colorStfMidtone09Shadow04Highlight06() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(ScreenTransformFunction(0.9f, 0.4f, 0.6f)) + mImage.save("fits-color-stf-09-04-06").second shouldBe "eb3d940d9fd2c8814e930715e89897c4" + } + + @Test + fun colorAutoStf() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(AutoScreenTransformFunction) + mImage.save("fits-color-auto-stf").second shouldBe "a9c3657d8597b927607eb438e666d3a0" + } + + @Test + fun colorScnrMaximumMask() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.MAXIMUM_MASK)) + mImage.save("fits-color-scnr-maximum-mask").second shouldBe "e7d2155e18ff1e3172f4e849ae983145" + } + + @Test + fun colorScnrAdditiveMask() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.ADDITIVE_MASK)) + mImage.save("fits-color-scnr-additive-mask").second shouldBe "a458c44cedcda704de16d80053fd87eb" + } + + @Test + fun colorScnrAverageNeutral() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.AVERAGE_NEUTRAL)) + mImage.save("fits-color-scnr-average-neutral").second shouldBe "e07345ffc4982a62301c95c76d3efb35" + } + + @Test + fun colorScnrMaximumNeutral() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.MAXIMUM_NEUTRAL)) + mImage.save("fits-color-scnr-maximum-neutral").second shouldBe "a1d4b04f57b001ba4a996bab0407fd7e" + } + + @Test + fun colorScnrMinimumNeutral() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + mImage.transform(SubtractiveChromaticNoiseReduction(ImageChannel.RED, 1f, ProtectionMethod.MINIMUM_NEUTRAL)) + mImage.save("fits-color-scnr-minimum-neutral").second shouldBe "8b7be57ff38da9c97b35d7888047c0f9" + } + + @Test + fun colorGrayscaleBt709() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + val nImage = mImage.transform(Grayscale.BT709) + nImage.save("fits-color-grayscale-bt709").second shouldBe "cab675aa35390a2d58cd48555d91054f" + } + + @Test + fun colorGrayscaleRmy() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + val nImage = mImage.transform(Grayscale.RMY) + nImage.save("fits-color-grayscale-rmy").second shouldBe "e113627002a4178d1010a2f6246e325f" + } + + @Test + fun colorGrayscaleY() { + val mImage = NGC3344_COLOR_32_FITS.fits().asImage() + val nImage = mImage.transform(Grayscale.Y) + nImage.save("fits-color-grayscale-y").second shouldBe "24dd4a7e0fa9e4be34c53c924a78a940" + } + + @Test + fun colorDebayer() { + val mImage = DEBAYER_FITS.fits().asImage() + val nImage = mImage.transform(AutoScreenTransformFunction) + nImage.save("fits-color-debayer").second shouldBe "86b5bdd67dfd6bbf5495afae4bf2bc04" + } + + @Test + fun colorNoDebayer() { + val mImage = DEBAYER_FITS.fits().asImage(false) + val nImage = mImage.transform(AutoScreenTransformFunction) + nImage.save("fits-color-no-debayer").second shouldBe "958ccea020deec1f0c075042a9ba37c3" } } 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 e13d4d49f..c06331961 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,6 +1,7 @@ package nebulosa.indi.client.device import nebulosa.indi.client.INDIClient +import nebulosa.indi.device.DeviceType import nebulosa.indi.device.gps.GPS import nebulosa.indi.device.gps.GPSCoordinateChanged import nebulosa.indi.device.gps.GPSTimeChanged @@ -17,6 +18,9 @@ internal open class GPSDevice( override val name: String, ) : INDIDevice(), GPS { + override val type + get() = DeviceType.GPS + @Volatile final override var hasGPS = true @Volatile final override var longitude = 0.0 @Volatile final override var latitude = 0.0 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 60757ac91..edce9e775 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,8 @@ import java.io.Closeable interface Device : INDIProtocolHandler, Closeable, Comparable { + val type: DeviceType + val sender: MessageSender val id: String diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceType.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceType.kt new file mode 100644 index 000000000..c3fa6f460 --- /dev/null +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceType.kt @@ -0,0 +1,12 @@ +package nebulosa.indi.device + +enum class DeviceType { + CAMERA, + MOUNT, + WHEEL, + FOCUSER, + ROTATOR, + GPS, + DOME, + SWITCH +} 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 49e72e3b4..3430142b9 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 @@ -2,6 +2,7 @@ package nebulosa.indi.device.camera import nebulosa.image.algorithms.transformation.CfaPattern import nebulosa.image.format.HeaderCard +import nebulosa.indi.device.DeviceType import nebulosa.indi.device.guide.GuideOutput import nebulosa.indi.device.thermometer.Thermometer import nebulosa.indi.protocol.PropertyState @@ -9,6 +10,9 @@ import java.time.Duration interface Camera : GuideOutput, Thermometer { + override val type + get() = DeviceType.CAMERA + val exposuring: Boolean val hasCoolerControl: Boolean diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/dome/Dome.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/dome/Dome.kt index 9331f44f2..e2641eb6e 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/dome/Dome.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/dome/Dome.kt @@ -1,5 +1,10 @@ package nebulosa.indi.device.dome import nebulosa.indi.device.Device +import nebulosa.indi.device.DeviceType -interface Dome : Device +interface Dome : Device { + + override val type + get() = DeviceType.DOME +} 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 90c27c12f..92f9af5ef 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 @@ -1,9 +1,13 @@ package nebulosa.indi.device.filterwheel import nebulosa.indi.device.Device +import nebulosa.indi.device.DeviceType interface FilterWheel : Device { + override val type + get() = DeviceType.WHEEL + val count: Int val position: Int 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 920702192..547a5e513 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 @@ -1,10 +1,14 @@ package nebulosa.indi.device.focuser import nebulosa.indi.device.Device +import nebulosa.indi.device.DeviceType import nebulosa.indi.device.thermometer.Thermometer interface Focuser : Device, Thermometer { + override val type + get() = DeviceType.FOCUSER + val moving: Boolean val position: Int diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt index 847336c4e..b10d4b0d5 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/mount/Mount.kt @@ -1,5 +1,6 @@ package nebulosa.indi.device.mount +import nebulosa.indi.device.DeviceType import nebulosa.indi.device.Parkable import nebulosa.indi.device.gps.GPS import nebulosa.indi.device.guide.GuideOutput @@ -9,6 +10,9 @@ import java.time.OffsetDateTime interface Mount : GuideOutput, GPS, Parkable { + override val type + get() = DeviceType.MOUNT + val slewing: Boolean val tracking: Boolean diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/rotator/Rotator.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/rotator/Rotator.kt index d22861982..a154381d9 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/rotator/Rotator.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/rotator/Rotator.kt @@ -1,9 +1,13 @@ package nebulosa.indi.device.rotator import nebulosa.indi.device.Device +import nebulosa.indi.device.DeviceType interface Rotator : Device { + override val type + get() = DeviceType.ROTATOR + val moving: Boolean val canAbort: Boolean diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Unsafe.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Unsafe.kt index ffcb216f2..b7e91d355 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Unsafe.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Unsafe.kt @@ -2,8 +2,8 @@ package nebulosa.math import kotlin.annotation.AnnotationTarget.* -@RequiresOptIn(level = RequiresOptIn.Level.WARNING) @Retention(AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) @Target(CLASS, ANNOTATION_CLASS, PROPERTY, FIELD, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS) @MustBeDocumented annotation class Unsafe diff --git a/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt index 04a6d68df..cdf0f583f 100644 --- a/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt +++ b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt @@ -4,6 +4,7 @@ import nebulosa.fits.FitsHeader import nebulosa.fits.FitsKeyword import nebulosa.image.format.HeaderCard import nebulosa.image.format.ReadableHeader +import nebulosa.log.debug import nebulosa.log.loggerFor import nebulosa.math.* import nebulosa.wcs.computeCdMatrix @@ -44,11 +45,11 @@ data class PlateSolution( val width = header.getIntOrNull(FitsKeyword.NAXIS1) ?: header.getInt("IMAGEW", 0) val height = header.getIntOrNull(FitsKeyword.NAXIS2) ?: header.getInt("IMAGEH", 0) - LOG.info( - "solution from {}: ORIE={}, SCALE={}, RA={}, DEC={}", - header, crota2.formatSignedDMS(), cdelt2.toArcsec, - crval1.formatHMS(), crval2.formatSignedDMS(), - ) + LOG.debug { + "solution from %s: ORIE=%s, SCALE=%f, RA=%s, DEC=%s".format( + header, crota2.formatSignedDMS(), cdelt2.toArcsec, crval1.formatHMS(), crval2.formatSignedDMS(), + ) + } return PlateSolution( true, crota2, cdelt2, crval1, crval2, abs(cdelt1 * width), abs(cdelt2 * height), diff --git a/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsByteArray.kt b/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsByteArray.kt index e80406a3f..a288d02ac 100644 --- a/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsByteArray.kt +++ b/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsByteArray.kt @@ -1,5 +1,4 @@ package nebulosa.retrofit @Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) annotation class RawAsByteArray diff --git a/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsString.kt b/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsString.kt index 52a40b028..b5fb95376 100644 --- a/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsString.kt +++ b/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RawAsString.kt @@ -1,5 +1,4 @@ package nebulosa.retrofit @Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) annotation class RawAsString diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/LinuxOnly.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/LinuxOnly.kt index 7e8dc5c96..e7a06c76a 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/LinuxOnly.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/LinuxOnly.kt @@ -5,7 +5,6 @@ import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.OS @Test -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) @EnabledOnOs(OS.LINUX) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) annotation class LinuxOnly diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/NonGitHubOnly.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/NonGitHubOnly.kt index c41bf4b14..ec8b329bf 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/NonGitHubOnly.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/NonGitHubOnly.kt @@ -5,6 +5,5 @@ import org.junit.jupiter.api.condition.EnabledIfSystemProperty @Test @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) @EnabledIfSystemProperty(named = "github", matches = "false") annotation class NonGitHubOnly diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/WindowsOnly.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/WindowsOnly.kt index 881f2e8de..4fb102bc7 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/WindowsOnly.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/WindowsOnly.kt @@ -5,7 +5,6 @@ import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.OS @Test -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) @EnabledOnOs(OS.WINDOWS) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) annotation class WindowsOnly