From e4fa39d7aa15e7b1ba2367ed4fe632f58ab9b807 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 8 Apr 2024 15:14:36 -0300 Subject: [PATCH] [api][desktop]: Improve Calibration --- api/schemas/objectbox.json | 96 +++---- .../polar/PolarAlignmentController.kt | 4 +- .../nebulosa/api/atlas/SkyAtlasController.kt | 4 +- .../database/PathPropertyConverter.kt | 15 ++ ...viceOrEntityParamMethodArgumentResolver.kt | 6 +- .../calibration/CalibrationFrameController.kt | 38 +-- .../api/calibration/CalibrationFrameEntity.kt | 6 +- .../api/calibration/CalibrationFrameGroup.kt | 1 + .../calibration/CalibrationFrameRepository.kt | 22 +- .../calibration/CalibrationFrameService.kt | 142 +++++----- .../nebulosa/api/cameras/CameraController.kt | 22 +- .../api/focusers/FocuserController.kt | 16 +- .../api/guiding/GuideOutputController.kt | 8 +- .../nebulosa/api/image/ImageController.kt | 4 +- .../nebulosa/api/indi/INDIController.kt | 6 +- .../nebulosa/api/mounts/MountController.kt | 38 +-- .../api/sequencer/SequencerController.kt | 4 +- .../nebulosa/api/wheels/WheelController.kt | 10 +- .../api/wizard/flat/FlatWizardController.kt | 4 +- desktop/app/main.ts | 4 +- desktop/src/app/app.module.ts | 2 + .../calibration/calibration.component.html | 140 ++++------ .../calibration/calibration.component.scss | 11 + .../app/calibration/calibration.component.ts | 252 ++++++++++-------- desktop/src/app/camera/camera.component.ts | 2 +- desktop/src/shared/services/api.service.ts | 14 +- .../shared/services/browser-window.service.ts | 6 +- desktop/src/shared/types/app.types.ts | 1 + desktop/src/shared/types/calibration.types.ts | 5 +- desktop/src/styles.scss | 3 +- .../main/kotlin/nebulosa/fits/FitsFormat.kt | 5 + .../main/kotlin/nebulosa/fits/FitsHelper.kt | 9 +- .../src/main/kotlin/nebulosa/fits/FitsPath.kt | 2 - .../src/test/kotlin/FitsFormatTest.kt | 14 + nebulosa-fits/src/test/kotlin/FitsReadTest.kt | 20 +- .../src/test/kotlin/FitsWriteTest.kt | 2 +- nebulosa-wcs/src/test/kotlin/LibWCSTest.kt | 3 +- .../main/kotlin/nebulosa/xisf/XisfFormat.kt | 10 +- .../main/kotlin/nebulosa/xisf/XisfHelper.kt | 9 +- .../src/main/kotlin/nebulosa/xisf/XisfPath.kt | 2 - .../src/test/kotlin/XisfFormatTest.kt | 7 + 41 files changed, 535 insertions(+), 434 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/beans/converters/database/PathPropertyConverter.kt create mode 100644 nebulosa-fits/src/test/kotlin/FitsFormatTest.kt diff --git a/api/schemas/objectbox.json b/api/schemas/objectbox.json index a3669305b..04ec84af3 100644 --- a/api/schemas/objectbox.json +++ b/api/schemas/objectbox.json @@ -4,81 +4,81 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "1:8996632906249699640", - "lastPropertyId": "13:4967132491159177650", + "id": "1:659766577132862050", + "lastPropertyId": "13:7107587391686688306", "name": "CalibrationFrameEntity", "properties": [ { - "id": "1:8753104016604228424", + "id": "1:2268113761903193037", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:5256997905701910721", + "id": "2:1598928230620079098", "name": "type", - "indexId": "1:6629699679938480062", + "indexId": "1:7442085554692140361", "type": 5, "flags": 8 }, { - "id": "3:4250539975904011338", - "name": "camera", - "indexId": "2:8670761666150034104", + "id": "3:7997224882771694883", + "name": "name", + "indexId": "2:6070065848831656560", "type": 9, "flags": 2048 }, { - "id": "4:2964126873299902795", + "id": "4:1812743003555250552", "name": "filter", - "indexId": "3:356806529647771872", + "indexId": "3:5789122857555378891", "type": 9, "flags": 2048 }, { - "id": "5:6592821044234392872", + "id": "5:5895526385719301255", "name": "exposureTime", "type": 6 }, { - "id": "6:7841731947961734124", + "id": "6:1217849729581288112", "name": "temperature", "type": 8 }, { - "id": "7:8769358817044866175", + "id": "7:4483343404132984385", "name": "width", "type": 5 }, { - "id": "8:9066846022258802237", + "id": "8:4673309899326922479", "name": "height", "type": 5 }, { - "id": "9:6089591003549470592", + "id": "9:4928491775921601028", "name": "binX", "type": 5 }, { - "id": "10:1137944585964286888", + "id": "10:7407952871178343222", "name": "binY", "type": 5 }, { - "id": "11:75379120605439289", + "id": "11:1772645455690083085", "name": "gain", "type": 8 }, { - "id": "12:5234537218429949481", + "id": "12:1957209335781305189", "name": "path", - "indexId": "4:1409612170773192505", + "indexId": "4:407713126153778253", "type": 9, "flags": 2080 }, { - "id": "13:4967132491159177650", + "id": "13:7107587391686688306", "name": "enabled", "type": 1 } @@ -86,25 +86,25 @@ "relations": [] }, { - "id": "2:2865903592621070186", - "lastPropertyId": "3:4231519923961266979", + "id": "2:2703822036195087198", + "lastPropertyId": "3:4649483922621116787", "name": "PreferenceEntity", "properties": [ { - "id": "1:6437649959717305879", + "id": "1:8352093925685013458", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:3424721012607374437", + "id": "2:8332376147713309804", "name": "key", - "indexId": "5:3959175721933196667", + "indexId": "5:422443843445456626", "type": 9, "flags": 34848 }, { - "id": "3:4231519923961266979", + "id": "3:4649483922621116787", "name": "value", "type": 9 } @@ -112,28 +112,28 @@ "relations": [] }, { - "id": "3:7591326030822090985", - "lastPropertyId": "4:960749109935496711", + "id": "3:134389271270421743", + "lastPropertyId": "4:3302609175063994624", "name": "SatelliteEntity", "properties": [ { - "id": "1:8984524405419355357", + "id": "1:9030098363904692142", "name": "id", "type": 6, "flags": 129 }, { - "id": "2:954886680255875455", + "id": "2:111042583962333373", "name": "name", "type": 9 }, { - "id": "3:2209800562197387575", + "id": "3:7030648240130620532", "name": "tle", "type": 9 }, { - "id": "4:960749109935496711", + "id": "4:3302609175063994624", "name": "groups", "type": 30 } @@ -141,68 +141,68 @@ "relations": [] }, { - "id": "4:1799461706223884117", - "lastPropertyId": "12:1425111719526355640", + "id": "4:7940094622027054665", + "lastPropertyId": "12:7594216486430476300", "name": "SimbadEntity", "properties": [ { - "id": "1:8675363815775912010", + "id": "1:7823466820865818841", "name": "id", "type": 6, "flags": 129 }, { - "id": "2:9134078346895015096", + "id": "2:3815672930103478384", "name": "name", "type": 9 }, { - "id": "3:5687549887167089900", + "id": "3:9175198490205670707", "name": "type", "type": 5 }, { - "id": "4:425502224150658936", + "id": "4:2382314709752878141", "name": "rightAscensionJ2000", "type": 8 }, { - "id": "5:2116112963954541453", + "id": "5:505218497375297076", "name": "declinationJ2000", "type": 8 }, { - "id": "6:3944877011180871841", + "id": "6:8873025996435250547", "name": "magnitude", "type": 8 }, { - "id": "7:7502412627527780934", + "id": "7:3149730687054027260", "name": "pmRA", "type": 8 }, { - "id": "8:2354824452575366292", + "id": "8:3208786528041087924", "name": "pmDEC", "type": 8 }, { - "id": "9:266214111788703179", + "id": "9:2691231717244757834", "name": "parallax", "type": 8 }, { - "id": "10:3283290633306994233", + "id": "10:5070213892916650707", "name": "radialVelocity", "type": 8 }, { - "id": "11:2537767302764251008", + "id": "11:6365445684038101726", "name": "redshift", "type": 8 }, { - "id": "12:1425111719526355640", + "id": "12:7594216486430476300", "name": "constellation", "type": 5 } @@ -210,8 +210,8 @@ "relations": [] } ], - "lastEntityId": "4:1799461706223884117", - "lastIndexId": "5:3959175721933196667", + "lastEntityId": "4:7940094622027054665", + "lastIndexId": "5:422443843445456626", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt index 487ee934b..b0e54e1af 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt @@ -16,7 +16,7 @@ class PolarAlignmentController( @PutMapping("darv/{camera}/{guideOutput}/start") fun darvStart( - @DeviceOrEntityParam camera: Camera, @DeviceOrEntityParam guideOutput: GuideOutput, + camera: Camera, guideOutput: GuideOutput, @RequestBody body: DARVStartRequest, ) = polarAlignmentService.darvStart(camera, guideOutput, body) @@ -27,7 +27,7 @@ class PolarAlignmentController( @PutMapping("tppa/{camera}/{mount}/start") fun tppaStart( - @DeviceOrEntityParam camera: Camera, @DeviceOrEntityParam mount: Mount, + camera: Camera, mount: Mount, @RequestBody body: TPPAStartRequest, ) = polarAlignmentService.tppaStart(camera, mount, body) diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt index 5251bfee0..30ecf59f5 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt @@ -115,14 +115,14 @@ class SkyAtlasController( @GetMapping("satellites/{satellite}/position") fun positionOfSatellite( - @DeviceOrEntityParam satellite: SatelliteEntity, + satellite: SatelliteEntity, @LocationParam location: Location, @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfSatellite(location, satellite, dateTime) @GetMapping("satellites/{satellite}/altitude-points") fun altitudePointsOfSatellite( - @DeviceOrEntityParam satellite: SatelliteEntity, + satellite: SatelliteEntity, @LocationParam location: Location, @DateAndTimeParam dateTime: LocalDate, @RequestParam(required = false, defaultValue = "1") stepSize: Int, diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/database/PathPropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/database/PathPropertyConverter.kt new file mode 100644 index 000000000..b3d27b1c6 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/database/PathPropertyConverter.kt @@ -0,0 +1,15 @@ +package nebulosa.api.beans.converters.database + +import io.objectbox.converter.PropertyConverter +import java.nio.file.Path + +class PathPropertyConverter : PropertyConverter { + + override fun convertToEntityProperty(databaseValue: String?): Path? { + return databaseValue?.let(Path::of) + } + + override fun convertToDatabaseValue(entityProperty: Path?): String? { + return entityProperty?.toString() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParamMethodArgumentResolver.kt index 85c6c2eff..08e8a4f89 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParamMethodArgumentResolver.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/device/DeviceOrEntityParamMethodArgumentResolver.kt @@ -4,6 +4,8 @@ import nebulosa.api.atlas.SatelliteEntity import nebulosa.api.atlas.SatelliteRepository import nebulosa.api.beans.converters.annotation import nebulosa.api.beans.converters.parameter +import nebulosa.api.calibration.CalibrationFrameEntity +import nebulosa.api.calibration.CalibrationFrameRepository import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.Device import nebulosa.indi.device.camera.Camera @@ -24,11 +26,13 @@ import org.springframework.web.server.ResponseStatusException @Component class DeviceOrEntityParamMethodArgumentResolver( private val satelliteRepository: SatelliteRepository, + private val calibrationFrameRepository: CalibrationFrameRepository, private val connectionService: ConnectionService, ) : HandlerMethodArgumentResolver { private val entityResolvers = mapOf, (String) -> Any?>( SatelliteEntity::class.java to { satelliteRepository.find(it.toLong()) }, + CalibrationFrameEntity::class.java to { calibrationFrameRepository.find(it.toLong()) }, Device::class.java to { connectionService.device(it) }, Camera::class.java to { connectionService.camera(it) }, Mount::class.java to { connectionService.mount(it) }, @@ -49,7 +53,7 @@ class DeviceOrEntityParamMethodArgumentResolver( ): Any? { val requestParam = parameter.annotation() val parameterName = requestParam?.name?.ifBlank { null } ?: parameter.parameterName ?: "id" - val parameterValue = webRequest.parameter(parameterName) ?: requestParam?.defaultValue + val parameterValue = webRequest.parameter(parameterName) ?: requestParam?.defaultValue?.ifBlank { null } val entity = entityByParameterValue(parameter.parameterType, parameterValue) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt index c52f57053..199bf8db7 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt @@ -1,37 +1,41 @@ package nebulosa.api.calibration -import nebulosa.api.beans.converters.device.DeviceOrEntityParam -import nebulosa.indi.device.camera.Camera +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 +@Validated @RestController @RequestMapping("calibration-frames") class CalibrationFrameController( private val calibrationFrameService: CalibrationFrameService, ) { - @GetMapping("{camera}") - fun groups(@DeviceOrEntityParam camera: Camera): List { + @GetMapping + fun groups() = calibrationFrameService.groups() + + @GetMapping("{name}") + fun groupedCalibrationFrames(@PathVariable name: String): List { var id = 0 - val groupedFrames = calibrationFrameService.groupedCalibrationFrames(camera.name) - return groupedFrames.map { CalibrationFrameGroup(id++, it.key, it.value) } + val groupedFrames = calibrationFrameService.groupedCalibrationFrames(name) + return groupedFrames.map { CalibrationFrameGroup(++id, name, it.key, it.value) } } - @PutMapping("{camera}") - fun upload(@DeviceOrEntityParam camera: Camera, @RequestParam path: Path): List { - return calibrationFrameService.upload(camera.name, path) + @PutMapping("{name}") + fun upload(@PathVariable name: String, @RequestParam path: Path): List { + return calibrationFrameService.upload(name, path) } - @PatchMapping("{id}") + @PatchMapping("{frame}") fun edit( - @PathVariable id: Long, - @RequestParam(required = false) path: String? = "", - @RequestParam enabled: Boolean, - ) = calibrationFrameService.edit(id, path, enabled) + frame: CalibrationFrameEntity, + @Valid @NotBlank @RequestParam name: String, @RequestParam enabled: Boolean, + ) = calibrationFrameService.edit(frame, name, enabled) - @DeleteMapping("{id}") - fun delete(@PathVariable id: Long) { - calibrationFrameService.delete(id) + @DeleteMapping("{frame}") + fun delete(frame: CalibrationFrameEntity) { + calibrationFrameService.delete(frame) } } diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt index 9bf5519b2..669bffbec 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt @@ -2,14 +2,16 @@ package nebulosa.api.calibration import io.objectbox.annotation.* import nebulosa.api.beans.converters.database.FrameTypePropertyConverter +import nebulosa.api.beans.converters.database.PathPropertyConverter import nebulosa.api.database.BoxEntity import nebulosa.indi.device.camera.FrameType +import java.nio.file.Path @Entity data class CalibrationFrameEntity( @Id override var id: Long = 0L, @Index @Convert(converter = FrameTypePropertyConverter::class, dbType = Int::class) var type: FrameType = FrameType.LIGHT, - @Index var camera: String? = null, + @Index var name: String = "", @Index var filter: String? = null, var exposureTime: Long = 0L, var temperature: Double = 0.0, @@ -18,6 +20,6 @@ data class CalibrationFrameEntity( var binX: Int = 0, var binY: Int = 0, var gain: Double = 0.0, - @Unique var path: String? = null, + @Unique @Convert(converter = PathPropertyConverter::class, dbType = String::class) var path: Path? = null, var enabled: Boolean = true, ) : BoxEntity diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt index 1320e3499..7e4171cda 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameGroup.kt @@ -2,6 +2,7 @@ package nebulosa.api.calibration data class CalibrationFrameGroup( val id: Int, + val name: String, val key: CalibrationGroupKey, 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 8db72ad5a..10399bf92 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt @@ -13,27 +13,29 @@ import org.springframework.stereotype.Component class CalibrationFrameRepository(@Qualifier("calibrationFrameBox") override val box: Box) : BoxRepository() { - fun findAll(camera: String): List { + fun groups() = box.all.map { it.name }.distinct() + + fun findAll(name: String): List { return box.query() - .equal(CalibrationFrameEntity_.camera, camera, CASE_SENSITIVE) + .equal(CalibrationFrameEntity_.name, name, CASE_SENSITIVE) .build() .use { it.find() } } @Synchronized - fun delete(camera: String, path: String) { + fun delete(name: String, path: String) { return box.query() - .equal(CalibrationFrameEntity_.camera, camera, CASE_SENSITIVE) + .equal(CalibrationFrameEntity_.name, name, CASE_SENSITIVE) .equal(CalibrationFrameEntity_.path, path, CASE_SENSITIVE) .build() .use { it.remove() } } - fun darkFrames(camera: String, width: Int, height: Int, bin: Int, exposureTime: Long, gain: Double): List { + fun darkFrames(name: String, width: Int, height: Int, bin: Int, exposureTime: Long, gain: Double): List { return box.query() .equal(CalibrationFrameEntity_.type, FrameType.DARK.ordinal) .equal(CalibrationFrameEntity_.enabled, true) - .equal(CalibrationFrameEntity_.camera, camera, CASE_SENSITIVE) + .equal(CalibrationFrameEntity_.name, name, CASE_SENSITIVE) .equal(CalibrationFrameEntity_.width, width) .equal(CalibrationFrameEntity_.height, height) .equal(CalibrationFrameEntity_.binX, bin) @@ -44,11 +46,11 @@ class CalibrationFrameRepository(@Qualifier("calibrationFrameBox") override val .use { it.find() } } - fun biasFrames(camera: String, width: Int, height: Int, bin: Int, gain: Double): List { + fun biasFrames(name: String, width: Int, height: Int, bin: Int, gain: Double): List { return box.query() .equal(CalibrationFrameEntity_.type, FrameType.BIAS.ordinal) .equal(CalibrationFrameEntity_.enabled, true) - .equal(CalibrationFrameEntity_.camera, camera, CASE_SENSITIVE) + .equal(CalibrationFrameEntity_.name, name, CASE_SENSITIVE) .equal(CalibrationFrameEntity_.width, width) .equal(CalibrationFrameEntity_.height, height) .equal(CalibrationFrameEntity_.binX, bin) @@ -58,11 +60,11 @@ class CalibrationFrameRepository(@Qualifier("calibrationFrameBox") override val .use { it.find() } } - fun flatFrames(camera: String, filter: String?, width: Int, height: Int, bin: Int): List { + fun flatFrames(name: String, filter: String?, width: Int, height: Int, bin: Int): List { return box.query() .equal(CalibrationFrameEntity_.type, FrameType.FLAT.ordinal) .equal(CalibrationFrameEntity_.enabled, true) - .equal(CalibrationFrameEntity_.camera, camera, CASE_SENSITIVE) + .equal(CalibrationFrameEntity_.name, name, CASE_SENSITIVE) .also { if (filter.isNullOrBlank()) it.isNull(CalibrationFrameEntity_.filter) else it.equal(CalibrationFrameEntity_.filter, filter, CASE_INSENSITIVE) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index f2def0291..0d0eb8cf6 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -10,6 +10,8 @@ import nebulosa.image.format.ImageHdu import nebulosa.image.format.ReadableHeader import nebulosa.indi.device.camera.FrameType import nebulosa.log.loggerFor +import nebulosa.xisf.isXisf +import nebulosa.xisf.xisf import org.springframework.stereotype.Service import java.nio.file.Path import java.util.* @@ -27,81 +29,89 @@ class CalibrationFrameService( private val calibrationFrameRepository: CalibrationFrameRepository, ) { - fun calibrate(camera: String, image: Image, createNew: Boolean = false): Image { - val darkFrame = findBestDarkFrames(camera, image).firstOrNull() - val biasFrame = findBestBiasFrames(camera, image).firstOrNull() - val flatFrame = findBestFlatFrames(camera, image).firstOrNull() - - return if (darkFrame != null || biasFrame != null || flatFrame != null) { - var transformedImage = if (createNew) image.clone() else image - var calibrationImage = Image(transformedImage.width, transformedImage.height, Header.Empty, transformedImage.mono) + fun calibrate(name: String, image: Image, createNew: Boolean = false): Image { + return synchronized(image) { + val darkFrame = findBestDarkFrames(name, image).firstOrNull() + val biasFrame = findBestBiasFrames(name, image).firstOrNull() + val flatFrame = findBestFlatFrames(name, image).firstOrNull() + + if (darkFrame != null || biasFrame != null || flatFrame != null) { + var transformedImage = if (createNew) image.clone() else image + var calibrationImage = Image(transformedImage.width, transformedImage.height, Header.Empty, transformedImage.mono) + + if (biasFrame != null) { + calibrationImage = biasFrame.path!!.fits().use(calibrationImage::load)!! + transformedImage = transformedImage.transform(BiasSubtraction(calibrationImage)) + LOG.info("bias frame subtraction applied. frame={}", biasFrame) + } else { + LOG.info( + "no bias frames found. width={}, height={}, bin={}, gain={}", + image.width, image.height, image.header.binX, image.header.gain + ) + } - if (biasFrame != null) { - calibrationImage = biasFrame.path!!.fits().use(calibrationImage::load)!! - transformedImage = transformedImage.transform(BiasSubtraction(calibrationImage)) - LOG.info("bias frame subtraction applied. frame={}", biasFrame) - } else { - LOG.info( - "no bias frames found. width={}, height={}, bin={}, gain={}", - image.width, image.height, image.header.binX, image.header.gain - ) - } + if (darkFrame != null) { + calibrationImage = darkFrame.path!!.fits().use(calibrationImage::load)!! + transformedImage = transformedImage.transform(DarkSubtraction(calibrationImage)) + LOG.info("dark frame subtraction applied. frame={}", darkFrame) + } else { + LOG.info( + "no dark frames found. width={}, height={}, bin={}, exposureTime={}, gain={}", + image.width, image.height, image.header.binX, image.header.exposureTimeInMicroseconds, image.header.gain + ) + } - if (darkFrame != null) { - calibrationImage = darkFrame.path!!.fits().use(calibrationImage::load)!! - transformedImage = transformedImage.transform(DarkSubtraction(calibrationImage)) - LOG.info("dark frame subtraction applied. frame={}", darkFrame) - } else { - LOG.info( - "no dark frames found. width={}, height={}, bin={}, exposureTime={}, gain={}", - image.width, image.height, image.header.binX, image.header.exposureTimeInMicroseconds, image.header.gain - ) - } + if (flatFrame != null) { + calibrationImage = flatFrame.path!!.fits().use(calibrationImage::load)!! + transformedImage = transformedImage.transform(FlatCorrection(calibrationImage)) + LOG.info("flat frame correction applied. frame={}", flatFrame) + } else { + LOG.info( + "no flat frames found. filter={}, width={}, height={}, bin={}", + image.header.filter, image.width, image.height, image.header.binX + ) + } - if (flatFrame != null) { - calibrationImage = flatFrame.path!!.fits().use(calibrationImage::load)!! - transformedImage = transformedImage.transform(FlatCorrection(calibrationImage)) - LOG.info("flat frame correction applied. frame={}", flatFrame) + transformedImage } else { LOG.info( - "no flat frames found. filter={}, width={}, height={}, bin={}", - image.header.filter, image.width, image.height, image.header.binX + "no calibration frames found. width={}, height={}, bin={}, gain={}, filter={}, exposureTime={}", + image.width, image.height, image.header.binX, image.header.gain, image.header.filter, image.header.exposureTimeInMicroseconds ) + image } - - transformedImage - } else { - LOG.info( - "no calibration frames found. width={}, height={}, bin={}, gain={}, filter={}, exposureTime={}", - image.width, image.height, image.header.binX, image.header.gain, image.header.filter, image.header.exposureTimeInMicroseconds - ) - image } } - fun groupedCalibrationFrames(camera: String): Map> { - val frames = calibrationFrameRepository.findAll(camera) + fun groups() = calibrationFrameRepository.groups() + + fun groupedCalibrationFrames(name: String): Map> { + val frames = calibrationFrameRepository.findAll(name) return frames.groupBy(CalibrationGroupKey::from) } - fun upload(camera: String, path: Path): List { + fun upload(name: String, path: Path): List { val files = if (path.isRegularFile() && path.isFits) listOf(path) - else if (path.isDirectory()) path.listDirectoryEntries("*.{fits,fit}").filter { it.isRegularFile() } + else if (path.isDirectory()) path.listDirectoryEntries("*.{fits,fit,xisf}").filter { it.isRegularFile() } else return emptyList() - return upload(camera, files) + return upload(name, files) } @Synchronized - fun upload(camera: String, files: List): List { + fun upload(name: String, files: List): List { val frames = ArrayList(files.size) for (file in files) { - calibrationFrameRepository.delete(camera, "$file") + calibrationFrameRepository.delete(name, "$file") try { - file.fits().use { fits -> - val hdu = fits.filterIsInstance().firstOrNull() ?: return@use + val image = if (file.isFits()) file.fits() + else if (file.isXisf()) file.xisf() + else continue + + image.use { + val hdu = image.filterIsInstance().firstOrNull() ?: return@use val header = hdu.header val frameType = header.frameType?.takeIf { it != FrameType.LIGHT } ?: return@use @@ -111,10 +121,10 @@ class CalibrationFrameService( val filter = if (frameType == FrameType.FLAT) header.filter else null val frame = CalibrationFrameEntity( - 0L, frameType, camera, filter, + 0L, frameType, name, filter, exposureTime, temperature, header.width, header.height, header.binX, header.binY, - gain, "$file", + gain, file, ) calibrationFrameRepository.save(frame) @@ -128,25 +138,23 @@ class CalibrationFrameService( return frames } - fun edit(id: Long, path: String?, enabled: Boolean): CalibrationFrameEntity { - return with(calibrationFrameRepository.find(id)!!) { - if (!path.isNullOrBlank()) this.path = path - this.enabled = enabled - calibrationFrameRepository.save(this) - } + fun edit(frame: CalibrationFrameEntity, name: String, enabled: Boolean): CalibrationFrameEntity { + frame.name = name + frame.enabled = enabled + return calibrationFrameRepository.save(frame) } - fun delete(id: Long) { - calibrationFrameRepository.delete(id) + fun delete(frame: CalibrationFrameEntity) { + calibrationFrameRepository.delete(frame) } // exposureTime, temperature, width, height, binX, binY, gain. - fun findBestDarkFrames(camera: String, image: Image): List { + fun findBestDarkFrames(name: String, image: Image): List { val header = image.header val temperature = header.temperature val frames = calibrationFrameRepository - .darkFrames(camera, image.width, image.height, header.binX, header.exposureTimeInMicroseconds, header.gain) + .darkFrames(name, image.width, image.height, header.binX, header.exposureTimeInMicroseconds, header.gain) if (frames.isEmpty()) return emptyList() @@ -159,19 +167,19 @@ class CalibrationFrameService( } // filter, width, height, binX, binY. - fun findBestFlatFrames(camera: String, image: Image): List { + fun findBestFlatFrames(name: String, image: Image): List { val filter = image.header.filter // TODO: Generate master from matched frames. return calibrationFrameRepository - .flatFrames(camera, filter, image.width, image.height, image.header.binX) + .flatFrames(name, filter, image.width, image.height, image.header.binX) } // width, height, binX, binY, gain. - fun findBestBiasFrames(camera: String, image: Image): List { + fun findBestBiasFrames(name: String, image: Image): List { // TODO: Generate master from matched frames. return calibrationFrameRepository - .biasFrames(camera, image.width, image.height, image.header.binX, image.header.gain) + .biasFrames(name, image.width, image.height, image.header.binX, image.header.gain) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt index 7b283dcf5..65cbe2bf8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt @@ -23,33 +23,33 @@ class CameraController( } @GetMapping("{camera}") - fun camera(@DeviceOrEntityParam camera: Camera): Camera { + fun camera(camera: Camera): Camera { return camera } @PutMapping("{camera}/connect") - fun connect(@DeviceOrEntityParam camera: Camera) { + fun connect(camera: Camera) { cameraService.connect(camera) } @PutMapping("{camera}/disconnect") - fun disconnect(@DeviceOrEntityParam camera: Camera) { + fun disconnect(camera: Camera) { cameraService.disconnect(camera) } @PutMapping("{camera}/snoop") fun snoop( - @DeviceOrEntityParam camera: Camera, - @DeviceOrEntityParam(required = false) mount: Mount?, - @DeviceOrEntityParam(required = false) wheel: FilterWheel?, - @DeviceOrEntityParam(required = false) focuser: Focuser?, + camera: Camera, + @RequestParam(required = false) mount: Mount?, + @RequestParam(required = false) wheel: FilterWheel?, + @RequestParam(required = false) focuser: Focuser?, ) { cameraService.snoop(camera, mount, wheel, focuser) } @PutMapping("{camera}/cooler") fun cooler( - @DeviceOrEntityParam camera: Camera, + camera: Camera, @RequestParam enabled: Boolean, ) { cameraService.cooler(camera, enabled) @@ -57,7 +57,7 @@ class CameraController( @PutMapping("{camera}/temperature/setpoint") fun setpointTemperature( - @DeviceOrEntityParam camera: Camera, + camera: Camera, @RequestParam @Valid @Range(min = -50, max = 50) temperature: Double, ) { cameraService.setpointTemperature(camera, temperature) @@ -65,12 +65,12 @@ class CameraController( @PutMapping("{camera}/capture/start") fun startCapture( - @DeviceOrEntityParam camera: Camera, + camera: Camera, @RequestBody body: CameraStartCaptureRequest, ) = cameraService.startCapture(camera, body) @PutMapping("{camera}/capture/abort") - fun abortCapture(@DeviceOrEntityParam camera: Camera) { + fun abortCapture(camera: Camera) { cameraService.abortCapture(camera) } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt index 2e344a369..d5f3c0ba1 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt @@ -20,23 +20,23 @@ class FocuserController( } @GetMapping("{focuser}") - fun focuser(@DeviceOrEntityParam focuser: Focuser): Focuser { + fun focuser(focuser: Focuser): Focuser { return focuser } @PutMapping("{focuser}/connect") - fun connect(@DeviceOrEntityParam focuser: Focuser) { + fun connect(focuser: Focuser) { focuserService.connect(focuser) } @PutMapping("{focuser}/disconnect") - fun disconnect(@DeviceOrEntityParam focuser: Focuser) { + fun disconnect(focuser: Focuser) { focuserService.disconnect(focuser) } @PutMapping("{focuser}/move-in") fun moveIn( - @DeviceOrEntityParam focuser: Focuser, + focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveIn(focuser, steps) @@ -44,7 +44,7 @@ class FocuserController( @PutMapping("{focuser}/move-out") fun moveOut( - @DeviceOrEntityParam focuser: Focuser, + focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveOut(focuser, steps) @@ -52,20 +52,20 @@ class FocuserController( @PutMapping("{focuser}/move-to") fun moveTo( - @DeviceOrEntityParam focuser: Focuser, + focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveTo(focuser, steps) } @PutMapping("{focuser}/abort") - fun abort(@DeviceOrEntityParam focuser: Focuser) { + fun abort(focuser: Focuser) { focuserService.abort(focuser) } @PutMapping("{focuser}/sync") fun sync( - @DeviceOrEntityParam focuser: Focuser, + focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.sync(focuser, steps) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt index e7f790faa..c7167368a 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -22,23 +22,23 @@ class GuideOutputController( } @GetMapping("{guideOutput}") - fun guideOutput(@DeviceOrEntityParam guideOutput: GuideOutput): GuideOutput { + fun guideOutput(guideOutput: GuideOutput): GuideOutput { return guideOutput } @PutMapping("{guideOutput}/connect") - fun connect(@DeviceOrEntityParam guideOutput: GuideOutput) { + fun connect(guideOutput: GuideOutput) { guideOutputService.connect(guideOutput) } @PutMapping("{guideOutput}/disconnect") - fun disconnect(@DeviceOrEntityParam guideOutput: GuideOutput) { + fun disconnect(guideOutput: GuideOutput) { guideOutputService.disconnect(guideOutput) } @PutMapping("{guideOutput}/pulse") fun pulse( - @DeviceOrEntityParam guideOutput: GuideOutput, + guideOutput: GuideOutput, @RequestParam direction: GuideDirection, @RequestParam @DurationMin(nanos = 0L) @DurationMax(seconds = 60L) duration: Duration, ) { diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt index ee5d59398..5571e32be 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt @@ -21,7 +21,7 @@ class ImageController( @PostMapping fun openImage( @RequestParam path: Path, - @DeviceOrEntityParam(required = false) camera: Camera?, + @RequestParam(required = false) camera: Camera?, @RequestBody transformation: ImageTransformation, output: HttpServletResponse, ) = imageService.openImage(path, camera, transformation, output) @@ -34,7 +34,7 @@ class ImageController( @PutMapping("save-as") fun saveImageAs( @RequestParam path: Path, - @DeviceOrEntityParam(required = false) camera: Camera?, + @RequestParam(required = false) camera: Camera?, @RequestBody save: SaveImage ) { imageService.saveImageAs(path, save, camera) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt index ea9f2621b..5e72a3d3d 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt @@ -13,20 +13,20 @@ class INDIController( ) { @GetMapping("{device}/properties") - fun properties(@DeviceOrEntityParam device: Device): Collection> { + fun properties(device: Device): Collection> { return indiService.properties(device) } @PutMapping("{device}/send") fun sendProperty( - @DeviceOrEntityParam device: Device, + device: Device, @RequestBody @Valid body: INDISendProperty, ) { return indiService.sendProperty(device, body) } @GetMapping("{device}/log") - fun log(@DeviceOrEntityParam device: Device): List { + fun log(device: Device): List { return synchronized(device.messages) { device.messages } } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index c3d8d1d67..0640d612b 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -32,23 +32,23 @@ class MountController( } @GetMapping("{mount}") - fun mount(@DeviceOrEntityParam mount: Mount): Mount { + fun mount(mount: Mount): Mount { return mount } @PutMapping("{mount}/connect") - fun connect(@DeviceOrEntityParam mount: Mount) { + fun connect(mount: Mount) { mountService.connect(mount) } @PutMapping("{mount}/disconnect") - fun disconnect(@DeviceOrEntityParam mount: Mount) { + fun disconnect(mount: Mount) { mountService.disconnect(mount) } @PutMapping("{mount}/tracking") fun tracking( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam enabled: Boolean, ) { mountService.tracking(mount, enabled) @@ -56,7 +56,7 @@ class MountController( @PutMapping("{mount}/sync") fun sync( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -66,7 +66,7 @@ class MountController( @PutMapping("{mount}/slew") fun slew( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -76,7 +76,7 @@ class MountController( @PutMapping("{mount}/goto") fun goTo( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -85,18 +85,18 @@ class MountController( } @PutMapping("{mount}/home") - fun home(@DeviceOrEntityParam mount: Mount) { + fun home(mount: Mount) { mountService.home(mount) } @PutMapping("{mount}/abort") - fun abort(@DeviceOrEntityParam mount: Mount) { + fun abort(mount: Mount) { mountService.abort(mount) } @PutMapping("{mount}/track-mode") fun trackMode( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam mode: TrackMode, ) { mountService.trackMode(mount, mode) @@ -104,7 +104,7 @@ class MountController( @PutMapping("{mount}/slew-rate") fun slewRate( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam @Valid @NotBlank rate: String, ) { mountService.slewRate(mount, mount.slewRates.first { it.name == rate }) @@ -112,7 +112,7 @@ class MountController( @PutMapping("{mount}/move") fun move( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam direction: GuideDirection, @RequestParam enabled: Boolean, ) { @@ -120,18 +120,18 @@ class MountController( } @PutMapping("{mount}/park") - fun park(@DeviceOrEntityParam mount: Mount) { + fun park(mount: Mount) { mountService.park(mount) } @PutMapping("{mount}/unpark") - fun unpark(@DeviceOrEntityParam mount: Mount) { + fun unpark(mount: Mount) { mountService.unpark(mount) } @PutMapping("{mount}/coordinates") fun coordinates( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam @Valid @NotBlank longitude: String, @RequestParam @Valid @NotBlank latitude: String, @RequestParam(required = false, defaultValue = "0.0") elevation: Double, @@ -141,7 +141,7 @@ class MountController( @PutMapping("{mount}/datetime") fun dateTime( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @DateAndTimeParam dateTime: LocalDateTime, @RequestParam @Valid @Range(min = -720, max = 720) offsetInMinutes: Int, ) { @@ -149,7 +149,7 @@ class MountController( } @GetMapping("{mount}/location/{type}") - fun celestialLocation(@DeviceOrEntityParam mount: Mount, @PathVariable type: CelestialLocationType): ComputedLocation { + fun celestialLocation(mount: Mount, @PathVariable type: CelestialLocationType): ComputedLocation { return when (type) { CelestialLocationType.ZENITH -> mountService.computeZenithLocation(mount) CelestialLocationType.NORTH_POLE -> mountService.computeNorthCelestialPoleLocation(mount) @@ -162,7 +162,7 @@ class MountController( @GetMapping("{mount}/location") fun location( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam rightAscension: String, @RequestParam declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @RequestParam(required = false, defaultValue = "true") equatorial: Boolean, @@ -177,7 +177,7 @@ class MountController( @PutMapping("{mount}/point-here") fun pointMountHere( - @DeviceOrEntityParam mount: Mount, + mount: Mount, @RequestParam path: Path, @RequestParam @Valid @PositiveOrZero x: Double, @RequestParam @Valid @PositiveOrZero y: Double, diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt index 67ed86dc8..33029c474 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt @@ -16,12 +16,12 @@ class SequencerController( @PutMapping("{camera}/start") fun startSequencer( - @DeviceOrEntityParam camera: Camera, + camera: Camera, @RequestBody @Valid body: SequencePlanRequest, ) = sequencerService.start(camera, body) @PutMapping("{camera}/stop") - fun stopSequencer(@DeviceOrEntityParam camera: Camera) { + fun stopSequencer(camera: Camera) { sequencerService.stop(camera) } } diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt index 417204e75..c3691443b 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt @@ -21,23 +21,23 @@ class WheelController( } @GetMapping("{wheel}") - fun wheel(@DeviceOrEntityParam wheel: FilterWheel): FilterWheel { + fun wheel(wheel: FilterWheel): FilterWheel { return wheel } @PutMapping("{wheel}/connect") - fun connect(@DeviceOrEntityParam wheel: FilterWheel) { + fun connect(wheel: FilterWheel) { wheelService.connect(wheel) } @PutMapping("{wheel}/disconnect") - fun disconnect(@DeviceOrEntityParam wheel: FilterWheel) { + fun disconnect(wheel: FilterWheel) { wheelService.disconnect(wheel) } @PutMapping("{wheel}/move-to") fun moveTo( - @DeviceOrEntityParam wheel: FilterWheel, + wheel: FilterWheel, @RequestParam @Valid @PositiveOrZero position: Int, ) { wheelService.moveTo(wheel, position) @@ -45,7 +45,7 @@ class WheelController( @PutMapping("{wheel}/sync") fun sync( - @DeviceOrEntityParam wheel: FilterWheel, + wheel: FilterWheel, @RequestParam @Valid @NotEmpty names: String, ) { wheelService.sync(wheel, names.split(",")) diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardController.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardController.kt index 5beecd7bd..0653502e8 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardController.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardController.kt @@ -15,12 +15,12 @@ class FlatWizardController( ) { @PutMapping("{camera}/start") - fun startCapture(@DeviceOrEntityParam camera: Camera, @RequestBody @Valid body: FlatWizardRequest) { + fun startCapture(camera: Camera, @RequestBody @Valid body: FlatWizardRequest) { flatWizardService.startCapture(camera, body) } @PutMapping("{camera}/stop") - fun stopCapture(@DeviceOrEntityParam camera: Camera) { + fun stopCapture(camera: Camera) { flatWizardService.stopCapture(camera) } } diff --git a/desktop/app/main.ts b/desktop/app/main.ts index 7ee09f962..b8ab325f0 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -173,6 +173,7 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { frame: false, modal, parent, width: savedSize?.width || width, height: savedSize?.height || height, + minHeight: options.minHeight || 200, x: savedPos?.x ?? undefined, y: savedPos?.y ?? undefined, resizable: serve || resizable, @@ -208,6 +209,8 @@ function createWindow(options: OpenWindow, parent?: BrowserWindow) { }) window.on('close', () => { + console.info('window closed: ', id, window.id) + const homeWindow = browserWindows.get('home') if (!modal) { @@ -392,7 +395,6 @@ try { ipcMain.handle('FILE.OPEN', async (event, data?: OpenFile) => { const ownerWindow = findWindowById(event.sender.id) - const value = await dialog.showOpenDialog(ownerWindow!.window, { filters: data?.filters, properties: ['openFile'], diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 423d76b89..63d56fcc9 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -39,6 +39,7 @@ import { TagModule } from 'primeng/tag' import { TieredMenuModule } from 'primeng/tieredmenu' import { ToastModule } from 'primeng/toast' import { TooltipModule } from 'primeng/tooltip' +import { TreeModule } from 'primeng/tree' import { CameraExposureComponent } from '../shared/components/camera-exposure/camera-exposure.component' import { DeviceListButtonComponent } from '../shared/components/device-list-button/device-list-button.component' import { DeviceListMenuComponent } from '../shared/components/device-list-menu/device-list-menu.component' @@ -160,6 +161,7 @@ import { SettingsComponent } from './settings/settings.component' TieredMenuModule, ToastModule, TooltipModule, + TreeModule, ], providers: [ AnglePipe, diff --git a/desktop/src/app/calibration/calibration.component.html b/desktop/src/app/calibration/calibration.component.html index 7b4ebf8ee..79721ab78 100644 --- a/desktop/src/app/calibration/calibration.component.html +++ b/desktop/src/app/calibration/calibration.component.html @@ -1,98 +1,60 @@
-
-

Groups

+
+
- - - - Type - # - Filter - Duration - Size - Bin - T. (°C) - Gain - - - - - {{ item.key.type }} - {{ item.frames.length }} - {{ item.key.filter ?? '' }} - {{ item.key.exposureTime | exposureTime }} - {{ item.key.width }}x{{ item.key.height }} - {{ item.key.binX }}x{{ item.key.binY }} - {{ item.key.temperature }} - {{ item.key.gain }} - + + +
+ @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') { + + + + } +
+
-
-
-
-

Frames

- -
-
- - - - - - - Type - Filter - Duration - Size - Bin - T. (°C) - Gain - - - - - - - - {{ item.type }} - {{ item.filter ?? '' }} - {{ item.exposureTime | exposureTime }} - {{ item.width }}x{{ item.height }} - {{ item.binX }}x{{ item.binY }} - {{ item.temperature }} - {{ item.gain }} - - - +
+
+
+ + +
-
-
- - - - -
-
- - - -
-
+ + + +
-
\ No newline at end of file + + + + \ No newline at end of file diff --git a/desktop/src/app/calibration/calibration.component.scss b/desktop/src/app/calibration/calibration.component.scss index e69de29bb..2034f765b 100644 --- a/desktop/src/app/calibration/calibration.component.scss +++ b/desktop/src/app/calibration/calibration.component.scss @@ -0,0 +1,11 @@ +:host { + ::ng-deep { + .p-treenode-label { + width: 100%; + } + + .p-tree-wrapper { + max-height: 288px; + } + } +} \ No newline at end of file diff --git a/desktop/src/app/calibration/calibration.component.ts b/desktop/src/app/calibration/calibration.component.ts index 6c77001db..1fec3de87 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -1,16 +1,19 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' -import { ActivatedRoute } from '@angular/router' +import { AfterViewInit, Component, HostListener, OnDestroy } from '@angular/core' import { dirname } from 'path' -import { CheckboxChangeEvent } from 'primeng/checkbox' +import { TreeNode } from 'primeng/api' 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 { Camera } from '../../shared/types/camera.types' import { AppComponent } from '../app.component' -export const CALIBRATION_DIR_KEY = 'calibration.directory' +export type CalibrationNode = Required, 'key' | 'label' | 'data' | 'children'>> & TreeNode + +export type TreeNodeData = + { type: 'NAME', data: string } | + { type: 'GROUP', data: CalibrationFrameGroup } | + { type: 'FRAME', data: CalibrationFrame } @Component({ selector: 'app-calibration', @@ -19,154 +22,193 @@ export const CALIBRATION_DIR_KEY = 'calibration.directory' }) export class CalibrationComponent implements AfterViewInit, OnDestroy { - camera!: Camera - - groups: CalibrationFrameGroup[] = [] - group?: CalibrationFrameGroup - frame?: CalibrationFrame + readonly frames: CalibrationNode[] = [] - get groupIsEnabled() { - return !!this.group && !this.group.frames.find(e => !e.enabled) - } + showNewGroupDialog = false + newGroupName = '' + newGroupDialogSave: () => void = () => { } constructor( - private app: AppComponent, + app: AppComponent, private api: ApiService, - electron: ElectronService, + private electron: ElectronService, private browserWindow: BrowserWindowService, - private route: ActivatedRoute, private preference: PreferenceService, - ngZone: NgZone, ) { app.title = 'Calibration' - - app.topMenu.push({ - icon: 'mdi mdi-image-plus', - tooltip: 'Add file', - command: async () => { - const preference = this.preference.calibrationPreference.get() - const path = await electron.openImage({ defaultPath: preference.openPath }) - - if (path) { - preference.openPath = dirname(path) - this.preference.calibrationPreference.set(preference) - this.upload(path) - } - }, - }) - - app.topMenu.push({ - icon: 'mdi mdi-folder-plus', - tooltip: 'Add folder', - command: async () => { - const preference = this.preference.calibrationPreference.get() - const path = await electron.openDirectory({ defaultPath: preference.openPath }) - - if (path) { - preference.openPath = path - this.preference.calibrationPreference.set(preference) - this.upload(path) - } - }, - }) - - electron.on('DATA.CHANGED', (data: Camera) => { - ngZone.run(() => { - if (data.name !== this.camera.name) { - this.loadForCamera(data, true) - } - }) - }) } - async ngAfterViewInit() { - this.route.queryParams.subscribe(async e => { - const camera = JSON.parse(decodeURIComponent(e.data)) as Camera - this.loadForCamera(camera) - }) + ngAfterViewInit() { + this.load() } @HostListener('window:unload') ngOnDestroy() { } - private loadForCamera(camera: Camera, reload: boolean = false) { - this.camera = camera - this.app.subTitle = this.camera.name - return reload ? this.reload() : this.load() + private makeTreeNode(key: string, label: string, data: TreeNodeData): CalibrationNode { + return { key, label, data, children: [] } } - private async upload(path: string) { - const frames = await this.api.uploadCalibrationFrame(this.camera!, path) + addGroup(name: string) { + const node = this.frames.find(e => e.label === name) + ?? this.makeTreeNode(`group-${name}`, name, { type: 'NAME', data: name }) - if (frames.length > 0) { - this.load() + if (this.frames.indexOf(node) < 0) { + this.frames.push(node) } + + return node } - private async load() { - this.groups = await this.api.calibrationFrames(this.camera) + addFrameGroup(name: string | CalibrationNode, group: CalibrationFrameGroup) { + const parent = typeof name === 'string' + ? this.frames.find(e => e.label === name) + : name + + if (parent) { + const node = this.makeTreeNode(`frame-group-${group.id}`, `Frame`, { type: 'GROUP', data: group }) + parent.children.push(node) + return node + } + + return undefined } - private async reload() { - this.group = undefined - this.groupSelected() - this.load() + addFrame(group: string | CalibrationNode, frame: CalibrationFrame) { + const parent = typeof group === 'string' + ? this.frames.find(e => e.label === group) + : group + + if (parent) { + const node = this.makeTreeNode(`frame-${frame.id}`, `Frame`, { type: 'FRAME', data: frame }) + parent.children.push(node) + return node + } + + 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 }) + + if (path) { + preference.openPath = dirname(path) + this.preference.calibrationPreference.set(preference) + this.upload(node, path) + } + } } - groupSelected() { - this.frame = undefined + async openDirectoryToUpload(node: CalibrationNode) { + if (node.data.type === 'NAME') { + const preference = this.preference.calibrationPreference.get() + const path = await this.electron.openDirectory({ defaultPath: preference.openPath }) + + if (path) { + preference.openPath = path + this.preference.calibrationPreference.set(preference) + this.upload(node, path) + } + } } - groupChecked(event: CheckboxChangeEvent) { - this.group?.frames?.forEach(e => e.enabled = event.checked) + private async upload(node: CalibrationNode, path: string) { + if (node.data.type === 'NAME') { + const frames = await this.api.uploadCalibrationFrame(node.data.data, path) + + if (frames.length > 0) { + this.load() + } + } } - async frameChecked(frame: CalibrationFrame, event: CheckboxChangeEvent) { - await this.api.editCalibrationFrame(frame) + private async load() { + this.frames.length = 0 + + const names = await this.api.calibrationGroups() + + for (const name of names) { + const nameNode = this.addGroup(name) + + const groups = await this.api.calibrationFrames(name) + + for (const group of groups) { + const frameGroupNode = this.addFrameGroup(nameNode, group)! + + for (const frame of group.frames) { + this.addFrame(frameGroupNode, frame) + } + } + } } openImage(frame: CalibrationFrame) { this.browserWindow.openImage({ path: frame.path }) } - replaceFrame(frame: CalibrationFrame) { - console.info(frame) + toggleCalibrationFrame(node: CalibrationNode, enabled: boolean) { + if (node.data.type === 'FRAME') { + this.api.editCalibrationFrame(node.data.data) + } } - async deleteFrame(frame: CalibrationFrame) { - await this.api.deleteCalibrationFrame(frame) - - if (this.frame === frame) { - this.frame = undefined + async deleteFrame(node: CalibrationNode) { + if (node.data.type === 'FRAME') { + await this.api.deleteCalibrationFrame(node.data.data) + this.load() } + } - let index = this.group?.frames?.findIndex(e => e.id === frame.id) ?? -1 - - if (index >= 0) { - this.group!.frames.splice(index, 1) - - if (!this.group!.frames.length) { - index = this.groups.indexOf(this.group!) + private calibrationFrameFromNode(node: CalibrationNode) { + const frames: CalibrationFrame[] = [] - if (index >= 0) { - this.groups.splice(index, 1) - this.group = undefined + function recursive(node: TreeNode) { + if (node.data!.type === 'NAME' || node.data!.type === 'GROUP') { + for (const child of node.children!) { + recursive(child) } + } else { + frames.push(node.data!.data) } } + + recursive(node) + + return frames + } + + showNewGroupDialogForAdd() { + this.newGroupDialogSave = () => { + this.addGroup(this.newGroupName) + this.showNewGroupDialog = false + } + + this.newGroupName = '' + this.showNewGroupDialog = true } - async deleteGroupFrames(group: CalibrationFrameGroup) { - for (const frame of group.frames) { - await this.api.deleteCalibrationFrame(frame) + showNewGroupDialogForEdit(node: CalibrationNode) { + if (node.data.type === 'NAME') { + this.newGroupDialogSave = async () => { + const frames = this.calibrationFrameFromNode(node) + + for (const frame of frames) { + frame.name = this.newGroupName + await this.api.editCalibrationFrame(frame) + } - if (frame === this.frame) { - this.frame = undefined + this.showNewGroupDialog = false + this.load() } - } - if (group === this.group) { - this.group === undefined + this.newGroupName = node.data.data + this.showNewGroupDialog = true } } + + editGroupName() { + this.showNewGroupDialog = false + } } \ No newline at end of file diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index df728c345..c54772ebe 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -383,7 +383,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } openCameraCalibration() { - return this.browserWindow.openCalibration({ data: this.camera, bringToFront: true }) + return this.browserWindow.openCalibration({ bringToFront: true }) } private makeCameraStartCapture(): CameraStartCapture { diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 20ac87aef..74d241524 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -485,17 +485,21 @@ export class ApiService { // CALIBRATION - calibrationFrames(camera: Camera) { - return this.http.get(`calibration-frames/${camera.name}`) + calibrationGroups() { + return this.http.get('calibration-frames') } - uploadCalibrationFrame(camera: Camera, path: string) { + calibrationFrames(name: string) { + return this.http.get(`calibration-frames/${name}`) + } + + uploadCalibrationFrame(name: string, path: string) { const query = this.http.query({ path }) - return this.http.put(`calibration-frames/${camera.name}?${query}`) + return this.http.put(`calibration-frames/${name}?${query}`) } editCalibrationFrame(frame: CalibrationFrame) { - const query = this.http.query({ path: frame.path, enabled: frame.enabled }) + const query = this.http.query({ name: frame.name, enabled: frame.enabled }) return this.http.patch(`calibration-frames/${frame.id}?${query}`) } diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index ddf7ea10d..7d073d1fb 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -113,9 +113,9 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'calculator', path: 'calculator', data: undefined }) } - openCalibration(options: OpenWindowOptionsWithData) { - Object.assign(options, { icon: 'stack', width: 510, height: 508 }) - this.openWindow({ ...options, id: 'calibration', path: 'calibration' }) + openCalibration(options: OpenWindowOptions = {}) { + Object.assign(options, { icon: 'stack', width: 420, height: 400, minHeight: 400 }) + this.openWindow({ ...options, id: 'calibration', path: 'calibration', data: undefined }) } openAbout() { diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts index 59dff8d69..9f8716b63 100644 --- a/desktop/src/shared/types/app.types.ts +++ b/desktop/src/shared/types/app.types.ts @@ -35,6 +35,7 @@ export interface OpenWindowOptions { height?: number | string bringToFront?: boolean requestFocus?: boolean + minHeight?: number } export interface OpenWindowOptionsWithData extends OpenWindowOptions { diff --git a/desktop/src/shared/types/calibration.types.ts b/desktop/src/shared/types/calibration.types.ts index dc38a6f68..52ad91d64 100644 --- a/desktop/src/shared/types/calibration.types.ts +++ b/desktop/src/shared/types/calibration.types.ts @@ -3,7 +3,7 @@ import { FrameType } from './camera.types' export interface CalibrationFrame { id: number type: FrameType - camera: string + name: string filter?: string exposureTime: number temperature: number @@ -18,7 +18,8 @@ export interface CalibrationFrame { export interface CalibrationFrameGroup { id: number - key: Omit + name: string + key: Omit frames: CalibrationFrame[] } diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index 72f564196..b7a22fac6 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -235,7 +235,8 @@ p-contextmenu *, p-dropdown *, p-dropdownitem *, .p-multiselect-header *, -.no-draggable-region { +.no-draggable-region, +.p-button.p-dialog-header-close { -webkit-app-region: no-drag; } diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt index 33fc460f2..fef1fce0e 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt @@ -5,12 +5,14 @@ import nebulosa.image.format.* import nebulosa.io.* import nebulosa.log.loggerFor import okio.Buffer +import okio.BufferedSource import okio.Sink import java.io.EOFException import kotlin.math.max data object FitsFormat : ImageFormat { + const val SIGNATURE = "SIMPLE" const val BLOCK_SIZE = 2880 @JvmStatic @@ -20,6 +22,9 @@ data object FitsFormat : ImageFormat { return max(0L, remainingByteCount) } + @JvmStatic + fun BufferedSource.readSignature() = readString(6L, Charsets.US_ASCII) + fun isImageHdu(header: ReadableHeader) = header.getBoolean(FitsKeyword.SIMPLE) || header.getStringOrNull(FitsKeyword.XTENSION) == "IMAGE" diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt index b829eb438..5409fe082 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt @@ -2,10 +2,13 @@ package nebulosa.fits +import nebulosa.fits.FitsFormat.readSignature import nebulosa.image.format.ReadableHeader import nebulosa.io.SeekableSource import nebulosa.math.Angle import nebulosa.math.deg +import okio.buffer +import okio.source import java.io.File import java.nio.file.Path import java.time.Duration @@ -87,8 +90,10 @@ inline val ReadableHeader.instrument inline fun SeekableSource.fits() = Fits().also { it.read(this) } -inline fun String.fits() = FitsPath(this).also(FitsPath::read) - inline fun Path.fits() = FitsPath(this).also(FitsPath::read) inline fun File.fits() = FitsPath(this).also(FitsPath::read) + +inline fun File.isFits() = source().buffer().use { it.readSignature() == FitsFormat.SIGNATURE } + +inline fun Path.isFits() = source().buffer().use { it.readSignature() == FitsFormat.SIGNATURE } diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsPath.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsPath.kt index 765b4dedc..b7d91f2c6 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsPath.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsPath.kt @@ -13,8 +13,6 @@ data class FitsPath(val path: Path) : Fits(), Closeable { constructor(file: File) : this(file.toPath()) - constructor(path: String) : this(Path.of(path)) - fun read() { read(source) } diff --git a/nebulosa-fits/src/test/kotlin/FitsFormatTest.kt b/nebulosa-fits/src/test/kotlin/FitsFormatTest.kt new file mode 100644 index 000000000..081321c31 --- /dev/null +++ b/nebulosa-fits/src/test/kotlin/FitsFormatTest.kt @@ -0,0 +1,14 @@ +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import nebulosa.fits.isFits +import nebulosa.test.AbstractFitsAndXisfTest + +class FitsFormatTest : AbstractFitsAndXisfTest() { + + init { + "should be fits format" { + NGC3344_COLOR_8_FITS.isFits().shouldBeTrue() + M82_COLOR_16_XISF.isFits().shouldBeFalse() + } + } +} diff --git a/nebulosa-fits/src/test/kotlin/FitsReadTest.kt b/nebulosa-fits/src/test/kotlin/FitsReadTest.kt index ce38e8d58..ca4e1d328 100644 --- a/nebulosa-fits/src/test/kotlin/FitsReadTest.kt +++ b/nebulosa-fits/src/test/kotlin/FitsReadTest.kt @@ -10,70 +10,70 @@ class FitsReadTest : AbstractFitsAndXisfTest() { init { "mono:8-bit" { - val hdu = NGC3344_MONO_8_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_MONO_8_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 1 hdu.header.bitpix shouldBe Bitpix.BYTE } "mono:16-bit" { - val hdu = NGC3344_MONO_16_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_MONO_16_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 1 hdu.header.bitpix shouldBe Bitpix.SHORT } "mono:32-bit" { - val hdu = NGC3344_MONO_32_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_MONO_32_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 1 hdu.header.bitpix shouldBe Bitpix.INTEGER } "mono:32-bit floating-point" { - val hdu = NGC3344_MONO_F32_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_MONO_F32_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 1 hdu.header.bitpix shouldBe Bitpix.FLOAT } "mono:64-bit floating-point" { - val hdu = NGC3344_MONO_F64_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_MONO_F64_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 1 hdu.header.bitpix shouldBe Bitpix.DOUBLE } "color:8-bit" { - val hdu = NGC3344_COLOR_8_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_COLOR_8_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 4 hdu.header.bitpix shouldBe Bitpix.BYTE } "color:16-bit" { - val hdu = NGC3344_COLOR_16_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_COLOR_16_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 4 hdu.header.bitpix shouldBe Bitpix.SHORT } "color:32-bit" { - val hdu = NGC3344_COLOR_32_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_COLOR_32_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 4 hdu.header.bitpix shouldBe Bitpix.INTEGER } "color:32-bit floating-point" { - val hdu = NGC3344_COLOR_F32_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_COLOR_F32_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 4 hdu.header.bitpix shouldBe Bitpix.FLOAT } "color:64-bit floating-point" { - val hdu = NGC3344_COLOR_F64_FITS.fits().filterIsInstance().first() + val hdu = closeAfterEach(NGC3344_COLOR_F64_FITS.fits()).filterIsInstance().first() hdu.width shouldBeExactly 256 hdu.height shouldBeExactly 256 hdu.numberOfChannels shouldBeExactly 4 diff --git a/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt b/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt index f455b85c5..3f560ff36 100644 --- a/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt +++ b/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt @@ -11,7 +11,7 @@ class FitsWriteTest : AbstractFitsAndXisfTest() { init { "mono" { - val hdu0 = NGC3344_MONO_8_FITS.fits().filterIsInstance().first() + val hdu0 = closeAfterEach(NGC3344_MONO_8_FITS.fits()).filterIsInstance().first() val data = ByteArray(69120) FitsFormat.write(data.sink(), listOf(hdu0)) data.toByteString(2880, 66240).md5().hex() shouldBe "e1735e21c94dc49885fabc429406e573" diff --git a/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt b/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt index 28fdd9383..18e21d869 100644 --- a/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt +++ b/nebulosa-wcs/src/test/kotlin/LibWCSTest.kt @@ -8,6 +8,7 @@ import nebulosa.math.formatHMS import nebulosa.math.formatSignedDMS import nebulosa.test.NonGitHubOnlyCondition import nebulosa.wcs.WCS +import java.nio.file.Path import kotlin.random.Random // https://www.atnf.csiro.au/people/mcalabre/WCS/example_data.html @@ -53,7 +54,7 @@ class LibWCSTest : StringSpec() { } private fun readHeaderFromFits(name: String): ReadableHeader { - return "src/test/resources/$name.fits".fits().use { it.first!!.header } + return Path.of("src/test/resources/$name.fits").fits().use { it.first!!.header } } companion object { diff --git a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfFormat.kt b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfFormat.kt index c97d20646..3db2be341 100644 --- a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfFormat.kt +++ b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfFormat.kt @@ -12,6 +12,7 @@ import nebulosa.io.* import nebulosa.xisf.XisfMonolithicFileHeader.* import nebulosa.xml.escapeXml import okio.Buffer +import okio.BufferedSource import okio.Sink import okio.Timeout import java.io.ByteArrayInputStream @@ -26,10 +27,12 @@ import kotlin.math.min */ data object XisfFormat : ImageFormat { + const val SIGNATURE = "XISF0100" + override fun read(source: SeekableSource): List { return Buffer().use { buffer -> - source.read(buffer, 8) // XISF0100 - check(buffer.readString(Charsets.US_ASCII) == "XISF0100") { "invalid magic bytes" } + source.read(buffer, 8) + check(buffer.readSignature() == SIGNATURE) { "invalid signature" } // Header length (4) + reserved (4) source.read(buffer, 8) @@ -157,6 +160,9 @@ data object XisfFormat : ImageFormat { return initialHeaderSize } + @JvmStatic + fun BufferedSource.readSignature() = readString(8L, Charsets.US_ASCII) + @JvmStatic internal fun Buffer.readPixel(format: SampleFormat, byteOrder: ByteOrder): Float { return when (format) { diff --git a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHelper.kt b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHelper.kt index d48ad55b1..cc69a0293 100644 --- a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHelper.kt +++ b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHelper.kt @@ -2,11 +2,16 @@ package nebulosa.xisf +import nebulosa.xisf.XisfFormat.readSignature +import okio.buffer +import okio.source import java.io.File import java.nio.file.Path -inline fun String.xisf() = XisfPath(this).also(XisfPath::read) - inline fun Path.xisf() = XisfPath(this).also(XisfPath::read) inline fun File.xisf() = XisfPath(this).also(XisfPath::read) + +inline fun File.isXisf() = source().buffer().use { it.readSignature() == XisfFormat.SIGNATURE } + +inline fun Path.isXisf() = source().buffer().use { it.readSignature() == XisfFormat.SIGNATURE } diff --git a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfPath.kt b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfPath.kt index 2581951c1..f8d115156 100644 --- a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfPath.kt +++ b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfPath.kt @@ -13,8 +13,6 @@ data class XisfPath(val path: Path) : Xisf(), Closeable { constructor(file: File) : this(file.toPath()) - constructor(path: String) : this(Path.of(path)) - fun read() { read(source) } diff --git a/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt b/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt index 289992b3c..79ac306e4 100644 --- a/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt +++ b/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt @@ -1,4 +1,6 @@ import io.kotest.engine.spec.tempfile +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.floats.shouldBeExactly import io.kotest.matchers.ints.shouldBeExactly @@ -12,10 +14,15 @@ import nebulosa.io.seekableSink import nebulosa.io.seekableSource import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.xisf.XisfFormat +import nebulosa.xisf.isXisf class XisfFormatTest : AbstractFitsAndXisfTest() { init { + "should be xisf format" { + NGC3344_COLOR_8_FITS.isXisf().shouldBeFalse() + M82_COLOR_16_XISF.isXisf().shouldBeTrue() + } "mono:planar:8" { val source = closeAfterEach(M82_MONO_8_XISF.seekableSource()) val hdus = XisfFormat.read(source)