From 510addcd7b3ca343259ffaf7da9da099cfde674f Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 1 Aug 2024 13:55:17 -0300 Subject: [PATCH 1/9] [api][desktop]: Refactor Angular code --- .../nebulosa/api/cameras/CameraSerializer.kt | 2 + .../api/focusers/FocuserSerializer.kt | 1 + .../nebulosa/api/framing/FramingService.kt | 14 +- .../api/guiding/GuideOutputSerializer.kt | 1 + .../nebulosa/api/guiding/GuidingController.kt | 5 - .../nebulosa/api/guiding/GuidingService.kt | 4 - .../kotlin/nebulosa/api/guiding/SettleInfo.kt | 5 - .../nebulosa/api/mounts/MountSerializer.kt | 1 + .../api/rotators/RotatorSerializer.kt | 1 + .../nebulosa/api/wheels/WheelSerializer.kt | 1 + api/src/main/resources/HIPS_SURVEYS.json | 118 ++++ desktop/.prettierignore | 6 + desktop/.vscode/extensions.json | 2 +- desktop/app/local.storage.ts | 55 -- desktop/app/main.ts | 9 +- desktop/app/package-lock.json | 274 +++++++++ desktop/app/package.json | 1 + desktop/app/window.manager.ts | 44 +- desktop/camera.png | Bin 36265 -> 41971 bytes desktop/focuser.png | Bin 13187 -> 13106 bytes desktop/package.json | 4 +- desktop/src/app/about/about.component.html | 161 +----- desktop/src/app/about/about.component.ts | 53 ++ .../app/alignment/alignment.component.html | 66 +-- .../app/alignment/alignment.component.scss | 4 - .../src/app/alignment/alignment.component.ts | 319 +++++------ desktop/src/app/app.component.html | 3 - desktop/src/app/app.component.scss | 23 - desktop/src/app/app.component.ts | 5 +- desktop/src/app/app.module.ts | 6 +- desktop/src/app/atlas/atlas.component.html | 52 +- desktop/src/app/atlas/atlas.component.scss | 78 ++- desktop/src/app/atlas/atlas.component.ts | 58 +- .../app/autofocus/autofocus.component.html | 28 +- .../src/app/autofocus/autofocus.component.ts | 177 +++--- .../app/calculator/calculator.component.html | 1 - .../app/calculator/calculator.component.ts | 6 +- .../calculator/formula/formula.component.html | 2 +- .../calculator/formula/formula.component.ts | 8 +- .../calibration/calibration.component.scss | 42 +- .../app/calibration/calibration.component.ts | 3 +- desktop/src/app/camera/camera.component.html | 130 ++--- desktop/src/app/camera/camera.component.ts | 537 +++++++----------- .../app/camera/exposure-time.component.html | 32 ++ .../src/app/camera/exposure-time.component.ts | 207 +++++++ .../filterwheel/filterwheel.component.html | 8 +- .../app/filterwheel/filterwheel.component.ts | 153 +++-- .../flat-wizard/flat-wizard.component.html | 36 +- .../app/flat-wizard/flat-wizard.component.ts | 163 +++--- .../src/app/focuser/focuser.component.html | 34 +- desktop/src/app/focuser/focuser.component.ts | 102 ++-- .../src/app/framing/framing.component.html | 31 +- desktop/src/app/framing/framing.component.ts | 131 ++--- desktop/src/app/guider/guider.component.html | 132 +++-- desktop/src/app/guider/guider.component.ts | 217 ++++--- desktop/src/app/home/home.component.html | 32 +- desktop/src/app/home/home.component.scss | 6 +- desktop/src/app/home/home.component.ts | 283 ++++----- desktop/src/app/image/image.component.html | 38 +- desktop/src/app/image/image.component.ts | 50 +- desktop/src/app/indi/indi.component.scss | 24 +- desktop/src/app/indi/indi.component.ts | 35 +- .../property/indi-property.component.scss | 26 +- .../indi/property/indi-property.component.ts | 10 +- desktop/src/app/mount/mount.component.html | 80 +-- desktop/src/app/mount/mount.component.ts | 214 +++---- .../src/app/rotator/rotator.component.html | 25 +- desktop/src/app/rotator/rotator.component.ts | 72 +-- .../app/sequencer/sequencer.component.html | 16 +- .../app/sequencer/sequencer.component.scss | 30 +- .../src/app/sequencer/sequencer.component.ts | 57 +- .../src/app/settings/settings.component.html | 140 ++--- .../src/app/settings/settings.component.ts | 151 +++-- .../src/app/stacker/stacker.component.html | 8 +- desktop/src/app/stacker/stacker.component.ts | 68 +-- .../camera-exposure.component.ts | 22 +- .../camera-info/camera-info.component.ts | 10 +- .../device-chooser.component.ts | 44 +- .../device-list-menu.component.scss | 4 +- .../device-list-menu.component.ts | 26 +- .../dialog-menu/dialog-menu.component.html | 4 +- .../dialog-menu/dialog-menu.component.scss | 8 +- .../dialog-menu/dialog-menu.component.ts | 24 +- .../shared/components/map/map.component.html | 4 +- .../shared/components/map/map.component.scss | 4 - .../shared/components/map/map.component.ts | 10 +- .../menu-bar/menu-bar.component.html | 5 +- .../menu-bar/menu-bar.component.scss | 0 .../components/menu-bar/menu-bar.component.ts | 3 +- .../menu-item/menu-item.component.ts | 2 +- .../shared/components/moon/moon.component.ts | 14 +- .../path-chooser/path-chooser.component.scss | 0 .../path-chooser/path-chooser.component.ts | 35 +- .../slide-menu/slide-menu.component.ts | 2 +- .../dialogs/confirm/confirm.dialog.scss | 0 .../shared/dialogs/confirm/confirm.dialog.ts | 3 +- .../dialogs/location/location.dialog.html | 8 +- .../dialogs/location/location.dialog.scss | 0 .../dialogs/location/location.dialog.ts | 5 +- ...lable.ts => spinnable-number.directive.ts} | 4 +- .../directives/stop-propagation.directive.ts | 6 +- .../interceptors/location.interceptor.ts | 4 +- .../src/shared/pipes/dropdown-options.pipe.ts | 4 + desktop/src/shared/pipes/enum.pipe.ts | 7 +- desktop/src/shared/services/api.service.ts | 33 +- .../shared/services/browser-window.service.ts | 14 +- .../src/shared/services/electron.service.ts | 8 +- desktop/src/shared/services/pinger.service.ts | 37 -- .../src/shared/services/preference.service.ts | 116 ++-- desktop/src/shared/services/ticker.service.ts | 37 ++ desktop/src/shared/types/about.types.ts | 13 + desktop/src/shared/types/alignment.types.ts | 116 +++- desktop/src/shared/types/app.types.ts | 11 - desktop/src/shared/types/atlas.types.ts | 10 +- desktop/src/shared/types/autofocus.type.ts | 72 ++- desktop/src/shared/types/camera.types.ts | 474 ++++++++++------ desktop/src/shared/types/device.types.ts | 1 + desktop/src/shared/types/flat-wizard.types.ts | 43 +- desktop/src/shared/types/focuser.types.ts | 23 +- desktop/src/shared/types/framing.types.ts | 42 ++ desktop/src/shared/types/guider.types.ts | 131 ++++- desktop/src/shared/types/home.types.ts | 57 +- desktop/src/shared/types/image.types.ts | 4 +- desktop/src/shared/types/mount.types.ts | 80 +-- desktop/src/shared/types/platesolver.types.ts | 35 +- desktop/src/shared/types/rotator.types.ts | 19 +- desktop/src/shared/types/sequencer.types.ts | 58 +- desktop/src/shared/types/settings.types.ts | 86 ++- desktop/src/shared/types/stacker.types.ts | 79 ++- .../src/shared/types/stardetector.types.ts | 36 +- desktop/src/shared/types/wheel.types.ts | 57 +- desktop/src/styles.scss | 8 +- desktop/tsconfig.json | 4 +- desktop/tsconfig.serve.json | 3 +- .../nebulosa/hips2fits/Hips2FitsService.kt | 1 + .../nebulosa/indi/client/device/GPSDevice.kt | 4 + .../kotlin/nebulosa/indi/device/Device.kt | 2 + .../kotlin/nebulosa/indi/device/DeviceType.kt | 12 + .../nebulosa/indi/device/camera/Camera.kt | 4 + .../kotlin/nebulosa/indi/device/dome/Dome.kt | 7 +- .../indi/device/filterwheel/FilterWheel.kt | 4 + .../nebulosa/indi/device/focuser/Focuser.kt | 4 + .../nebulosa/indi/device/mount/Mount.kt | 4 + .../nebulosa/indi/device/rotator/Rotator.kt | 4 + .../nebulosa/platesolver/PlateSolution.kt | 11 +- 145 files changed, 3844 insertions(+), 3103 deletions(-) create mode 100644 api/src/main/resources/HIPS_SURVEYS.json create mode 100644 desktop/.prettierignore delete mode 100644 desktop/app/local.storage.ts delete mode 100644 desktop/src/app/alignment/alignment.component.scss delete mode 100644 desktop/src/app/app.component.scss create mode 100644 desktop/src/app/camera/exposure-time.component.html create mode 100644 desktop/src/app/camera/exposure-time.component.ts delete mode 100644 desktop/src/shared/components/menu-bar/menu-bar.component.scss delete mode 100644 desktop/src/shared/components/path-chooser/path-chooser.component.scss delete mode 100644 desktop/src/shared/dialogs/confirm/confirm.dialog.scss delete mode 100644 desktop/src/shared/dialogs/location/location.dialog.scss rename desktop/src/shared/directives/{input-number-scrollable.ts => spinnable-number.directive.ts} (83%) delete mode 100644 desktop/src/shared/services/pinger.service.ts create mode 100644 desktop/src/shared/services/ticker.service.ts create mode 100644 desktop/src/shared/types/about.types.ts create mode 100644 nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/DeviceType.kt 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/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/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/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..59c2ea254 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', () => { diff --git a/desktop/app/package-lock.json b/desktop/app/package-lock.json index 70ead49e0..cf0062c14 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.17.1" } }, @@ -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.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", diff --git a/desktop/app/package.json b/desktop/app/package.json index cd83ee94f..ba7fc32bc 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.17.1" } } diff --git a/desktop/app/window.manager.ts b/desktop/app/window.manager.ts index 418fd8df5..9959d0081 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}`) @@ -108,12 +115,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 +136,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 +147,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 +205,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 +265,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 { diff --git a/desktop/camera.png b/desktop/camera.png index 8cff6758022f0547a254b8c16033f0c6895fa8f2..0d9668ac1546572d1fa3b193ebd96c89fd043488 100644 GIT binary patch literal 41971 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 z zjvJPPaIYWC8#hTLNJ4${C0V@|Sxnfv16Y-HtDx|>?=6%YhlPZmp_E`w)cS=ffD}mV-Qh3JMA@`wuk>=PHcXszSd5e9#hHHMWSLmI`{PunA(w5)~ahR4Up;6$s zZ*^MkY-mJ&6TAESRqA85t(3dX9&OG1@amlLhwmMMEO#o-dt70(D+u~MYx~&Q^mo*? zC{Sz?>4XtMo_zYXI+&9U8;d5(W9Qdkj|LD-|jZ3P*F-&gS; z5}pjc5?;Im-;@E&BPpffo?+rXA0{BudlXPKaMa&` znMX`etu896sFRF8DahU0-&~HthQAu>r2SxRXEYvY#1NR!P(z7>+KZOII+j z#8!evKLQ14j_1ZYBh?j%s`*h_6JWg6?_sJfTV^oOn3?Z1(Q`HeIRAl~?~UWQu9lXT z)d_p7+{cnL?fYG+si{#lDk7R5<>u~3*I`)fkg8PQ+-mksLrsLyHk{Xp7_}1SE#O&} zVxM5F`V@5>YMG^(T9%@_Gwe;0D<^u39TVW|fic!I?3$s*D6ruViiwF4uO^%!Ko962 z9sTr}ulyr^dFjpNP59-Bi*N2>QWAs^6|a?gvFJ8hPJNRg2t^4MB{80c(B+ipdhim< zhYuejFtue2e1tt7gUb9arv??c*9NCfKqlX$VC%%3flBXi9ma zegW)T)-{edfWv`+RohV*K4=USgNJ=KnA^}`!UrR69~ve?;e?44M)ofVJ=C8+JJvIx zkA(0+dz|U{bLRR-g8JlyH%3eRSu!FB;{Frww!Xet0lEn@%3FXQ1884rQ7xj}hs}jA zieG3QhtYangF(tFu-;RrPW}EYK20Py$W*LXOG})l33>>I)u<4!zrVjFfJG_>=mo)) zU%L(V#T7_m@r0j1xPs^HQG938UzKwJ^VZ=&(eLj-%Eo8q-kH<~Xc-~pDS22S;9YO; znpZbH#SgE7^I)o=P$Q@--^*O>fB%p{GbIP=28Jz}chVgo2St9Sz%U;30N6a%fb2`m zN&zE>!t4Xgg&l;t4_OpLHdDklxFZk)DNIqo)P4b}Cvm$`ynk$HxN+-Sqmj+vbWhPq z!W0E>*i+`JC)HvEtp;?5vg{$h$FnPL113b*aS^j3Fl9;?x~!r1<;Or5@XBNt=`d_U zdP|&6Uj0Z5J`s?5b67;o^Qu^z=PAsIJ59ZD7}a{tYg7j!4%t_dHGQzepRIU$%VA9r*b4M+>$9;Nw+8F>J=(=`9->IlSdg!w#F1824e)RV=L$vt zJ%58shzoA$;0-H0n82P#KLHLrtU*Lp)+3)H41ivdVv+C{?ej5S-l0!iu~&h`-RRB_ zgwO&~gp1=M=!%FTC(1p@=IkcE3Oo%BwN_=e(F{F*RM@hWBZ&D-9co{+#f0z=qHeq^ z@EzPrT1Lk0(zti%XFlMuL&JmT5+Q2Kj62h;r-@*d=B@z321fN6#$^~EM=_=cqxT#D z_oN?PM_a{nFkYb25`3}aU%k+=W5?#k+No1U7&Q7Osb$^8PMspqHXk1YxM%iArl6Xr%@6 z%@X?|avs`q@ZO2Q=>cW{_f8U;Otb=E;Y37ax06h~PKs*+xW_N4v(=;{QMq*!Ka1I{a z?_Z(nf4*Ih1zlv`CK4Y8Mh3Wa*sHETl(6Y8LuN%HIc5%ucG#S-4|FwGP-5i{^Zyje zrRTbabVWwo&y+%TM0xWtbKKR7Hf6z*%PDnz8<;kw^&SRjKw96hwmy{6qvHe|j0{EB zNcslkroT|h>U1ojXil?9&Yz^ViJGF&jWre}ggnTAgqIX1eb}{P&`+bYgLwTJg8XQB zUy{41L>!n@pzJ)-eP*mRY&wmeH38A0|odQN8+kp=etU8P{s>(zm z^g0`-G2*Irz(w#&^y_i|H5AF%1XrL=Mt7VGs0T5HnDPVaQQ!_E^q%0l0APTMQUGzX zW2p#)$sG**_mDu|T6gk#*WKJ+=+NZcB zUNWFJHiNMmQkFMnlz&iMKz5#1D&;t)4Q}Ks&;99d(UG;{pVP6HY2zIk0@)9aUjQr& z`0s9+l*vy~$BBb=7Ym9Sd8WcfZ~BOghmNp2Zyl0eY*uYwQWdlA+AJNY3J+A zBfH`TakD>w=3${{#$$Js+ARDV;_K0F%cJ(bWvTU-)4m?eq<^l+EUSVcDmH>!0;?uzLzqDL|a3&eBlzNldX5(W!nphDaI z$Ou%Y&GJC;kQ$f0Xde6fzsbo-wYwwzzL03A2;riQjJ{Y4CHq!2%nI}lbi`+5s2c93 z&h#h#GN5ZLcf0KPRgrD45pUfc@T@@u1IsIHx3YC1#8a~gphyHmY^p`N=!TgT`?vRW7jM8KInLNVvjGZOd!QaJqMVp7GL3F>D`=lj~Sxf27LW* zuE<1BQ7HBp+U-UHGsc(uy|8c%Ssn4u9w-AP&7qG^kEo>WG>~wav$)n6`4GG#dR{|> zL`har(ayaknPAxH*<_79q^AFaiaGSjlXsY)$|8`Xv%ZYtmLLs)pA|wN5`a3eVv10B zq7LN*cax}Lk%^Jyq_13w`(`{3>Jq_bVfI|LLySbSB)S6dDqC{Q&KbK-3a_rMp>m-C zeQ*%pqI9lRg{VIgwfw;R1w-YZ68`z%FjSih@TPN{yhl&81I3vNmXR{nJB#RP;&C~v+{2O`+!jq zFRp&5n%<-eI4jbV=;Y`2f4G<+f*~4Q2X>?Ly~J*S5?wJY3h0=b&m)oIH_3>ttCO~# zX1Fa4(c<*L9Ed9c->3$>ZK4VR3RsEQDDOId*%ORL2C>^5gkd6#pWy!Z3@O13NE{vr zL0m>O1#$2#91vLeU8D$bRHoE*BlAD*sGO8?UHnI0zfNnV5RVVm4WB0}yU7 zc#?JK!wD-^4FS?H;_`trE<&Db<0db}>^W`A=J(@GvhpV|W6@02hDpJ|*qA@azA)4y z`Vka8PXN(^PnMP`3SH!h6DNFqeaX0ol=*$)P7qB#z!|cmM~|i~tRsC8xCJuRQR02E zAs;+^2*UkcWNa^~g(IL8gWKS-GAT-i=J49{NhvZs>H)xGoqgp%f*3G;$M@?W8Tp9E z0u=iPmhNRxbU>yidlDU;DRm$gfe1hRdUd6_1LzX+*t}7g|ij85q{!J_hu?iZ6xL@@9tzQHJ1F%xw<0IS;6{r^2(MV{!1d z7z%d;sv9?M-1B$Gh9vlQIxhcE{CI}^(@{j#ExwBClyQnWw-|HNFgB9X?~7v|t!e_E zG{HGX;j(5UkCFpj*eQpRCzrnK-{5IrTexONoeuXLOjjT)$o?@fAcv)n)R4KD_ZSrd zp4kklhW_EB_dJQ^n`H=KyC^bHUcoeJ4xz9)bB?S zMTtUeH3 zv`z%%MpSMn0#cJ>J~TE45*q?OEb!zb17?UP(42aK%Vlw@5kR$Csgq47#%(CD34bJf z_z&19#7esR1_kZ^l;HP5ZlW=slAr~nr*|R-VHgO>BNAg#4Wix4UKG*I)UQUR^9@cC z2rgtP9haTgmK{N+sv&kC;h2c`M+Gi4R?c96DC<1ea?Tm+sK|Xq1X6o*IMNXK^(uOX zU#PSOan~o=LvHx)I_#+ zU{BYuhA7@0O$@i_D?bA^5E?_@bXFM)PAn;mh%V44s0o@+7^v;6(w5)(dhlc(C3~21 zcU3dTeAyn62Ps}{pL_a^Uu03XiZm_Xzf57n<}7q%M!VdS;p7wn0(s@>+e8&HiinW( zGpF}n*}B`QQb=UVv%{(jIR0LQYp`s<>ayaPM}E^xZwFPz+crusDA(6@jp6W=Fo7U3 zDlM{FTep8*8B~*@x83dZWlzC>f=+psnM>PsX(J(RM_JmM_>Nz;FkbxY?sE@`>bsyp&U>0wGRr+9lHkE>GSjdvJLy+K~_Se zm;G_!soGsXhGo$|G6p88IZ}ym;@bFyrfOQBS4dr`HBQcSr&8119HPka@#}R&!M^T&S0*XyZ2EC zVDoppve`;X=edRf%?E%i%x7G1+1Ej^KjpD{*E^rBEh|3)b?mN8T^01}n5k`nT?iR^ zIdHxUZ2PWZZ;)tG1)+l`nBxi05=nwJ>&&3b1EdkPh}JW9eu!#9tXyF;p0{THr}VIUaeL(nl`vMg+IPXSrwo(BJ%-CBMeTj$2dQN0|p`~2*ua%m=N<3Z2vuuCnDPP zbTD8Xr|IuU&}nL#TcxT#G&KexfE;)fHHdEa{IA?bUQ)PyTM!01n+R)4#JK@{ZE0!g+-Vh+FJ-Py zz!Z>U-*tB%yaxmebmTe|KE#uq?AEJr!$*#Gk&eTXL0HuGd&b*L*ju4MMpwdzhs+K= z5_&xy6=oYhr9A{Dh}`@RBLE*}mydhT@>4hVj-Y<4#l!_{X^t~z%Dzc<_6RxkO!t-$ z;&SM-Kntb80stgb2y3yhK`a(vL#!Z21L9*W2t(zFAv1P+dzYF%IrZbAXo`8v0Nob8 zT_^roHv8V&%k-~#Gh|>@@88Q*l%l`DjYp1+#Z-E29MK6;8qI)w&vDz%uLs*2cNrbz z;^HFI4gkSMzvdhU{Kd}37S~}+J*2yDZe~VcYynFWP(>iKZfUt4_pxKCop0Hv<}bva z>Me5($DXsB9a2M1M-GGYQy}^^xC>{S@QRBc1p5&nNuVm{X6_-wg8Y0;0lP*9)A-D4 zj1yWkU`++JA8OE&@h5!i^PhX!Az7*n+Tf@xIEz& z0Wx7OW7MxR<4QEj1#>H@4% z**Fb`(!k}_ClAshPfjgP_niaG163N_y#+DYZSQ)uFx^MkwqQiry6Ys$nqY5lxy(Qk zPvAfO_KzPwnt!~$c**^F~fl~9Rd)(=8BG*=r7=F^8z=uvuF)PY%59$ zt1`vLkevSY>rcU6v5uQLnY*^+^Sb1|RYq6ZpCr?X<5!rF`S0Axml@X$D)V)%j4>lUwD7|33DY3+cr5@M(;sP)j{jf!JD zCFV(0bE8r~pM1&wU6o(P6-Ik@I=}8)LwlXfERXfY#x*GmRTn8F14l>QBI+VOw93RV zdmxN+{9${dIMue;n$@iS?iHbTlK*PR(7k1zMUj733e+xzwC?e2IvIz<0HSVyvtjs=@ z#MXcFbW%{+-{*l1sI&+o$NH-C&7Q*dh*KDxbDcQRaAPW@K;hpn9inv&kq&48P>1LM zsF+gQfe!Q)4)`1fTD{pf#@T<95id2P$+3MqENz8EO`yx53ZsRA1%iViUuM_)qWoZeq!5<<~JVwy+80Ib|wQ3FAw6yet_anZ2yFxTd**x1mX6#1fImB~L zDJd3|m8Ts>?qLa~YX3`R`u^Lu0}u#eUW6sIOn2_W$%3z)TvGrkQO9DiSOlz`G^?tX z^Q?XlX$u3phx(bEl9I+7(p=hk!uW)kl#~?8@Ncw3`xomRjJ%>ql>IQ_MO+6UwtVH_ zgQVntoND_K_j^j63E%4r^r?+oX=oH+0&Zz}Y4QwAf`DG*<-i3;D(E4@0V?Yk6oeP_ zPfWn9oDHT)Xab?A^+x}h8+^R|>EETLE9T}p17UYD zR9;PMhC-cW$eaC#>3LX~p9tmV_WkwHyRD6fRqVp9M&w{TvwF_@YO8{SCevPuHyEl1 zZ>OeZU{HiK0W2)UkRSd1S_3Zqi)IpGW|%to0X^>H*|v2nChecJ&z`i#{Q!-1POlu4 z-I{N06a(ppoKk$tigrH#s{3j1HZDWj&8>7@oIqK*xTsJX0IP#wCZ&ChyDtur>F*jt zO4h~Gr%wFAsSl!E0T_H zjZlw=ou8i%W{*E)$VlYQBXK@})nyT2O!$r+Ybn^(1%G3$RE_lZix)3m#-c{r@95C= zTv4%z=rXl5H$RJs1P})p`;uuujR&xWfEm!uCLt&w)J5Li)o?CXL>B29QVMxuDAIfP zVUiv=4`|MDrJYU#`&2<$-wa}>dwfdI@hP;O7*a>!AO;dvW|_*hZL0WY_PfdrOo28E>#OyJXGJFq3iYl%9hUAQe<-VI7VFt!USvsnT^Jq~c0j@}tjJQy zF1&s)GwpcRKUmmdYZUlMyV-)4j}f!r2_`Zo&nv6Lb8HHGHghOnQN`a?SNFN*@MckA zw84t33U|;@0R=yRQ77cCAlM@8umBG@=pLn!675@J>01~Tgg4Lg64G#=QbDVM_Aaob zL>%NZY-jygg)G%PueS45y$;NKaqTpX-IT4Z5d#9*3F$vSzKo6zmmc7{XM5Zv4Oyw| zITWzKfN$YLQHYI^&O<06V{S#ALvJSIZbU(p-dN; z%P;bE9bm|H$H@95fvrjS-SExV$`mDvc#x|SQ&K*@a;z?@xfST=sur-!QLLNZzDtIu z{MRyidTd_du{7zb;-B(8YI6Tk0%#@_xB1(vRWcl(^1PO3j5Ce4%67BH$kSu$&3N|z z$7oz={N>2~=13*NnZPo4mu-Bz6;wAi%YHX5R2f$p5nQOFh`E9<8aT|%Ui`^z^h#Z* z$gE`wIK|@2nrH49t+=1pknk)&Aaa8t`m|#|mZ!s1&kMp7?B|Dv`US@zD#1YDJSI}U zetuWbiUWMI#|Q-a9&5vL9^;*z`&V$<1fggWaQl19Wq#}|wlXkHv@U4*b%w=kfhnQ6 zr$tl+NQ)fEAuLR)SrX|o?Qxv-@@4d8Wq9wMCA$ckD~eEv+)ZKr2yQGfM?c*OVgfiA z_pse*_wKEFbEWbWM3x{jQijEm(yS}8jmzr5>cB}Q*Ln)=5GLQC*QN?ua{|wZ+2ijO ztf2#t|D*OdMECPPwyz5;2^kAY%tFAzMtg}ZNHBX*X%HA5>StnHMF{)QM+3YV=|q9e zNU-YQ9*!0smG!j`Wfsnui!LoKA-2F37zj4Igv3jLOoa6xmI{HVK+fW}Hs86tbOjX* zK}4wX-R&^h6^5WtP^7QmPJC}6Z_Kvd;=~eo&BIhci?o=H8XhBIz~6G5p*qfdfN0R- z01pW~z1PWrAn*V~P=>tyl+%^a$BgjM({-N4gM?HO_MWN^4h|>73eAb`IPyJ8 zGlH0fX-NQ+cz)Lkn0Kg?S-=A|hv_O?|%-&103E;*eJsa z3=ogLnOsN7ba_4McTDXv-cEs0`3ZLcTFdn>FRS-=J&}riW`!yCL%50FE-IB-Y$$OY ze+1~2I2QtvMRo%11L{fQ&9?3lN+KATc{X8bj|;>1SGGUc%+V*+mtv!o*h*FN{lf=x z)FDIy>-h>rDMtV4orX#_$ELzwxRVo9I`Fj30NM6DawXx` zCjODD9Zw3K@TAd?Y5mJ(x$B1bBY}Qny4={1H|nvHAY1#ZOD;WtDJ3GiGY%pF|Aep< zBxb5S2KoFLEs=pY2daex;aAGyKDQgWk+4~X)!P*_vr23@d^a=DfH7X40jV?!%wp*2 zf&tZ__vM76CJobRTIlZqn4(7iN$NDlg2Jw*0hnvnpR#r6m89OgOfOtYa9|dF9*;!F<%A6ul2ykcjn!-WPTsBkF(Q21_WU z3IA%K#)L&Mlvdl{Ay*P>4uuIZ*#YeB1LxgM1cJB@_0vgA`C{Dud`jxGd_U2?xcy>E zbnx{8%Iw4a-|s%EDigWpNnH;T1z|%0b_H;Wvv=>FLUEa8_YD(q6q;ydz&ay!trV}% zVmMMdQ*#hk5CZNtFn=~O>}rtHH;=fMqJSgtOfpzI29|isK7QS9RqZ#S{?R@9a^ca@j-Hig3nDpl4Yyz=arXnLWl#UDo zGjtK{g=ZkKgeBx(a7bAGvWB0B&nrCKh?7JBlL_4prihzYD)$VT_Vft*SXtkFL)<$x zLU7@l$>t>tZ3qS~8tJo~p)=sp5m+YR%27}j(d;anbDe+biy1vut6ZvfN8uTOfT)8A zRt{F{yj8#|Kxcv>j15QxOK^!2CSzIi9A^A#zqwxB;_iBWu$9z1A-VT`;W>KNu*kL1 z#A*!Q8q48-)1Y&~p;zdfqwvu8Lid2RT3x|~mL-SZEYe`9buUzShn9)bv6z6wo9<|I zkIyrr`NtpMLOl!BiJg9~<~&J+gU)Os_b0Q~kudxfnYP56g^9xUiw5EvTKX3Kt)!bn zv@83g>&CxCa8A{3PZ9zUL^!RW;vq&E;L#`XKGmGxdcb5-_#x@A*`spn`>m^SD7}g2 z2+fTBWS6ky-yfUdSgurm^#<{7mo~RJ4yLcFcZ!z&ky8lU*d}W~3zQ4Khe$}Xgf#&& z&`;FnW2U5S#5jc*aR9=_st^EjgI++#N>iBDh&^SsFY>AP6e-e*C0>DDh(1%d{(7u? z(lkv>XsEYr94o9RtBzQ&!G0Z`t`k`yj9DJD$Tb*}A}Ib0za}hjb8@aALlMCe9S!6< z$R#MP33qaI<9Hm)uj9t$c%LeM$+>l_dg`aY>s#}p1AC3L@A`hpX=32m@JukCdeO^y z+{7M|>rSZ)dtqa$oX9RL++LMvbr78vm9|8|KIjsVQEdD7kk0e4dug9%E@s<7$V5O} z4U~;bd7B{{_X_Bp!&sXikck}d5kLXIFVEi6WO9P|>_0R$(_!kziabN(4{9z`^b1Vs zT;1+^;>_kQRq__p-DcpMmqMzZ#H$=dxRHPWv0roCnjIhZj^z{RqWth9PzYvTd*_Jn zh|$K6kaNsGwdRgMfmF_xnMHxjr_`j#B_N!!qP9rlJs%~A7yuD25bfa9$E{W`+z&6u9{f-naNt7rFU?C}xZOpa zo^~TS>yab65%SICqkZ6RjNXa!sQV-M>5P&6&V z$|~l)8hfs^fpJH}L%~3}SHmai#+YdyV0$$|$K72NxfKt$c;q!d{~JTH_AyE}-d)8~G7jlofuNY6HA~9~2Wa zoZ_AG=bT#g(%S=-*{w)juYPHEH~*G(JZpEoNHE<+ZD4GpvzsDucVwh0=P+L|kNI@V zz8lL+I?C|bm(u^i zpN&i6)rz_@_BG0#JXK-`&H->p*rNzvbN$SG8TGjEXV0Dhdi{NUyQ~2o6GO{}cb}QPBccUW&`WjuSH991CAofRjpLQV;U-EP@PW4s`7y$0saw8(zk{w>u7G?tt7|4 zr}y2p0A`c!y|5Y;S28F?(F{K@tLHo)0h@d}B;sF){}7M3^!0@+N^w1P+S(A2NZHai9ptIc$4^(`A%$beKWfc{+78rXy~m2I8fW}Q*2A7Z9}7JzC5X z9J|0z9i8)R0|8~T$f^0dao;Lj=4C@3Zm*HHXFKZZtz$f8IcP8pN@`)J z^9@p@LI0L?__QW*oD8L9E2D2#w~c|jsdlF?pOvzWoIUe&|JSs-L!X6hX2dPuG7?h5 z4)L_i-2xWkkt-$pYdcsPj_e=O3L0v=D*UAD5d%?%W60jT;SQV`!oEK3>dWZDbdp|U zbYqjdtBE&E+1ln8y}nQJqqbYOZe2$9gUkX(Z?&lZ3SsdNi;A9}2x05}v2K5q%2+1$ z)#^^>gexZ!Dj@U~06PfEKFV06MZ)z4xK{%Le}tgr>-#j@jd)YkS`%HQ#~4@f!#fU5GNg6{Sx84xJyA<%sT3U{ z>P{S3^Ap}(M=*`WDNulDm>?#Gm3!LkETVM-9Cq-=AiU5)jtLqGhC73x#(-iVVsK*1 zEGL)=NM>JQKZVIDDpFWUZ-Zi-Gyq`k9?q)y?z!PkxctMG0gw3V_3N{syF7NbWK2OZ zWLWM-u%5v&Iv{v;D=_3ZxJE$t1xe$5_$H8n2P~1Un`sKu7VJ4frilR*F);tHi4>Gy z4ES5E)Mu#u33)W#bfV>p*YwY!NQS>3iqvKxHe{elf6cR`!%&Ryc|n;eh*o~hq69r5 zA;|$h3V;jY_5%h2tlK01r^xYQrn2~dsTN@f^?y@hw@RCu#+OrSb`M{=coD}ReF6cA zCCjkr1B7{Ej$?b#M;#KsqY9)$-0v;B_t!feEP@7K}(<|jGQ{NO=>aG`yazD z%uDV6Ma}JY`$KlPLz=ObIvy^6cnk7>5bpaNTfqb%k*_s#0FhmF_;k z%ey3kK;Z+2Kqgc7aCm(brWK#GvR-LtT0L*#=H~wJ`SbaWg3L^t=z=#8CK9|#;v65$ z@UWO?&@y7gBaP+?jT%_+5%ceINtS7a2M05f#@#hcFhv6$g|Y7-JlH{KBAmWJTk2eP zK8;O>bn@RclVQI$?W1}+HH+6H!aGcS-Hqd(bkmQVJUQ#Nk_^gEof+JWh?5mj;$afA zshNwn_sO68OJA4@itWDa=v6Sl= zGkfm4UcK7;Ybg3cPqBU7kIA;-`}5!<6M{AnZhs67Jwnt#w+4`?z0eJriZJm5v+OcD zU+|zGgC7K1{}MQ!$X7oQ6qm(W0Js8-?=0jobK@FfSd=78+S z96T46Uj4s{Dy@4xU~Kk3beSxtY-|Rb>TxE-e-U2Ym$Cp>!Ld#TFgh^go6VBcN#+*W zFP4o@O@7TG*ct*D!ehQj_=o{=5@tS^868@->->UbVac6Wg&eUfqk4=O=hdu%f{RlZ zh$AzQ@4kW}21tj*`2#;OE3)AHfkp0B8|hdd^B|Zn4 zd{3|q9xe=bdD!d2-fY46J&8l;Wvbb~NS`HkD-0_^9#w@usYg2PxtoGa)UKUp7xy%J zpzuYc##?aTKOi(RyQkx_}O$1oKzT3b^oD~~ffowqoISB-cc-+KSO z%Y%UIg~$(GAp&xJ!ZMa!`|W+*b33F7EerA=8Pr2KXi5okmh|HrI9HX70hU`>Qp5>~ zI2a)oc+Ob30nFim#R%V&0^-+c?3!6;4J=zHU3X?AA((vMF3v|jJ_HE^uO>SF%KhD=7{J(S1Y0SBf|j{oB!fHzafwlG(S~?kGk(( zpBn)k;`g6FX)|ST49vayX%8fam0(ZIEG%Soig@3@59x8|MlWJMw|eGzWNO4a0iZJ| zZEN8|V{nA+48)wmLE=J4*3f@n#0`MvM-sHQS9s}sug-f#0B+#!UkZ~qc>(kfN)>J# zEo(XSwEYH1pP;g%CYj4#T3Pu5HaxQ3dyME_0Q(>#PE!IwoH#E5XPPZ9Oj@Uz7Tm=w z2X0Y>p)2fH>ror{;6Pra#p^h91xJKEh6P@mivV|9uoB%zG?C6@smWu8oCuqUw^;r6 zTx@2+2>}i|wn0Np{biR8rhlL0;Gza)>3swwunY)EQ@S4C+8+#eQtYm_fuc-%;D8^p zFbM1bokq^>*&i7UQ(L5cK$ykKle;ll{%<-_Kjn+com#+kL7X_}>@0@95Y6U+ zf1P51hq&nnD9h~&>M2slQ^d%ybZBTu0WJlI^(oqEr)#0Kt$n?XIjxV7F zBs7+!M#y4t@(HDOPu3sAD7-xxOb0NmykOn~HoXv5%e?3Gw_G34>QHKHo0q`nilDd` z$27ZPyH^95Bo6Naeoevs=nJv3;Hd{**&_r$y70Gmz!SrOVsryPt@-?!=AmpHi*Ig9 zilQNt=zU=Hu)sJ)ShpeeeQ0UP(eDA}dG3PP)<*(MYFHNqf_D8?r)$4heQe%>3IV7% zxCgUl zpOKFF2{Rle@ROic6GC}81Qk`bo!IVy8UbhWBF_y;rL+!~bJIvt%BLMTo%l}qZeZV6fu zS|{jg3I9j@;9z0${TwFno;Dy}BghUs?9ODUB#AR{;4cB^TtJR6Cx(329P%dVv$kvk zC5bLgA%HmYc9M67Rgl}sN!mgeR)(D%9bi=|*Fogzc)lf8R4_Zw*M9jKGkG_)t{G#b2e z@o@2vss6EYuB>so&D+N$zr1CSXHepPe}snr-LFHg*G?xdo|CKO_imWm!T)aBo@SU| z$1(lpT3U&R<8Ow!ZG!44Ze$zI<5| zT)r$Ud- z>%T6X7J7g^zO=HwzKZyQ|C>KLvtU1U=EzWwwbFWfWlCcXS21_(gX~1FI;Y{5!m+jA zc^BMcgB46~n>y~@NN;_6_j0p_X;c~t*o^d_e= z^ie@pzz~b1%s%rPiDgsmp+Y*1l9->?mX~V-G=EkE=QSmVW>sgJ})Y(ZA^14 zur?zo`EBJu#j+IdwAP=k6y8S%&2QIg<0Jm^tvkl2Z#v{nuRQx{Q|RfK zI+|c7y{G3MH`Uu5?07rUFf;P(kJY$YqG&!Ph_k*Gi7`Vg;c?9)iqSb_avK7F4GyL{ z+fFq-w+|o69bfRWSr>Mnr74*{6ky+^P`Wd0qS&1^#{W#Ae?jhekPJl+&7r07qi%Di z+ph(^9*R!hMoHsbZ_2OODBkpDB}T$OrkGq^O@Pdi<&66KhTYHBmlVi@hJN=4t&)g0 z!oPa`z3iKk&hr`Y+f$U>Wcm30(txGm5+|8j-EM%&hzOOy)*6FYLvZyKfh%=z3SgPTQ>A`ATksrG zDUx;O1O?Ep~j^F5A-Iq^d30Z5a z&``K!VOX@)=@ZRZc!g}uL^XxC4C6vxuQ{9&pexAY;d2?!`l;AY=Wk7J z3VloIN+r|#$Q+KXeW>M_dRtPG=j=3S9I5NxJn!PfI@MajANaCua(=Ir{mwuWo}j{2 zuYp#koyYh0e=YNxyY!f!=1j1EMXyTp@RoNjQ;t9Hj^{oipA1%dJ$B~iR99ZmifHzA zHTRvH8}>``@+EZ*-pK7KTcez*cCNTA?{MP6m)xcPQj=TG%K8QQ?k8}whZ4)godrdM zxo-QMv>n#vj;4t@Ab9^!Lh88!Qd(Bzw!W-WW4D<+2VBC9J0Cipve6Qu@YXJ0->Mw# zb+4Z0UY*6x=(eEd=`@Wr4TFK10+E)&KWcljUOW`6yDxc?Uj6JQ<+9x*;Z1r!_wUu; zTpaNJ)SYauAC7dcqtQp7apQ3%4JL=lt4C1YleFg_&D|fTq)Cb({s%O4GdJlwWlUz z^+nwLORkD9*raPPCTu#oHoKFfcaDnOShU(cyV5}Kd{>#LTxf(+b$!Kqox&|&J}Pna zNd)&hSCCiGKjhu+Uc;)baNI7uiGuaVjq03PSHWw13gMEz5sz=i_uDS+5g*lMZR3d# zczZ}oK-kLKt)Rb!&T4(;#PUmtiKvPXZbRl5YYabpr{a1tI}tAAmj7gk#1gN?Nj1(U*1sPnlM&h4XZeE?ZdqG z>RQ3q+0HnnkJ3!himf}VW}MkwU1=byH9d37NPqw09Mij(tBn~MSyx|G*HKs2YK-3g z^KNM8*HY%B>kES&6DK3eJ%o8HVwIDS_8 zWX(;#3WNlNd7h1AFz(dLUWueX7hrDVzSDZ#`i-;1MUwuXc(Y5onl&WL7dsvYncrOB zUw&$uewEFWI!5}Q^%ud)mD!4iC6~0ce036js>_|5{kjlqvEbr-x!z@}?m+XEGIuXB z$}sCb-Ap6R}8S)z^4^?;GGl*C}kQBO%ZHYK_kAi@-+gax!$n+(jh1z^)4t`%aP#V#l;x@$+oo{u03Uetx$j$G zvX&k9Q%P*f<9ZXbqSA2Ph}k%{?>ohN*0i;?VJrI+GO^+J#2xk(J1s2jZ8#)Tc|Du_ z2XD6;@(%vf%YM3|WZyy35gN4{f>zcN$DB5HZ71c|kMjo>ZmrE?xizu8$;ir6Lg%bd z_S%!VZ;v&{5_Mx7|L$%n-$el+obIrd< zyx_LE6{T+Ab1JGGSALGz7qYVXWuzpSWM-#ln_HS6qdpfc_e{U~=~H(paa+r?$3`_3 z_6HtN&uywKaMa02O~2CeVqk{qzMHYp`_gEGB7OnCilOLF^(%KYui9?;@v>0kp zNF}E?=xpW{g$l1ylker?QyS%$?`io&>nQ{t?zR{zdn+5mEJ2>GP;XyZb13KgC(6yc zb{*;%9k?Aox4eI=^lI2*TobeM_)s=#qP!c1UfFSiJ;A-h%eAL(_!X+$Y0evSN1&o{ zIx>2avgOU{G{xpkPj7lWUmncv|1#=V_E&_QS=6H>@Kv9WY+_BH7R&l=<{vgGulvLL zLT*i-KsI7!s1jT@Mf&%vV+gbxS^C=H=G&*M(n1%ytuR`f*X>F9te&G~?6{K zWk$(Oq$5FT1u<2qIz-MZ z{=J4#qW$5sio2rk@3EN3Z6VWKJLhz_@_rq|&h1;)hfa@#KFm(s^ibbh>pB^$*@Ed_ z7dq|>Lisw=32JqmjzfK9%~f-wS`u9vN0rKLB)1#wrC4QH4&=Bsa45~#Gesp`U{LM# zm1l{OaTu7CgBcy6g8{!d`vg+H~HRfa-L566D!or}|s3p)RL ztnu88aQo#`W_#D4Q_j7RYrg)gb(+t5n~R*2*y%fKzRnF-_cFSdERVEOk;~|J@F9gf z;!Wg347Q*8X&SSrcs@Nm7)^-s~^WGUyCoGJ^)y;D-VxkenWDjX0< zZJ)5n|GACguI2Q3^7pKagYPLcr|m+m{f2VwH@vHltVER9Qx8{h<)=EaM0*`dIFPO& z=b0OJHj5x7r8Kg=b?cF)kJQA!^LUM7Q4BVFU0lDvu|s0(2{-m zO)apqg7#j-wl7CtNUaTrD)KANKTQ_cSwg1f#wcmPgp_Zjz*|1#lIQ+b!+@@tbNC1E zH!of@{_Tm|AL^=CCiBsSi4!IDZr7Q~pE{Hhymj;q?@n19>af$>bL$&c_*{`4O$(7I zCuL5UCBIS`^>OUkOSYY*ETSrPmy@&S0B@Ac#1W@g%_DEz7k3HGSaKLI(rzbFqQIlw z@v7~tM$I$PoW?@$ zWYb;S18LM`Pm?Ko`=xBU-txwjZN`Nz@OWRtfO1UE@CMt>3t?eZ8Yz`xr76~Sf*Pc9H z_h8Q!&jTKujmEo2|CQ1CMz?yY zpw%HU1v;otg?vD*O^@-jd?tORi{bO%gSL$r~$)DbKba$OiELL?>HGXXS zo4QySd~rIoS}9bTCQ(U|%d8_lR*iki|IDeybqeY6iV6zWpJOY2Wxr_h{9cUY%9#kt zE%1{sBsE8De{y@6y6uFA|D%1>IWHZ>TIjNN*>;vsiuVvz*Tk!(T62p%f*)V3h#{TC zjdm~kM!C^HdinDm8yELO_W1NO2uG$HIxi%xwf$C^TZ)Ku2jO7weP z8rBOMNk8dZ-(C@0ZeV+*LF>M*t-LP5HlYLuJuR3^ed@1j3fJu_Mz}G#E z14YiOq{~z?KVr%^Gtkj>O>Nnxv?|E+*wtg1`CfPKO6Y<1qU-*DGICyCdCR|6t5Y@V zV6#Ut^8Lq%uXmF+3jODf9?sEcDjKpdC7m9c_}VbBN$zyU;-~n|Gm58fXkJMUpYM47 zDAZRrlWk>m(4yb9kBOP-o8TGo)s3u)+0=<)(|+zQg{AukMsj`bTof7laL|wOZihf- z-^?|GmEB2k!n#qLpo=8(_HOHx>b5h>a!g=Rs2VetVMJj z*ZR_#J=8CG%rG{@K0dwGjFQiFV}zQCs%$~#L)NM0J;zM=z5k|rxVCcaYH?i7V0*nb zH2pW4hv{=CXY^^VZCf3~^KUM{jC9&S)1(-s+vj&pE4_&ABvqiSQtB6vIr9Ez?c;lV zvlHV~)UR3ZY5nAKd$fgq;;frb&o{>((_hvs=RQ|)2z_*O(Xg8Td5|W^r*fcg#Exa= z({N4>l`Zc`qx-_+ngFR5oe|BznC~qr4k^Y?2WCpkgd$_oVrcJi(AotAx~^MM_KGowNxlv3Y+m8|k#X}{yUnyl zK6`9Pu$rt*^OL)}oPGmuPZ#WYNFF9re?T-y#^%(WZ5*)I#MGB&<3-~TJ~jDEoHn-r zW8fKG(Z|*I&nJ)P%x~NDO0s1iZStvy_b03$eHngvBY$JDpzY?-Q7t=?rcYVhqlB~< zAA&^&EboLkw(|>e?xK9xJrO^b>-Ol(Ew<<7GeU!UxihJQ7705If7k46v*E7nZB~xW zwI_@C-4W>K(VqU-=)2jQ^zD1MSMMHneE#WbJc$$rf73aaGrZx)CHP=ER%RUBdovlw z9f5NNdk#wPP1&8qk!q5$!I+Aa8uM9^7z?@s6%vROn59|oRsI}VDBr&6mA{{#^PYFp zi?Kh_d@pGBZ5(>2l^|=@8gkl>O!Js7V}Z^Or-@AWZ{oOF@%lzy?)6c~IadO6d z-B*tTve>Xyrc<<& zk!_||l9$mQ?vf}kzq0%LwaymNLz{BH1?czA2*3C;S~Ypc$?6O!hhJ}qkb(F@pvz1{85cKuga99KjTu?*4g=`rzZe+CN6GMN$>x& zd&$4G{o>i}o?mKJ?OT1NliA|^)yyML)Jn5{o(ir0`EHx)jnlIv?6Q@V<>p?=o4Lz( z=@CBf)Hi=t8!U5O+;4s3Yw)L&W^=CU+J^m4_FaD+cz|_*)m#gizOuU!WxHj8;duKk za9iD?>#uPhBDGM$7vEY{WV@$bI>W8SepuNLdA?z?p9 zd-&oHH)5Xb-!rRp|L=w;#lIKzn{SQGdmx+sc-7|3ulmYY{9GvUqNY4BKiNbIxXA{% zcM5c}5b)lB1Ou7Ep9uyPGTG`b(ZI3ozS*lxf8Y4`sC>t3>+8Kj1;0=1Nz%BL!F*Ic zUs26`W`n)Mf!E3pxD(zayCq**X!`o2<(?UvlR8hnb^bc*(6`s0T6UfBku=;P{f32k z@8=`Hs|G-~*}p7d0xqfVzL>FO z;4m9-r5^B@b_a!mnD3r*b}eC5>a#50JJ;7=H~n((Y3sQ*3{3wRq~-p$ylAYo$bb0t ziI42TYd_;3ZQQvcZg2mLJ1IrW`j$Lc`}Ih_Px6<`(;u$dy14h+_UP%`ulj!7<>Y2t z$@yx?i>mBG9VLmJ)ESG%UB(I3Eq&W~zZ^IZplIbB1)d1_^#AJ7 VE{lk4hB^iy@O1TaS?83{1ORQTQ4s(D literal 36265 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�sy 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 diff --git a/desktop/focuser.png b/desktop/focuser.png index dde69a2d4182f9ae58ae9ba09ab26a7c53b011bd..262acf026e80c6081dfbaf17cec9b9c96a4fecb5 100644 GIT binary patch literal 13106 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 Rz~8gLNr}mcmI&+m{6F!O3%&pV literal 13187 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 F{{Ydk4Rrtj diff --git a/desktop/package.json b/desktop/package.json index d6df77814..bcd0db24c 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": { @@ -81,9 +82,6 @@ "typescript-eslint": "7.16.1", "wait-on": "7.2.0" }, - "overrides": { - "axios": "1.6.2" - }, "engines": { "node": ">= 20.11.1" }, diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index 940303b31..f63d817d8 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

+ + +
+ @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..9687dc168 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 packageJson from '../../../package.json' +import { DependencyItem, FLAT_ICON_URL, IconItem } from '../../shared/types/about.types' import { AppComponent } from '../app.component' @Component({ @@ -6,7 +8,58 @@ import { AppComponent } from '../app.component' templateUrl: './about.component.html', }) export class AboutComponent { + protected readonly codename = packageJson.codename + protected readonly version = packageJson.version + protected readonly description = packageJson.description + 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}/photo-filter_4892829`, name: 'Photo filter', author: 'Freepik - Flaticon' }) + this.icons.push({ link: `${FLAT_ICON_URL}/blackhole_6704410`, name: 'Blackhole', author: 'Freepik - Flaticon' }) + } + + private mapDependencies() { + for (const [name, version] of Object.entries(packageJson.dependencies)) { + this.dependencies.push(this.mapDependency(name, version)) + } + + for (const [name, version] of Object.entries(packageJson.devDependencies)) { + this.dependencies.push(this.mapDependency(name, version)) + } + } + + private mapDependency(name: string, version: string) { + const link = `https://www.npmjs.com/package/${name}` + version = version.includes('#') ? version.split('#')[1] : version + return { name, version, link } as DependencyItem } } diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 0fb4e41bc..8f4e687c1 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()" /> @@ -45,11 +45,11 @@ class="absolute flex flex-row align-items-center gap-1" style="right: 8px; top: -2px"> + 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.alignmentMethod = '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.alignmentMethod = '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 }}
- - - + spinnableNumber /> @@ -182,7 +182,7 @@ inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" [(ngModel)]="closeApproachDistance" - scrollableNumber /> + spinnableNumber /> + spinnableNumber />
@@ -741,7 +741,7 @@ inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" [(ngModel)]="skyObjectFilter.magnitude[0]" - scrollableNumber /> + spinnableNumber />
@@ -757,7 +757,7 @@ inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" [(ngModel)]="skyObjectFilter.magnitude[1]" - scrollableNumber /> + spinnableNumber /> @@ -830,12 +830,12 @@
-
+
-
+
- Enable + Date Time
-
+
-
- Hour + -
-
- Minute + spinnableNumber /> + + + -
+ spinnableNumber /> + +
+ + + +
@@ -900,4 +907,5 @@ #ephemerisMenu [model]="ephemerisModel" [header]="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..982eeb03c 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { AfterContentInit, AfterViewInit, Component, ElementRef, 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' @@ -20,9 +20,10 @@ import { CONSTELLATIONS, CloseApproach, Constellation, + DEFAULT_BODY_POSITION, + DEFAULT_LOCATION, + DEFAULT_SEARCH_FILTER, DeepSkyObject, - EMPTY_BODY_POSITION, - EMPTY_SEARCH_FILTER, Location, MinorPlanet, MinorPlanetSearchItem, @@ -37,23 +38,27 @@ import { 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 + // TODO: juntar locations e dateTime num objeto + protected locations: Location[] = [structuredClone(DEFAULT_LOCATION)] + // TODO: location fica em Atlas preference + protected location = this.locations[0] + get refreshing() { return this.refreshingPosition || this.refreshingChart } - readonly bodyPosition = structuredClone(EMPTY_BODY_POSITION) + readonly bodyPosition = structuredClone(DEFAULT_BODY_POSITION) moonIlluminated = 1 moonWaning = false @@ -114,7 +119,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, skyObject?: DeepSkyObject skyObjectItems: DeepSkyObject[] = [] skyObjectSearchText = '' - readonly skyObjectFilter = structuredClone(EMPTY_SEARCH_FILTER) + readonly skyObjectFilter = structuredClone(DEFAULT_SEARCH_FILTER) showSkyObjectFilter = false readonly constellationOptions: (Constellation | 'ALL')[] = ['ALL', ...CONSTELLATIONS] @@ -133,8 +138,8 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, @ViewChild('deviceMenu') private readonly deviceMenu!: DeviceListMenuComponent - @ViewChild('calendarPanel') - private readonly calendarPanel!: OverlayPanel + @ViewChild('dateTimeAndLocationPanel') + private readonly dateTimeAndLocationPanel!: OverlayPanel @ViewChild('chart') private readonly chart!: UIChart @@ -394,8 +399,6 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, private refreshTimer?: Subscription private refreshTabCount = 0 - private location: Location - readonly settings: SettingsDialog = { showDialog: false, } @@ -423,15 +426,15 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, }) 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) => { + electron.on('LOCATION.CHANGED', async () => { await ngZone.run(() => { - this.location = event + this.loadLocations() return this.refreshTab(true, true) }) }) @@ -440,12 +443,15 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, await this.loadTabFromData(event) }) - this.location = this.preference.selectedLocation.get() + const settings = this.preference.settings.get() + this.location = settings.locations[settings.location] // TODO: Refresh graph and twilight if hours past 12 (noon) } async ngOnInit() { + Chart.register(zoomPlugin) + this.loadPreference() const types = await this.api.skyObjectTypes() this.skyObjectFilter.types = ['ALL', ...types] @@ -476,10 +482,6 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, async ngAfterViewInit() { await this.refreshTab() - - this.calendarPanel.onOverlayClick = (e) => { - e.stopImmediatePropagation() - } } @HostListener('window:unload') @@ -701,7 +703,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, Object.assign(this.bodyPosition, bodyPosition) } else { this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + Object.assign(this.bodyPosition, DEFAULT_BODY_POSITION) } } // Minor Planet. @@ -719,7 +721,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, Object.assign(this.bodyPosition, bodyPosition) } else { this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + Object.assign(this.bodyPosition, DEFAULT_BODY_POSITION) } } // Sky Object. @@ -732,7 +734,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, Object.assign(this.bodyPosition, bodyPosition) } else { this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + Object.assign(this.bodyPosition, DEFAULT_BODY_POSITION) } } // Satellite. @@ -745,7 +747,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, Object.assign(this.bodyPosition, bodyPosition) } else { this.name = undefined - Object.assign(this.bodyPosition, EMPTY_BODY_POSITION) + Object.assign(this.bodyPosition, DEFAULT_BODY_POSITION) } } @@ -865,6 +867,12 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, } } + private loadLocations() { + const settings = this.preference.settings.get() + this.locations = settings.locations + this.location = this.locations.find((e) => e.id === this.location.id) ?? this.locations[settings.location] + } + private loadPreference() { const preference = this.preference.skyAtlasPreference.get() @@ -873,6 +881,8 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, const enabled = satellite?.enabled ?? AtlasComponent.DEFAULT_SATELLITE_FILTERS.includes(group) this.satelliteSearchGroup.set(group, enabled) } + + this.loadLocations() } savePreference() { 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.scss b/desktop/src/app/calibration/calibration.component.scss index 3f694e53b..627dc317d 100644 --- a/desktop/src/app/calibration/calibration.component.scss +++ b/desktop/src/app/calibration/calibration.component.scss @@ -1,33 +1,31 @@ -:host { - ::ng-deep { - .p-treenode-label { - width: 100%; - } +neb-calibration { + .p-treenode-label { + width: 100%; + } - .p-tree-wrapper { - max-height: 288px; - } + .p-tree-wrapper { + max-height: 288px; + } - .p-tree { - .p-tree-container { - padding-right: 4px; + .p-tree { + .p-tree-container { + padding-right: 4px; - .p-treenode { - padding: 0; + .p-treenode { + padding: 0; - .p-treenode-content { - padding: 0 0.5rem; - } + .p-treenode-content { + padding: 0 0.5rem; } } } + } - .p-treenode-leaf > .p-treenode-content .p-tree-toggler { - display: none; - } + .p-treenode-leaf > .p-treenode-content .p-tree-toggler { + display: none; + } - .p-tree-empty-message { - padding: 1rem 0.5rem; - } + .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..e47421233 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component } from '@angular/core' +import { AfterViewInit, Component, ViewEncapsulation } from '@angular/core' import { dirname } from 'path' import { TreeDragDropService, TreeNode } from 'primeng/api' import { TreeNodeDropEvent } from 'primeng/tree' @@ -24,6 +24,7 @@ export type TreeNodeData = { type: 'NAME'; data: string } | { type: 'GROUP'; dat templateUrl: './calibration.component.html', styleUrls: ['./calibration.component.scss'], providers: [TreeDragDropService], + encapsulation: ViewEncapsulation.None, }) export class CalibrationComponent implements AfterViewInit { readonly frames: CalibrationNode[] = [] diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 7f00245aa..2dfafe08c 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 +209,7 @@ + spinnableNumber />
+ spinnableNumber />
@@ -272,7 +256,7 @@
+ spinnableNumber />
+ spinnableNumber />
+ spinnableNumber />
+ spinnableNumber />
@@ -347,7 +331,7 @@ Subframe
@@ -374,7 +358,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -391,7 +375,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -422,7 +406,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -439,7 +423,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -468,33 +452,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 +579,7 @@ locale="en" [minFractionDigits]="1" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber />
@@ -610,7 +594,7 @@ [(ngModel)]="dither.request.afterExposures" [step]="1" (ngModelChange)="savePreference()" - scrollableNumber /> + spinnableNumber /> @@ -660,7 +644,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 +654,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 +664,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..49077ab43 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -1,92 +1,43 @@ 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, + DEFAULT_CAMERA, + DEFAULT_CAMERA_PREFERENCE, ExposureTimeUnit, FrameType, updateCameraStartCaptureFromCamera, } from '../../shared/types/camera.types' import { Device } 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 +93,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 +143,59 @@ 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' + } + 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 +204,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 +220,51 @@ 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 (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('WHEEL.UPDATED', (event) => { + if (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('FOCUSER.UPDATED', (event) => { + if (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('ROTATOR.UPDATED', (event) => { + if (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 () => { + electronService.on('CALIBRATION.CHANGED', async () => { await 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,72 +275,80 @@ 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.loadCameraStartCaptureOnDialogMode(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) } } - private async loadCameraStartCaptureForDialogMode(data?: CameraDialogInput) { + private async loadCameraStartCaptureOnDialogMode(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 = 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,15 +357,11 @@ 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) => { + const makeMenuItem = (selected: boolean, command: () => void, device?: Device) => { return { icon: device ? 'mdi mdi-connection' : 'mdi mdi-close', label: device?.name ?? 'None', @@ -410,72 +369,72 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { slideMenu: [], command: (event: MenuItemCommandEvent) => { command() - this.preference.equipmentForDevice(this.camera).set(this.equipment) + 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) + this.preference.mount = mounts.find((e) => e.name === this.preference.mount?.name) const makeMountItem = (mount?: Mount) => { - return makeItem(this.equipment.mount?.name === mount?.name, () => (this.equipment.mount = mount), mount) + return makeMenuItem(this.preference.mount?.name === mount?.name, () => (this.preference.mount = mount), mount) } - slideMenu[0].slideMenu.push(makeMountItem()) + menu[0].slideMenu.push(makeMountItem()) for (const mount of mounts) { - slideMenu[0].slideMenu.push(makeMountItem(mount)) + 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) + this.preference.wheel = wheels.find((e) => e.name === this.preference.wheel?.name) - const makeWheelItem = (wheel?: FilterWheel) => { - return makeItem(this.equipment.wheel?.name === wheel?.name, () => (this.equipment.wheel = wheel), wheel) + const makeWheelItem = (wheel?: Wheel) => { + return makeMenuItem(this.preference.wheel?.name === wheel?.name, () => (this.preference.wheel = wheel), wheel) } - slideMenu[1].slideMenu.push(makeWheelItem()) + menu[1].slideMenu.push(makeWheelItem()) for (const wheel of wheels) { - slideMenu[1].slideMenu.push(makeWheelItem(wheel)) + 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) + this.preference.focuser = focusers.find((e) => e.name === this.preference.focuser?.name) const makeFocuserItem = (focuser?: Focuser) => { - return makeItem(this.equipment.focuser?.name === focuser?.name, () => (this.equipment.focuser = focuser), focuser) + return makeMenuItem(this.preference.focuser?.name === focuser?.name, () => (this.preference.focuser = focuser), focuser) } - slideMenu[2].slideMenu.push(makeFocuserItem()) + menu[2].slideMenu.push(makeFocuserItem()) for (const focuser of focusers) { - slideMenu[2].slideMenu.push(makeFocuserItem(focuser)) + 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) + this.preference.rotator = rotators.find((e) => e.name === this.preference.rotator?.name) const makeRotatorItem = (rotator?: Rotator) => { - return makeItem(this.equipment.rotator?.name === rotator?.name, () => (this.equipment.rotator = rotator), rotator) + return makeMenuItem(this.preference.rotator?.name === rotator?.name, () => (this.preference.rotator = rotator), rotator) } - slideMenu[3].slideMenu.push(makeRotatorItem()) + menu[3].slideMenu.push(makeRotatorItem()) for (const rotator of rotators) { - slideMenu[3].slideMenu.push(makeRotatorItem(rotator)) + menu[3].slideMenu.push(makeRotatorItem(rotator)) } } @@ -508,7 +467,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 +481,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 +489,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 +510,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 +537,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 = 0 - let shutterPosition: Undefinable - - 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,182 +583,73 @@ 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) { + static async showAsDialog(window: BrowserWindowService, mode: CameraMode, camera: Camera, request: CameraStartCapture) { const result = await window.openCameraDialog({ mode, camera, request }) if (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..309d64741 --- /dev/null +++ b/desktop/src/app/camera/exposure-time.component.ts @@ -0,0 +1,207 @@ +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 readonly exposureTime: number = 0 + + @Output() + readonly exposureTimeChange = new EventEmitter() + + @Input() + protected readonly unit: ExposureTimeUnit = 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(ExposureTimeUnit.MINUTE) + }, + }, + { + label: 'Second (s)', + command: () => { + this.exposureTimeUnitChanged(ExposureTimeUnit.SECOND) + }, + }, + { + label: 'Millisecond (ms)', + command: () => { + this.exposureTimeUnitChanged(ExposureTimeUnit.MILLISECOND) + }, + }, + { + label: 'Microsecond (µs)', + command: () => { + this.exposureTimeUnitChanged(ExposureTimeUnit.MICROSECOND) + }, + }, + ] + + 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, ExposureTimeUnit.MICROSECOND) + break + case 'min': + case 'max': + this.exposureTimeMinMaxChanged() + break + } + } + } + + ngAfterViewInit() { + if (!this.normalize(this.exposureTime)) { + this.updateExposureTime(this.current.exposureTime, this.unit, this.unit) + } + } + + protected exposureTimeUnitChanged(value: ExposureTimeUnit) { + this.updateExposureTime(this.current.exposureTime, value, this.unit) + } + + protected exposureTimeChanged(value: number, from: ExposureTimeUnit = this.unit) { + this.updateExposureTime(value, this.unit, from) + } + + protected exposureTimeMinMaxChanged() { + this.updateExposureTime(this.current.exposureTime, this.unit, this.unit) + } + + protected exposureTimeUnitWheeled(event: WheelEvent) { + if (event.deltaY) { + const units: ExposureTimeUnit[] = [ExposureTimeUnit.MINUTE, ExposureTimeUnit.SECOND, ExposureTimeUnit.MILLISECOND, ExposureTimeUnit.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) { + 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 (this.exposureTime !== exposureTimeInMicroseconds) { + this.exposureTimeChange.emit(exposureTimeInMicroseconds) + } + + if (this.unit !== unit) { + this.unitChange.emit(unit) + } + } + + private normalize(exposureTime: number) { + if (!this.normalized) { + return false + } + + 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)) { + console.log('exposure time normalized:', exposureTime, unit) + this.updateExposureTime(exposureTime, unit, ExposureTimeUnit.MICROSECOND) + return true + } + } + } + + return false + } + + static computeExposureTime(exposureTime: number, to: ExposureTimeUnit, from: ExposureTimeUnit = 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 ExposureTimeUnit.MINUTE: + return 1 + case ExposureTimeUnit.SECOND: + return 60 + case ExposureTimeUnit.MILLISECOND: + return 60000 + case ExposureTimeUnit.MICROSECOND: + 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..a7979cd71 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,23 @@ 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' get canShowInfo() { return this.mode === 'CAPTURE' @@ -52,25 +52,25 @@ 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 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 @@ -113,11 +113,9 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab }) this.filterChangeSubscription = this.filterChangePublisher.pipe(debounceTime(1500)).subscribe(async (filter) => { - this.savePreference() - 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.loadWheelStartCaptureOnDialogMode(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.ticker.unregister(this) this.filterChangeSubscription?.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 loadWheelStartCaptureOnDialogMode(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 @@ -257,21 +260,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 +285,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 +301,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)) } } - 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,8 +353,8 @@ 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) + const preference = this.preferenceService.wheel(this.wheel).get() + const filters = makeFilter(this.wheel, this.filters, preference.shutterPosition) if (filters !== this.filters) { this.filters = filters @@ -362,23 +366,14 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab 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()) + this.filters = makeFilter(this.wheel, this.filters, this.preference.shutterPosition) } } 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,11 +384,11 @@ 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) { + static async showAsDialog(window: BrowserWindowService, mode: WheelMode, wheel: Wheel, request: CameraStartCapture) { const result = await window.openWheelDialog({ mode, wheel, request }) if (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.primeService.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.primeService.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 +42,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.primeService.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..7f00a41a1 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,7 @@
@@ -292,18 +292,16 @@ header="Connection" [modal]="true" [draggable]="false" - [(visible)]="showConnectionDialog" + [(visible)]="connectionDialog.showDialog" [style]="{ width: '90vw' }"> -
+
+ [(ngModel)]="connectionDialog.connection.name" />
@@ -312,7 +310,7 @@ + [(ngModel)]="connectionDialog.connection.host" />
@@ -322,25 +320,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" /> button { diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index c8d706bec..a1b4ce8de 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,4 +1,4 @@ -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' @@ -8,21 +8,20 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' -import { Camera } from '../../shared/types/camera.types' +import { Camera, isCamera, isGuideHead } 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 { ConnectionDetails, DEFAULT_CONNECTION_DETAILS, DEFAULT_HOME_CONNECTION_DIALOG, DEFAULT_HOME_PREFERENCE, 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 { Wheel } from '../../shared/types/wheel.types' import { AppComponent } from '../app.component' interface MappedDevice { CAMERA: Camera MOUNT: Mount FOCUSER: Focuser - WHEEL: FilterWheel + WHEEL: Wheel ROTATOR: Rotator } @@ -34,30 +33,42 @@ function scrollPageOf(element: Element) { selector: 'neb-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class HomeComponent implements AfterContentInit { + protected readonly preference = structuredClone(DEFAULT_HOME_PREFERENCE) + protected connection?: ConnectionDetails + protected readonly connectionDialog = structuredClone(DEFAULT_HOME_CONNECTION_DIALOG) + + protected cameras: Camera[] = [] + protected mounts: Mount[] = [] + protected focusers: Focuser[] = [] + protected wheels: Wheel[] = [] + protected rotators: Rotator[] = [] + protected domes: Camera[] = [] + protected switches: Camera[] = [] + + protected currentPage = 0 + + protected readonly deviceModel: MenuItem[] = [] + + protected readonly imageModel: SlideMenuItem[] = [ + { + icon: 'mdi mdi-image-plus', + label: 'Open new image', + slideMenu: [], + command: () => { + return this.openImage(true) + }, + }, + ] + @ViewChild('deviceMenu') private readonly deviceMenu!: DeviceListMenuComponent @ViewChild('imageMenu') private readonly imageMenu!: DeviceListMenuComponent - readonly connectionTypes = Array.from(CONNECTION_TYPES) - showConnectionDialog = false - connections: ConnectionDetails[] = [] - connection?: ConnectionDetails - newConnection?: [ConnectionDetails, Undefinable] - - cameras: Camera[] = [] - mounts: Mount[] = [] - focusers: Focuser[] = [] - wheels: FilterWheel[] = [] - rotators: Rotator[] = [] - domes: Camera[] = [] - switches: Camera[] = [] - - currentPage = 0 - get connected() { return !!this.connection && this.connection.connected } @@ -122,46 +133,13 @@ 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) - }, - }, - ] - - 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) - }) - }) - - this.electron.on(`${type}.DETACHED`, (event) => { - this.ngZone.run(() => { - onRemove(event.device as never) - }) - }) - - this.electron.on(`${type}.UPDATED`, (event) => { - this.ngZone.run(() => { - onUpdate(event.device as never) - }) - }) - } - constructor( app: AppComponent, - private readonly electron: ElectronService, - private readonly browserWindow: BrowserWindowService, + private readonly electronService: ElectronService, + private readonly browserWindowService: BrowserWindowService, private readonly api: ApiService, - private readonly prime: PrimeService, - private readonly preference: PreferenceService, + private readonly primeService: PrimeService, + private readonly preferenceService: PreferenceService, private readonly ngZone: NgZone, ) { app.title = 'Nebulosa' @@ -251,23 +229,18 @@ export class HomeComponent implements AfterContentInit { }, ) - 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,71 @@ export class HomeComponent implements AfterContentInit { } } - addConnection() { - this.newConnection = [structuredClone(EMPTY_CONNECTION_DETAILS), undefined] - this.showConnectionDialog = true + private startListening(type: K, onAdd: (device: MappedDevice[K]) => number, onRemove: (device: MappedDevice[K]) => number, onUpdate: (device: MappedDevice[K]) => void) { + this.electronService.on(`${type}.ATTACHED`, (event) => { + this.ngZone.run(() => { + onAdd(event.device as never) + }) + }) + + this.electronService.on(`${type}.DETACHED`, (event) => { + this.ngZone.run(() => { + onRemove(event.device as never) + }) + }) + + this.electronService.on(`${type}.UPDATED`, (event) => { + this.ngZone.run(() => { + onUpdate(event.device as never) + }) + }) + } + + protected addConnection() { + this.connectionDialog.edited = false + this.connectionDialog.connection = structuredClone(DEFAULT_CONNECTION_DETAILS) + this.connectionDialog.showDialog = true } - editConnection(connection: ConnectionDetails, event: MouseEvent) { - this.newConnection = [structuredClone(connection), connection] - this.showConnectionDialog = 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 +324,13 @@ export class HomeComponent implements AfterContentInit { } catch (e) { console.error(e) - this.prime.message('Connection failed', 'error') + this.primeService.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,6 +354,23 @@ export class HomeComponent implements AfterContentInit { return DeviceChooserComponent.handleDisconnectDevice(this.api, event.device, event.item) } + protected readonly deviceMenuToolbarBuilder = (device: Device): MenuItem[] => { + if (isCamera(device) && !isGuideHead(device)) { + return [ + { + icon: 'mdi mdi-wrench', + label: 'Calibration', + command: (e) => { + e.originalEvent?.stopImmediatePropagation() + return this.browserWindowService.openCalibration() + }, + }, + ] + } else { + return [] + } + } + private async openDevice(type: K) { this.deviceModel.length = 0 @@ -377,8 +384,7 @@ 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) @@ -388,43 +394,43 @@ export class HomeComponent implements AfterContentInit { private async openDeviceWindow(type: K, device: MappedDevice[K]) { switch (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 }) + 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' }) + this.preference.imagePath = dirname(path) + this.savePreference() + + await this.browserWindowService.openImage({ path, source: 'PATH' }) } } else { const camera = await this.imageMenu.show(this.cameras) if (camera && camera !== 'NONE') { - await this.browserWindow.openCameraImage(camera) + await this.browserWindowService.openCameraImage(camera) } } } - async open(type: HomeWindowType) { + protected async open(type: HomeWindowType) { switch (type) { case 'MOUNT': case 'CAMERA': @@ -434,43 +440,43 @@ 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 'ABOUT': - await this.browserWindow.openAbout() + await this.browserWindowService.openAbout() break } } @@ -482,7 +488,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 +500,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 +527,7 @@ export class HomeComponent implements AfterContentInit { } } - scrolled(event: Event) { + protected scrolled(event: Event) { function isVisible(element: Element) { const bound = element.getBoundingClientRect() @@ -540,15 +546,17 @@ export class HomeComponent implements AfterContentInit { } this.currentPage = page + + event.stopImmediatePropagation() } - scrollTo(event: Event, page: number) { + protected scrollTo(event: Event, page: number) { this.currentPage = 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 +568,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/image.component.html b/desktop/src/app/image/image.component.html index a9670890b..23710ccfd 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -226,7 +226,7 @@ [(ngModel)]="annotation.minorPlanetsMagLimit" [minFractionDigits]="1" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -455,7 +455,7 @@ styleClass="p-inputtext-sm border-0 w-full" [showButtons]="true" [(ngModel)]="solver.radius" - scrollableNumber /> + spinnableNumber />
@@ -470,7 +470,7 @@ [(ngModel)]="solver.focalLength" [allowEmpty]="false" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -485,7 +485,7 @@ [(ngModel)]="solver.pixelSize" [allowEmpty]="false" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -626,7 +626,7 @@ styleClass="p-inputtext-sm border-0 w-full" [(ngModel)]="stretchShadow" locale="en" - scrollableNumber /> + spinnableNumber /> @@ -637,7 +637,7 @@ styleClass="p-inputtext-sm border-0 w-full" [(ngModel)]="stretchHighlight" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -660,7 +660,7 @@ styleClass="p-inputtext-sm border-0 w-full" [(ngModel)]="stretchMidtone" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -742,7 +742,7 @@ [(ngModel)]="scnr.amount" locale="en" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber />
@@ -935,7 +935,7 @@ [(ngModel)]="starDetection.minSNR" locale="en" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber />
@@ -953,7 +953,7 @@ [(ngModel)]="starDetection.maxStars" locale="en" [allowEmpty]="false" - scrollableNumber /> + spinnableNumber />
@@ -1090,7 +1090,7 @@ styleClass="p-inputtext-sm border-0" [showButtons]="true" [(ngModel)]="fov.focalLength" - scrollableNumber /> + spinnableNumber />
@@ -1100,7 +1100,7 @@ styleClass="p-inputtext-sm border-0" [showButtons]="true" [(ngModel)]="fov.aperture" - scrollableNumber /> + spinnableNumber />
@@ -1122,7 +1122,7 @@ [min]="1" [max]="9999" [(ngModel)]="fov.cameraSize.width" - scrollableNumber /> + spinnableNumber />
@@ -1134,7 +1134,7 @@ [min]="1" [max]="9999" [(ngModel)]="fov.cameraSize.height" - scrollableNumber /> + spinnableNumber />
@@ -1150,7 +1150,7 @@ [maxFractionDigits]="2" [(ngModel)]="fov.pixelSize.width" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -1166,7 +1166,7 @@ [maxFractionDigits]="2" [(ngModel)]="fov.pixelSize.height" locale="en" - scrollableNumber /> + spinnableNumber />
@@ -1182,7 +1182,7 @@ [maxFractionDigits]="2" [(ngModel)]="fov.barlowReducer" locale="en" - scrollableNumber /> + spinnableNumber /> @@ -1194,7 +1194,7 @@ [min]="1" [max]="5" [(ngModel)]="fov.bin" - scrollableNumber /> + spinnableNumber /> @@ -1210,7 +1210,7 @@ [maxFractionDigits]="2" [(ngModel)]="fov.rotation" locale="en" - scrollableNumber /> + spinnableNumber /> --> diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index f7a960a32..0b72a763a 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -19,8 +19,8 @@ import { Camera } from '../../shared/types/camera.types' import { AnnotationInfoDialog, DEFAULT_FOV, + DEFAULT_IMAGE_SOLVED, DetectedStar, - EMPTY_IMAGE_SOLVED, FITSHeaderItem, FOV, IMAGE_STATISTICS_BIT_OPTIONS, @@ -44,6 +44,8 @@ import { StarDetectionDialog, } from '../../shared/types/image.types' import { Mount } from '../../shared/types/mount.types' +import { PlateSolverRequest } from '../../shared/types/platesolver.types' +import { StarDetectionRequest } from '../../shared/types/stardetector.types' import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @@ -167,7 +169,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { radius: 4, focalLength: 0, pixelSize: 0, - solved: structuredClone(EMPTY_IMAGE_SOLVED), + solved: structuredClone(DEFAULT_IMAGE_SOLVED), } crossHair = false @@ -824,7 +826,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.starDetection.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([]) } @@ -846,13 +848,18 @@ export class ImageComponent implements AfterViewInit, OnDestroy { 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.preference.settings.get().starDetector[this.starDetection.type], + type: this.starDetection.type, + minSNR: this.starDetection.minSNR, + maxStars: this.starDetection.maxStars, + } + + Object.assign(this.starDetection, this.preference.settings.get().starDetector[this.starDetection.type]) try { this.starDetection.running = true - this.starDetection.stars = await this.api.detectStars(path, options) + this.starDetection.stars = await this.api.detectStars(path, request) } finally { this.starDetection.running = false } @@ -1173,10 +1180,14 @@ export class ImageComponent implements AfterViewInit, OnDestroy { 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.preference.settings.get().plateSolver[this.solver.type], + type: this.solver.type, + pixelSize: this.solver.pixelSize, + focalLength: this.solver.focalLength, + } + + const solved = await this.api.solverStart(request, path, this.solver.blind, this.solver.centerRA, this.solver.centerDEC, this.solver.radius) this.savePreference() this.updateImageSolved(solved) @@ -1197,7 +1208,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } 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 @@ -1249,12 +1260,11 @@ 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 }, beforeMouseDown: (e) => { - // return e.target !== this.image.nativeElement - return e.target === this.roi.nativeElement + return e.target !== this.image.nativeElement }, }) @@ -1385,8 +1395,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { 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) => { @@ -1424,8 +1432,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) @@ -1447,8 +1454,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..b718a8b59 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() } }) } diff --git a/desktop/src/app/indi/property/indi-property.component.scss b/desktop/src/app/indi/property/indi-property.component.scss index fc6120b85..33a3607f1 100644 --- a/desktop/src/app/indi/property/indi-property.component.scss +++ b/desktop/src/app/indi/property/indi-property.component.scss @@ -3,19 +3,19 @@ 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..87e58f06a 100644 --- a/desktop/src/app/indi/property/indi-property.component.ts +++ b/desktop/src/app/indi/property/indi-property.component.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core' +import { AfterContentInit, Component, EventEmitter, Input, Output } from '@angular/core' import { INDIProperty, INDIPropertyItem, INDISendProperty, INDISendPropertyItem } from '../../../shared/types/device.types' @Component({ @@ -6,12 +6,12 @@ import { INDIProperty, INDIPropertyItem, INDISendProperty, INDISendPropertyItem templateUrl: './indi-property.component.html', styleUrls: ['./indi-property.component.scss'], }) -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 +24,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..c3bcd4512 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 @@
+ spinnableNumber />
diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index a744a786f..4dd76c69c 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -7,48 +7,28 @@ 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 { PrimeService } from '../../shared/services/prime.service' -import { Angle, ComputedLocation, Constellation, EMPTY_COMPUTED_LOCATION, SkyAtlasInput, SkyAtlasTab } from '../../shared/types/atlas.types' -import { EMPTY_MOUNT, Mount, MountPreference, MountRemoteControlDialog, MountRemoteControlType, MoveDirectionType, PierSide, SlewRate, TargetCoordinateType, TrackMode } from '../../shared/types/mount.types' +import { Tickable, Ticker } from '../../shared/services/ticker.service' +import { ComputedLocation, DEFAULT_COMPUTED_LOCATION, SkyAtlasTab } from '../../shared/types/atlas.types' +import { DEFAULT_MOUNT, DEFAULT_MOUNT_PREFERENCE, Mount, MountRemoteControlDialog, MountRemoteControlType, MoveDirectionType, SlewRate, TrackMode } from '../../shared/types/mount.types' import { AppComponent } from '../app.component' -import { FramingData } from '../framing/framing.component' @Component({ selector: 'neb-mount', templateUrl: './mount.component.html', }) -export class MountComponent implements AfterContentInit, OnDestroy, Pingable { - readonly mount = structuredClone(EMPTY_MOUNT) - - slewing = false - parking = false - parked = false - trackModes: TrackMode[] = ['SIDEREAL'] - trackMode: TrackMode = 'SIDEREAL' - slewRates: SlewRate[] = [] - slewRate?: SlewRate - tracking = false - canPark = false - canHome = false - slewingDirection?: MoveDirectionType - - rightAscensionJ2000: Angle = '00h00m00s' - declinationJ2000: Angle = `00°00'00"` - rightAscension: Angle = '00h00m00s' - declination: Angle = `00°00'00"` - azimuth: Angle = `000°00'00"` - altitude: Angle = `+00°00'00"` - lst = '00:00' - constellation?: Constellation - timeLeftToMeridianFlip = '00:00' - meridianAt = '00:00' - pierSide: PierSide = 'NEITHER' - targetCoordinateType: TargetCoordinateType = 'JNOW' - targetRightAscension: Angle = '00h00m00s' - targetDeclination: Angle = `00°00'00"` - targetComputedLocation = structuredClone(EMPTY_COMPUTED_LOCATION) +export class MountComponent implements AfterContentInit, OnDestroy, Tickable { + protected readonly mount = structuredClone(DEFAULT_MOUNT) + + protected tracking = false + protected trackMode: TrackMode = 'SIDEREAL' + protected slewRate?: SlewRate + protected slewingDirection?: MoveDirectionType + + protected readonly preference = structuredClone(DEFAULT_MOUNT_PREFERENCE) + protected currentComputedLocation = structuredClone(DEFAULT_COMPUTED_LOCATION) + protected targetComputedLocation = structuredClone(DEFAULT_COMPUTED_LOCATION) private readonly computeCoordinatePublisher = new Subject() private readonly computeTargetCoordinatePublisher = new Subject() @@ -61,8 +41,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { 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,12 +50,13 @@ 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: SkyAtlasTab.SKY_OBJECT, + filter: { rightAscension: this.currentComputedLocation.rightAscensionJ2000, declination: this.currentComputedLocation.declinationJ2000 }, + }, + { bringToFront: true }, + ) }, }, ] @@ -88,7 +68,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { 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,9 +201,9 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { }, ] - targetCoordinateCommand = this.targetCoordinateModel[0] + protected targetCoordinateCommand = this.targetCoordinateModel[0] - readonly remoteControl: MountRemoteControlDialog = { + protected readonly remoteControl: MountRemoteControlDialog = { showDialog: false, type: 'LX200', host: '0.0.0.0', @@ -229,17 +214,17 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { 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 primeService: PrimeService, + 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 +238,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) }) } }) @@ -316,13 +301,13 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { 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) + 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 +316,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 +334,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 +342,44 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { } } - async showRemoteControlDialog() { + protected async showRemoteControlDialog() { this.remoteControl.data = 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) } catch { - this.prime.message('Failed to start remote control', 'error') + this.primeService.message('Failed to start remote control', 'error') } } - async stopRemoteControl(type: MountRemoteControlType) { + protected async stopRemoteControl(type: MountRemoteControlType) { await this.api.mountRemoteControlStop(this.mount, type) this.remoteControl.data = 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 +389,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { } } - moveTo(direction: MoveDirectionType, pressed: boolean, event?: MouseEvent) { + protected moveTo(direction: MoveDirectionType, pressed: boolean, event?: MouseEvent) { if (!event || event.button === 0) { this.slewingDirection = pressed ? direction : undefined @@ -441,50 +429,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 +471,44 @@ 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) + const { targetRightAscension, targetDeclination, targetCoordinateType } = this.preference + const computedLocation = await this.api.mountComputeLocation(this.mount, targetCoordinateType === 'J2000', targetRightAscension, targetDeclination, true, true, true) this.targetComputedLocation = computedLocation } } 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) }) } }) @@ -51,23 +48,23 @@ export class RotatorComponent implements AfterViewInit, OnDestroy, Pingable { 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) + 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..c6ddd1e9e 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -19,7 +19,7 @@ styleClass="p-inputtext-sm border-0" [allowEmpty]="false" (ngModelChange)="savePlan()" - scrollableNumber /> + spinnableNumber />
@@ -59,7 +59,7 @@ tooltipPosition="bottom" [positionTop]="8"> + spinnableNumber /> @@ -116,7 +116,7 @@ [(ngModel)]="plan.dither.afterExposures" [step]="1" (ngModelChange)="savePlan()" - scrollableNumber /> + spinnableNumber /> @@ -163,7 +163,7 @@ [(ngModel)]="plan.autoFocus.afterElapsedTime" [step]="1" (ngModelChange)="savePlan()" - scrollableNumber /> + spinnableNumber /> @@ -183,7 +183,7 @@ [(ngModel)]="plan.autoFocus.afterExposures" [step]="1" (ngModelChange)="savePlan()" - scrollableNumber /> + spinnableNumber /> @@ -203,7 +203,7 @@ [(ngModel)]="plan.autoFocus.afterTemperatureChange" [step]="1" (ngModelChange)="savePlan()" - scrollableNumber /> + spinnableNumber /> @@ -223,7 +223,7 @@ [(ngModel)]="plan.autoFocus.afterHFDIncrease" [step]="1" (ngModelChange)="savePlan()" - scrollableNumber /> + spinnableNumber /> 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..8ff97e04c 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -1,22 +1,22 @@ 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 { 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 { 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, CameraCaptureEvent, CameraStartCapture, DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, 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_PLAN, SEQUENCE_ENTRY_PROPERTIES, SequenceCaptureMode, SequenceEntryProperty, SequencePlan, SequencerEvent } 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' @@ -29,22 +29,23 @@ export const SEQUENCER_SAVED_PATH_KEY = 'sequencer.savedPath' selector: 'neb-sequencer', templateUrl: './sequencer.component.html', styleUrls: ['./sequencer.component.scss'], + encapsulation: ViewEncapsulation.None, }) -export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable { +export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable { cameras: Camera[] = [] mounts: Mount[] = [] - wheels: FilterWheel[] = [] + wheels: Wheel[] = [] focusers: Focuser[] = [] rotators: Rotator[] = [] camera?: Camera mount?: Mount - wheel?: FilterWheel + wheel?: Wheel focuser?: Focuser rotator?: Rotator readonly captureModes: SequenceCaptureMode[] = ['FULLY', 'INTERLEAVED'] - readonly plan = structuredClone(EMPTY_SEQUENCE_PLAN) + readonly plan = structuredClone(DEFAULT_SEQUENCE_PLAN) private entryToApply?: CameraStartCapture private entryToApplyCount: [number, number] = [0, 0] @@ -129,7 +130,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable private readonly electron: ElectronService, private readonly preference: PreferenceService, private readonly prime: PrimeService, - private readonly pinger: Pinger, + private readonly ticker: Ticker, ngZone: NgZone, ) { app.title = 'Sequencer' @@ -140,7 +141,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable command: () => { this.updateSavedPath() - Object.assign(this.plan, structuredClone(EMPTY_SEQUENCE_PLAN)) + Object.assign(this.plan, structuredClone(DEFAULT_SEQUENCE_PLAN)) this.add() }, }) @@ -253,7 +254,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } 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) @@ -268,10 +269,10 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable @HostListener('window:unload') ngOnDestroy() { - this.pinger.unregister(this) + this.ticker.unregister(this) } - async ping() { + async tick() { 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) @@ -306,6 +307,9 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable frameFormat: camera?.frameFormats[0], autoSave: true, autoSubFolderMode: 'OFF', + filterPosition: 0, + shutterPosition: 0, + focusOffset: 0, dither: { enabled: false, amount: 0, @@ -389,18 +393,17 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable 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] - 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 + const settings = this.preference.settings.get() + this.plan.namingFormat.light ??= settings.namingFormat.light || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.light + this.plan.namingFormat.dark ??= settings.namingFormat.dark || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.dark + this.plan.namingFormat.flat ??= settings.namingFormat.flat || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.flat + this.plan.namingFormat.bias ??= settings.namingFormat.bias || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.bias return this.plan.entries.length } resetCameraCaptureNamingFormat(type: FrameType) { - const namingFormatPreference = this.preference.cameraCaptureNamingFormatPreference.get() - resetCameraCaptureNamingFormat(type, this.plan.namingFormat, namingFormatPreference) + resetCameraCaptureNamingFormat(type, this.plan.namingFormat, this.preference.settings.get().namingFormat) this.savePlan() } @@ -435,7 +438,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } async cameraChanged() { - await this.ping() + await this.tick() this.updateEntriesFromCamera(this.camera) } @@ -449,19 +452,19 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable } mountChanged() { - return this.ping() + return this.tick() } focuserChanged() { - return this.ping() + return this.tick() } wheelChanged() { - return this.ping() + return this.tick() } rotatorChanged() { - return this.ping() + return this.tick() } savePlan() { diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index a096e3ffa..0d2c87435 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -18,9 +18,9 @@
@@ -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()" /> () + private readonly locationChangePublisher = new Subject() + private readonly locationChangeSubscription?: Subscription - starDetectorType: StarDetectorType = 'ASTAP' - readonly starDetectors = new Map() + get location() { + return this.preference.locations[this.preference.location] ?? DEFAULT_LOCATION + } - 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.send('LOCATION.CHANGED', location) }) } + ngAfterViewInit() { + this.loadPreference() + } + 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.preference.location = this.preference.locations.length - 1 + + this.locationChanged() } - 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.indexOf(this.location) if (index >= 0) { - this.locations.splice(index, 1) - this.location = this.locations[0]! + this.preference.locations.splice(index, 1) + this.preference.location = 0 - this.save() - this.locationChangePublisher.next(this.location) + this.locationChanged() } } } - locationChanged() { - console.log(this.locations) - this.save() + protected locationChanged(location?: Location) { + if (location) { + this.preference.location = this.preference.locations.indexOf(location) + } + + this.savePreference() + this.locationChangePublisher.next(this.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.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..5c9e9ca81 100644 --- a/desktop/src/app/stacker/stacker.component.html +++ b/desktop/src/app/stacker/stacker.component.html @@ -1,7 +1,7 @@
@@ -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..af943bac9 100644 --- a/desktop/src/app/stacker/stacker.component.ts +++ b/desktop/src/app/stacker/stacker.component.ts @@ -4,7 +4,7 @@ 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({ @@ -12,8 +12,9 @@ import { AppComponent } from '../app.component' templateUrl: './stacker.component.html', }) export class StackerComponent implements AfterViewInit { - running = false - readonly request = structuredClone(EMPTY_STACKING_REQUEST) + protected running = false + protected readonly preference = structuredClone(DEFAULT_STACKER_PREFERENCE) + protected request = this.preference.request get referenceTarget() { return this.request.targets.find((e) => e.enabled && e.reference && e.type === 'LIGHT') @@ -29,10 +30,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 +44,11 @@ export class StackerComponent implements AfterViewInit { this.running = await this.api.stackerIsRunning() } - async openImages() { + 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 +70,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 +88,11 @@ export class StackerComponent implements AfterViewInit { } } - openTargetImage(target: StackingTarget) { - return this.browserWindow.openImage({ path: target.path, id: 'stacker', source: 'PATH' }) + protected openTargetImage(target: StackingTarget) { + return 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 +100,13 @@ 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 + 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 +117,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/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.ts b/desktop/src/shared/components/camera-info/camera-info.component.ts index dd180f422..4e62ec9c4 100644 --- a/desktop/src/shared/components/camera-info/camera-info.component.ts +++ b/desktop/src/shared/components/camera-info/camera-info.component.ts @@ -1,6 +1,6 @@ import { Component, Input } 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', @@ -9,16 +9,16 @@ import { FilterWheel } from '../../types/wheel.types' }) 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 get hasFilter() { return !!this.wheel && !!this.info.filterPosition && this.wheel.connected 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..a2b835954 100644 --- a/desktop/src/shared/components/device-chooser/device-chooser.component.ts +++ b/desktop/src/shared/components/device-chooser/device-chooser.component.ts @@ -12,22 +12,22 @@ import { MenuItem } from '../menu-item/menu-item.component' }) 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..ce6d0db6b 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts @@ -1,4 +1,4 @@ -import { Component, 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 { isGuideHead } from '../../types/camera.types' @@ -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() @@ -45,9 +49,11 @@ export class DeviceListMenuComponent { constructor(private readonly prime: PrimeService) {} - 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) @@ -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/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..903a9e8c3 100644 --- a/desktop/src/shared/components/menu-bar/menu-bar.component.html +++ b/desktop/src/shared/components/menu-bar/menu-bar.component.html @@ -39,7 +39,10 @@ } @else { + [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.ts b/desktop/src/shared/components/menu-item/menu-item.component.ts index 85f0ff880..605102cd0 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.ts +++ b/desktop/src/shared/components/menu-item/menu-item.component.ts @@ -57,5 +57,5 @@ export interface SlideMenuItem extends MenuItem { }) export class MenuItemComponent { @Input({ required: true }) - readonly item!: MenuItem + protected readonly item!: MenuItem } diff --git a/desktop/src/shared/components/moon/moon.component.ts b/desktop/src/shared/components/moon/moon.component.ts index 77a77e97c..4825b51b1 100644 --- a/desktop/src/shared/components/moon/moon.component.ts +++ b/desktop/src/shared/components/moon/moon.component.ts @@ -6,20 +6,20 @@ import { AfterViewInit, Component, ElementRef, Input, OnChanges, ViewChild } fro styleUrls: ['./moon.component.scss'], }) 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() 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..4191b402f 100644 --- a/desktop/src/shared/components/path-chooser/path-chooser.component.ts +++ b/desktop/src/shared/components/path-chooser/path-chooser.component.ts @@ -1,57 +1,46 @@ -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 readonly path?: string @Output() readonly pathChange = new EventEmitter() constructor(private readonly electron: 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 })) if (path) { - this.path = path this.pathChange.emit(path) localStorage.setItem(key, path) } 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..a180c25cc 100644 --- a/desktop/src/shared/components/slide-menu/slide-menu.component.ts +++ b/desktop/src/shared/components/slide-menu/slide-menu.component.ts @@ -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/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..9a60a74ca 100644 --- a/desktop/src/shared/dialogs/confirm/confirm.dialog.ts +++ b/desktop/src/shared/dialogs/confirm/confirm.dialog.ts @@ -4,8 +4,7 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog' import { PrimeService } from '../../services/prime.service' @Component({ - templateUrl: './confirm.dialog.html', - styleUrls: ['./confirm.dialog.scss'], + templateUrl: './confirm.dialog.html' }) export class ConfirmDialog { readonly header: string diff --git a/desktop/src/shared/dialogs/location/location.dialog.html b/desktop/src/shared/dialogs/location/location.dialog.html index e7b01f44e..8bb727601 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.html +++ b/desktop/src/shared/dialogs/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.scss b/desktop/src/shared/dialogs/location/location.dialog.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/shared/dialogs/location/location.dialog.ts b/desktop/src/shared/dialogs/location/location.dialog.ts index 1b7e979b8..be8865098 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.ts +++ b/desktop/src/shared/dialogs/location/location.dialog.ts @@ -1,12 +1,11 @@ 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' @Component({ selector: 'neb-location', templateUrl: './location.dialog.html', - styleUrls: ['./location.dialog.scss'], }) export class LocationDialog implements AfterViewInit { @ViewChild('map') @@ -27,7 +26,7 @@ export class LocationDialog implements AfterViewInit { @Optional() config?: DynamicDialogConfig, ) { if (config) { - this.location = config.data ?? structuredClone(EMPTY_LOCATION) + this.location = config.data ?? structuredClone(DEFAULT_LOCATION) } } 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/location.interceptor.ts b/desktop/src/shared/interceptors/location.interceptor.ts index 3efc4f402..b45d9a143 100644 --- a/desktop/src/shared/interceptors/location.interceptor.ts +++ b/desktop/src/shared/interceptors/location.interceptor.ts @@ -11,10 +11,10 @@ export class LocationInterceptor implements HttpInterceptor { intercept(req: HttpRequest, next: HttpHandler): Observable> { if (req.urlWithParams.includes('hasLocation')) { - const location = this.preference.selectedLocation.get() + const { location, locations } = this.preference.settings.get() req = req.clone({ - headers: req.headers.set(LocationInterceptor.HEADER_KEY, JSON.stringify(location)), + headers: req.headers.set(LocationInterceptor.HEADER_KEY, JSON.stringify(locations[location])), }) } diff --git a/desktop/src/shared/pipes/dropdown-options.pipe.ts b/desktop/src/shared/pipes/dropdown-options.pipe.ts index 0f231c974..7daf27113 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -3,6 +3,7 @@ import { Hemisphere } from '../types/alignment.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 { ConnectionType } from '../types/home.types' import { Bitpix, ImageChannel, ImageFormat, SCNRProtectionMethod } from '../types/image.types' import { MountRemoteControlType } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' @@ -34,6 +35,7 @@ export interface DropdownOptions { STACKER: StackerType[] SETTINGS_TAB: SettingsTabKey[] STACKER_GROUP_TYPE: StackerGroupType[] + CONNECTION_TYPE: ConnectionType[] } @Pipe({ name: 'dropdownOptions' }) @@ -84,6 +86,8 @@ 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] } return [] diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index a681c0a5a..607919209 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -2,7 +2,7 @@ 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' @@ -43,6 +43,7 @@ export type EnumPipeKey = | StackerGroupType | SettingsTabKey | SequencerState + | ExposureTimeUnit | 'ALL' @Pipe({ name: 'enum' }) @@ -271,11 +272,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 +366,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/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 0c5d9f7d4..5cd060b33 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -10,7 +10,7 @@ import { FlatWizardRequest } from '../types/flat-wizard.types' import { Focuser } from '../types/focuser.types' import { HipsSurvey } from '../types/framing.types' import { GuideDirection, GuideOutput, Guider, GuiderHistoryStep, SettleInfo } from '../types/guider.types' -import { ConnectionStatus, ConnectionType, Equipment } from '../types/home.types' +import { ConnectionStatus, ConnectionType } 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 { PlateSolverRequest } from '../types/platesolver.types' @@ -18,7 +18,7 @@ import { Rotator } from '../types/rotator.types' import { SequencePlan } 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' @@ -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) } @@ -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`) } diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 1cf09cac6..0b2b1d638 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -1,14 +1,14 @@ 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' @@ -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' }) } @@ -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/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 06d1637e7..1e275d470 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -22,7 +22,7 @@ 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[] = [ { name: 'All', extensions: ['fits', 'fit', 'xisf'] }, @@ -49,9 +49,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 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..832b42255 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 { AlignmentPreference, alignmentPreferenceWithDefault, DEFAULT_ALIGNMENT_PREFERENCE } from '../types/alignment.types' +import { DEFAULT_SKY_ATLAS_PREFERENCE, SkyAtlasPreference } from '../types/atlas.types' +import { AutoFocusPreference, autoFocusPreferenceWithDefault, DEFAULT_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 { 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, FOV, ImagePreference } 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 } 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,47 @@ 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 home = new PreferenceData(this.storage, 'home', () => structuredClone(DEFAULT_HOME_PREFERENCE), homePreferenceWithDefault) + readonly imagePreference = new PreferenceData(this.storage, 'image', () => structuredClone(DEFAULT_IMAGE_PREFERENCE)) + readonly skyAtlasPreference = new PreferenceData(this.storage, 'atlas', () => structuredClone(DEFAULT_SKY_ATLAS_PREFERENCE)) + readonly alignment = new PreferenceData(this.storage, 'alignment', () => structuredClone(DEFAULT_ALIGNMENT_PREFERENCE), alignmentPreferenceWithDefault) 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 sequencerPreference = new PreferenceData(this.storage, 'sequencer', () => structuredClone(DEFAULT_SEQUENCER_PREFERENCE)) + 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/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/app.types.ts b/desktop/src/shared/types/app.types.ts index 5c95403fe..f2bef9531 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -89,14 +89,3 @@ export interface JsonFile { } 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 diff --git a/desktop/src/shared/types/atlas.types.ts b/desktop/src/shared/types/atlas.types.ts index 8ef9bca6b..656afae5e 100644 --- a/desktop/src/shared/types/atlas.types.ts +++ b/desktop/src/shared/types/atlas.types.ts @@ -19,7 +19,7 @@ export interface SearchFilter { types: (SkyObjectType | 'ALL')[] } -export const EMPTY_SEARCH_FILTER: SearchFilter = { +export const DEFAULT_SEARCH_FILTER: SearchFilter = { text: '', rightAscension: '00h00m00s', declination: `+000°00'00"`, @@ -40,7 +40,7 @@ export interface SkyAtlasPreference { fast: boolean } -export const EMPTY_SKY_ATLAS_PREFERENCE: SkyAtlasPreference = { +export const DEFAULT_SKY_ATLAS_PREFERENCE: SkyAtlasPreference = { satellites: [], fast: false, } @@ -342,7 +342,7 @@ export interface BodyPosition extends EquatorialCoordinate, EquatorialCoordinate leading: boolean } -export const EMPTY_BODY_POSITION: BodyPosition = { +export const DEFAULT_BODY_POSITION: BodyPosition = { rightAscensionJ2000: '00h00m00s', declinationJ2000: `+000°00'00"`, rightAscension: '00h00m00s', @@ -438,7 +438,7 @@ export interface ComputedLocation extends EquatorialCoordinate, EquatorialCoordi pierSide: PierSide } -export const EMPTY_COMPUTED_LOCATION: ComputedLocation = { +export const DEFAULT_COMPUTED_LOCATION: ComputedLocation = { constellation: 'AND', meridianAt: '00:00', timeLeftToMeridianFlip: '00:00', @@ -525,7 +525,7 @@ export interface Location { offsetInMinutes: number } -export const EMPTY_LOCATION: Location = { +export const DEFAULT_LOCATION: Location = { id: 0, name: 'Null Island', latitude: 0, 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/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 4948c7139..60136dc83 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,187 @@ export type ExposureMode = 'SINGLE' | 'FIXED' | 'LOOP' export type LiveStackerType = 'SIRIL' | 'PIXINSIGHT' +export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' | 'EXPOSURING' | 'WAITING' | 'SETTLING' | 'DITHERING' | 'STACKING' | 'PAUSING' | 'PAUSED' | 'EXPOSURE_FINISHED' | 'CAPTURE_FINISHED' + export enum ExposureTimeUnit { - MINUTE = 'm', - SECOND = 's', - MILLISECOND = 'ms', - MICROSECOND = 'µs', + MINUTE = 'MINUTE', + SECOND = 'SECOND', + MILLISECOND = 'MILLISECOND', + MICROSECOND = 'MICROSECOND', } -export function isCamera(device?: Device): device is Camera { - return !!device && 'exposuring' in device +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 isGuideHead(device?: Device): device is GuideHead { - return isCamera(device) && isCompanionDevice(device) && !!device.main +export interface GuideHead extends Camera, CompanionDevice {} + +export interface Dither { + enabled: boolean + amount: number + raOnly: boolean + afterExposures: number } -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 CameraCaptureNamingFormat { + light?: string + dark?: string + flat?: string + bias?: string +} + +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 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 const EMPTY_CAMERA: Camera = { +export interface CameraNamingFormatDialog { + showDialog: boolean + format: CameraCaptureNamingFormat +} + +export const DEFAULT_CAMERA: Camera = { + type: 'CAMERA', sender: '', id: '', exposuring: false, @@ -139,41 +256,48 @@ 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 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 DEFAULT_DITHER: Dither = { + enabled: false, + amount: 1.5, + raOnly: false, + afterExposures: 5, } -export const EMPTY_CAMERA_START_CAPTURE: CameraStartCapture = { +export const DEFAULT_CAMERA_START_CAPTURE: CameraStartCapture = { + enabled: true, exposureTime: 1, exposureAmount: 1, exposureDelay: 0, @@ -188,22 +312,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: 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)) @@ -220,105 +379,54 @@ export function updateCameraStartCaptureFromCamera(request: CameraStartCapture, if (camera.frameFormats.length && (!request.frameFormat || !camera.frameFormats.includes(request.frameFormat))) request.frameFormat = camera.frameFormats[0] } -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 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 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 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 CameraPreference extends CameraStartCapture { - setpointTemperature: number - exposureTimeUnit: ExposureTimeUnit - exposureMode: ExposureMode - subFrame: boolean +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 const EMPTY_CAMERA_PREFERENCE: CameraPreference = { - ...EMPTY_CAMERA_START_CAPTURE, - setpointTemperature: 0, - exposureTimeUnit: ExposureTimeUnit.MICROSECOND, - exposureMode: 'SINGLE', - subFrame: false, -} - -export interface CameraStepInfo { - remainingTime: number - progress: number - elapsedTime: number -} - -export const EMPTY_CAMERA_STEP_INFO: CameraStepInfo = { - remainingTime: 0, - progress: 0, - elapsedTime: 0, -} - -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..98ce89892 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 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..d18844f8e 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -55,7 +55,7 @@ export interface ImageSolved extends EquatorialCoordinateJ2000 { radius: number } -export const EMPTY_IMAGE_SOLVED: ImageSolved = { +export const DEFAULT_IMAGE_SOLVED: ImageSolved = { solved: false, orientation: 0, scale: 0, @@ -136,7 +136,7 @@ export interface ImagePreference { starDetection?: StarDetectionImagePreference } -export const EMPTY_IMAGE_PREFERENCE: ImagePreference = { +export const DEFAULT_IMAGE_PREFERENCE: ImagePreference = { solver: { type: 'ASTAP', radius: 4, diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts index 3f9c409f6..f37ecc686 100644 --- a/desktop/src/shared/types/mount.types.ts +++ b/desktop/src/shared/types/mount.types.ts @@ -42,7 +42,38 @@ export interface Mount extends EquatorialCoordinate, GPS, GuideOutput, Parkable guideRateNS: number } -export const EMPTY_MOUNT: Mount = { +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 interface MountRemoteControlDialog { + showDialog: boolean + type: MountRemoteControlType + host: string + port: number + data: 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 +105,22 @@ 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 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..c573e17c5 100644 --- a/desktop/src/shared/types/platesolver.types.ts +++ b/desktop/src/shared/types/platesolver.types.ts @@ -1,21 +1,23 @@ 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 +} + +export interface PlateSolverRequest extends PlateSolverSettings { + type: PlateSolverType 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 +25,28 @@ export const EMPTY_PLATE_SOLVER_REQUEST: PlateSolverRequest = { timeout: 300, slot: 1, } + +export const DEFAULT_PLATE_SOLVER_REQUEST: PlateSolverRequest = { + ...DEFAULT_PLATE_SOLVER_SETTINGS, + type: 'ASTAP', +} + +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..48f85619f 100644 --- a/desktop/src/shared/types/sequencer.types.ts +++ b/desktop/src/shared/types/sequencer.types.ts @@ -1,5 +1,4 @@ -import type { AutoSubFolderMode, CameraCaptureEvent, CameraStartCapture, Dither } from './camera.types' -import type { CameraCaptureNamingFormat } from './settings.types' +import { DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, DEFAULT_DITHER, type AutoSubFolderMode, type CameraCaptureEvent, type CameraCaptureNamingFormat, type CameraStartCapture, type Dither } from './camera.types' export type SequenceCaptureMode = 'FULLY' | 'INTERLEAVED' @@ -39,33 +38,6 @@ export interface SequencePlan { 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: {}, -} - export interface SequencerEvent extends MessageEvent { id: number elapsedTime: number @@ -80,6 +52,30 @@ export interface SequencerPreference { plan: SequencePlan } -export const EMPTY_SEQUENCER_PREFERENCE: SequencerPreference = { - plan: structuredClone(EMPTY_SEQUENCE_PLAN), +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_PLAN: SequencePlan = { + initialDelay: 0, + captureMode: 'FULLY', + autoSubFolderMode: 'OFF', + entries: [], + dither: DEFAULT_DITHER, + autoFocus: DEFAULT_AUTO_FOCUS_AFTER_CONDITIONS, + namingFormat: DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, +} + +export const DEFAULT_SEQUENCER_PREFERENCE: SequencerPreference = { + plan: structuredClone(DEFAULT_SEQUENCE_PLAN), } diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts index 402676bb3..c21133100 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -1,24 +1,78 @@ -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 } 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: number // selected location index +} + +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: 0, } -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 ??= source.location + + if (!preference.locations?.length) { + preference.locations = structuredClone(source.locations) + preference.location = 0 + } + + return preference as SettingsPreference } export function resetCameraCaptureNamingFormat(type: FrameType, format: CameraCaptureNamingFormat, defaultValue?: CameraCaptureNamingFormat) { 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..5287170a7 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 isFilterWheel(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..d8421db68 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; } 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..44fe8ddb5 100644 --- a/desktop/tsconfig.serve.json +++ b/desktop/tsconfig.serve.json @@ -9,11 +9,12 @@ "module": "CommonJS", "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-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-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-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt index 04a6d68df..97404a9ae 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=%f, 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), From 0cd0e83d2a979989f3bc57a1a84554a95f10c886 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 3 Aug 2024 00:02:30 -0300 Subject: [PATCH 2/9] [api][desktop]: Improve Calibration --- api/schemas/objectbox.json | 92 ++-- .../calibration/CalibrationFrameController.kt | 31 +- .../api/calibration/CalibrationFrameEntity.kt | 31 +- .../api/calibration/CalibrationFrameGroup.kt | 8 - .../calibration/CalibrationFrameRepository.kt | 22 +- .../calibration/CalibrationFrameService.kt | 62 ++- .../api/calibration/CalibrationGroupKey.kt | 28 - desktop/app/window.manager.ts | 26 +- desktop/src/app/about/about.component.ts | 2 +- desktop/src/app/app.module.ts | 2 + .../calibration/calibration.component.html | 216 ++++---- .../calibration/calibration.component.scss | 31 -- .../app/calibration/calibration.component.ts | 483 ++++++++++++------ desktop/src/app/home/home.component.html | 12 +- desktop/src/app/home/home.component.ts | 22 +- .../src/app/stacker/stacker.component.html | 2 +- desktop/src/app/stacker/stacker.component.ts | 21 +- desktop/src/assets/icons/calibration.png | Bin 0 -> 862 bytes desktop/src/assets/icons/photo-filter.png | Bin 1657 -> 0 bytes .../menu-item/menu-item.component.ts | 3 + desktop/src/shared/pipes/path.pipe.ts | 26 + desktop/src/shared/services/api.service.ts | 9 +- .../shared/services/browser-window.service.ts | 2 +- .../src/shared/services/preference.service.ts | 4 +- desktop/src/shared/types/calibration.types.ts | 41 +- desktop/src/shared/types/home.types.ts | 2 +- desktop/src/shared/types/image.types.ts | 14 +- desktop/src/styles.scss | 4 +- 28 files changed, 679 insertions(+), 517 deletions(-) delete mode 100644 api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/calibration/CalibrationGroupKey.kt delete mode 100644 desktop/src/app/calibration/calibration.component.scss create mode 100644 desktop/src/assets/icons/calibration.png delete mode 100644 desktop/src/assets/icons/photo-filter.png create mode 100644 desktop/src/shared/pipes/path.pipe.ts 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/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/desktop/app/window.manager.ts b/desktop/app/window.manager.ts index 9959d0081..2e0b1d518 100644 --- a/desktop/app/window.manager.ts +++ b/desktop/app/window.manager.ts @@ -298,8 +298,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'] @@ -321,7 +325,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, { @@ -337,7 +341,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, { @@ -353,7 +357,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) => { @@ -373,7 +377,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) @@ -386,7 +390,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() @@ -405,30 +409,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/src/app/about/about.component.ts b/desktop/src/app/about/about.component.ts index 9687dc168..16dd1149c 100644 --- a/desktop/src/app/about/about.component.ts +++ b/desktop/src/app/about/about.component.ts @@ -43,8 +43,8 @@ export class AboutComponent { 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}/photo-filter_4892829`, name: 'Photo filter', author: 'Freepik - 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() { diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 1e0b95071..3de22f9f2 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -96,6 +96,7 @@ import { RotatorComponent } from './rotator/rotator.component' import { SequencerComponent } from './sequencer/sequencer.component' import { SettingsComponent } from './settings/settings.component' import { StackerComponent } from './stacker/stacker.component' +import { PathPipe } from '../shared/pipes/path.pipe' @NgModule({ declarations: [ @@ -140,6 +141,7 @@ import { StackerComponent } from './stacker/stacker.component' MountComponent, NoDropdownDirective, PathChooserComponent, + PathPipe, RotatorComponent, SequencerComponent, SettingsComponent, 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 627dc317d..000000000 --- a/desktop/src/app/calibration/calibration.component.scss +++ /dev/null @@ -1,31 +0,0 @@ -neb-calibration { - .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 e47421233..7fc78f33a 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -1,264 +1,411 @@ -import { AfterViewInit, Component, ViewEncapsulation } 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) { + console.log('selected frame removed', frame) + 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/home/home.component.html b/desktop/src/app/home/home.component.html index 7f00a41a1..f2de2bf52 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -236,6 +236,15 @@
Flat Wizard
+
+ + +
Calibration
+
+
+ (deviceDisconnect)="deviceDisconnected($event)" /> { - if (isCamera(device) && !isGuideHead(device)) { - return [ - { - icon: 'mdi mdi-wrench', - label: 'Calibration', - command: (e) => { - e.originalEvent?.stopImmediatePropagation() - return this.browserWindowService.openCalibration() - }, - }, - ] - } else { - return [] - } - } - private async openDevice(type: K) { this.deviceModel.length = 0 @@ -475,6 +458,9 @@ export class HomeComponent implements AfterContentInit { case 'CALCULATOR': await this.browserWindowService.openCalculator() break + case 'CALIBRATION': + await this.browserWindowService.openCalibration() + break case 'ABOUT': await this.browserWindowService.openAbout() break diff --git a/desktop/src/app/stacker/stacker.component.html b/desktop/src/app/stacker/stacker.component.html index 5c9e9ca81..4cc290230 100644 --- a/desktop/src/app/stacker/stacker.component.html +++ b/desktop/src/app/stacker/stacker.component.html @@ -31,7 +31,7 @@ [metaKeySelection]="false" [style]="{ width: '100%', height: '296px' }" [listStyle]="{ maxHeight: '296px', height: '296px' }" - emptyMessage="No files"> + emptyMessage="No frames"> diff --git a/desktop/src/app/stacker/stacker.component.ts b/desktop/src/app/stacker/stacker.component.ts index af943bac9..35ed8c021 100644 --- a/desktop/src/app/stacker/stacker.component.ts +++ b/desktop/src/app/stacker/stacker.component.ts @@ -1,4 +1,4 @@ -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' @@ -11,11 +11,13 @@ import { AppComponent } from '../app.component' selector: 'neb-stacker', templateUrl: './stacker.component.html', }) -export class StackerComponent implements AfterViewInit { +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') } @@ -44,6 +46,11 @@ export class StackerComponent implements AfterViewInit { this.running = await this.api.stackerIsRunning() } + @HostListener('window:unload') + ngOnDestroy() { + void this.closeFrameWindow() + } + protected async openImages() { try { this.running = true @@ -88,8 +95,8 @@ export class StackerComponent implements AfterViewInit { } } - protected openTargetImage(target: StackingTarget) { - return this.browserWindowService.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' }) } protected deleteTarget(target: StackingTarget) { @@ -100,6 +107,12 @@ export class StackerComponent implements AfterViewInit { } } + private async closeFrameWindow() { + if (this.frameId) { + await this.electronService.closeWindow(undefined, this.frameId) + } + } + protected async startStacking() { const settings = this.preferenceService.settings.get() diff --git a/desktop/src/assets/icons/calibration.png b/desktop/src/assets/icons/calibration.png new file mode 100644 index 0000000000000000000000000000000000000000..77f50c6ae1a95cc59e43e14ace9097b3b5ea8f11 GIT binary patch literal 862 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=EX7WqAsj$Z!;#Vf4nJ zFx~)R#$PeZihzQWC9V-A!TD(=<%vb94CUqJdYO6I#mR{Use1WE>9gP2NHH)l-S%{G z45^s&cD8S}aG*%r{p$2tTRc}AE?c^G$A&OXuBcT?9gD1&+8>y^;GIiMSHK0OrWZ~w z7hCHO_=-1ii#I87G$nCg6=TufqG`I)H#2Fnr&0WI->FK%Mdsfes^^6!$epWw&pr7} ziiCpzi=)6comc1EWAv`S+;vdVVWoy4$1Cd?^E=5#TcXyM?p}92@sU{f(O9*(wQ=3k zwf%N?&i+5sKSZlfw$w*+QHj>)7q6zCKHspF>De=}Jcd_h4Hb(Q&U=2aP-gPijhlj8 z)`poD74MugF}zUb2{)_q_s>sMK6Q8V_G_;#Oewff`|F3}opWj8dV(vf-6orJinRSO zF%;ZwXS8ck=NFE01-)a(j|WRFuzRNv6C%Sd)$ok(5Zgtudw-;Y7|I#)mN2|yxZ}Zi zhjGV5rX9=$%B%(K58SvO@EzzCIUw60t=VABU=CKet%kAe(@$^X&3n80m=*ubpc*$^EXIIf7bCJj^F=*ie4oPeZZV|LnW|Nf+$mpv1AKlNusI1AzajkV5!!5qG6+rBX?WYjfg3LfOQ5r~?# zORtjS?S+h5*V2YryQh6h^k0-`Vj`=zcz={p(cz=b2eQJ0^%*{tE&KV5QQ_jFBs0V8 zC+qb7Glb3F&G_%Srahw}c&~;yifc%1Y zKN8p9PiZvoyjF5;lhnTI4f9rB>NFG1x9(bXeg1ZzJ2{q%>nblMi#FVqef9Tlbh+QG hGeW?W*5Z36UwrwqPrtk~6M=bw!PC{xWt~$(698CEb@KoK literal 0 HcmV?d00001 diff --git a/desktop/src/assets/icons/photo-filter.png b/desktop/src/assets/icons/photo-filter.png deleted file mode 100644 index d3f7a892d75eaa33910f5b86a1205413c173088f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1657 zcmV-<28Q{GP)fUJ9K`vTe0zr>rflbV^-C7Wprhc@fh_?%gn33gh(J$JvBpa6017eN!KDLR0u<`C_~|#a3&<`O z%L^uJmBjdo2;ZUd8>DpNJdUj~AXhe9u)RfxSZ81*8RjT4eg#0I9+g@$*oh$DPKc(l z)dB>Xc?VK8u#iOgVJ!JkG=hZr1dTcy5t_%A03q?1Y1=_jlHjQ%$`MpfP4jes2>TeO z8d5Zt*b<;qtV6?ZXr~dNLwRAEr}{|pXAEkQ#5hTq&(UeM9IFww1c-`!Zg{pB+BJE$ z5hHhsCuWnMSfZ5(yNGepLZrsno(J4k-k?d@hCanOs^g>4vz=Du^(jh@CjXaZpORsQ zsU}Jll8}w4AH>m2u*LHh9m;x2^gqa!tw^B|5=&?kuMidmQ!WJUR3^0p@$~Ie2PZB<+3&ekU$NH6TnqSI4M#=?2X%}oFSqdCd+@< zQcf1ffEbAVz@PC;0muM|*U z5ugr49Z(GvgM1c5J8;o_^)rMy0YnGL-5~CoFm40U<}9i-B97nC4c494K~w^LjvUFp zEY=5x$x)myb7Sq!VgW!Aswd$8EO84E{{VY{uw#G8NfUr#T9_yssb;!)l_ zS#C;x1gYuXDFl!pjsZWWJ)oK3J^}9g%sm855MY|m+#Dw^fam~r17<#Ra|#QdD;4`b}$>eJzt zb`Qa39{^qb3g@r-lJ3Ltj!?3>H{>B2i@%qS-pRW>-2+~_`gJ-x<|V8nD?_IwKVr(C zs|2V%RlxacIb+vqECvjWUH^VLkD8`T`un;K>nffP-)w3zv~x`t=m~izCGV8BUbt@1 zQouz5WCwJzGm|&eD?(6|A2qadO&9Qcbv}BtjdKk-#8d%5SHGlo!e0!^3Zj&k#;9+U z$OLOEpAR;!>Lw?{SWwpp08p|p%H|E@Z|&M0FSWt(zx}ORI>_4_B5bZm!hXOAuIU0( zQM^zm!#n<}mlhkHG+W12E#Q9gvUEP)GRV20pYl?bHOn3{0RSq>R4U4*PW;R@9}RaE zUanAidWpRO)b#>P$EDj|zW+G@!09VK4(%8uKg;^AldH#oMBHj04}Dk_-08M=Ht4Dr z(AMLprXgo+Dm)$ajd^tRgqgTmd-QApSr_ALg$$o$rNEVL-nCSp!nnwc6 zbO19=It6gu8JzG4HGKj|`95$vZT<`p9-%53{$2kAgN^?Ks#Ak>00000NkvXXu0mjf DMA+lo 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 605102cd0..3d25971c2 100644 --- a/desktop/src/shared/components/menu-item/menu-item.component.ts +++ b/desktop/src/shared/components/menu-item/menu-item.component.ts @@ -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 { 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/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 5cd060b33..4972131e5 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -3,7 +3,7 @@ import moment from 'moment' import { DARVStart, TPPAStart } from '../types/alignment.types' import { Angle, BodyPosition, CloseApproach, ComputedLocation, Constellation, DeepSkyObject, MinorPlanet, Satellite, SatelliteGroupType, SkyObjectType, Twilight } from '../types/atlas.types' import { AutoFocusRequest } from '../types/autofocus.type' -import { CalibrationFrame, CalibrationFrameGroup } from '../types/calibration.types' +import { 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' @@ -576,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) { @@ -584,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) { diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 0b2b1d638..ff3724398 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -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' }) } diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index 832b42255..f0635a816 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core' import { AlignmentPreference, alignmentPreferenceWithDefault, DEFAULT_ALIGNMENT_PREFERENCE } from '../types/alignment.types' import { DEFAULT_SKY_ATLAS_PREFERENCE, SkyAtlasPreference } from '../types/atlas.types' import { AutoFocusPreference, autoFocusPreferenceWithDefault, DEFAULT_AUTO_FOCUS_PREFERENCE } from '../types/autofocus.type' -import { CalibrationPreference } from '../types/calibration.types' +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' @@ -86,7 +86,7 @@ export class PreferenceService { readonly skyAtlasPreference = new PreferenceData(this.storage, 'atlas', () => structuredClone(DEFAULT_SKY_ATLAS_PREFERENCE)) readonly alignment = new PreferenceData(this.storage, 'alignment', () => structuredClone(DEFAULT_ALIGNMENT_PREFERENCE), alignmentPreferenceWithDefault) readonly imageFOVs = new PreferenceData(this.storage, 'image.fovs', () => []) - readonly calibrationPreference = new PreferenceData(this.storage, 'calibration', () => ({}) as CalibrationPreference) + readonly calibrationPreference = new PreferenceData(this.storage, 'calibration', () => structuredClone(DEFAULT_CALIBRATION_PREFERENCE), calibrationPreferenceWithDefault) readonly sequencerPreference = new PreferenceData(this.storage, 'sequencer', () => structuredClone(DEFAULT_SEQUENCER_PREFERENCE)) readonly stacker = new PreferenceData(this.storage, 'stacker', () => structuredClone(DEFAULT_STACKER_PREFERENCE), stackerPreferenceWithDefault) readonly guider = new PreferenceData(this.storage, 'guider', () => structuredClone(DEFAULT_GUIDER_PREFERENCE), guiderPreferenceWithDefault) 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/home.types.ts b/desktop/src/shared/types/home.types.ts index 98ce89892..5004688fb 100644 --- a/desktop/src/shared/types/home.types.ts +++ b/desktop/src/shared/types/home.types.ts @@ -1,6 +1,6 @@ import type { DeviceType } from './device.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 type ConnectionType = 'INDI' | 'ALPACA' diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index d18844f8e..2da2f3a1f 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -1,6 +1,6 @@ import type { Point, Size } from 'electron' import type { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from './atlas.types' -import type { Camera, CameraStartCapture } from './camera.types' +import type { Camera, CameraStartCapture, FrameType } from './camera.types' import type { PlateSolverRequest } from './platesolver.types' import type { StarDetectionRequest } from './stardetector.types' @@ -16,6 +16,18 @@ export type Bitpix = 'BYTE' | 'SHORT' | 'INTEGER' | 'LONG' | 'FLOAT' | 'DOUBLE' export type LiveStackingMode = 'NONE' | 'RAW' | 'STACKED' +export interface Image { + type: FrameType + width: number + height: number + binX: number + binY: number + exposureTime: number + temperature?: number + gain: number + filter?: string +} + export interface FITSHeaderItem { name: string value: string diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index d8421db68..7339a0894 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -483,8 +483,8 @@ p-tieredmenu *, } ::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 4px; + height: 4px; } ::-webkit-scrollbar-thumb { From a773eee674473fd642d3591dba50f4aaca1a59bd Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 3 Aug 2024 11:10:10 -0300 Subject: [PATCH 3/9] [desktop]: Generate application info on build and use it on About --- desktop/app/main.ts | 2 +- desktop/eslint.config.mjs | 4 ++ desktop/package.json | 8 ++-- desktop/scripts/nebulosa.mjs | 47 +++++++++++++++++++ desktop/src/app/about/about.component.html | 12 +++-- desktop/src/app/about/about.component.ts | 21 ++++----- desktop/src/app/home/home.component.ts | 5 +- desktop/src/assets/data/.gitignore | 1 + desktop/src/shared/pipes/skyObject.pipe.ts | 6 +-- .../src/shared/services/electron.service.ts | 6 +-- .../shared/services/remote-storage.service.ts | 30 ------------ 11 files changed, 82 insertions(+), 60 deletions(-) create mode 100644 desktop/scripts/nebulosa.mjs create mode 100644 desktop/src/assets/data/.gitignore delete mode 100644 desktop/src/shared/services/remote-storage.service.ts diff --git a/desktop/app/main.ts b/desktop/app/main.ts index 59c2ea254..4f3c7b2ad 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -69,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/eslint.config.mjs b/desktop/eslint.config.mjs index 6176d33fc..32b27756c 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', diff --git a/desktop/package.json b/desktop/package.json index bf67d2c86..e6e43e6e4 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -12,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", @@ -31,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", 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 f63d817d8..7c59df1fe 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -38,11 +38,17 @@
-

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 (icon of icons; track $index) {
@for (dep of dependencies; track $index) { (type: K) { + private async openDevice(type: keyof MappedDevice) { this.deviceModel.length = 0 const devices: Device[] = @@ -362,8 +362,7 @@ export class HomeComponent implements AfterContentInit { : type === 'MOUNT' ? this.mounts : type === 'FOCUSER' ? this.focusers : type === 'WHEEL' ? this.wheels - : type === 'ROTATOR' ? this.rotators - : [] + : this.rotators if (devices.length === 0) return 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/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/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 1e275d470..9609f6eeb 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' 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) - } -} From f9367abc8a2d89f9004e95ac0ee7e55ddf2874bb Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 3 Aug 2024 12:00:58 -0300 Subject: [PATCH 4/9] [desktop]: Move the buttons that open Camera's Image --- desktop/eslint.config.mjs | 7 + desktop/src/app/home/home.component.html | 8 +- desktop/src/app/home/home.component.ts | 293 ++++++++++++----------- desktop/src/shared/types/wheel.types.ts | 2 +- 4 files changed, 169 insertions(+), 141 deletions(-) diff --git a/desktop/eslint.config.mjs b/desktop/eslint.config.mjs index 32b27756c..900b0beff 100644 --- a/desktop/eslint.config.mjs +++ b/desktop/eslint.config.mjs @@ -58,6 +58,13 @@ export default tseslint.config( ignorePrimitives: true, }, ], + '@typescript-eslint/no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + }, + ], }, }, ) diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index f2de2bf52..6dbb5e008 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -359,9 +359,5 @@ #deviceMenu [disableIfDeviceIsNotConnected]="false" (deviceConnect)="deviceConnected($event)" - (deviceDisconnect)="deviceDisconnected($event)" /> - + (deviceDisconnect)="deviceDisconnected($event)" + [toolbarBuilder]="deviceMenuToolbarBuilder" /> diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index cb49b7d6e..24ad324c9 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -8,23 +8,15 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' -import { Camera } from '../../shared/types/camera.types' -import { Device } from '../../shared/types/device.types' -import { Focuser } from '../../shared/types/focuser.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 { Mount } from '../../shared/types/mount.types' -import { Rotator } from '../../shared/types/rotator.types' -import { Wheel } from '../../shared/types/wheel.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: Wheel - ROTATOR: Rotator -} - function scrollPageOf(element: Element) { return parseInt(element.getAttribute('scroll-page') ?? '0') } @@ -58,7 +50,7 @@ export class HomeComponent implements AfterContentInit { label: 'Open new image', slideMenu: [], command: () => { - return this.openImage(true) + return this.openImage() }, }, ] @@ -66,9 +58,6 @@ export class HomeComponent implements AfterContentInit { @ViewChild('deviceMenu') private readonly deviceMenu!: DeviceListMenuComponent - @ViewChild('imageMenu') - private readonly imageMenu!: DeviceListMenuComponent - get connected() { return !!this.connection && this.connection.connected } @@ -133,6 +122,22 @@ export class HomeComponent implements AfterContentInit { return this.connection?.type === 'ALPACA' && this.hasDevices } + 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 [] + } + } + constructor( app: AppComponent, private readonly electronService: ElectronService, @@ -140,94 +145,89 @@ export class HomeComponent implements AfterContentInit { private readonly api: ApiService, private readonly primeService: PrimeService, private readonly preferenceService: PreferenceService, - private readonly ngZone: NgZone, + 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) - }, - ) + electronService.on('CAMERA.ATTACHED', (event) => { + ngZone.run(() => { + this.deviceAdded(event.device) + }) + }) + electronService.on(`CAMERA.DETACHED`, (event) => { + ngZone.run(() => { + this.deviceRemoved(event.device) + }) + }) + electronService.on(`CAMERA.UPDATED`, (event) => { + ngZone.run(() => { + this.deviceUpdated(event.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) + }) + }) electronService.on('CONNECTION.CLOSED', async (event) => { if (this.connection?.id === event.id) { @@ -252,24 +252,56 @@ export class HomeComponent implements AfterContentInit { } } - private startListening(type: K, onAdd: (device: MappedDevice[K]) => number, onRemove: (device: MappedDevice[K]) => number, onUpdate: (device: MappedDevice[K]) => void) { - this.electronService.on(`${type}.ATTACHED`, (event) => { - this.ngZone.run(() => { - onAdd(event.device as never) - }) - }) + 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) + } + } - this.electronService.on(`${type}.DETACHED`, (event) => { - this.ngZone.run(() => { - onRemove(event.device as never) - }) - }) + 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) + } + } - this.electronService.on(`${type}.UPDATED`, (event) => { - this.ngZone.run(() => { - onUpdate(event.device as never) - }) - }) + 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() { @@ -354,7 +386,7 @@ export class HomeComponent implements AfterContentInit { return DeviceChooserComponent.handleDisconnectDevice(this.api, event.device, event.item) } - private async openDevice(type: keyof MappedDevice) { + private async openDevice(type: DeviceType) { this.deviceModel.length = 0 const devices: Device[] = @@ -362,19 +394,20 @@ export class HomeComponent implements AfterContentInit { : type === 'MOUNT' ? this.mounts : type === 'FOCUSER' ? this.focusers : type === 'WHEEL' ? this.wheels - : this.rotators + : type === 'ROTATOR' ? this.rotators + : [] if (devices.length === 0) return 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.browserWindowService.openMount(device as Mount, { bringToFront: true }) break @@ -393,22 +426,14 @@ export class HomeComponent implements AfterContentInit { } } - private async openImage(force: boolean = false) { - if (force || this.cameras.length === 0) { - const path = await this.electronService.openImage({ defaultPath: this.preference.imagePath }) - - if (path) { - this.preference.imagePath = dirname(path) - this.savePreference() + private async openImage() { + const path = await this.electronService.openImage({ defaultPath: this.preference.imagePath }) - await this.browserWindowService.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.browserWindowService.openCameraImage(camera) - } + await this.browserWindowService.openImage({ path, source: 'PATH' }) } } diff --git a/desktop/src/shared/types/wheel.types.ts b/desktop/src/shared/types/wheel.types.ts index 5287170a7..2ebb46491 100644 --- a/desktop/src/shared/types/wheel.types.ts +++ b/desktop/src/shared/types/wheel.types.ts @@ -71,7 +71,7 @@ export const DEFAULT_WHEEL_PREFERENCE: WheelPreference = { shutterPosition: 0, } -export function isFilterWheel(device?: Device): device is Wheel { +export function isWheel(device?: Device): device is Wheel { return !!device && device.type === 'WHEEL' } From 955a670a2fb46516998c49ee6230e3b8b344d9a2 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 4 Aug 2024 15:29:53 -0300 Subject: [PATCH 5/9] [api][desktop]: Refactor Image --- .../api/image/AnnotateImageRequest.kt | 9 + ...mageAnnotation.kt => ImageAnnotatation.kt} | 2 +- .../nebulosa/api/image/ImageController.kt | 11 +- .../kotlin/nebulosa/api/image/ImageInfo.kt | 4 +- .../kotlin/nebulosa/api/image/ImageService.kt | 53 +- .../nebulosa/api/image/ImageTransformation.kt | 7 +- .../api/platesolver/PlateSolverController.kt | 12 +- .../api/platesolver/PlateSolverRequest.kt | 9 + .../api/platesolver/PlateSolverService.kt | 17 +- desktop/app/window.manager.ts | 1 - desktop/src/app/app.module.ts | 4 +- .../app/calibration/calibration.component.ts | 1 - desktop/src/app/camera/camera.component.ts | 3 +- .../src/app/camera/exposure-time.component.ts | 40 +- .../app/filterwheel/filterwheel.component.ts | 2 - desktop/src/app/image/crosshair.component.ts | 53 ++ desktop/src/app/image/image.component.html | 301 ++++--- desktop/src/app/image/image.component.scss | 52 +- desktop/src/app/image/image.component.ts | 783 ++++++++---------- .../menu-bar/menu-bar.component.html | 2 +- .../menu-item/menu-item.component.html | 2 +- .../interceptors/confirmation.interceptor.ts | 2 - .../src/shared/pipes/dropdown-options.pipe.ts | 5 +- .../src/shared/pipes/enum-dropdown.pipe.ts | 8 +- desktop/src/shared/pipes/enum.pipe.ts | 4 +- desktop/src/shared/services/api.service.ts | 16 +- .../shared/services/confirmation.service.ts | 1 - .../src/shared/services/electron.service.ts | 2 - .../src/shared/services/preference.service.ts | 5 +- desktop/src/shared/types/angular.types.ts | 4 + desktop/src/shared/types/camera.types.ts | 9 +- desktop/src/shared/types/image.types.ts | 457 +++++++--- desktop/src/shared/types/platesolver.types.ts | 16 +- desktop/tsconfig.serve.json | 3 +- .../main/kotlin/nebulosa/fits/FitsFormat.kt | 5 +- .../nebulosa/fits/SeekableSourceImageData.kt | 10 +- .../transformation/ScreenTransformFunction.kt | 8 +- .../test/kotlin/FitsTransformAlgorithmTest.kt | 734 ++++++++-------- .../nebulosa/platesolver/PlateSolution.kt | 2 +- 39 files changed, 1404 insertions(+), 1255 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/image/AnnotateImageRequest.kt rename api/src/main/kotlin/nebulosa/api/image/{ImageAnnotation.kt => ImageAnnotatation.kt} (98%) create mode 100644 desktop/src/app/image/crosshair.component.ts create mode 100644 desktop/src/shared/types/angular.types.ts 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/ImageAnnotation.kt b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotatation.kt similarity index 98% rename from api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt rename to api/src/main/kotlin/nebulosa/api/image/ImageAnnotatation.kt index 1db713fd5..e6365e937 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotatation.kt @@ -12,7 +12,7 @@ import nebulosa.skycatalog.DeepSkyObject import nebulosa.skycatalog.SkyObject import nebulosa.skycatalog.SkyObjectType -data class ImageAnnotation( +data class ImageAnnotatation( override val x: Double, override val y: Double, @JvmField val star: StarDSO? = null, 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..ea7704882 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -7,7 +7,7 @@ import nebulosa.api.atlas.SimbadEntityRepository import nebulosa.api.calibration.CalibrationFrameService import nebulosa.api.connection.ConnectionService import nebulosa.api.framing.FramingService -import nebulosa.api.image.ImageAnnotation.StarDSO +import nebulosa.api.image.ImageAnnotatation.StarDSO import nebulosa.fits.* import nebulosa.image.Image import nebulosa.image.algorithms.computation.Histogram @@ -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,16 +183,16 @@ 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() } - val annotations = Vector(64) + val annotations = Vector(64) val tasks = ArrayList>(2) 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 @@ -218,8 +219,8 @@ class ImageService( val declination = it[2].deg.takeIf(Angle::isFinite) ?: return@forEach val (x, y) = wcs.skyToPix(rightAscension, declination) val magnitude = it[6].replace(INVALID_MAG_CHARS, "").toDoubleOrNull() ?: SkyObject.UNKNOWN_MAGNITUDE - val minorPlanet = ImageAnnotation.MinorPlanet(0L, it[0], rightAscension, declination, magnitude) - val annotation = ImageAnnotation(x, y, minorPlanet = minorPlanet) + val minorPlanet = ImageAnnotatation.MinorPlanet(0L, it[0], rightAscension, declination, magnitude) + val annotation = ImageAnnotatation(x, y, minorPlanet = minorPlanet) annotations.add(annotation) count++ } @@ -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) @@ -253,8 +254,8 @@ class ImageService( val astrometric = barycentric.observe(entry).equatorial() val (x, y) = wcs.skyToPix(astrometric.longitude.normalized, astrometric.latitude) - val annotation = if (entry.type.classification == ClassificationType.STAR) ImageAnnotation(x, y, star = StarDSO(entry)) - else ImageAnnotation(x, y, dso = StarDSO(entry)) + val annotation = if (entry.type.classification == ClassificationType.STAR) ImageAnnotatation(x, y, star = StarDSO(entry)) + else ImageAnnotatation(x, y, dso = StarDSO(entry)) annotations.add(annotation) count++ } @@ -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/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/desktop/app/window.manager.ts b/desktop/app/window.manager.ts index 2e0b1d518..8e7770918 100644 --- a/desktop/app/window.manager.ts +++ b/desktop/app/window.manager.ts @@ -97,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) } diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 3de22f9f2..46138ff7b 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -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' @@ -88,6 +89,7 @@ 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' @@ -96,7 +98,6 @@ import { RotatorComponent } from './rotator/rotator.component' import { SequencerComponent } from './sequencer/sequencer.component' import { SettingsComponent } from './settings/settings.component' import { StackerComponent } from './stacker/stacker.component' -import { PathPipe } from '../shared/pipes/path.pipe' @NgModule({ declarations: [ @@ -112,6 +113,7 @@ import { PathPipe } from '../shared/pipes/path.pipe' CameraInfoComponent, CameraExposureComponent, ConfirmDialog, + CrossHairComponent, DeviceChooserComponent, DeviceListMenuComponent, DialogMenuComponent, diff --git a/desktop/src/app/calibration/calibration.component.ts b/desktop/src/app/calibration/calibration.component.ts index 7fc78f33a..3a9ffa19c 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -270,7 +270,6 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { let index = this.selectedFrames.indexOf(frame) if (index >= 0) { - console.log('selected frame removed', frame) this.selectedFrames.splice(index, 1) this.frameSelected() } diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 49077ab43..dd129a542 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -19,7 +19,6 @@ import { CameraStartCapture, DEFAULT_CAMERA, DEFAULT_CAMERA_PREFERENCE, - ExposureTimeUnit, FrameType, updateCameraStartCaptureFromCamera, } from '../../shared/types/camera.types' @@ -331,7 +330,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { this.preference.exposureMode = 'FIXED' this.request.exposureAmount = 1 } else if (mode === 'DARV') { - this.preference.exposureTimeUnit = ExposureTimeUnit.SECOND + this.preference.exposureTimeUnit = 'SECOND' } this.ditherMenuItem.visible = this.hasDither diff --git a/desktop/src/app/camera/exposure-time.component.ts b/desktop/src/app/camera/exposure-time.component.ts index 309d64741..5fc16707d 100644 --- a/desktop/src/app/camera/exposure-time.component.ts +++ b/desktop/src/app/camera/exposure-time.component.ts @@ -16,7 +16,7 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { readonly exposureTimeChange = new EventEmitter() @Input() - protected readonly unit: ExposureTimeUnit = ExposureTimeUnit.MICROSECOND + protected readonly unit: ExposureTimeUnit = 'MICROSECOND' @Output() readonly unitChange = new EventEmitter() @@ -52,25 +52,25 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { { label: 'Minute (m)', command: () => { - this.exposureTimeUnitChanged(ExposureTimeUnit.MINUTE) + this.exposureTimeUnitChanged('MINUTE') }, }, { label: 'Second (s)', command: () => { - this.exposureTimeUnitChanged(ExposureTimeUnit.SECOND) + this.exposureTimeUnitChanged('SECOND') }, }, { label: 'Millisecond (ms)', command: () => { - this.exposureTimeUnitChanged(ExposureTimeUnit.MILLISECOND) + this.exposureTimeUnitChanged('MILLISECOND') }, }, { label: 'Microsecond (µs)', command: () => { - this.exposureTimeUnitChanged(ExposureTimeUnit.MICROSECOND) + this.exposureTimeUnitChanged('MICROSECOND') }, }, ] @@ -86,7 +86,7 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { this.exposureTimeUnitChanged(change.currentValue) break case 'exposureTime': - this.exposureTimeChanged(change.currentValue, ExposureTimeUnit.MICROSECOND) + this.exposureTimeChanged(change.currentValue, 'MICROSECOND') break case 'min': case 'max': @@ -116,7 +116,7 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { protected exposureTimeUnitWheeled(event: WheelEvent) { if (event.deltaY) { - const units: ExposureTimeUnit[] = [ExposureTimeUnit.MINUTE, ExposureTimeUnit.SECOND, ExposureTimeUnit.MILLISECOND, ExposureTimeUnit.MICROSECOND] + const units: ExposureTimeUnit[] = ['MINUTE', 'SECOND', 'MILLISECOND', 'MICROSECOND'] const index = units.indexOf(this.unit) if (index >= 0) { @@ -157,10 +157,10 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { return false } - const factors = [ - { unit: ExposureTimeUnit.MINUTE, time: 60000000 }, - { unit: ExposureTimeUnit.SECOND, time: 1000000 }, - { unit: ExposureTimeUnit.MILLISECOND, time: 1000 }, + const factors: { unit: ExposureTimeUnit; time: number }[] = [ + { unit: 'MINUTE', time: 60000000 }, + { unit: 'SECOND', time: 1000000 }, + { unit: 'MILLISECOND', time: 1000 }, ] for (const { unit, time } of factors) { @@ -169,8 +169,7 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { // exposureTime is multiple of time. if (k === Math.floor(k)) { - console.log('exposure time normalized:', exposureTime, unit) - this.updateExposureTime(exposureTime, unit, ExposureTimeUnit.MICROSECOND) + this.updateExposureTime(exposureTime, unit, 'MICROSECOND') return true } } @@ -179,7 +178,7 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { return false } - static computeExposureTime(exposureTime: number, to: ExposureTimeUnit, from: ExposureTimeUnit = ExposureTimeUnit.MICROSECOND) { + static computeExposureTime(exposureTime: number, to: ExposureTimeUnit, from: ExposureTimeUnit = 'MICROSECOND') { if (to === from) { return exposureTime } @@ -192,13 +191,18 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { static exposureUnitFactor(unit: ExposureTimeUnit) { switch (unit) { - case ExposureTimeUnit.MINUTE: + case 'MINUTE': + case 'm' as ExposureTimeUnit: return 1 - case ExposureTimeUnit.SECOND: + case 'SECOND': + case 's' as ExposureTimeUnit: return 60 - case ExposureTimeUnit.MILLISECOND: + case 'MILLISECOND': + case 'ms' as ExposureTimeUnit: return 60000 - case ExposureTimeUnit.MICROSECOND: + case 'MICROSECOND': + case 'us' as ExposureTimeUnit: + case 'µs' as ExposureTimeUnit: return 60000000 default: return 0 diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index a7979cd71..fa98dfac6 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -248,8 +248,6 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab 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) } 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 23710ccfd..db0c1780f 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,8 @@
Minor Planets
@@ -217,13 +186,14 @@
@@ -232,8 +202,10 @@
@@ -265,17 +237,17 @@
- +
@@ -285,7 +257,7 @@ @@ -295,55 +267,55 @@
+ *ngIf="astronomicalObject.info.constellation">
+ *ngIf="astronomicalObject.info.magnitude">
+ *ngIf="astronomicalObject.info.type">
+ *ngIf="astronomicalObject.info.distance"> @@ -351,10 +323,10 @@ @@ -362,32 +334,32 @@
+ [(ngModel)]="solver.request.blind" />
@@ -428,9 +400,9 @@ + [(ngModel)]="solver.request.centerRA" />
@@ -438,9 +410,9 @@ + [(ngModel)]="solver.request.centerDEC" />
@@ -449,17 +421,17 @@
- @if (solver.type === 'SIRIL' || solver.type === 'PIXINSIGHT') { + @if (solver.request.type === 'SIRIL' || solver.request.type === 'PIXINSIGHT') {
@@ -482,7 +454,7 @@ [step]="0.01" styleClass="p-inputtext-sm border-0 w-full" [showButtons]="true" - [(ngModel)]="solver.pixelSize" + [(ngModel)]="solver.request.pixelSize" [allowEmpty]="false" locale="en" spinnableNumber /> @@ -624,7 +596,7 @@ [max]="65536" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="stretchShadow" + [(ngModel)]="stretch.transformation.shadow" locale="en" spinnableNumber /> @@ -635,7 +607,7 @@ [max]="65536" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="stretchHighlight" + [(ngModel)]="stretch.transformation.highlight" locale="en" spinnableNumber /> @@ -646,8 +618,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,7 +630,7 @@ [max]="65536" [showButtons]="true" styleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="stretchMidtone" + [(ngModel)]="stretch.transformation.midtone" locale="en" spinnableNumber /> @@ -669,7 +641,7 @@ class="mt-3 px-2" [min]="0" [max]="65536" - [(ngModel)]="stretchMidtone" /> + [(ngModel)]="stretch.transformation.midtone" />
@@ -708,18 +680,19 @@
@@ -760,13 +733,13 @@
@@ -791,7 +764,7 @@ @@ -804,7 +777,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="imageInfo.statistics.count" /> + [value]="statistics.statistics.count" />
@@ -814,7 +787,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 +797,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 +807,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 +817,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 +827,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 +837,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 +847,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 +876,7 @@ @@ -912,7 +885,7 @@
+ *ngIf="starDetector.request.type !== 'SIRIL'"> @@ -941,7 +914,7 @@
+ *ngIf="starDetector.request.type === 'SIRIL'"> @@ -966,7 +939,7 @@ pInputText readonly class="p-inputtext-sm border-0 w-full" - [value]="starDetection.stars.length" /> + [value]="starDetector.stars.length" />
@@ -976,7 +949,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 +959,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 +969,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 +989,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 +999,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 +1009,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 +1019,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 +1028,7 @@
@@ -1082,14 +1055,14 @@ @@ -1099,7 +1072,7 @@ @@ -1109,7 +1082,7 @@ @@ -1121,7 +1094,7 @@ [showButtons]="true" [min]="1" [max]="9999" - [(ngModel)]="fov.cameraSize.width" + [(ngModel)]="fov.selected.cameraSize.width" spinnableNumber /> @@ -1133,7 +1106,7 @@ [showButtons]="true" [min]="1" [max]="9999" - [(ngModel)]="fov.cameraSize.height" + [(ngModel)]="fov.selected.cameraSize.height" spinnableNumber /> @@ -1148,7 +1121,7 @@ [step]="0.01" [minFractionDigits]="0" [maxFractionDigits]="2" - [(ngModel)]="fov.pixelSize.width" + [(ngModel)]="fov.selected.pixelSize.width" locale="en" spinnableNumber /> @@ -1164,7 +1137,7 @@ [step]="0.01" [minFractionDigits]="0" [maxFractionDigits]="2" - [(ngModel)]="fov.pixelSize.height" + [(ngModel)]="fov.selected.pixelSize.height" locale="en" spinnableNumber /> @@ -1180,7 +1153,7 @@ [step]="0.01" [minFractionDigits]="0" [maxFractionDigits]="2" - [(ngModel)]="fov.barlowReducer" + [(ngModel)]="fov.selected.barlowReducer" locale="en" spinnableNumber /> @@ -1193,7 +1166,7 @@ [showButtons]="true" [min]="1" [max]="5" - [(ngModel)]="fov.bin" + [(ngModel)]="fov.selected.bin" spinnableNumber /> @@ -1216,27 +1189,9 @@ -->
- - + +
+
+ + +
+
+
+
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 0b72a763a..6e96e0ed0 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -1,8 +1,8 @@ -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' @@ -14,39 +14,43 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' -import { Angle, EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' +import { EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' import { - AnnotationInfoDialog, - DEFAULT_FOV, + AstronomicalObjectDialog, + 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, - 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 { PlateSolverRequest } from '../../shared/types/platesolver.types' import { StarDetectionRequest } from '../../shared/types/stardetector.types' -import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation' +import { CoordinateInterpolator } from '../../shared/utils/coordinate-interpolation' import { AppComponent } from '../app.component' @Component({ @@ -55,191 +59,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: [], - } - - readonly annotationInfo: AnnotationInfoDialog = { - 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, - }, + transformation: this.transformation.scnr, } - readonly solver: ImageSolverDialog = { + protected readonly astronomicalObject: AstronomicalObjectDialog = { showDialog: false, - running: false, - type: 'ASTAP', - blind: true, - centerRA: '', - centerDEC: '', - radius: 4, - focalLength: 0, - pixelSize: 0, - solved: structuredClone(DEFAULT_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 } }, } @@ -285,7 +158,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() }, } @@ -296,7 +170,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() }, } @@ -319,7 +194,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() }, } @@ -328,7 +203,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-list-box', label: 'FITS Header', command: () => { - this.fitsHeaders.showDialog = true + this.headers.showDialog = true }, } @@ -341,7 +216,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) }) } }, @@ -352,7 +227,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) @@ -379,7 +254,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.annotation.showDialog = true }, check: (event) => { - event.originalEvent?.stopImmediatePropagation() this.annotation.visible = !!event.checked }, } @@ -391,11 +265,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 }, } @@ -427,7 +300,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, @@ -448,25 +321,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', @@ -488,14 +342,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 primeService: PrimeService, ngZone: NgZone, ) { app.title = 'Image' @@ -540,22 +436,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') } @@ -564,9 +456,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() @@ -575,13 +467,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() }) @@ -642,19 +534,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 } } } @@ -667,7 +561,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) => { @@ -677,11 +572,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() }, @@ -694,7 +590,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { label: 'Open', icon: 'mdi mdi-wrench', command: () => { - return this.browserWindow.openCalibration() + return this.browserWindowService.openCalibration() }, }) @@ -703,13 +599,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() } }, @@ -722,7 +618,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) { @@ -730,29 +626,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 @@ -761,7 +656,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 @@ -776,19 +671,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) } @@ -796,9 +691,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') { @@ -822,8 +718,8 @@ 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, DEFAULT_IMAGE_SOLVED) @@ -831,37 +727,29 @@ export class ImageComponent implements AfterViewInit, OnDestroy { 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 request: StarDetectionRequest = { - ...this.preference.settings.get().starDetector[this.starDetection.type], - type: this.starDetection.type, - minSNR: this.starDetection.minSNR, - maxStars: this.starDetection.maxStars, + ...this.starDetector.request, + ...this.preferenceService.settings.get().starDetector[this.starDetector.request.type], } - Object.assign(this.starDetection, this.preference.settings.get().starDetector[this.starDetection.type]) - try { - this.starDetection.running = true - this.starDetection.stars = await this.api.detectStars(path, request) + 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 @@ -870,12 +758,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) @@ -887,29 +775,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') @@ -917,7 +805,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 @@ -948,7 +838,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 @@ -956,29 +846,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 } @@ -986,46 +872,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) { @@ -1034,13 +912,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 @@ -1051,94 +929,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) { @@ -1146,34 +1031,36 @@ 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) { @@ -1181,15 +1068,12 @@ export class ImageComponent implements AfterViewInit, OnDestroy { try { const request: PlateSolverRequest = { - ...this.preference.settings.get().plateSolver[this.solver.type], - type: this.solver.type, - pixelSize: this.solver.pixelSize, - focalLength: this.solver.focalLength, + ...this.solver.request, + ...this.preferenceService.settings.get().plateSolver[this.solver.request.type], } - const solved = await this.api.solverStart(request, path, this.solver.blind, this.solver.centerRA, this.solver.centerDEC, this.solver.radius) + const solved = await this.api.solverStart(request, path) - this.savePreference() this.updateImageSolved(solved) } catch { this.updateImageSolved(this.imageInfo?.solved) @@ -1203,7 +1087,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - solverStop() { + protected solverStop() { return this.api.solverStop() } @@ -1218,27 +1102,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, @@ -1247,11 +1131,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, @@ -1261,21 +1149,23 @@ export class ImageComponent implements AfterViewInit, OnDestroy { return true }, beforeWheel: (e) => { - return e.target !== this.image.nativeElement && e.target !== this.roi.nativeElement + 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.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() } @@ -1284,7 +1174,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() } @@ -1293,48 +1183,35 @@ 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 - } - - cancelEditFOV() { - this.fov.edited = undefined - } - - 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 - } + this.fov.selected = fov } private computeFOV(fov: FOV) { @@ -1379,50 +1256,36 @@ export class ImageComponent implements AfterViewInit, OnDestroy { 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() } } 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.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.primeService.confirm('Are you sure that you want to proceed?'))) { return false } @@ -1444,7 +1307,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.primeService.confirm('Are you sure that you want to proceed?'))) { return false } 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 903a9e8c3..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) { + (onChange)="item.check?.($event); $event.originalEvent?.stopImmediatePropagation()" /> } @if (item.items?.length || item.slideMenu?.length) { diff --git a/desktop/src/shared/interceptors/confirmation.interceptor.ts b/desktop/src/shared/interceptors/confirmation.interceptor.ts index 3a29b4d5e..30f476127 100644 --- a/desktop/src/shared/interceptors/confirmation.interceptor.ts +++ b/desktop/src/shared/interceptors/confirmation.interceptor.ts @@ -15,7 +15,6 @@ 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) } @@ -24,7 +23,6 @@ export class ConfirmationInterceptor implements HttpInterceptor { if (idempotencyKey) { return res.pipe( finalize(() => { - console.info('unregistered confirmation:', req.method, req.urlWithParams, idempotencyKey) this.confirmation.unregister(idempotencyKey) }), ) diff --git a/desktop/src/shared/pipes/dropdown-options.pipe.ts b/desktop/src/shared/pipes/dropdown-options.pipe.ts index 7daf27113..7f7253c13 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -4,7 +4,7 @@ import { AutoFocusFittingMode, BacklashCompensationMode } from '../types/autofoc import { ExposureMode, FrameType, LiveStackerType } from '../types/camera.types' import { GuideDirection, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider.types' import { ConnectionType } from '../types/home.types' -import { Bitpix, ImageChannel, ImageFormat, SCNRProtectionMethod } from '../types/image.types' +import { Bitpix, IMAGE_STATISTICS_BIT_OPTIONS, ImageChannel, ImageFormat, ImageStatisticsBitOption, SCNRProtectionMethod } from '../types/image.types' import { MountRemoteControlType } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' import { SequenceCaptureMode } from '../types/sequencer.types' @@ -36,6 +36,7 @@ export interface DropdownOptions { SETTINGS_TAB: SettingsTabKey[] STACKER_GROUP_TYPE: StackerGroupType[] CONNECTION_TYPE: ConnectionType[] + IMAGE_STATISTICS_BIT_OPTIONS: ImageStatisticsBitOption[] } @Pipe({ name: 'dropdownOptions' }) @@ -88,6 +89,8 @@ export class DropdownOptionsPipe implements PipeTransform { return ['LUMINANCE', 'RED', 'GREEN', 'BLUE', 'MONO', 'RGB'] as DropdownOptions[K] case 'CONNECTION_TYPE': return ['INDI', 'ALPACA'] as DropdownOptions[K] + case 'IMAGE_STATISTICS_BIT_OPTIONS': + return IMAGE_STATISTICS_BIT_OPTIONS 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..2d344d126 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 { DropdownItem } from '../types/angular.types' import { EnumPipe, EnumPipeKey } from './enum.pipe' -export interface EnumDropdownItem { - label: string - value: EnumPipeKey -} - @Pipe({ name: 'enumDropdown' }) export class EnumDropdownPipe implements PipeTransform { constructor(private readonly enumPipe: EnumPipe) {} - transform(value: EnumPipeKey[]): EnumDropdownItem[] { + transform(value: EnumPipeKey[]): 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 607919209..d3f3ecb63 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -5,7 +5,7 @@ import { AutoFocusFittingMode, AutoFocusState, BacklashCompensationMode } from ' 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 { Bitpix, ImageChannel, SCNRProtectionMethod } from '../types/image.types' import { MountRemoteControlType } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' import { SequenceCaptureMode, SequencerState } from '../types/sequencer.types' @@ -44,6 +44,7 @@ export type EnumPipeKey = | SettingsTabKey | SequencerState | ExposureTimeUnit + | ImageChannel | 'ALL' @Pipe({ name: 'enum' }) @@ -206,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', diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 4972131e5..6675e181d 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -11,7 +11,7 @@ import { Focuser } from '../types/focuser.types' import { HipsSurvey } from '../types/framing.types' import { GuideDirection, GuideOutput, Guider, GuiderHistoryStep, SettleInfo } from '../types/guider.types' import { ConnectionStatus, ConnectionType } from '../types/home.types' -import { CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAnnotation, ImageInfo, ImageSaveDialog, ImageSolved, ImageTransformation } from '../types/image.types' +import { AnnotateImageRequest, CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAnnotation, ImageInfo, ImageMousePosition, ImageSaveDialog, ImageSolved, ImageTransformation } from '../types/image.types' import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlType, SlewRate, TrackMode } from '../types/mount.types' import { PlateSolverRequest } from '../types/platesolver.types' import { Rotator } from '../types/rotator.types' @@ -174,8 +174,8 @@ 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}`) } @@ -536,9 +536,9 @@ export class ApiService { 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) { + const query = this.http.query({ path, hasLocation: true }) + return this.http.put(`image/annotations?${query}`, request) } saveImageAs(path: string, save: ImageSaveDialog, camera?: Camera) { @@ -663,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/confirmation.service.ts b/desktop/src/shared/services/confirmation.service.ts index 3f996eb4c..45918bfb5 100644 --- a/desktop/src/shared/services/confirmation.service.ts +++ b/desktop/src/shared/services/confirmation.service.ts @@ -26,7 +26,6 @@ export class ConfirmationService { } async processConfirmationEvent(event: ConfirmationEvent) { - console.info('processing confirmation event', event) const response = await this.prime.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 9609f6eeb..4204fb9c5 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -126,8 +126,6 @@ export class ElectronService { } on(channel: K, listener: (arg: EventMappedType[K]) => void) { - console.info('listening to channel: %s', channel) - this.ipcRenderer.on(channel, (_, arg) => { listener(arg) }) diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index f0635a816..f757898a6 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -9,7 +9,7 @@ import { DEFAULT_FOCUSER_PREFERENCE, Focuser, FocuserPreference, focuserPreferen 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, FOV, ImagePreference } from '../types/image.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 } from '../types/sequencer.types' @@ -82,10 +82,9 @@ export class PreferenceService { } readonly home = new PreferenceData(this.storage, 'home', () => structuredClone(DEFAULT_HOME_PREFERENCE), homePreferenceWithDefault) - readonly imagePreference = new PreferenceData(this.storage, 'image', () => structuredClone(DEFAULT_IMAGE_PREFERENCE)) + readonly imagePreference = new PreferenceData(this.storage, 'image', () => structuredClone(DEFAULT_IMAGE_PREFERENCE), imagePreferenceWithDefault) readonly skyAtlasPreference = new PreferenceData(this.storage, 'atlas', () => structuredClone(DEFAULT_SKY_ATLAS_PREFERENCE)) readonly alignment = new PreferenceData(this.storage, 'alignment', () => structuredClone(DEFAULT_ALIGNMENT_PREFERENCE), alignmentPreferenceWithDefault) - readonly imageFOVs = new PreferenceData(this.storage, 'image.fovs', () => []) readonly calibrationPreference = new PreferenceData(this.storage, 'calibration', () => structuredClone(DEFAULT_CALIBRATION_PREFERENCE), calibrationPreferenceWithDefault) readonly sequencerPreference = new PreferenceData(this.storage, 'sequencer', () => structuredClone(DEFAULT_SEQUENCER_PREFERENCE)) readonly stacker = new PreferenceData(this.storage, 'stacker', () => structuredClone(DEFAULT_STACKER_PREFERENCE), stackerPreferenceWithDefault) diff --git a/desktop/src/shared/types/angular.types.ts b/desktop/src/shared/types/angular.types.ts new file mode 100644 index 000000000..713244b9b --- /dev/null +++ b/desktop/src/shared/types/angular.types.ts @@ -0,0 +1,4 @@ +export interface DropdownItem { + label: string + value: T +} diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 60136dc83..b59246e2b 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -22,12 +22,7 @@ export type LiveStackerType = 'SIRIL' | 'PIXINSIGHT' export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' | 'EXPOSURING' | 'WAITING' | 'SETTLING' | 'DITHERING' | 'STACKING' | 'PAUSING' | 'PAUSED' | 'EXPOSURE_FINISHED' | 'CAPTURE_FINISHED' -export enum ExposureTimeUnit { - MINUTE = 'MINUTE', - SECOND = 'SECOND', - MILLISECOND = 'MILLISECOND', - MICROSECOND = 'MICROSECOND', -} +export type ExposureTimeUnit = 'MINUTE' | 'SECOND' | 'MILLISECOND' | 'MICROSECOND' export interface Camera extends GuideOutput, Thermometer { readonly exposuring: boolean @@ -323,7 +318,7 @@ export const DEFAULT_CAMERA_START_CAPTURE: CameraStartCapture = { export const DEFAULT_CAMERA_PREFERENCE: CameraPreference = { request: DEFAULT_CAMERA_START_CAPTURE, setpointTemperature: 0, - exposureTimeUnit: ExposureTimeUnit.MICROSECOND, + exposureTimeUnit: 'MICROSECOND', exposureMode: 'SINGLE', subFrame: false, } diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 2da2f3a1f..4f7a15348 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, FrameType } from './camera.types' -import type { PlateSolverRequest } from './platesolver.types' -import type { StarDetectionRequest } from './stardetector.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,6 +18,10 @@ export type Bitpix = 'BYTE' | 'SHORT' | 'INTEGER' | 'LONG' | 'FLOAT' | 'DOUBLE' export type LiveStackingMode = 'NONE' | 'RAW' | 'STACKED' +export type ImageCalibrationSource = 'CAMERA' | 'MENU' + +export type ImageMousePosition = Point + export interface Image { type: FrameType width: number @@ -28,7 +34,18 @@ export interface Image { filter?: string } -export interface FITSHeaderItem { +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 } @@ -39,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 } @@ -67,17 +82,6 @@ export interface ImageSolved extends EquatorialCoordinateJ2000 { radius: number } -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 interface CoordinateInterpolation { ma: number[] md: number[] @@ -111,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 @@ -134,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 DEFAULT_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 @@ -174,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 { @@ -199,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 @@ -234,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 @@ -275,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 { @@ -296,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 ImageAnnotationDialog { - showDialog: boolean - running: boolean - visible: boolean +export interface AnnotateImageRequest { useStarsAndDSOs: boolean useMinorPlanets: boolean minorPlanetsMagLimit: number includeMinorPlanetsWithoutMagnitude: boolean useSimbad: boolean +} + +export interface ImageAnnotationDialog { + showDialog: boolean + running: boolean + visible: boolean + request: AnnotateImageRequest data: ImageAnnotation[] } @@ -323,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 = { + useStarsAndDSOs: true, + useMinorPlanets: false, + minorPlanetsMagLimit: 18.0, + includeMinorPlanetsWithoutMagnitude: true, + 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.useStarsAndDSOs ??= source.useStarsAndDSOs + request.useMinorPlanets ??= source.useMinorPlanets + 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/platesolver.types.ts b/desktop/src/shared/types/platesolver.types.ts index c573e17c5..b79bc1f52 100644 --- a/desktop/src/shared/types/platesolver.types.ts +++ b/desktop/src/shared/types/platesolver.types.ts @@ -1,3 +1,5 @@ +import type { Angle } from './atlas.types' + export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP' | 'SIRIL' | 'PIXINSIGHT' export interface PlateSolverSettings { @@ -11,8 +13,12 @@ export interface PlateSolverSettings { export interface PlateSolverRequest extends PlateSolverSettings { type: PlateSolverType - pixelSize?: number - focalLength?: number + blind: boolean + centerRA: Angle + centerDEC: Angle + radius: Angle + pixelSize: number + focalLength: number } export const NOVA_ASTROMETRY_NET_URL = 'https://nova.astrometry.net/' @@ -29,6 +35,12 @@ export const DEFAULT_PLATE_SOLVER_SETTINGS: PlateSolverSettings = { 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) { diff --git a/desktop/tsconfig.serve.json b/desktop/tsconfig.serve.json index 44fe8ddb5..06b36ba89 100644 --- a/desktop/tsconfig.serve.json +++ b/desktop/tsconfig.serve.json @@ -2,11 +2,10 @@ "compilerOptions": { "sourceMap": true, "declaration": false, - "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "resolveJsonModule": true, - "module": "CommonJS", + "module": "NodeNext", "target": "ES2022", "noFallthroughCasesInSwitch": true, "esModuleInterop": true, 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-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-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt index 97404a9ae..cdf0f583f 100644 --- a/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt +++ b/nebulosa-platesolver/src/main/kotlin/nebulosa/platesolver/PlateSolution.kt @@ -46,7 +46,7 @@ data class PlateSolution( val height = header.getIntOrNull(FitsKeyword.NAXIS2) ?: header.getInt("IMAGEH", 0) LOG.debug { - "solution from %s: ORIE=%f, SCALE=%f, RA=%s, DEC=%s".format( + "solution from %s: ORIE=%s, SCALE=%f, RA=%s, DEC=%s".format( header, crota2.formatSignedDMS(), cdelt2.toArcsec, crval1.formatHMS(), crval2.formatSignedDMS(), ) } From 04784eadc3d495a25f5ed1c19766aaf53d40cf5f Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 6 Aug 2024 17:37:38 -0300 Subject: [PATCH 6/9] [api][desktop]: Refactor Sky Atlas --- .../kotlin/nebulosa/api/atlas/MinorPlanet.kt | 6 +- .../api/beans/annotations/Subscriber.kt | 1 - .../api/beans/converters/angle/AngleParam.kt | 1 - .../converters/device/DeviceOrEntityParam.kt | 1 - .../converters/location/LocationParam.kt | 1 - .../beans/converters/time/DateAndTimeParam.kt | 3 +- ...mageAnnotatation.kt => ImageAnnotation.kt} | 2 +- .../kotlin/nebulosa/api/image/ImageService.kt | 14 +- desktop/package-lock.json | 10 - desktop/package.json | 1 - desktop/src/app/app.module.ts | 4 +- desktop/src/app/atlas/atlas.component.html | 196 +++-- desktop/src/app/atlas/atlas.component.ts | 633 +++++++--------- desktop/src/app/image/image.component.html | 2 +- desktop/src/app/mount/mount.component.ts | 4 +- .../src/app/settings/settings.component.html | 8 +- .../src/app/settings/settings.component.ts | 25 +- .../location/location.dialog.html | 0 .../location/location.dialog.ts | 4 +- .../menu-item/menu-item.component.ts | 2 +- .../components/moon/moon.component.html | 3 +- .../shared/components/moon/moon.component.ts | 27 +- .../interceptors/location.interceptor.ts | 26 +- .../src/shared/pipes/dropdown-options.pipe.ts | 14 +- .../src/shared/pipes/enum-dropdown.pipe.ts | 4 +- desktop/src/shared/services/api.service.ts | 78 +- .../src/shared/services/electron.service.ts | 4 + .../src/shared/services/preference.service.ts | 4 +- desktop/src/shared/types/angular.types.ts | 26 + desktop/src/shared/types/api.types.ts | 4 +- desktop/src/shared/types/app.types.ts | 47 +- desktop/src/shared/types/atlas.types.ts | 692 +++++++++++++----- desktop/src/shared/types/settings.types.ts | 9 +- .../src/main/kotlin/nebulosa/math/Unsafe.kt | 2 +- .../nebulosa/retrofit/RawAsByteArray.kt | 1 - .../kotlin/nebulosa/retrofit/RawAsString.kt | 1 - .../main/kotlin/nebulosa/test/LinuxOnly.kt | 3 +- .../kotlin/nebulosa/test/NonGitHubOnly.kt | 1 - .../main/kotlin/nebulosa/test/WindowsOnly.kt | 3 +- 39 files changed, 1041 insertions(+), 826 deletions(-) rename api/src/main/kotlin/nebulosa/api/image/{ImageAnnotatation.kt => ImageAnnotation.kt} (98%) rename desktop/src/shared/{dialogs => components}/location/location.dialog.html (100%) rename desktop/src/shared/{dialogs => components}/location/location.dialog.ts (88%) 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/image/ImageAnnotatation.kt b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt similarity index 98% rename from api/src/main/kotlin/nebulosa/api/image/ImageAnnotatation.kt rename to api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt index e6365e937..1db713fd5 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotatation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt @@ -12,7 +12,7 @@ import nebulosa.skycatalog.DeepSkyObject import nebulosa.skycatalog.SkyObject import nebulosa.skycatalog.SkyObjectType -data class ImageAnnotatation( +data class ImageAnnotation( override val x: Double, override val y: Double, @JvmField val star: StarDSO? = null, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index ea7704882..ee4dc029f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -7,7 +7,7 @@ import nebulosa.api.atlas.SimbadEntityRepository import nebulosa.api.calibration.CalibrationFrameService import nebulosa.api.connection.ConnectionService import nebulosa.api.framing.FramingService -import nebulosa.api.image.ImageAnnotatation.StarDSO +import nebulosa.api.image.ImageAnnotation.StarDSO import nebulosa.fits.* import nebulosa.image.Image import nebulosa.image.algorithms.computation.Histogram @@ -173,7 +173,7 @@ class ImageService( } @Synchronized - fun annotations(path: Path, request: AnnotateImageRequest, 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) { @@ -187,7 +187,7 @@ class ImageService( return emptyList() } - val annotations = Vector(64) + val annotations = Vector(64) val tasks = ArrayList>(2) val dateTime = image.header.observationDate ?: LocalDateTime.now() @@ -219,8 +219,8 @@ class ImageService( val declination = it[2].deg.takeIf(Angle::isFinite) ?: return@forEach val (x, y) = wcs.skyToPix(rightAscension, declination) val magnitude = it[6].replace(INVALID_MAG_CHARS, "").toDoubleOrNull() ?: SkyObject.UNKNOWN_MAGNITUDE - val minorPlanet = ImageAnnotatation.MinorPlanet(0L, it[0], rightAscension, declination, magnitude) - val annotation = ImageAnnotatation(x, y, minorPlanet = minorPlanet) + val minorPlanet = ImageAnnotation.MinorPlanet(0L, it[0], rightAscension, declination, magnitude) + val annotation = ImageAnnotation(x, y, minorPlanet = minorPlanet) annotations.add(annotation) count++ } @@ -254,8 +254,8 @@ class ImageService( val astrometric = barycentric.observe(entry).equatorial() val (x, y) = wcs.skyToPix(astrometric.longitude.normalized, astrometric.latitude) - val annotation = if (entry.type.classification == ClassificationType.STAR) ImageAnnotatation(x, y, star = StarDSO(entry)) - else ImageAnnotatation(x, y, dso = StarDSO(entry)) + val annotation = if (entry.type.classification == ClassificationType.STAR) ImageAnnotation(x, y, star = StarDSO(entry)) + else ImageAnnotation(x, y, dso = StarDSO(entry)) annotations.add(annotation) count++ } 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 e6e43e6e4..789d2b5f3 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -49,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", diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 46138ff7b..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,7 +57,6 @@ 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 { NoDropdownDirective } from '../shared/directives/no-dropdown.directive' import { SpinnableNumberDirective } from '../shared/directives/spinnable-number.directive' import { StopPropagationDirective } from '../shared/directives/stop-propagation.directive' @@ -135,7 +135,7 @@ import { StackerComponent } from './stacker/stacker.component' SpinnableNumberDirective, INDIComponent, INDIPropertyComponent, - LocationDialog, + LocationComponent, MapComponent, MenuBarComponent, MenuItemComponent, diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index ae52a34f4..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,7 +170,7 @@ [step]="1" [showButtons]="true" inputStyleClass="p-inputtext-sm border-0 w-full" - [(ngModel)]="closeApproachDays" + [(ngModel)]="minorPlanet.closeApproach.days" spinnableNumber /> @@ -181,13 +183,13 @@ [showButtons]="true" inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" - [(ngModel)]="closeApproachDistance" + [(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" + [(ngModel)]="skyObject.search.filter.radius" spinnableNumber />
+ @let constellations = ['ALL'].concat('CONSTELLATION' | dropdownOptions);
+ @let skyObjectTypes = ['ALL'].concat('SKY_OBJECT_TYPE' | dropdownOptions); @@ -756,7 +758,7 @@ [showButtons]="true" inputStyleClass="p-inputtext-sm border-0 w-full" locale="en" - [(ngModel)]="skyObjectFilter.magnitude[1]" + [(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,33 +803,14 @@ icon="mdi mdi-restore" label="Reset" size="small" - (onClick)="resetSatelliteFilter()" /> + (onClick)="resetSatelliteSearchGroups()" /> - -
- - -
-
-
- - + (onClick)="searchSatellite()" />
@@ -837,8 +822,8 @@
Date Time + [(ngModel)]="dateTimeAndLocation.manual" + (ngModelChange)="manualDateTimeChanged()" />
@@ -889,7 +874,8 @@ + [header]="body.name" /> diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 982eeb03c..cdcbe3554 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -1,12 +1,11 @@ -import { AfterContentInit, AfterViewInit, Component, ElementRef, HostListener, NgZone, OnDestroy, OnInit, ViewChild, ViewEncapsulation } 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' @@ -16,24 +15,27 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' +import { extractDate, extractTime } from '../../shared/types/angular.types' import { - CONSTELLATIONS, - CloseApproach, - Constellation, - DEFAULT_BODY_POSITION, + AltitudeDataPoint, + BodyTabType, + BodyTag, + DEFAULT_BODY_TAB_REFRESH, + DEFAULT_DATE_TIME_AND_LOCATION, DEFAULT_LOCATION, - DEFAULT_SEARCH_FILTER, - DeepSkyObject, + 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' @@ -45,106 +47,20 @@ import { AppComponent } from '../app.component' encapsulation: ViewEncapsulation.None, }) export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { - refreshingPosition = false - refreshingChart = false - tab = SkyAtlasTab.SUN - - // TODO: juntar locations e dateTime num objeto + 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)] - // TODO: location fica em Atlas preference - protected location = this.locations[0] - - get refreshing() { - return this.refreshingPosition || this.refreshingChart - } - - readonly bodyPosition = structuredClone(DEFAULT_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(DEFAULT_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('dateTimeAndLocationPanel') - private readonly dateTimeAndLocationPanel!: OverlayPanel - @ViewChild('chart') - private readonly chart!: UIChart - - readonly altitudeData: ChartData = { + 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. @@ -267,7 +183,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, ], } - readonly altitudeOptions: ChartOptions = { + protected readonly altitudeOptions: ChartOptions = { responsive: true, plugins: { legend: { @@ -376,54 +292,73 @@ 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 - readonly settings: SettingsDialog = { - showDialog: false, + @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 + } + } + + get position() { + return this.body.position + } + + 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 preferenceService: PreferenceService, private readonly skyObjectPipe: SkyObjectPipe, - private readonly prime: PrimeService, + private readonly primeService: PrimeService, 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 and Location', @@ -432,10 +367,13 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, }, }) - electron.on('LOCATION.CHANGED', async () => { - await ngZone.run(() => { + electron.on('LOCATION.CHANGED', (location) => { + ngZone.run(() => { this.loadLocations() - return this.refreshTab(true, true) + + if (this.dateTimeAndLocation.location.id === location.id) { + void this.refreshTab(true, true) + } }) }) @@ -443,18 +381,13 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, await this.loadTabFromData(event) }) - const settings = this.preference.settings.get() - this.location = settings.locations[settings.location] - // 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() { @@ -468,8 +401,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() } }) @@ -486,277 +419,240 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, @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.primeService.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.skyObjectPipe.transform(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, DEFAULT_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, DEFAULT_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, DEFAULT_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, DEFAULT_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], @@ -793,109 +689,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 updateAltitudeDataPoints(points?: AltitudeDataPoint[]) { + if (points?.length) { + AtlasComponent.removePointsBelowZero(points) + this.altitudeData.datasets[9].data = points + } else { + this.altitudeData.datasets[9].data = [] } } private loadLocations() { - const settings = this.preference.settings.get() + const settings = this.preferenceService.settings.get() this.locations = settings.locations - this.location = this.locations.find((e) => e.id === this.location.id) ?? this.locations[settings.location] + 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] } 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) - } + Object.assign(this.preference, this.preferenceService.skyAtlasPreference.get()) + this.satellite.search.filter.groups = this.preference.satellites + this.dateTimeAndLocation.location = this.preference.location this.loadLocations() } - savePreference() { - const preference = this.preference.skyAtlasPreference.get() - - preference.satellites = SATELLITE_GROUPS.map((group) => { - return { group, enabled: this.satelliteSearchGroup.get(group) ?? false } - }) - - 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 @@ -904,7 +799,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.primeService.confirm('Are you sure that you want to proceed?')) { return false } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index db0c1780f..6f1c38e48 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -854,7 +854,7 @@
{ return this.browserWindowService.openSkyAtlas( { - tab: SkyAtlasTab.SKY_OBJECT, + tab: BodyTabType.SKY_OBJECT, filter: { rightAscension: this.currentComputedLocation.rightAscensionJ2000, declination: this.currentComputedLocation.declinationJ2000 }, }, { bringToFront: true }, diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 0d2c87435..999c36db7 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -19,7 +19,7 @@
- {{ location.name || '?' }} + {{ preference.location.name || '?' }}
@@ -56,8 +56,8 @@
+ [location]="preference.location" + (locationChange)="locationChanged($event)" />
diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 1ab68982b..7dd07b6c1 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -27,10 +27,6 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { private readonly locationChangePublisher = new Subject() private readonly locationChangeSubscription?: Subscription - get location() { - return this.preference.locations[this.preference.location] ?? DEFAULT_LOCATION - } - get plateSolver() { return this.preference.plateSolver[this.plateSolverType] } @@ -55,7 +51,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { app.title = 'Settings' this.locationChangeSubscription = this.locationChangePublisher.pipe(debounceTime(2000)).subscribe((location) => { - return this.electronService.send('LOCATION.CHANGED', location) + return this.electronService.locationChanged(location) }) } @@ -71,32 +67,26 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { const location = structuredClone(DEFAULT_LOCATION) location.id = +new Date() this.preference.locations.push(location) - this.preference.location = this.preference.locations.length - 1 - - this.locationChanged() + this.locationChanged(location) } protected deleteLocation() { if (this.preference.locations.length > 1) { - const index = this.preference.locations.indexOf(this.location) + const index = this.preference.locations.findIndex((e) => e.id === this.preference.location.id) if (index >= 0) { this.preference.locations.splice(index, 1) - this.preference.location = 0 - - this.locationChanged() + this.locationChanged(this.preference.locations[0]) } } } protected locationChanged(location?: Location) { if (location) { - this.preference.location = this.preference.locations.indexOf(location) + this.preference.location = location + this.savePreference() + this.locationChangePublisher.next(location) } - - this.savePreference() - - this.locationChangePublisher.next(this.location) } protected resetCameraCaptureNamingFormat(type: FrameType) { @@ -106,6 +96,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { 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] } protected savePreference() { diff --git a/desktop/src/shared/dialogs/location/location.dialog.html b/desktop/src/shared/components/location/location.dialog.html similarity index 100% rename from desktop/src/shared/dialogs/location/location.dialog.html rename to desktop/src/shared/components/location/location.dialog.html diff --git a/desktop/src/shared/dialogs/location/location.dialog.ts b/desktop/src/shared/components/location/location.dialog.ts similarity index 88% rename from desktop/src/shared/dialogs/location/location.dialog.ts rename to desktop/src/shared/components/location/location.dialog.ts index be8865098..166e7d5d3 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.ts +++ b/desktop/src/shared/components/location/location.dialog.ts @@ -1,13 +1,13 @@ 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 { DEFAULT_LOCATION, Location } from '../../types/atlas.types' +import { MapComponent } from '../map/map.component' @Component({ selector: 'neb-location', templateUrl: './location.dialog.html', }) -export class LocationDialog implements AfterViewInit { +export class LocationComponent implements AfterViewInit { @ViewChild('map') private readonly map!: MapComponent 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 3d25971c2..3f08f8ad8 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 { 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 diff --git a/desktop/src/shared/components/moon/moon.component.html b/desktop/src/shared/components/moon/moon.component.html index e8086e306..70136bde4 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)"> diff --git a/desktop/src/shared/components/moon/moon.component.ts b/desktop/src/shared/components/moon/moon.component.ts index 4825b51b1..382e2c69d 100644 --- a/desktop/src/shared/components/moon/moon.component.ts +++ b/desktop/src/shared/components/moon/moon.component.ts @@ -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/interceptors/location.interceptor.ts b/desktop/src/shared/interceptors/location.interceptor.ts index b45d9a143..b8ef3715b 100644 --- a/desktop/src/shared/interceptors/location.interceptor.ts +++ b/desktop/src/shared/interceptors/location.interceptor.ts @@ -11,11 +11,29 @@ export class LocationInterceptor implements HttpInterceptor { intercept(req: HttpRequest, next: HttpHandler): Observable> { if (req.urlWithParams.includes('hasLocation')) { - const { location, locations } = this.preference.settings.get() + const params = new URLSearchParams(req.urlWithParams) + const hasLocation = params.get('hasLocation') - req = req.clone({ - headers: req.headers.set(LocationInterceptor.HEADER_KEY, JSON.stringify(locations[location])), - }) + if (!hasLocation || hasLocation === 'true') { + const location = this.preference.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.preference.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 7f7253c13..f1f51b59a 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -1,5 +1,6 @@ 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' @@ -36,7 +37,10 @@ export interface DropdownOptions { SETTINGS_TAB: SettingsTabKey[] STACKER_GROUP_TYPE: StackerGroupType[] CONNECTION_TYPE: ConnectionType[] - IMAGE_STATISTICS_BIT_OPTIONS: ImageStatisticsBitOption[] + IMAGE_STATISTICS_BIT_OPTION: ImageStatisticsBitOption[] + SATELLITE_GROUP_TYPE: SatelliteGroupType[] + CONSTELLATION: Constellation[] + SKY_OBJECT_TYPE: SkyObjectType[] } @Pipe({ name: 'dropdownOptions' }) @@ -89,8 +93,14 @@ export class DropdownOptionsPipe implements PipeTransform { return ['LUMINANCE', 'RED', 'GREEN', 'BLUE', 'MONO', 'RGB'] as DropdownOptions[K] case 'CONNECTION_TYPE': return ['INDI', 'ALPACA'] as DropdownOptions[K] - case 'IMAGE_STATISTICS_BIT_OPTIONS': + 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] } return [] diff --git a/desktop/src/shared/pipes/enum-dropdown.pipe.ts b/desktop/src/shared/pipes/enum-dropdown.pipe.ts index 2d344d126..1e02fa89e 100644 --- a/desktop/src/shared/pipes/enum-dropdown.pipe.ts +++ b/desktop/src/shared/pipes/enum-dropdown.pipe.ts @@ -1,12 +1,12 @@ import { Pipe, PipeTransform } from '@angular/core' import { DropdownItem } from '../types/angular.types' -import { EnumPipe, EnumPipeKey } from './enum.pipe' +import { EnumPipe } from './enum.pipe' @Pipe({ name: 'enumDropdown' }) export class EnumDropdownPipe implements PipeTransform { constructor(private readonly enumPipe: EnumPipe) {} - transform(value: EnumPipeKey[]): DropdownItem[] { + transform(value: T[]): DropdownItem[] { return value.map((value) => { return { label: this.enumPipe.transform(value), value } }) diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 6675e181d..3b26e3d1d 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core' -import moment from 'moment' import { DARVStart, TPPAStart } from '../types/alignment.types' -import { Angle, BodyPosition, CloseApproach, ComputedLocation, Constellation, DeepSkyObject, MinorPlanet, Satellite, SatelliteGroupType, SkyObjectType, Twilight } from '../types/atlas.types' +import { 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 } from '../types/calibration.types' import { Camera, CameraStartCapture } from '../types/camera.types' @@ -445,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}`) } @@ -502,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}`) } @@ -519,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}`) } @@ -530,14 +530,14 @@ 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, request: AnnotateImageRequest) { - const query = this.http.query({ path, hasLocation: true }) + 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) } diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 4204fb9c5..aa8012ddb 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -237,4 +237,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/preference.service.ts b/desktop/src/shared/services/preference.service.ts index f757898a6..1d91e565a 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core' import { AlignmentPreference, alignmentPreferenceWithDefault, DEFAULT_ALIGNMENT_PREFERENCE } from '../types/alignment.types' -import { DEFAULT_SKY_ATLAS_PREFERENCE, SkyAtlasPreference } from '../types/atlas.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' @@ -83,7 +83,7 @@ export class PreferenceService { 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)) + 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)) diff --git a/desktop/src/shared/types/angular.types.ts b/desktop/src/shared/types/angular.types.ts index 713244b9b..054afff2d 100644 --- a/desktop/src/shared/types/angular.types.ts +++ b/desktop/src/shared/types/angular.types.ts @@ -1,4 +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())}-${padNumber(date.getDay())}` +} + +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 f2bef9531..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,4 +67,22 @@ export interface JsonFile { json: T } -export interface SaveJson extends OpenFile, JsonFile {} +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 656afae5e..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 + tags: BodyTag[] +} + +export interface SunTab extends BodyTab { + image: string +} + +export type MoonTab = BodyTab + +export interface PlanetItem { name: string - type: 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 DEFAULT_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 DEFAULT_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 DEFAULT_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 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 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 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 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 DEFAULT_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/settings.types.ts b/desktop/src/shared/types/settings.types.ts index c21133100..b75a777d9 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -1,5 +1,5 @@ import type { Location } from './atlas.types' -import { DEFAULT_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' @@ -15,7 +15,7 @@ export interface SettingsPreference { stacker: Record namingFormat: CameraCaptureNamingFormat locations: Location[] - location: number // selected location index + location: Location } export const DEFAULT_SETTINGS_PREFERENCE: SettingsPreference = { @@ -40,7 +40,7 @@ export const DEFAULT_SETTINGS_PREFERENCE: SettingsPreference = { }, namingFormat: DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, locations: [DEFAULT_LOCATION], - location: 0, + location: DEFAULT_LOCATION, } export function settingsPreferenceWithDefault(preference?: Partial, source: SettingsPreference = DEFAULT_SETTINGS_PREFERENCE) { @@ -65,11 +65,10 @@ export function settingsPreferenceWithDefault(preference?: Partial Date: Wed, 7 Aug 2024 13:33:56 -0300 Subject: [PATCH 7/9] [api][desktop]: Refactor Mount Remote Control --- .../nebulosa/api/mounts/MountController.kt | 6 +-- .../nebulosa/api/mounts/MountRemoteControl.kt | 6 +-- ...lType.kt => MountRemoteControlProtocol.kt} | 2 +- .../nebulosa/api/mounts/MountService.kt | 12 ++--- desktop/src/app/mount/mount.component.html | 12 ++--- desktop/src/app/mount/mount.component.ts | 51 ++++++++----------- desktop/src/app/rotator/rotator.component.ts | 4 +- .../src/shared/pipes/dropdown-options.pipe.ts | 6 +-- desktop/src/shared/pipes/enum.pipe.ts | 4 +- desktop/src/shared/services/api.service.ts | 10 ++-- desktop/src/shared/types/mount.types.ts | 22 ++++++-- 11 files changed, 69 insertions(+), 66 deletions(-) rename api/src/main/kotlin/nebulosa/api/mounts/{MountRemoteControlType.kt => MountRemoteControlProtocol.kt} (59%) 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/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/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index c3bcd4512..153f63b0a 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -402,13 +402,13 @@
- +
@@ -435,7 +435,7 @@
- + 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', @@ -61,7 +61,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { }, ] - readonly targetCoordinateModel: SlideMenuItem[] = [ + protected readonly targetCoordinateModel: SlideMenuItem[] = [ { icon: 'mdi mdi-telescope', label: 'Go To', @@ -203,14 +203,6 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { protected targetCoordinateCommand = this.targetCoordinateModel[0] - protected readonly remoteControl: MountRemoteControlDialog = { - showDialog: false, - type: 'LX200', - host: '0.0.0.0', - port: 10001, - data: [], - } - constructor( private readonly app: AppComponent, private readonly api: ApiService, @@ -299,8 +291,8 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { ngAfterContentInit() { this.route.queryParams.subscribe(async (e) => { - const mount = JSON.parse(decodeURIComponent(e['data'] as string)) as Mount - await this.mountChanged(mount) + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as Mount + await this.mountChanged(data) this.ticker.register(this, 30000) }) } @@ -343,22 +335,22 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { } protected async showRemoteControlDialog() { - this.remoteControl.data = await this.api.mountRemoteControlList(this.mount) + this.remoteControl.controls = await this.api.mountRemoteControlList(this.mount) this.remoteControl.showDialog = true } 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.primeService.message('Failed to start remote control', 'error') } } - protected 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) } protected async goTo() { @@ -389,7 +381,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { } } - protected 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 @@ -478,8 +470,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { protected async computeTargetCoordinates() { if (this.mount.connected) { const { targetRightAscension, targetDeclination, targetCoordinateType } = this.preference - const computedLocation = await this.api.mountComputeLocation(this.mount, targetCoordinateType === 'J2000', targetRightAscension, targetDeclination, true, true, true) - this.targetComputedLocation = computedLocation + Object.assign(this.targetComputedLocation, await this.api.mountComputeLocation(this.mount, targetCoordinateType === 'J2000', targetRightAscension, targetDeclination, true, true, true)) } } diff --git a/desktop/src/app/rotator/rotator.component.ts b/desktop/src/app/rotator/rotator.component.ts index 58e39976d..51c51ad4b 100644 --- a/desktop/src/app/rotator/rotator.component.ts +++ b/desktop/src/app/rotator/rotator.component.ts @@ -46,8 +46,8 @@ export class RotatorComponent implements AfterViewInit, OnDestroy, Tickable { ngAfterViewInit() { this.route.queryParams.subscribe(async (e) => { - const rotator = JSON.parse(decodeURIComponent(e['data'] as string)) as Rotator - await this.rotatorChanged(rotator) + const data = JSON.parse(decodeURIComponent(e['data'] as string)) as Rotator + await this.rotatorChanged(data) this.ticker.register(this, 30000) }) } diff --git a/desktop/src/shared/pipes/dropdown-options.pipe.ts b/desktop/src/shared/pipes/dropdown-options.pipe.ts index f1f51b59a..254c8e578 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -6,7 +6,7 @@ import { ExposureMode, FrameType, LiveStackerType } from '../types/camera.types' import { GuideDirection, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider.types' import { ConnectionType } from '../types/home.types' import { Bitpix, IMAGE_STATISTICS_BIT_OPTIONS, ImageChannel, ImageFormat, ImageStatisticsBitOption, SCNRProtectionMethod } from '../types/image.types' -import { MountRemoteControlType } from '../types/mount.types' +import { MountRemoteControlProtocol } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' import { SequenceCaptureMode } from '../types/sequencer.types' import { SettingsTabKey } from '../types/settings.types' @@ -23,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[] @@ -65,7 +65,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] diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index d3f3ecb63..5c5e26936 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -6,7 +6,7 @@ import { CameraCaptureState, ExposureMode, ExposureTimeUnit, FrameType, LiveStac import { FlatWizardState } from '../types/flat-wizard.types' import { GuideDirection, GuideState, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider.types' import { Bitpix, ImageChannel, SCNRProtectionMethod } from '../types/image.types' -import { MountRemoteControlType } from '../types/mount.types' +import { MountRemoteControlProtocol } from '../types/mount.types' import { PlateSolverType } from '../types/platesolver.types' import { SequenceCaptureMode, SequencerState } from '../types/sequencer.types' import { SettingsTabKey } from '../types/settings.types' @@ -36,7 +36,7 @@ export type EnumPipeKey = | LiveStackerType | GuiderPlotMode | GuiderYAxisUnit - | MountRemoteControlType + | MountRemoteControlProtocol | SequenceCaptureMode | Bitpix | StackerType diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 3b26e3d1d..d8001862f 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -12,7 +12,7 @@ import { HipsSurvey } from '../types/framing.types' import { GuideDirection, GuideOutput, Guider, GuiderHistoryStep, SettleInfo } from '../types/guider.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, MountRemoteControlType, SlewRate, TrackMode } from '../types/mount.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' @@ -179,8 +179,8 @@ export class ApiService { 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}`) } @@ -188,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}`) } diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts index f37ecc686..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 @@ -43,7 +47,7 @@ export interface Mount extends EquatorialCoordinate, GPS, GuideOutput, Parkable } export interface MountRemoteControl { - type: MountRemoteControlType + protocol: MountRemoteControlProtocol mount: Mount running: boolean rightAscension: Angle @@ -59,10 +63,10 @@ export interface MountRemoteControl { export interface MountRemoteControlDialog { showDialog: boolean - type: MountRemoteControlType + protocol: MountRemoteControlProtocol host: string port: number - data: MountRemoteControl[] + controls: MountRemoteControl[] } export interface MountPreference { @@ -105,6 +109,14 @@ export const DEFAULT_MOUNT: Mount = { parked: false, } +export const DEFAULT_MOUNT_REMOTE_CONTROL_DIALOG: MountRemoteControlDialog = { + showDialog: false, + protocol: 'LX200', + host: '0.0.0.0', + port: 10001, + controls: [], +} + export const DEFAULT_MOUNT_PREFERENCE: MountPreference = { targetCoordinateType: 'JNOW', targetRightAscension: '00h00m00s', From 0496264e8c22c1e4c9146865398441e5a8a205d4 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 8 Aug 2024 23:56:56 -0300 Subject: [PATCH 8/9] [api][desktop]: Refactor Sequencer --- .../api/sequencer/SequenceCaptureMode.kt | 10 - .../api/sequencer/SequencerCaptureMode.kt | 10 + .../api/sequencer/SequencerController.kt | 2 +- .../api/sequencer/SequencerExecutor.kt | 2 +- ...PlanRequest.kt => SequencerPlanRequest.kt} | 6 +- .../api/sequencer/SequencerService.kt | 2 +- .../nebulosa/api/sequencer/SequencerTask.kt | 49 +- desktop/src/app/app.component.ts | 10 +- desktop/src/app/camera/camera.component.html | 3 +- desktop/src/app/camera/camera.component.ts | 194 ++++-- .../src/app/camera/exposure-time.component.ts | 36 +- .../app/filterwheel/filterwheel.component.ts | 10 +- desktop/src/app/image/image.component.html | 18 +- desktop/src/app/indi/indi.component.ts | 2 +- .../app/sequencer/sequencer.component.html | 150 ++--- .../src/app/sequencer/sequencer.component.ts | 598 +++++++++--------- .../camera-info/camera-info.component.html | 62 +- .../camera-info/camera-info.component.scss | 28 - .../camera-info/camera-info.component.ts | 10 +- .../path-chooser/path-chooser.component.ts | 3 +- desktop/src/shared/constants.ts | 2 + .../src/shared/pipes/dropdown-options.pipe.ts | 7 +- desktop/src/shared/pipes/enum.pipe.ts | 4 +- desktop/src/shared/pipes/exposureTime.pipe.ts | 2 +- desktop/src/shared/services/api.service.ts | 8 +- .../src/shared/services/preference.service.ts | 4 +- desktop/src/shared/types/angular.types.ts | 2 +- desktop/src/shared/types/camera.types.ts | 3 + desktop/src/shared/types/image.types.ts | 16 +- desktop/src/shared/types/sequencer.types.ts | 141 ++++- desktop/src/shared/types/settings.types.ts | 8 +- desktop/src/styles.scss | 8 + 32 files changed, 816 insertions(+), 594 deletions(-) delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceCaptureMode.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequencerCaptureMode.kt rename api/src/main/kotlin/nebulosa/api/sequencer/{SequencePlanRequest.kt => SequencerPlanRequest.kt} (84%) delete mode 100644 desktop/src/shared/components/camera-info/camera-info.component.scss 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/desktop/src/app/app.component.ts b/desktop/src/app/app.component.ts index 2673cefce..1fbfd8a75 100644 --- a/desktop/src/app/app.component.ts +++ b/desktop/src/app/app.component.ts @@ -13,9 +13,11 @@ export class AppComponent implements OnDestroy { readonly maximizable = !!window.preference.resizable readonly modal = window.preference.modal ?? false readonly topMenu: MenuItem[] = [] + subTitle? = '' pinned = false showTopBar = true + beforeClose?: () => boolean | Promise private readonly resizeObserver?: ResizeObserver @@ -83,7 +85,11 @@ export class AppComponent implements OnDestroy { return this.electron.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.electron.closeWindow(data) + } else { + return undefined + } } } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 2dfafe08c..f14507963 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -184,6 +184,7 @@ [canExposureTimeUnit]="canExposureTimeUnit" [min]="camera.exposureMin" [max]="camera.exposureMax" + [normalized]="mode !== 'CAPTURE'" (exposureTimeChange)="savePreference()" (unitChange)="savePreference()" class="w-full" /> @@ -468,7 +469,7 @@ diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index dd129a542..99f7e6255 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -22,7 +22,7 @@ import { 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 { Mount } from '../../shared/types/mount.types' import { Rotator } from '../../shared/types/rotator.types' @@ -182,6 +182,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { 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, @@ -220,7 +224,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { }) electronService.on('MOUNT.UPDATED', (event) => { - if (event.device.id === this.preference.mount?.id) { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.mount?.id) { ngZone.run(() => { if (this.preference.mount) { Object.assign(this.preference.mount, event.device) @@ -229,8 +233,24 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { } }) + 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 (event.device.id === this.preference.wheel?.id) { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.wheel?.id) { ngZone.run(() => { if (this.preference.wheel) { Object.assign(this.preference.wheel, event.device) @@ -239,8 +259,24 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { } }) + 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 (event.device.id === this.preference.focuser?.id) { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.focuser?.id) { ngZone.run(() => { if (this.preference.focuser) { Object.assign(this.preference.focuser, event.device) @@ -249,8 +285,24 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { } }) + 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 (event.device.id === this.preference.rotator?.id) { + if (this.mode === 'CAPTURE' && event.device.id === this.preference.rotator?.id) { ngZone.run(() => { if (this.preference.rotator) { Object.assign(this.preference.rotator, event.device) @@ -259,8 +311,24 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { } }) - electronService.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()) }) electronService.on('ROI.SELECTED', (event) => { @@ -359,15 +427,15 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { this.updateSubTitle() } - private async loadEquipment() { - const makeMenuItem = (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() + command: async (event: MenuItemCommandEvent) => { + await command() this.savePreference() event.parentItem?.slideMenu?.forEach((item) => (item.selected = item === event.item)) }, @@ -378,62 +446,102 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { // MOUNT - const mounts = await this.api.mounts() - this.preference.mount = mounts.find((e) => e.name === this.preference.mount?.name) + if (!type || type === 'MOUNT') { + menu[0].slideMenu.length = 0 - const makeMountItem = (mount?: Mount) => { - return makeMenuItem(this.preference.mount?.name === mount?.name, () => (this.preference.mount = mount), mount) - } + const mounts = await this.api.mounts() + this.preference.mount = mounts.find((e) => e.name === this.preference.mount?.name) - menu[0].slideMenu.push(makeMountItem()) + const makeMountItem = (mount?: Mount) => { + return makeMenuItem( + this.preference.mount?.name === mount?.name, + async () => { + this.preference.mount = mount && (await this.api.mount(mount.id)) + }, + mount, + ) + } - for (const mount of mounts) { - menu[0].slideMenu.push(makeMountItem(mount)) + menu[0].slideMenu.push(makeMountItem()) + + for (const mount of mounts) { + menu[0].slideMenu.push(makeMountItem(mount)) + } } // WHEEL - const wheels = await this.api.wheels() - this.preference.wheel = wheels.find((e) => e.name === this.preference.wheel?.name) + if (!type || type === 'WHEEL') { + menu[1].slideMenu.length = 0 - const makeWheelItem = (wheel?: Wheel) => { - return makeMenuItem(this.preference.wheel?.name === wheel?.name, () => (this.preference.wheel = wheel), wheel) - } + const wheels = await this.api.wheels() + this.preference.wheel = wheels.find((e) => e.name === this.preference.wheel?.name) - menu[1].slideMenu.push(makeWheelItem()) + const makeWheelItem = (wheel?: Wheel) => { + return makeMenuItem( + this.preference.wheel?.name === wheel?.name, + async () => { + this.preference.wheel = wheel && (await this.api.wheel(wheel.id)) + }, + wheel, + ) + } - for (const wheel of wheels) { - menu[1].slideMenu.push(makeWheelItem(wheel)) + menu[1].slideMenu.push(makeWheelItem()) + + for (const wheel of wheels) { + menu[1].slideMenu.push(makeWheelItem(wheel)) + } } // FOCUSER - const focusers = await this.api.focusers() - this.preference.focuser = focusers.find((e) => e.name === this.preference.focuser?.name) + if (!type || type === 'FOCUSER') { + menu[2].slideMenu.length = 0 - const makeFocuserItem = (focuser?: Focuser) => { - return makeMenuItem(this.preference.focuser?.name === focuser?.name, () => (this.preference.focuser = focuser), focuser) - } + const focusers = await this.api.focusers() + this.preference.focuser = focusers.find((e) => e.name === this.preference.focuser?.name) - menu[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) { - menu[2].slideMenu.push(makeFocuserItem(focuser)) + for (const focuser of focusers) { + menu[2].slideMenu.push(makeFocuserItem(focuser)) + } } // ROTATOR - const rotators = await this.api.rotators() - this.preference.rotator = rotators.find((e) => e.name === this.preference.rotator?.name) + if (!type || type === 'ROTATOR') { + menu[3].slideMenu.length = 0 - const makeRotatorItem = (rotator?: Rotator) => { - return makeMenuItem(this.preference.rotator?.name === rotator?.name, () => (this.preference.rotator = rotator), rotator) - } + const rotators = await this.api.rotators() + this.preference.rotator = rotators.find((e) => e.name === this.preference.rotator?.name) - menu[3].slideMenu.push(makeRotatorItem()) + const makeRotatorItem = (rotator?: Rotator) => { + return makeMenuItem( + this.preference.rotator?.name === rotator?.name, + async () => { + this.preference.rotator = rotator && (await this.api.rotator(rotator.id)) + }, + rotator, + ) + } + + menu[3].slideMenu.push(makeRotatorItem()) - for (const rotator of rotators) { - menu[3].slideMenu.push(makeRotatorItem(rotator)) + for (const rotator of rotators) { + menu[3].slideMenu.push(makeRotatorItem(rotator)) + } } } diff --git a/desktop/src/app/camera/exposure-time.component.ts b/desktop/src/app/camera/exposure-time.component.ts index 5fc16707d..d1ab3ecb8 100644 --- a/desktop/src/app/camera/exposure-time.component.ts +++ b/desktop/src/app/camera/exposure-time.component.ts @@ -10,13 +10,13 @@ import { ExposureTimeUnit } from '../../shared/types/camera.types' }) export class ExposureTimeComponent implements AfterViewInit, OnChanges { @Input({ required: true }) - protected readonly exposureTime: number = 0 + protected exposureTime: number = 0 @Output() readonly exposureTimeChange = new EventEmitter() @Input() - protected readonly unit: ExposureTimeUnit = 'MICROSECOND' + protected unit: ExposureTimeUnit = 'MICROSECOND' @Output() readonly unitChange = new EventEmitter() @@ -75,6 +75,8 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { }, ] + private exposureTimeInMicroseconds = 0 + ngOnChanges(changes: SimpleChanges) { for (const key in changes) { const change = changes[key] @@ -86,32 +88,33 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { this.exposureTimeUnitChanged(change.currentValue) break case 'exposureTime': - this.exposureTimeChanged(change.currentValue, 'MICROSECOND') + 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() { - if (!this.normalize(this.exposureTime)) { - this.updateExposureTime(this.current.exposureTime, this.unit, this.unit) - } + this.updateExposureTime(this.current.exposureTime, this.unit, this.unit) } protected exposureTimeUnitChanged(value: ExposureTimeUnit) { - this.updateExposureTime(this.current.exposureTime, value, this.unit) + this.updateExposureTime(this.current.exposureTime, value, this.unit, false) } - protected exposureTimeChanged(value: number, from: ExposureTimeUnit = this.unit) { - this.updateExposureTime(value, this.unit, from) + 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) + this.updateExposureTime(this.current.exposureTime, this.unit, this.unit, false) } protected exposureTimeUnitWheeled(event: WheelEvent) { @@ -131,7 +134,7 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { } } - private updateExposureTime(value: number, unit: ExposureTimeUnit, from: ExposureTimeUnit) { + private updateExposureTime(value: number, unit: ExposureTimeUnit, from: ExposureTimeUnit, normalize: boolean = this.normalized) { const a = ExposureTimeComponent.exposureUnitFactor(from) const b = ExposureTimeComponent.exposureUnitFactor(unit) @@ -143,11 +146,20 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { 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) } } @@ -169,7 +181,7 @@ export class ExposureTimeComponent implements AfterViewInit, OnChanges { // exposureTime is multiple of time. if (k === Math.floor(k)) { - this.updateExposureTime(exposureTime, unit, 'MICROSECOND') + this.updateExposureTime(exposureTime, unit, 'MICROSECOND', false) return true } } diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index fa98dfac6..3155597fc 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -351,13 +351,8 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab if (this.moving) return - const preference = this.preferenceService.wheel(this.wheel).get() - const filters = makeFilter(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() } @@ -365,7 +360,6 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab private loadPreference() { if (this.mode === 'CAPTURE' && this.wheel.name) { Object.assign(this.preference, this.preferenceService.wheel(this.wheel).get()) - this.filters = makeFilter(this.wheel, this.filters, this.preference.shutterPosition) } } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index 6f1c38e48..e0908e86d 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -142,7 +142,7 @@
Stars & DSOs
Minor Planets
@@ -186,7 +187,7 @@
+ @if (annotation.request.minorPlanetsMagLimit >= 20 || annotation.request.includeMinorPlanetsWithoutMagnitude) { +
+ + Can take a long time +
+ }

Minor planets Annotation uses the diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index b718a8b59..298d19fa7 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -81,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] diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index c6ddd1e9e..13ddc495a 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -18,7 +18,7 @@ locale="en" styleClass="p-inputtext-sm border-0" [allowEmpty]="false" - (ngModelChange)="savePlan()" + (ngModelChange)="savePreference()" spinnableNumber /> @@ -27,12 +27,12 @@ + (ngModelChange)="savePreference()" />

@@ -40,7 +40,7 @@ @@ -62,7 +62,7 @@ key="sequencer.savePath" [directory]="true" [(path)]="plan.savePath" - (pathChange)="savePlan()" + (pathChange)="savePreference()" class="w-full" />
@@ -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,7 +100,7 @@ [step]="0.1" locale="en" [minFractionDigits]="1" - (ngModelChange)="savePlan()" + (ngModelChange)="savePreference()" spinnableNumber /> @@ -115,7 +115,7 @@ [max]="1000" [(ngModel)]="plan.dither.afterExposures" [step]="1" - (ngModelChange)="savePlan()" + (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()" /> @@ -172,7 +172,7 @@ [binary]="true" [disabled]="running || !plan.autoFocus.enabled" [(ngModel)]="plan.autoFocus.afterExposuresEnabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> @@ -192,7 +192,7 @@ [binary]="true" [disabled]="running || !plan.autoFocus.enabled" [(ngModel)]="plan.autoFocus.afterTemperatureChangeEnabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> @@ -212,7 +212,7 @@ [binary]="true" [disabled]="running || !plan.autoFocus.enabled" [(ngModel)]="plan.autoFocus.afterHFDIncreaseEnabled" - (ngModelChange)="savePlan()" /> + (ngModelChange)="savePreference()" /> @@ -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.ts b/desktop/src/app/sequencer/sequencer.component.ts index 8ff97e04c..74fa18ca3 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -1,8 +1,9 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop' 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 { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' @@ -10,21 +11,18 @@ 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, DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, 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 { DEFAULT_SEQUENCE_PLAN, SEQUENCE_ENTRY_PROPERTIES, SequenceCaptureMode, SequenceEntryProperty, SequencePlan, SequencerEvent } from '../../shared/types/sequencer.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', @@ -32,33 +30,27 @@ export const SEQUENCER_SAVED_PATH_KEY = 'sequencer.savedPath' encapsulation: ViewEncapsulation.None, }) export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable { - cameras: Camera[] = [] - mounts: Mount[] = [] - wheels: Wheel[] = [] - focusers: Focuser[] = [] - rotators: Rotator[] = [] - - camera?: Camera - mount?: Mount - wheel?: Wheel - focuser?: Focuser - rotator?: Rotator - - readonly captureModes: SequenceCaptureMode[] = ['FULLY', 'INTERLEAVED'] - readonly plan = structuredClone(DEFAULT_SEQUENCE_PLAN) - - private entryToApply?: CameraStartCapture - private entryToApplyCount: [number, number] = [0, 0] - readonly availableEntryPropertiesToApply = new Map() - showEntryPropertiesToApplyDialog = false - readonly entryMenuModel: SlideMenuItem[] = [ + 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 }, }, { @@ -66,8 +58,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable label: 'Apply to all above', slideMenu: [], command: () => { - this.entryToApplyCount = [-1000, 0] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [-1000, 0] + this.property.showDialog = true }, }, { @@ -75,8 +67,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable label: 'Apply to above', slideMenu: [], command: () => { - this.entryToApplyCount = [-1, 0] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [-1, 0] + this.property.showDialog = true }, }, { @@ -84,8 +76,8 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable label: 'Apply to below', slideMenu: [], command: () => { - this.entryToApplyCount = [1, 0] - this.showEntryPropertiesToApplyDialog = true + this.property.count = [1, 0] + this.property.showDialog = true }, }, { @@ -93,104 +85,103 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable 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 browserWindowService: BrowserWindowService, + private readonly electronService: ElectronService, + private readonly preferenceService: PreferenceService, + private readonly primeService: PrimeService, 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(DEFAULT_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 primeService.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) { @@ -200,7 +191,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable } }) - electron.on('WHEEL.UPDATED', (event) => { + electronService.on('WHEEL.UPDATED', (event) => { const wheel = this.wheels.find((e) => e.id === event.device.id) if (wheel) { @@ -210,7 +201,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable } }) - electron.on('FOCUSER.UPDATED', (event) => { + electronService.on('FOCUSER.UPDATED', (event) => { const focuser = this.focusers.find((e) => e.id === event.device.id) if (focuser) { @@ -220,7 +211,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable } }) - electron.on('ROTATOR.UPDATED', (event) => { + electronService.on('ROTATOR.UPDATED', (event) => { const rotator = this.rotators.find((e) => e.id === event.device.id) if (rotator) { @@ -230,7 +221,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable } }) - electron.on('SEQUENCER.ELAPSED', (event) => { + electronService.on('SEQUENCER.ELAPSED', (event) => { ngZone.run(() => { if (this.running !== event.remainingTime > 0) { this.enableOrDisableTopbarMenu(event.remainingTime <= 0) @@ -247,10 +238,6 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable } }) }) - - for (const p of SEQUENCE_ENTRY_PROPERTIES) { - this.availableEntryPropertiesToApply.set(p, true) - } } async ngAfterContentInit() { @@ -262,9 +249,9 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable 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') @@ -273,141 +260,128 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable } async tick() { - 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) + 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)) } - 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', - filterPosition: 0, - shutterPosition: 0, - focusOffset: 0, - 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.primeService.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.primeService.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 settings = this.preference.settings.get() - this.plan.namingFormat.light ??= settings.namingFormat.light || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.light - this.plan.namingFormat.dark ??= settings.namingFormat.dark || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.dark - this.plan.namingFormat.flat ??= settings.namingFormat.flat || DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT.flat - this.plan.namingFormat.bias ??= settings.namingFormat.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) { - resetCameraCaptureNamingFormat(type, this.plan.namingFormat, this.preference.settings.get().namingFormat) - 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': @@ -421,175 +395,197 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable 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.tick() - - 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.tick() + protected async cameraChanged() { + if (this.plan.camera) { + await this.api.cameraListen(this.plan.camera) + this.updateSequencesFromCamera(this.plan.camera) + } + + this.savePreference() } - focuserChanged() { - return this.tick() + protected async mountChanged() { + if (this.plan.mount) { + await this.api.mountListen(this.plan.mount) + } + + this.savePreference() } - wheelChanged() { - return this.tick() + protected async focuserChanged() { + if (this.plan.focuser) { + await this.api.focuserListen(this.plan.focuser) + } + + this.savePreference() } - rotatorChanged() { - return this.tick() + 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/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 4e62ec9c4..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,11 +1,11 @@ -import { Component, Input } from '@angular/core' +import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core' import { CameraStartCapture } from '../../types/camera.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 }) @@ -20,6 +20,12 @@ export class CameraInfoComponent { @Input() 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/path-chooser/path-chooser.component.ts b/desktop/src/shared/components/path-chooser/path-chooser.component.ts index 4191b402f..8371df8cb 100644 --- a/desktop/src/shared/components/path-chooser/path-chooser.component.ts +++ b/desktop/src/shared/components/path-chooser/path-chooser.component.ts @@ -26,7 +26,7 @@ export class PathChooserComponent { protected readonly directory!: boolean @Input() - protected readonly path?: string + protected path?: string @Output() readonly pathChange = new EventEmitter() @@ -41,6 +41,7 @@ export class PathChooserComponent { const path = await (this.directory ? this.electron.openDirectory({ defaultPath }) : this.electron.openFile({ defaultPath })) if (path) { + this.path = path this.pathChange.emit(path) localStorage.setItem(key, path) } 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/pipes/dropdown-options.pipe.ts b/desktop/src/shared/pipes/dropdown-options.pipe.ts index 254c8e578..4055016d2 100644 --- a/desktop/src/shared/pipes/dropdown-options.pipe.ts +++ b/desktop/src/shared/pipes/dropdown-options.pipe.ts @@ -8,7 +8,7 @@ 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' @@ -32,7 +32,7 @@ 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[] @@ -41,6 +41,7 @@ export interface DropdownOptions { SATELLITE_GROUP_TYPE: SatelliteGroupType[] CONSTELLATION: Constellation[] SKY_OBJECT_TYPE: SkyObjectType[] + SEQUENCER_CAPTURE_MODE: SequencerCaptureMode[] } @Pipe({ name: 'dropdownOptions' }) @@ -101,6 +102,8 @@ export class DropdownOptionsPipe implements PipeTransform { 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.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index 5c5e26936..a2d2822b8 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -8,7 +8,7 @@ import { GuideDirection, GuideState, GuiderPlotMode, GuiderYAxisUnit } from '../ 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' @@ -37,7 +37,7 @@ export type EnumPipeKey = | GuiderPlotMode | GuiderYAxisUnit | MountRemoteControlProtocol - | SequenceCaptureMode + | SequencerCaptureMode | Bitpix | StackerType | StackerGroupType 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/services/api.service.ts b/desktop/src/shared/services/api.service.ts index d8001862f..4dc734514 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -15,7 +15,7 @@ import { AnnotateImageRequest, CoordinateInterpolation, DetectedStar, FOVCamera, 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 { Wheel } from '../types/wheel.types' @@ -633,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) } diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts index 1d91e565a..739bf6b3b 100644 --- a/desktop/src/shared/services/preference.service.ts +++ b/desktop/src/shared/services/preference.service.ts @@ -12,7 +12,7 @@ import { DEFAULT_HOME_PREFERENCE, HomePreference, homePreferenceWithDefault } fr 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 } from '../types/sequencer.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' @@ -86,7 +86,7 @@ export class PreferenceService { 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)) + 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) diff --git a/desktop/src/shared/types/angular.types.ts b/desktop/src/shared/types/angular.types.ts index 054afff2d..7abbd0d26 100644 --- a/desktop/src/shared/types/angular.types.ts +++ b/desktop/src/shared/types/angular.types.ts @@ -16,7 +16,7 @@ function padNumber(value: number) { } export function extractDate(date: Date) { - return `${date.getFullYear()}-${padNumber(date.getMonth())}-${padNumber(date.getDay())}` + return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}` } export function extractTime(date: Date, hasSeconds: boolean = true) { diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index b59246e2b..2bdebb1c8 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -277,6 +277,8 @@ export const CAMERA_CAPTURE_NAMING_FORMAT_DARK = '[camera]_[type]_[width]_[heigh 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, @@ -372,6 +374,7 @@ 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 function cameraPreferenceWithDefault(preference?: Partial, source: CameraPreference = DEFAULT_CAMERA_PREFERENCE) { diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts index 4f7a15348..5fbf79c7e 100644 --- a/desktop/src/shared/types/image.types.ts +++ b/desktop/src/shared/types/image.types.ts @@ -254,8 +254,8 @@ export interface ImageTransformation { } export interface AnnotateImageRequest { - useStarsAndDSOs: boolean - useMinorPlanets: boolean + starsAndDSOs: boolean + minorPlanets: boolean minorPlanetsMagLimit: number includeMinorPlanetsWithoutMagnitude: boolean useSimbad: boolean @@ -438,10 +438,10 @@ export const DEFAULT_STAR_DETECTOR_DIALOG: StarDetectorDialog = { } export const DEFAULT_ANNOTATE_IMAGE_REQUEST: AnnotateImageRequest = { - useStarsAndDSOs: true, - useMinorPlanets: false, - minorPlanetsMagLimit: 18.0, - includeMinorPlanetsWithoutMagnitude: true, + starsAndDSOs: true, + minorPlanets: false, + minorPlanetsMagLimit: 15.0, + includeMinorPlanetsWithoutMagnitude: false, useSimbad: false, } @@ -555,8 +555,8 @@ export function imageStretchWithDefault(stretch?: Partial, source: export function annotateImageRequestWithDefault(request?: Partial, source: AnnotateImageRequest = DEFAULT_ANNOTATE_IMAGE_REQUEST) { if (!request) return structuredClone(source) - request.useStarsAndDSOs ??= source.useStarsAndDSOs - request.useMinorPlanets ??= source.useMinorPlanets + request.starsAndDSOs ??= source.starsAndDSOs + request.minorPlanets ??= source.minorPlanets request.minorPlanetsMagLimit ??= source.minorPlanetsMagLimit request.includeMinorPlanetsWithoutMagnitude ??= source.includeMinorPlanetsWithoutMagnitude request.useSimbad ??= source.useSimbad diff --git a/desktop/src/shared/types/sequencer.types.ts b/desktop/src/shared/types/sequencer.types.ts index 48f85619f..c92cd986b 100644 --- a/desktop/src/shared/types/sequencer.types.ts +++ b/desktop/src/shared/types/sequencer.types.ts @@ -1,12 +1,31 @@ -import { DEFAULT_CAMERA_CAPTURE_NAMING_FORMAT, DEFAULT_DITHER, type AutoSubFolderMode, type CameraCaptureEvent, type CameraCaptureNamingFormat, type CameraStartCapture, type Dither } from './camera.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 @@ -22,20 +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 + camera?: Camera + mount?: Mount + wheel?: Wheel + focuser?: Focuser + rotator?: Rotator } export interface SequencerEvent extends MessageEvent { @@ -47,9 +66,17 @@ 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 = { @@ -66,16 +93,96 @@ export const DEFAULT_AUTO_FOCUS_AFTER_CONDITIONS: AutoFocusAfterConditions = { afterHFDIncreaseEnabled: false, } -export const DEFAULT_SEQUENCE_PLAN: SequencePlan = { +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', - entries: [], 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: structuredClone(DEFAULT_SEQUENCE_PLAN), + 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 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 b75a777d9..f7956d1a9 100644 --- a/desktop/src/shared/types/settings.types.ts +++ b/desktop/src/shared/types/settings.types.ts @@ -77,16 +77,16 @@ export function settingsPreferenceWithDefault(preference?: Partial Date: Fri, 9 Aug 2024 12:57:45 -0300 Subject: [PATCH 9/9] [desktop]: Refactor --- .../app/alignment/alignment.component.html | 2 +- .../src/app/alignment/alignment.component.ts | 6 +- desktop/src/app/app.component.ts | 27 ++++---- desktop/src/app/atlas/atlas.component.ts | 20 +++--- desktop/src/app/camera/camera.component.ts | 8 +-- .../app/filterwheel/filterwheel.component.ts | 20 +++--- .../app/flat-wizard/flat-wizard.component.ts | 8 +-- desktop/src/app/framing/framing.component.ts | 7 +-- desktop/src/app/home/home.component.html | 4 +- desktop/src/app/home/home.component.ts | 44 ++++++------- desktop/src/app/image/image.component.html | 62 +++++++++++-------- desktop/src/app/image/image.component.ts | 36 ++++++++--- desktop/src/app/indi/indi.component.ts | 4 +- .../property/indi-property.component.scss | 2 +- .../indi/property/indi-property.component.ts | 3 +- desktop/src/app/mount/mount.component.ts | 6 +- .../src/app/sequencer/sequencer.component.ts | 15 ++--- .../src/app/settings/settings.component.ts | 3 +- .../device-chooser.component.scss | 0 .../device-chooser.component.ts | 4 +- .../device-list-menu.component.ts | 6 +- .../histogram/histogram.component.scss | 8 --- .../histogram/histogram.component.ts | 4 +- .../components/location/location.dialog.ts | 4 +- .../menu-item/menu-item.component.scss | 0 .../menu-item/menu-item.component.ts | 4 +- .../components/moon/moon.component.html | 2 +- .../components/moon/moon.component.scss | 4 -- .../shared/components/moon/moon.component.ts | 4 +- .../path-chooser/path-chooser.component.ts | 4 +- .../slide-menu/slide-menu.component.scss | 0 .../slide-menu/slide-menu.component.ts | 4 +- .../shared/dialogs/confirm/confirm.dialog.ts | 8 +-- .../interceptors/confirmation.interceptor.ts | 6 +- .../interceptors/location.interceptor.ts | 6 +- .../{prime.service.ts => angular.service.ts} | 10 +-- desktop/src/shared/services/api.service.ts | 4 +- .../shared/services/browser-window.service.ts | 6 +- .../shared/services/confirmation.service.ts | 6 +- .../src/shared/services/electron.service.ts | 34 +++++----- desktop/src/shared/services/http.service.ts | 6 +- 41 files changed, 215 insertions(+), 196 deletions(-) delete mode 100644 desktop/src/shared/components/device-chooser/device-chooser.component.scss delete mode 100644 desktop/src/shared/components/histogram/histogram.component.scss delete mode 100644 desktop/src/shared/components/menu-item/menu-item.component.scss delete mode 100644 desktop/src/shared/components/moon/moon.component.scss delete mode 100644 desktop/src/shared/components/slide-menu/slide-menu.component.scss rename desktop/src/shared/services/{prime.service.ts => angular.service.ts} (80%) diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 8f4e687c1..3be7dfd3c 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -41,7 +41,7 @@ [info]="status" />
, ) { 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) @@ -49,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) } }) @@ -58,36 +58,37 @@ 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() } async close(data?: unknown, force: boolean = false) { if (!this.beforeClose || (await this.beforeClose()) || force) { - return await this.electron.closeWindow(data) + return await this.electronService.closeWindow(data) } else { return undefined } diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index cdcbe3554..dbcf90086 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -9,12 +9,11 @@ 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 { AltitudeDataPoint, @@ -351,10 +350,9 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, private readonly api: ApiService, private readonly browserWindowService: BrowserWindowService, private readonly route: ActivatedRoute, - electron: ElectronService, + electronService: ElectronService, private readonly preferenceService: PreferenceService, - private readonly skyObjectPipe: SkyObjectPipe, - private readonly primeService: PrimeService, + private readonly angularService: AngularService, ngZone: NgZone, ) { app.title = 'Sky Atlas' @@ -367,7 +365,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, }, }) - electron.on('LOCATION.CHANGED', (location) => { + electronService.on('LOCATION.CHANGED', (location) => { ngZone.run(() => { this.loadLocations() @@ -377,11 +375,9 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, }) }) - electron.on('DATA.CHANGED', async (event) => { + electronService.on('DATA.CHANGED', async (event) => { await this.loadTabFromData(event) }) - - // TODO: Refresh graph and twilight if hours past 12 (noon) } ngOnInit() { @@ -487,7 +483,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, this.minorPlanet.closeApproach.result = await this.api.closeApproachesOfMinorPlanets(this.minorPlanet.closeApproach.days, this.minorPlanet.closeApproach.lunarDistance, this.dateTimeAndLocation.dateTime) if (!this.minorPlanet.closeApproach.result.length) { - this.primeService.message('No close approaches found for the given days and lunar distance', 'warn') + this.angularService.message('No close approaches found for the given days and lunar distance', 'warn') } } finally { this.refresh.position = false @@ -504,7 +500,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, protected async skyObjectChanged() { if (this.skyObject.search.selected) { - this.skyObject.name = this.skyObjectPipe.transform(this.skyObject.search.selected, 'name') ?? '-' + this.skyObject.name = this.skyObject.search.selected.name await this.refreshTab(false, true) } } @@ -799,7 +795,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, } private async executeMount(action: (mount: Mount) => void | Promise) { - if (await this.primeService.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/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 99f7e6255..2362f69bb 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -350,7 +350,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { const data = JSON.parse(decodeURIComponent(e['data'] as string)) as unknown if (this.app.modal) { - await this.loadCameraStartCaptureOnDialogMode(data as CameraDialogInput) + await this.loadCameraStartCaptureForDialogMode(data as CameraDialogInput) } else { await this.cameraChanged(data as Camera) } @@ -379,7 +379,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { } } - private async loadCameraStartCaptureOnDialogMode(data?: CameraDialogInput) { + private async loadCameraStartCaptureForDialogMode(data?: CameraDialogInput) { if (data) { this.mode = data.mode await this.cameraChanged(data.camera) @@ -756,8 +756,8 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Tickable { } } - static async showAsDialog(window: BrowserWindowService, mode: CameraMode, 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/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 3155597fc..1cc4c9bbf 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -36,6 +36,9 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab protected mode: WheelMode = 'CAPTURE' + private readonly filterPublisher = new Subject() + private readonly filterSubscription?: Subscription + get canShowInfo() { return this.mode === 'CAPTURE' } @@ -56,9 +59,6 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab return this.filters[this.position - 1] } - private readonly filterChangePublisher = new Subject() - private readonly filterChangeSubscription?: Subscription - constructor( private readonly app: AppComponent, private readonly api: ApiService, @@ -112,7 +112,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab } }) - this.filterChangeSubscription = this.filterChangePublisher.pipe(debounceTime(1500)).subscribe(async (filter) => { + 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.electronService.send('WHEEL.RENAMED', { wheel: this.wheel, filter }) @@ -173,7 +173,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab const data = JSON.parse(decodeURIComponent(e['data'] as string)) as unknown if (this.app.modal) { - await this.loadWheelStartCaptureOnDialogMode(data as WheelDialogInput) + await this.loadCameraStartCaptureForDialogMode(data as WheelDialogInput) } else { await this.wheelChanged(data as Wheel) } @@ -192,7 +192,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab @HostListener('window:unload') ngOnDestroy() { this.ticker.unregister(this) - this.filterChangeSubscription?.unsubscribe() + this.filterSubscription?.unsubscribe() } async tick() { @@ -200,7 +200,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab if (this.focuser?.id) await this.api.focuserListen(this.focuser) } - private async loadWheelStartCaptureOnDialogMode(data?: WheelDialogInput) { + private async loadCameraStartCaptureForDialogMode(data?: WheelDialogInput) { if (data) { this.mode = data.mode await this.wheelChanged(data.wheel) @@ -307,7 +307,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab protected filterNameChanged(filter: Filter) { if (filter.name) { - this.filterChangePublisher.next(structuredClone(filter)) + this.filterPublisher.next(structuredClone(filter)) } } @@ -380,8 +380,8 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab return this.app.close(this.makeCameraStartCapture()) } - static async showAsDialog(window: BrowserWindowService, mode: WheelMode, wheel: Wheel, 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.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index c7f754327..9be7b2260 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -1,10 +1,10 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.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 { Tickable, Ticker } from '../../shared/services/ticker.service' import { Camera, DEFAULT_CAMERA, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' import { DEFAULT_FLAT_WIZARD_PREFERENCE } from '../../shared/types/flat-wizard.types' @@ -48,7 +48,7 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Tickable { private readonly api: ApiService, electronService: ElectronService, private readonly browserWindowService: BrowserWindowService, - private readonly primeService: PrimeService, + private readonly angularService: AngularService, private readonly preferenceService: PreferenceService, private readonly ticker: Ticker, ngZone: NgZone, @@ -62,10 +62,10 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Tickable { this.cameraExposure.handleCameraCaptureEvent(event.capture, true) } else if (event.state === 'CAPTURED') { this.running = false - this.primeService.message(`Flat frame captured`) + this.angularService.message('Flat frame captured') } else if (event.state === 'FAILED') { this.running = false - this.primeService.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') } }) }) diff --git a/desktop/src/app/framing/framing.component.ts b/desktop/src/app/framing/framing.component.ts index 9dc33726c..d01f82a70 100644 --- a/desktop/src/app/framing/framing.component.ts +++ b/desktop/src/app/framing/framing.component.ts @@ -1,10 +1,10 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' +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 { DEFAULT_FRAMING_PREFERENCE, HipsSurvey, LoadFraming } from '../../shared/types/framing.types' import { AppComponent } from '../app.component' @@ -15,7 +15,6 @@ import { AppComponent } from '../app.component' export class FramingComponent implements AfterViewInit, OnDestroy { protected readonly preference = structuredClone(DEFAULT_FRAMING_PREFERENCE) protected hipsSurveys: HipsSurvey[] = [] - protected loading = false private frameId = '' @@ -27,7 +26,7 @@ export class FramingComponent implements AfterViewInit, OnDestroy { private readonly browserWindowService: BrowserWindowService, private readonly electronService: ElectronService, private readonly preferenceService: PreferenceService, - private readonly primeService: PrimeService, + private readonly angularService: AngularService, ngZone: NgZone, ) { app.title = 'Framing' @@ -87,7 +86,7 @@ export class FramingComponent implements AfterViewInit, OnDestroy { } catch (e) { console.error(e) - this.primeService.message('Failed to retrieve the image', 'error') + this.angularService.message('Failed to retrieve the image', 'error') } finally { this.loading = false } diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 6dbb5e008..960215797 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -287,11 +287,11 @@
diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 24ad324c9..5bb19a589 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -3,11 +3,11 @@ 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, isCamera } from '../../shared/types/camera.types' import { Device, DeviceType } from '../../shared/types/device.types' import { Focuser, isFocuser } from '../../shared/types/focuser.types' @@ -40,7 +40,7 @@ export class HomeComponent implements AfterContentInit { protected domes: Camera[] = [] protected switches: Camera[] = [] - protected currentPage = 0 + protected page = 0 protected readonly deviceModel: MenuItem[] = [] @@ -55,6 +55,22 @@ export class HomeComponent implements AfterContentInit { }, ] + 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 @@ -122,28 +138,12 @@ export class HomeComponent implements AfterContentInit { return this.connection?.type === 'ALPACA' && this.hasDevices } - 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 [] - } - } - constructor( app: AppComponent, private readonly electronService: ElectronService, private readonly browserWindowService: BrowserWindowService, private readonly api: ApiService, - private readonly primeService: PrimeService, + private readonly angularService: AngularService, private readonly preferenceService: PreferenceService, ngZone: NgZone, ) { @@ -356,7 +356,7 @@ export class HomeComponent implements AfterContentInit { } catch (e) { console.error(e) - this.primeService.message('Connection failed', 'error') + this.angularService.message('Connection failed', 'error') } finally { await this.updateConnection() } @@ -555,13 +555,13 @@ export class HomeComponent implements AfterContentInit { } } - this.currentPage = page + this.page = page event.stopImmediatePropagation() } protected scrollTo(event: Event, page: number) { - this.currentPage = page + this.page = page this.scrollToPage(page) event.stopImmediatePropagation() } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index e0908e86d..3bec82bfe 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -1054,7 +1054,9 @@
@@ -1069,6 +1071,7 @@ styleClass="p-inputtext-sm border-0" [showButtons]="true" [(ngModel)]="fov.selected.focalLength" + (ngModelChange)="saveFOV()" spinnableNumber /> @@ -1079,6 +1082,7 @@ styleClass="p-inputtext-sm border-0" [showButtons]="true" [(ngModel)]="fov.selected.aperture" + (ngModelChange)="saveFOV()" spinnableNumber /> @@ -1101,6 +1105,7 @@ [min]="1" [max]="9999" [(ngModel)]="fov.selected.cameraSize.width" + (ngModelChange)="saveFOV()" spinnableNumber /> @@ -1113,6 +1118,7 @@ [min]="1" [max]="9999" [(ngModel)]="fov.selected.cameraSize.height" + (ngModelChange)="saveFOV()" spinnableNumber /> @@ -1128,6 +1134,7 @@ [minFractionDigits]="0" [maxFractionDigits]="2" [(ngModel)]="fov.selected.pixelSize.width" + (ngModelChange)="saveFOV()" locale="en" spinnableNumber /> @@ -1144,12 +1151,13 @@ [minFractionDigits]="0" [maxFractionDigits]="2" [(ngModel)]="fov.selected.pixelSize.height" + (ngModelChange)="saveFOV()" locale="en" spinnableNumber />
-
+
@@ -1173,6 +1182,7 @@ [min]="1" [max]="5" [(ngModel)]="fov.selected.bin" + (ngModelChange)="saveFOV()" spinnableNumber /> @@ -1188,79 +1198,81 @@ [minFractionDigits]="0" [maxFractionDigits]="2" [(ngModel)]="fov.rotation" + (ngModelChange)="saveFOV(false)" locale="en" 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()" />
} diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 6e96e0ed0..ca3e8607e 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -9,15 +9,16 @@ import { DeviceListMenuComponent } from '../../shared/components/device-list-men 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 { EquatorialCoordinateJ2000 } from '../../shared/types/atlas.types' import { Camera } from '../../shared/types/camera.types' import { AstronomicalObjectDialog, + DEFAULT_FOV, DEFAULT_IMAGE_ANNOTATION_DIALOG, DEFAULT_IMAGE_CALIBRATION, DEFAULT_IMAGE_DATA, @@ -391,7 +392,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private readonly electronService: ElectronService, private readonly browserWindowService: BrowserWindowService, private readonly preferenceService: PreferenceService, - private readonly primeService: PrimeService, + private readonly angularService: AngularService, ngZone: NgZone, ) { app.title = 'Image' @@ -441,7 +442,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { label: 'Settings', command: () => { this.settings.showDialog = true - } + }, }) electronService.on('CAMERA.CAPTURE_ELAPSED', async (event) => { @@ -1210,8 +1211,23 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - editFOV(fov: FOV) { - this.fov.selected = fov + private removeSelectedFOV() { + this.fov.selected = structuredClone(DEFAULT_FOV) + } + + protected selectFOV(fov: FOV) { + if (this.fov.selected === fov) { + this.removeSelectedFOV() + } else { + this.fov.selected = fov + } + } + + protected saveFOV(compute: boolean = true) { + // Edited. + if (this.fov.fovs.includes(this.fov.selected) && (!compute || this.computeFOV(this.fov.selected))) { + this.savePreference() + } } private computeFOV(fov: FOV) { @@ -1252,12 +1268,16 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } } - deleteFOV(fov: FOV) { + protected deleteFOV(fov: FOV) { const index = this.fov.fovs.indexOf(fov) if (index >= 0) { this.fov.fovs.splice(index, 1) this.savePreference() + + if (this.fov.selected === this.fov.fovs[index]) { + this.removeSelectedFOV() + } } } @@ -1285,7 +1305,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async executeCamera(action: (camera: Camera) => void | Promise, showConfirmation: boolean = true) { - if (showConfirmation && (await this.primeService.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 } @@ -1307,7 +1327,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async executeMount(action: (mount: Mount) => void | Promise, showConfirmation: boolean = true) { - if (showConfirmation && (await this.primeService.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 } diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 298d19fa7..f528be883 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -96,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) } @@ -108,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 33a3607f1..3c6597153 100644 --- a/desktop/src/app/indi/property/indi-property.component.scss +++ b/desktop/src/app/indi/property/indi-property.component.scss @@ -1,4 +1,4 @@ -:host { +neb-indi-property { background: rgba(0, 0, 0, 0.1); border-radius: 8px; display: block; diff --git a/desktop/src/app/indi/property/indi-property.component.ts b/desktop/src/app/indi/property/indi-property.component.ts index 87e58f06a..7ccde2aa4 100644 --- a/desktop/src/app/indi/property/indi-property.component.ts +++ b/desktop/src/app/indi/property/indi-property.component.ts @@ -1,10 +1,11 @@ -import { AfterContentInit, Component, EventEmitter, Input, 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 { @Input({ required: true }) diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index d6e372aad..1707f0d14 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -4,11 +4,11 @@ import hotkeys from 'hotkeys-js' import { Subject, Subscription, interval, throttleTime } from 'rxjs' import { SlideMenuItem } 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 { Tickable, Ticker } from '../../shared/services/ticker.service' import { BodyTabType, ComputedLocation, DEFAULT_COMPUTED_LOCATION } from '../../shared/types/atlas.types' import { DEFAULT_MOUNT, DEFAULT_MOUNT_PREFERENCE, DEFAULT_MOUNT_REMOTE_CONTROL_DIALOG, Mount, MountRemoteControlProtocol, MountSlewDirection, SlewRate, TrackMode } from '../../shared/types/mount.types' @@ -210,7 +210,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { electronService: ElectronService, private readonly preferenceService: PreferenceService, private readonly route: ActivatedRoute, - private readonly primeService: PrimeService, + private readonly angularService: AngularService, private readonly ticker: Ticker, ngZone: NgZone, ) { @@ -344,7 +344,7 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { 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.primeService.message('Failed to start remote control', 'error') + this.angularService.message('Failed to start remote control', 'error') } } diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index 74fa18ca3..c1a92261b 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -4,11 +4,11 @@ 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 { 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 { Tickable, Ticker } from '../../shared/services/ticker.service' import { JsonFile } from '../../shared/types/app.types' import { Camera, cameraCaptureNamingFormatWithDefault, FrameType, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types' @@ -151,7 +151,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable private readonly browserWindowService: BrowserWindowService, private readonly electronService: ElectronService, private readonly preferenceService: PreferenceService, - private readonly primeService: PrimeService, + private readonly angularService: AngularService, private readonly ticker: Ticker, ngZone: NgZone, ) { @@ -164,7 +164,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable app.beforeClose = async () => { if (!this.saveMenuItem.disabled) { - return !(await primeService.confirm('Are you sure you want to close the window? Please make sure to save before exiting to avoid losing any important changes.')) + 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 } @@ -267,8 +267,9 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable 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 } protected add() { @@ -301,7 +302,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable private loadPlanFromJson(file: JsonFile) { if (!this.loadPlan(file.json)) { - this.primeService.message(`No sequence found`, 'warn') + this.angularService.message('No sequence found', 'warn') this.add() } @@ -322,7 +323,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable return } - this.primeService.message(`Failed to load the file`, 'error') + this.angularService.message('Failed to load the file', 'error') this.preference.loadPath = undefined this.savePreference() diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 7dd07b6c1..4cf58566a 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, OnDestroy } from '@angular/core' +import { AfterViewInit, Component, HostListener, OnDestroy } from '@angular/core' import { debounceTime, Subject, Subscription } from 'rxjs' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' @@ -59,6 +59,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.loadPreference() } + @HostListener('window:unload') ngOnDestroy() { this.locationChangeSubscription?.unsubscribe() } 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 a2b835954..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,7 +8,7 @@ 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 }) 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 ce6d0db6b..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, 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' @@ -47,7 +47,7 @@ export class DeviceListMenuComponent { @ViewChild('menu') private readonly menu!: DialogMenuComponent - constructor(private readonly prime: PrimeService) {} + constructor(private readonly angularService: AngularService) {} show(devices: T[], selected?: NoInfer, header?: string) { const model: SlideMenuItem[] = [] @@ -57,7 +57,7 @@ export class DeviceListMenuComponent { 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 } 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/components/location/location.dialog.ts b/desktop/src/shared/components/location/location.dialog.ts index 166e7d5d3..90a7ebef0 100644 --- a/desktop/src/shared/components/location/location.dialog.ts +++ b/desktop/src/shared/components/location/location.dialog.ts @@ -9,7 +9,7 @@ import { MapComponent } from '../map/map.component' }) export class LocationComponent implements AfterViewInit { @ViewChild('map') - private readonly map!: MapComponent + private readonly map?: MapComponent @Input() readonly location!: Location @@ -31,7 +31,7 @@ export class LocationComponent implements AfterViewInit { } ngAfterViewInit() { - this.map.refresh() + this.map?.refresh() } save() { 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 3f08f8ad8..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,4 +1,4 @@ -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/angular.types' @@ -56,7 +56,7 @@ 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 }) diff --git a/desktop/src/shared/components/moon/moon.component.html b/desktop/src/shared/components/moon/moon.component.html index 70136bde4..960765765 100644 --- a/desktop/src/shared/components/moon/moon.component.html +++ b/desktop/src/shared/components/moon/moon.component.html @@ -2,4 +2,4 @@ #moon [height]="height" [width]="width" - style="filter: brightness(1.5)"> + 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 382e2c69d..16f13b385 100644 --- a/desktop/src/shared/components/moon/moon.component.ts +++ b/desktop/src/shared/components/moon/moon.component.ts @@ -1,9 +1,9 @@ -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 { @Input() 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 8371df8cb..92b525181 100644 --- a/desktop/src/shared/components/path-chooser/path-chooser.component.ts +++ b/desktop/src/shared/components/path-chooser/path-chooser.component.ts @@ -31,14 +31,14 @@ export class PathChooserComponent { @Output() readonly pathChange = new EventEmitter() - constructor(private readonly electron: ElectronService) {} + constructor(private readonly electronService: ElectronService) {} protected async choosePath() { const key = `pathChooser.${this.key}.defaultPath` 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 a180c25cc..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 }) diff --git a/desktop/src/shared/dialogs/confirm/confirm.dialog.ts b/desktop/src/shared/dialogs/confirm/confirm.dialog.ts index 9a60a74ca..0f65686bd 100644 --- a/desktop/src/shared/dialogs/confirm/confirm.dialog.ts +++ b/desktop/src/shared/dialogs/confirm/confirm.dialog.ts @@ -1,10 +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' + templateUrl: './confirm.dialog.html', }) export class ConfirmDialog { readonly header: string @@ -26,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/interceptors/confirmation.interceptor.ts b/desktop/src/shared/interceptors/confirmation.interceptor.ts index 30f476127..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,7 +15,7 @@ export class ConfirmationInterceptor implements HttpInterceptor { const idempotencyKey = req.headers.get(IdempotencyKeyInterceptor.HEADER_KEY) if (idempotencyKey) { - this.confirmation.register(idempotencyKey) + this.confirmationService.register(idempotencyKey) } const res = next.handle(req) @@ -23,7 +23,7 @@ export class ConfirmationInterceptor implements HttpInterceptor { if (idempotencyKey) { return res.pipe( finalize(() => { - 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 b8ef3715b..8e2af3dbc 100644 --- a/desktop/src/shared/interceptors/location.interceptor.ts +++ b/desktop/src/shared/interceptors/location.interceptor.ts @@ -7,7 +7,7 @@ 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')) { @@ -15,7 +15,7 @@ export class LocationInterceptor implements HttpInterceptor { const hasLocation = params.get('hasLocation') if (!hasLocation || hasLocation === 'true') { - const location = this.preference.settings.get().location + const location = this.preferenceService.settings.get().location req = req.clone({ headers: req.headers.set(LocationInterceptor.HEADER_KEY, JSON.stringify(location)), @@ -24,7 +24,7 @@ export class LocationInterceptor implements HttpInterceptor { const id = parseInt(hasLocation) if (id) { - const locations = this.preference.settings.get().locations + const locations = this.preferenceService.settings.get().locations const location = locations.find((e) => e.id === id) if (location) { 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 4dc734514..1278d9cab 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -24,12 +24,12 @@ 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) { diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index ff3724398..2ff5fa757 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -14,16 +14,16 @@ 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 = {}) { diff --git a/desktop/src/shared/services/confirmation.service.ts b/desktop/src/shared/services/confirmation.service.ts index 45918bfb5..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,7 +26,7 @@ export class ConfirmationService { } async processConfirmationEvent(event: ConfirmationEvent) { - 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 aa8012ddb..bf88acc10 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -24,13 +24,20 @@ import { Rotator } from '../types/rotator.types' import { SequencerEvent } from '../types/sequencer.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 @@ -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,11 +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) { + on(channel: K, listener: (arg: EventTypes[K]) => void) { this.ipcRenderer.on(channel, (_, arg) => { listener(arg) }) @@ -147,7 +154,7 @@ export class ElectronService { return this.openFile({ ...data, windowId: data?.windowId ?? window.id, - filters: IMAGE_FILE_FILTER, + filters: OPEN_IMAGE_FILE_FILTER, }) } @@ -155,7 +162,7 @@ export class ElectronService { return this.openFiles({ ...data, windowId: data?.windowId ?? window.id, - filters: IMAGE_FILE_FILTER, + filters: OPEN_IMAGE_FILE_FILTER, }) } @@ -163,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, }) } 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}`))