diff --git a/.gitignore b/.gitignore index 4a81c1a94..dd4e607b7 100644 --- a/.gitignore +++ b/.gitignore @@ -182,11 +182,6 @@ gradle-app.setting ### Gradle Cache ### .cache -### ObjectBox ### -objectbox*.dll -libobjectbox*.so -libobjectbox*.dylib - ### Kotlin ### .kotlin diff --git a/api/build.gradle.kts b/api/build.gradle.kts index eab48ae25..a3a43a4ed 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -2,8 +2,6 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { kotlin("jvm") - kotlin("kapt") - id("io.objectbox") id("com.gradleup.shadow") } @@ -40,7 +38,9 @@ dependencies { implementation(libs.javalin) implementation(libs.koin) implementation(libs.airline) - + implementation(libs.h2) + implementation(libs.bundles.exposed) + implementation(libs.flyway) testImplementation(project(":nebulosa-astrobin-api")) testImplementation(project(":nebulosa-skycatalog-stellarium")) testImplementation(project(":nebulosa-test")) @@ -56,10 +56,3 @@ tasks.withType { attributes["Main-Class"] = "nebulosa.api.MainKt" } } - -kapt { - arguments { - arg("objectbox.modelPath", "$projectDir/schemas/objectbox.json") - arg("objectbox.myObjectBoxPackage", "nebulosa.api.database") - } -} diff --git a/api/schemas/.gitignore b/api/schemas/.gitignore deleted file mode 100644 index 751553b3a..000000000 --- a/api/schemas/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.bak diff --git a/api/schemas/objectbox.json b/api/schemas/objectbox.json deleted file mode 100644 index 0f7413c13..000000000 --- a/api/schemas/objectbox.json +++ /dev/null @@ -1,220 +0,0 @@ -{ - "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", - "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", - "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", - "entities": [ - { - "id": "1:3544801173480775772", - "lastPropertyId": "13:3755368355153819967", - "name": "CalibrationFrameEntity", - "properties": [ - { - "id": "1:6440158350156700816", - "name": "id", - "type": 6, - "flags": 1 - }, - { - "id": "2:7830549305901803879", - "name": "type", - "indexId": "1:3705837194399110688", - "type": 5, - "flags": 8 - }, - { - "id": "3:8490362500884478696", - "name": "group", - "indexId": "2:2460719507268221169", - "type": 9, - "flags": 2048 - }, - { - "id": "4:169758157435742191", - "name": "filter", - "type": 9 - }, - { - "id": "5:5772177826523179837", - "name": "exposureTime", - "type": 6 - }, - { - "id": "6:979735190507089416", - "name": "temperature", - "type": 8 - }, - { - "id": "7:1567591787936780727", - "name": "width", - "type": 5 - }, - { - "id": "8:804894592407875320", - "name": "height", - "type": 5 - }, - { - "id": "9:7150567366206966047", - "name": "binX", - "type": 5 - }, - { - "id": "10:6904147472104067341", - "name": "binY", - "type": 5 - }, - { - "id": "11:5805422636156073861", - "name": "gain", - "type": 8 - }, - { - "id": "12:3861144650886065321", - "name": "path", - "type": 9 - }, - { - "id": "13:3755368355153819967", - "name": "enabled", - "type": 1 - } - ], - "relations": [] - }, - { - "id": "2:5695036645028998704", - "lastPropertyId": "3:5935807626551879093", - "name": "PreferenceEntity", - "properties": [ - { - "id": "1:1241938942467328378", - "name": "id", - "type": 6, - "flags": 1 - }, - { - "id": "2:5066364999797986961", - "name": "key", - "indexId": "3:361394127200064680", - "type": 9, - "flags": 34848 - }, - { - "id": "3:5935807626551879093", - "name": "value", - "type": 9 - } - ], - "relations": [] - }, - { - "id": "3:13725857459345728", - "lastPropertyId": "4:8575761112465612996", - "name": "SatelliteEntity", - "properties": [ - { - "id": "1:7008444193321057279", - "name": "id", - "type": 6, - "flags": 129 - }, - { - "id": "2:7254931361809919912", - "name": "name", - "type": 9 - }, - { - "id": "3:7655077553453802998", - "name": "tle", - "type": 9 - }, - { - "id": "4:8575761112465612996", - "name": "groups", - "type": 30 - } - ], - "relations": [] - }, - { - "id": "4:2355261488865870711", - "lastPropertyId": "12:8881688937650635468", - "name": "SimbadEntity", - "properties": [ - { - "id": "1:8754753767317947963", - "name": "id", - "type": 6, - "flags": 129 - }, - { - "id": "2:875189598014282513", - "name": "name", - "type": 9 - }, - { - "id": "3:1840539013499888018", - "name": "type", - "type": 5 - }, - { - "id": "4:8380920369067256416", - "name": "rightAscensionJ2000", - "type": 8 - }, - { - "id": "5:4114744755808135895", - "name": "declinationJ2000", - "type": 8 - }, - { - "id": "6:5877086147655445788", - "name": "magnitude", - "type": 8 - }, - { - "id": "7:4614518058111040649", - "name": "pmRA", - "type": 8 - }, - { - "id": "8:5619165542749552220", - "name": "pmDEC", - "type": 8 - }, - { - "id": "9:8196290885692683478", - "name": "parallax", - "type": 8 - }, - { - "id": "10:2681231197677728845", - "name": "radialVelocity", - "type": 8 - }, - { - "id": "11:2414643968839286765", - "name": "redshift", - "type": 8 - }, - { - "id": "12:8881688937650635468", - "name": "constellation", - "type": 5 - } - ], - "relations": [] - } - ], - "lastEntityId": "4:2355261488865870711", - "lastIndexId": "3:361394127200064680", - "lastRelationId": "0:0", - "lastSequenceId": "0:0", - "modelVersion": 5, - "modelVersionParserMinimum": 5, - "retiredEntityUids": [], - "retiredIndexUids": [], - "retiredPropertyUids": [], - "retiredRelationUids": [], - "version": 1 -} \ No newline at end of file diff --git a/api/src/main/kotlin/nebulosa/api/Nebulosa.kt b/api/src/main/kotlin/nebulosa/api/Nebulosa.kt index 8744b76ac..f75e3c3c9 100644 --- a/api/src/main/kotlin/nebulosa/api/Nebulosa.kt +++ b/api/src/main/kotlin/nebulosa/api/Nebulosa.kt @@ -13,6 +13,8 @@ import io.javalin.http.HttpStatus.BAD_REQUEST import io.javalin.json.JavalinJackson import nebulosa.api.converters.DeviceModule import nebulosa.api.core.ErrorResponse +import nebulosa.api.database.MainDatabaseMigrator +import nebulosa.api.database.SkyDatabaseMigrator import nebulosa.api.inject.* import nebulosa.json.PathModule import nebulosa.log.i @@ -20,6 +22,7 @@ import nebulosa.log.loggerFor import org.koin.core.context.startKoin import org.slf4j.LoggerFactory import java.net.ConnectException +import java.util.concurrent.ExecutorService @Command(name = "nebulosa") class Nebulosa : Runnable, AutoCloseable { @@ -65,6 +68,12 @@ class Nebulosa : Runnable, AutoCloseable { startKoin(koinApp) LOG.i("server is started at port: {}", app.port()) + + with(koinApp.koin) { + val executor = get() + executor.submit(get()) + executor.submit(get()) + } } private fun handleException(ex: Exception, ctx: Context) { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt index 8d7377ceb..af09a017b 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt @@ -1,6 +1,7 @@ package nebulosa.api.atlas import io.javalin.http.Header +import nebulosa.api.database.MainDatabaseMigrator import nebulosa.api.preference.PreferenceService import nebulosa.io.transferAndClose import nebulosa.log.d @@ -13,6 +14,8 @@ import nebulosa.time.IERSAB import nebulosa.time.IERSB import okhttp3.OkHttpClient import okhttp3.Request +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import java.nio.file.Path import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit @@ -24,13 +27,15 @@ class IERSUpdateTask( private val httpClient: OkHttpClient, private val preferenceService: PreferenceService, scheduledExecutorService: ScheduledExecutorService, -) : Runnable { +) : Runnable, KoinComponent { init { - scheduledExecutorService.schedule(this, 0L, TimeUnit.SECONDS) + scheduledExecutorService.schedule(this, 5L, TimeUnit.SECONDS) } override fun run() { + get().await() + val iersa = IERSA() val iersb = IERSB() diff --git a/api/src/main/kotlin/nebulosa/api/atlas/LibWCSDownloadTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/LibWCSDownloadTask.kt index 2704a52dd..dbbc8b7b8 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/LibWCSDownloadTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/LibWCSDownloadTask.kt @@ -1,6 +1,7 @@ package nebulosa.api.atlas import com.sun.jna.Platform +import nebulosa.api.database.MainDatabaseMigrator import nebulosa.api.preference.PreferenceService import nebulosa.io.transferAndCloseOutput import nebulosa.log.e @@ -10,6 +11,8 @@ import nebulosa.wcs.LibWCS import okhttp3.OkHttpClient import okhttp3.Request import org.apache.commons.codec.digest.DigestUtils +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import java.nio.file.Path import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit @@ -22,13 +25,15 @@ class LibWCSDownloadTask( private val httpClient: OkHttpClient, private val preferenceService: PreferenceService, scheduledExecutorService: ScheduledExecutorService, -) : Runnable { +) : Runnable, KoinComponent { init { scheduledExecutorService.schedule(this, 5L, TimeUnit.SECONDS) } override fun run() { + get().await() + var request = Request.Builder().get().url(VERSION_URL).build() val libraryUrl = LIBRARY_URLS[Platform.RESOURCE_PREFIX] diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt index bed2ebab2..c0be5284e 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteEntity.kt @@ -1,13 +1,29 @@ package nebulosa.api.atlas -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id -import nebulosa.api.database.BoxEntity +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.statements.UpdateBuilder -@Entity data class SatelliteEntity( - @Id(assignable = true) override var id: Long = 0L, + @JvmField var id: Long = 0L, @JvmField var name: String = "", @JvmField var tle: String = "", - @JvmField var groups: MutableList = ArrayList(0), -) : BoxEntity + @JvmField var groups: List = emptyList(), +) { + + fun mapTo(builder: UpdateBuilder, update: Boolean = false) { + if (!update) builder[SatelliteTable.id] = id + builder[SatelliteTable.name] = name + builder[SatelliteTable.tle] = tle + builder[SatelliteTable.groups] = groups.map { it.ordinal } + } + + companion object { + + fun from(row: ResultRow) = SatelliteEntity( + row[SatelliteTable.id], + row[SatelliteTable.name], + row[SatelliteTable.tle], + row[SatelliteTable.groups].map(SatelliteGroupType.entries::get), + ) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt index 26caa0270..ea2952de4 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt @@ -1,18 +1,55 @@ package nebulosa.api.atlas -import io.objectbox.Box -import io.objectbox.query.QueryBuilder.StringOrder.CASE_SENSITIVE -import nebulosa.api.repositories.BoxRepository +import nebulosa.api.database.contains +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction -class SatelliteRepository(override val box: Box) : BoxRepository() { +class SatelliteRepository(private val connection: Database) { - fun search(text: String? = null, groups: Iterable = emptyList(), id: Long = 0L): List { - val condition = and( - if (id > 0L) SatelliteEntity_.id.equal(id) else null, - if (text.isNullOrBlank()) null else SatelliteEntity_.name.containsInsensitive(text), - or(groups.map { SatelliteEntity_.groups.containsElement(it.name, CASE_SENSITIVE) }), - ) + operator fun get(id: Long) = transaction(connection) { + SatelliteTable + .selectAll() + .where { SatelliteTable.id eq id } + .firstOrNull() + ?.let(SatelliteEntity::from) + } + + fun search(text: String? = null, groups: List = emptyList(), id: Long = 0L) = transaction(connection) { + SatelliteTable + .selectAll() + .also { if (id > 0L) it.andWhere { SatelliteTable.id eq id } } + .also { if (!text.isNullOrBlank()) it.andWhere { SatelliteTable.name like "%$text%" } } + .also { + if (groups.isNotEmpty()) it.andWhere { + var expr = SatelliteTable.groups contains groups[0].ordinal + + for (i in 1 until groups.size) { + expr = expr or (SatelliteTable.groups contains groups[i].ordinal) + } + + expr + } + } + .map(SatelliteEntity::from) + } + + fun add(entity: SatelliteEntity) = transaction(connection) { + SatelliteTable + .insert { entity.mapTo(it) } get SatelliteTable.id + entity + } + + fun update(entity: SatelliteEntity) = transaction(connection) { + SatelliteTable + .update({ SatelliteTable.id eq entity.id }) { entity.mapTo(it, true) } + entity + } + + fun add(entities: Iterable) = transaction(connection) { + SatelliteTable.batchInsert(entities, false, false) { it.mapTo(this) } + } - return (condition?.let(box::query) ?: box.query()).build().use { it.findLazy() } + fun clear() = transaction(connection) { + SatelliteTable.deleteAll() > 0 } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteTable.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteTable.kt new file mode 100644 index 000000000..9bb82f1fd --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteTable.kt @@ -0,0 +1,12 @@ +package nebulosa.api.atlas + +import org.jetbrains.exposed.sql.Table + +object SatelliteTable : Table("SATELLITES") { + val id = long("ID") + val name = text("NAME") + val tle = text("TLE") + val groups = array("GROUPS") + + override val primaryKey = PrimaryKey(id) +} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt index a876ea6f8..9a6077913 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt @@ -1,5 +1,7 @@ package nebulosa.api.atlas +import nebulosa.api.database.MainDatabaseMigrator +import nebulosa.api.database.SkyDatabaseMigrator import nebulosa.api.message.MessageService import nebulosa.api.preference.PreferenceService import nebulosa.log.i @@ -7,6 +9,8 @@ import nebulosa.log.loggerFor import nebulosa.log.w import okhttp3.OkHttpClient import okhttp3.Request +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import java.util.concurrent.CompletableFuture import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit @@ -17,13 +21,16 @@ class SatelliteUpdateTask( private val preferenceService: PreferenceService, private val messageService: MessageService, scheduledExecutorService: ScheduledExecutorService, -) : Runnable { +) : Runnable, KoinComponent { init { scheduledExecutorService.schedule(this, 10L, TimeUnit.SECONDS) } override fun run() { + get().await() + get().await() + checkIsOutOfDateAndUpdate() } @@ -47,7 +54,7 @@ class SatelliteUpdateTask( } private fun updateTLEs(): Boolean { - satelliteRepository.deleteAll() + satelliteRepository.clear() messageService.sendMessage(SatelliteUpdateNotificationEvent.Started) @@ -63,7 +70,7 @@ class SatelliteUpdateTask( tasks.forEach(CompletableFuture<*>::get) return satelliteRepository - .save(data.values) + .add(data.values) .also { LOG.i("{} satellites updated", it.size) } .also { messageService.sendMessage(SatelliteUpdateNotificationEvent.Finished(it.size)) } .isNotEmpty() @@ -93,11 +100,11 @@ class SatelliteUpdateTask( synchronized(data) { if (id in data) { - data[id]!!.groups.add(group.name) + (data[id]!!.groups as MutableList).add(group) } else { val name = lines[0].trim() val tle = lines.joinToString("\n") - data[id] = SatelliteEntity(id, name, tle, mutableListOf(group.name)) + data[id] = SatelliteEntity(id, name, tle, mutableListOf(group)) } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntity.kt b/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntity.kt deleted file mode 100644 index 819eefa4b..000000000 --- a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntity.kt +++ /dev/null @@ -1,49 +0,0 @@ -package nebulosa.api.atlas - -import com.fasterxml.jackson.annotation.JsonIgnore -import io.objectbox.annotation.Convert -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id -import nebulosa.api.converters.database.ConstellationPropertyConverter -import nebulosa.api.converters.database.SkyObjectTypePropertyConverter -import nebulosa.api.database.BoxEntity -import nebulosa.math.Angle -import nebulosa.math.Velocity -import nebulosa.nova.astrometry.Body -import nebulosa.nova.astrometry.Constellation -import nebulosa.nova.astrometry.FixedStar -import nebulosa.nova.position.ICRF -import nebulosa.skycatalog.DeepSkyObject -import nebulosa.skycatalog.SkyObject -import nebulosa.skycatalog.SkyObjectType -import nebulosa.time.InstantOfTime - -@Entity -data class SimbadEntity( - @Id(assignable = true) override var id: Long = 0L, - override var name: String = "", - @Convert(converter = SkyObjectTypePropertyConverter::class, dbType = Int::class) - override var type: SkyObjectType = SkyObjectType.OBJECT_OF_UNKNOWN_NATURE, - override var rightAscensionJ2000: Angle = 0.0, - override var declinationJ2000: Angle = 0.0, - override var magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, - override var pmRA: Angle = 0.0, - override var pmDEC: Angle = 0.0, - override var parallax: Angle = 0.0, - override var radialVelocity: Velocity = 0.0, - override var redshift: Double = 0.0, - @Convert(converter = ConstellationPropertyConverter::class, dbType = Int::class) override var constellation: Constellation = Constellation.AND, -) : DeepSkyObject, BoxEntity, Body { - - @delegate:Transient private val star by lazy { FixedStar(rightAscensionJ2000, declinationJ2000, pmRA, pmDEC, parallax, radialVelocity) } - - override val center - @JsonIgnore get() = 0 - - override val target - @JsonIgnore get() = Int.MIN_VALUE - - override fun observedAt(observer: ICRF) = star.observedAt(observer) - - override fun compute(time: InstantOfTime) = star.compute(time) -} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntityRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntityRepository.kt deleted file mode 100644 index c00beede5..000000000 --- a/api/src/main/kotlin/nebulosa/api/atlas/SimbadEntityRepository.kt +++ /dev/null @@ -1,37 +0,0 @@ -package nebulosa.api.atlas - -import io.objectbox.Box -import io.objectbox.kotlin.equal -import nebulosa.api.repositories.BoxRepository -import nebulosa.math.Angle -import nebulosa.math.toDegrees -import nebulosa.nova.astrometry.Constellation -import nebulosa.skycatalog.SkyObject -import nebulosa.skycatalog.SkyObjectType - -class SimbadEntityRepository(override val box: Box) : BoxRepository() { - - fun search( - name: String? = null, constellation: Constellation? = null, - rightAscension: Angle = 0.0, declination: Angle = 0.0, radius: Angle = 0.0, - magnitudeMin: Double = SkyObject.MAGNITUDE_MIN, magnitudeMax: Double = SkyObject.MAGNITUDE_MAX, - type: SkyObjectType? = null, id: Long = 0L, - ): List { - val useFilter = radius > 0.0 && radius.toDegrees in 0.1..90.0 - - val condition = and( - if (id > 0L) SimbadEntity_.id.equal(id) else null, - if (magnitudeMin in SkyObject.MAGNITUDE_RANGE) SimbadEntity_.magnitude.greaterOrEqual(magnitudeMin) else null, - if (magnitudeMax in SkyObject.MAGNITUDE_RANGE) SimbadEntity_.magnitude.lessOrEqual(magnitudeMax) else null, - if (type != null) SimbadEntity_.type equal type.ordinal else null, - if (constellation != null) SimbadEntity_.constellation equal constellation.ordinal else null, - if (name.isNullOrBlank()) null else SimbadEntity_.name containsInsensitive name, - ) - - return with(condition?.let(box::query) ?: box.query()) { - if (useFilter) filter(SkyObjectInsideCoordinate(rightAscension, declination, radius)) - order(SimbadEntity_.magnitude) - build() - }.use { if (useFilter) it.find() else it.find(0, 5000) } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt index 905556bde..c744651b5 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt @@ -34,7 +34,7 @@ class SkyAtlasController( app.get("sky-atlas/sky-objects/types", ::skyObjectTypes) app.get("sky-atlas/sky-objects/{id}/position", ::positionOfSkyObject) app.get("sky-atlas/sky-objects/{id}/altitude-points", ::altitudePointsOfSkyObject) - app.get("sky-atlas/satellites/", ::searchSatellites) + app.get("sky-atlas/satellites", ::searchSatellites) app.get("sky-atlas/satellites/{id}/position", ::positionOfSatellite) app.get("sky-atlas/satellites/{id}/altitude-points", ::altitudePointsOfSatellite) } @@ -138,10 +138,10 @@ class SkyAtlasController( val rightAscension = ctx.queryParam("rightAscension") ?: "" val declination = ctx.queryParam("declination") ?: "" val radius = ctx.queryParam("radius")?.toDouble() ?: 0.0 - val constellation = ctx.queryParam("constellation")?.let(Constellation::valueOf) + val constellation = ctx.queryParam("constellation")?.enumOf() val magnitudeMin = ctx.queryParam("magnitudeMin")?.toDouble() ?: SkyObject.MAGNITUDE_MIN val magnitudeMax = ctx.queryParam("magnitudeMax")?.toDouble() ?: SkyObject.MAGNITUDE_MAX - val type = ctx.queryParam("type")?.let(SkyObjectType::valueOf) + val type = ctx.queryParam("type")?.enumOf() val id = ctx.queryParam("id")?.toLong() ?: 0L val result = skyAtlasService.searchSkyObject( @@ -157,7 +157,7 @@ class SkyAtlasController( } private fun positionOfSatellite(ctx: Context) { - val satellite = satelliteRepository.find(ctx.pathParam("id").toLong().positive()) ?: return + val satellite = satelliteRepository[ctx.pathParam("id").toLong().positive()] ?: return val location = ctx.location().notNull() val date = ctx.queryParam("date").notNull().localDate() val time = ctx.queryParam("time").notNull().localTime() @@ -166,7 +166,7 @@ class SkyAtlasController( } private fun altitudePointsOfSatellite(ctx: Context) { - val satellite = satelliteRepository.find(ctx.pathParam("id").toLong().positive()) ?: return + val satellite = satelliteRepository[ctx.pathParam("id").toLong().positive()] ?: return val location = ctx.location().notNull() val date = ctx.queryParam("date").notNull().localDate() val time = ctx.queryParam("time").notNull().localTime() @@ -178,7 +178,7 @@ class SkyAtlasController( private fun searchSatellites(ctx: Context) { val text = ctx.queryParam("text") ?: "" val id = ctx.queryParam("id")?.toLong() ?: 0L - val groups = ctx.queryParams("groups").map(SatelliteGroupType::valueOf) + val groups = ctx.queryParams("group").map(SatelliteGroupType::valueOf) ctx.json(skyAtlasService.searchSatellites(text, groups, id)) } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt index 6ece3acce..e1f5d49ae 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt @@ -44,7 +44,7 @@ class SkyAtlasService( private val bodyEphemerisProvider: BodyEphemerisProvider, private val smallBodyDatabaseService: SmallBodyDatabaseService, private val satelliteRepository: SatelliteRepository, - private val simbadEntityRepository: SimbadEntityRepository, + private val skyObjectEntityRepository: SkyObjectEntityRepository, private val httpClient: OkHttpClient, private val objectMapper: ObjectMapper, private val moonPhaseFinder: MoonPhaseFinder, @@ -52,7 +52,7 @@ class SkyAtlasService( ) { private val positions = HashMap() - private val cachedSimbadEntities = HashMap() + private val cachedSkyObjectEntities = HashMap() private val targetLocks = HashMap() private val cachedMoonPhases = HashMap>() @@ -63,7 +63,7 @@ class SkyAtlasService( scheduledExecutorService.scheduleAtFixedRate(::refreshImageOfSun, 0L, 15L, TimeUnit.MINUTES) } - val objectTypes: Collection by lazy { simbadEntityRepository.findAll().map { it.type }.toSortedSet() } + val objectTypes: Collection by lazy { skyObjectEntityRepository.objectTypes } fun imageOfSun(output: HttpServletResponse) { output.contentType = "image/png" @@ -84,8 +84,8 @@ class SkyAtlasService( } fun positionOfSkyObject(location: GeographicCoordinate, id: Long, dateTime: LocalDateTime): BodyPosition { - val target = cachedSimbadEntities[id] ?: simbadEntityRepository.find(id) ?: throw NotFoundResponse("Cannot found sky object: [$id]") - cachedSimbadEntities[id] = target + val target = cachedSkyObjectEntities[id] ?: skyObjectEntityRepository[id] ?: throw NotFoundResponse("Cannot found sky object: [$id]") + cachedSkyObjectEntities[id] = target val distance = SkyObject.distanceFor(target.parallax.toMas) return positionOfBody(target, location, dateTime)!! .copy(magnitude = target.magnitude, constellation = target.constellation, distance = distance.toLightYears, distanceUnit = "ly") @@ -177,8 +177,8 @@ class SkyAtlasService( } fun altitudePointsOfSkyObject(location: GeographicCoordinate, id: Long, dateTime: LocalDateTime, stepSize: Int): List { - val target = cachedSimbadEntities[id] ?: simbadEntityRepository.find(id) ?: throw NotFoundResponse("Cannot found sky object: [$id]") - cachedSimbadEntities[id] = target + val target = cachedSkyObjectEntities[id] ?: skyObjectEntityRepository[id] ?: throw NotFoundResponse("Cannot found sky object: [$id]") + cachedSkyObjectEntities[id] = target val ephemeris = bodyEphemeris(target, location, dateTime, true) return altitudePointsOfBody(ephemeris, stepSize) } @@ -220,7 +220,7 @@ class SkyAtlasService( constellation: Constellation? = null, magnitudeMin: Double = SkyObject.MAGNITUDE_MIN, magnitudeMax: Double = SkyObject.MAGNITUDE_MAX, type: SkyObjectType? = null, id: Long = 0L, - ) = simbadEntityRepository.search(text, constellation, rightAscension, declination, radius, magnitudeMin, magnitudeMax, type, id) + ) = skyObjectEntityRepository.search(text, constellation, rightAscension, declination, radius, magnitudeMin, magnitudeMax, type, id) fun refreshImageOfSun() { val request = Request.Builder() diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt index 9c429dff7..206fca6e6 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt @@ -1,5 +1,7 @@ package nebulosa.api.atlas +import nebulosa.api.database.MainDatabaseMigrator +import nebulosa.api.database.SkyDatabaseMigrator import nebulosa.api.message.MessageService import nebulosa.api.preference.PreferenceService import nebulosa.log.e @@ -8,29 +10,36 @@ import nebulosa.log.loggerFor import okhttp3.OkHttpClient import okhttp3.Request import okio.source +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit class SkyAtlasUpdateTask( private val httpClient: OkHttpClient, - private val simbadEntityRepository: SimbadEntityRepository, + private val skyObjectEntityRepository: SkyObjectEntityRepository, private val preferenceService: PreferenceService, private val messageService: MessageService, scheduledExecutorService: ScheduledExecutorService, -) : Runnable { +) : Runnable, KoinComponent { init { scheduledExecutorService.schedule(this, 0L, TimeUnit.SECONDS) } override fun run() { + get().await() + get().await() + var request = Request.Builder().get().url(VERSION_URL).build() httpClient.newCall(request).execute().use { response -> if (response.isSuccessful) { val newestVersion = response.body!!.string().trim() - if (newestVersion != preferenceService.getText(VERSION_KEY) || simbadEntityRepository.isEmpty()) { + if (newestVersion != preferenceService.getText(VERSION_KEY) || skyObjectEntityRepository.size == 0L) { + skyObjectEntityRepository.clear() + LOG.i("Sky Atlas database is out of date. downloading...") messageService.sendMessage(SkyAtlasUpdateNotificationEvent.Started) @@ -46,9 +55,9 @@ class SkyAtlasUpdateTask( httpClient.newCall(request).execute().use { if (it.isSuccessful) { it.body!!.byteStream().source().use { source -> - SimbadDatabaseReader(source).use { reader -> + SkyDatabaseReader(source).use { reader -> for (entity in reader) { - simbadEntityRepository.save(entity) + skyObjectEntityRepository.add(entity) } } } @@ -66,9 +75,9 @@ class SkyAtlasUpdateTask( preferenceService.putText(VERSION_KEY, newestVersion) messageService.sendMessage(SkyAtlasUpdateNotificationEvent.Finished(newestVersion)) - LOG.i("Sky Atlas database was updated. version={}, size={}", newestVersion, simbadEntityRepository.size) + LOG.i("Sky Atlas database was updated. version={}, size={}", newestVersion, skyObjectEntityRepository.size) } else { - LOG.i("Sky Atlas database is up to date. version={}, size={}", newestVersion, simbadEntityRepository.size) + LOG.i("Sky Atlas database is up to date. version={}, size={}", newestVersion, skyObjectEntityRepository.size) } } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseReader.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyDatabaseReader.kt similarity index 78% rename from api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseReader.kt rename to api/src/main/kotlin/nebulosa/api/atlas/SkyDatabaseReader.kt index 13efa549f..7f3c7b66d 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseReader.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyDatabaseReader.kt @@ -11,16 +11,16 @@ import okio.Source import okio.buffer import okio.gzip -class SimbadDatabaseReader(source: Source) : Iterator, AutoCloseable { +class SkyDatabaseReader(source: Source) : Iterator, AutoCloseable { private val buffer = if (source is BufferedSource) source else source.gzip().buffer() override fun hasNext() = !buffer.exhausted() - override fun next(): SimbadEntity { + override fun next(): SkyObjectEntity { val id = buffer.readLong() val byteCount = buffer.readShort().toLong() and 0xFFFF - val name = buffer.readString(byteCount, Charsets.UTF_8) + val name = buffer.readString(byteCount, Charsets.UTF_8).split(SkyDatabaseWriter.NAME_SEPARATOR) val type = SkyObjectType.entries[buffer.readByte().toInt() and 0xFF] val rightAscension = buffer.readFloat().toDouble().deg val declination = buffer.readFloat().toDouble().deg @@ -33,7 +33,7 @@ class SimbadDatabaseReader(source: Source) : Iterator, AutoCloseab // val constellation = Constellation.entries[buffer.readByte().toInt() and 0xFF] val constellation = SkyObject.constellationFor(rightAscension, declination) - return SimbadEntity(id, name, type, rightAscension, declination, magnitude, pmRA, pmDEC, parallax, radialVelocity, redshift, constellation) + return SkyObjectEntity(id, name, type, rightAscension, declination, magnitude, pmRA, pmDEC, parallax, radialVelocity, redshift, constellation) } override fun close() { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseWriter.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyDatabaseWriter.kt similarity index 87% rename from api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseWriter.kt rename to api/src/main/kotlin/nebulosa/api/atlas/SkyDatabaseWriter.kt index df26d5b2c..6f188cba2 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SimbadDatabaseWriter.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyDatabaseWriter.kt @@ -8,13 +8,13 @@ import okio.Sink import okio.buffer import okio.gzip -class SimbadDatabaseWriter(sink: Sink) : AutoCloseable { +class SkyDatabaseWriter(sink: Sink) : AutoCloseable { private val buffer = if (sink is BufferedSink) sink else sink.gzip().buffer() - fun write(entity: SimbadEntity) { + fun write(entity: SkyObjectEntity) { write( - entity.id, entity.name, entity.type, + entity.id, entity.name.joinToString(NAME_SEPARATOR), entity.type, entity.rightAscensionJ2000, entity.declinationJ2000, entity.magnitude, entity.pmRA, entity.pmDEC, entity.parallax, entity.radialVelocity, entity.redshift, @@ -57,4 +57,9 @@ class SimbadDatabaseWriter(sink: Sink) : AutoCloseable { buffer.flush() buffer.close() } + + companion object { + + const val NAME_SEPARATOR = "|" + } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectEntity.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectEntity.kt new file mode 100644 index 000000000..1683117cc --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectEntity.kt @@ -0,0 +1,77 @@ +package nebulosa.api.atlas + +import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.api.atlas.SkyDatabaseWriter.Companion.NAME_SEPARATOR +import nebulosa.math.Angle +import nebulosa.math.Velocity +import nebulosa.nova.astrometry.Body +import nebulosa.nova.astrometry.Constellation +import nebulosa.nova.astrometry.FixedStar +import nebulosa.nova.position.ICRF +import nebulosa.skycatalog.DeepSkyObject +import nebulosa.skycatalog.SkyObject +import nebulosa.skycatalog.SkyObjectType +import nebulosa.time.InstantOfTime +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.statements.UpdateBuilder + +data class SkyObjectEntity( + override var id: Long = 0L, + override var name: List = emptyList(), + override var type: SkyObjectType = SkyObjectType.OBJECT_OF_UNKNOWN_NATURE, + override var rightAscensionJ2000: Angle = 0.0, + override var declinationJ2000: Angle = 0.0, + override var magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, + override var pmRA: Angle = 0.0, + override var pmDEC: Angle = 0.0, + override var parallax: Angle = 0.0, + override var radialVelocity: Velocity = 0.0, + override var redshift: Double = 0.0, + override var constellation: Constellation = Constellation.AND, +) : DeepSkyObject, Body { + + @delegate:Transient private val star by lazy { FixedStar(rightAscensionJ2000, declinationJ2000, pmRA, pmDEC, parallax, radialVelocity) } + + override val center + @JsonIgnore get() = 0 + + override val target + @JsonIgnore get() = Int.MIN_VALUE + + override fun observedAt(observer: ICRF) = star.observedAt(observer) + + override fun compute(time: InstantOfTime) = star.compute(time) + + fun mapTo(builder: UpdateBuilder) { + // builder[SkyObjectTable.id] = id + builder[SkyObjectTable.name] = name.joinToString(NAME_SEPARATOR) + builder[SkyObjectTable.type] = type + builder[SkyObjectTable.rightAscension] = rightAscensionJ2000 + builder[SkyObjectTable.declination] = declinationJ2000 + builder[SkyObjectTable.magnitude] = magnitude + builder[SkyObjectTable.pmRA] = pmRA + builder[SkyObjectTable.pmDEC] = pmDEC + builder[SkyObjectTable.parallax] = parallax + builder[SkyObjectTable.radialVelocity] = radialVelocity + builder[SkyObjectTable.redshift] = redshift + builder[SkyObjectTable.constellation] = constellation + } + + companion object { + + fun from(row: ResultRow) = SkyObjectEntity( + row[SkyObjectTable.id], + row[SkyObjectTable.name].split(NAME_SEPARATOR), + row[SkyObjectTable.type], + row[SkyObjectTable.rightAscension], + row[SkyObjectTable.declination], + row[SkyObjectTable.magnitude], + row[SkyObjectTable.pmRA], + row[SkyObjectTable.pmDEC], + row[SkyObjectTable.parallax], + row[SkyObjectTable.radialVelocity], + row[SkyObjectTable.redshift], + row[SkyObjectTable.constellation], + ) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectEntityRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectEntityRepository.kt new file mode 100644 index 000000000..a7fdf9107 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectEntityRepository.kt @@ -0,0 +1,80 @@ +package nebulosa.api.atlas + +import nebulosa.math.Angle +import nebulosa.math.cos +import nebulosa.math.sin +import nebulosa.math.toDegrees +import nebulosa.nova.astrometry.Constellation +import nebulosa.skycatalog.SkyObject +import nebulosa.skycatalog.SkyObjectType +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.functions.math.ACosFunction +import org.jetbrains.exposed.sql.functions.math.CosFunction +import org.jetbrains.exposed.sql.functions.math.SinFunction +import org.jetbrains.exposed.sql.transactions.transaction +import java.math.BigDecimal + +class SkyObjectEntityRepository(private val connection: Database) { + + operator fun get(id: Long) = transaction(connection) { + SkyObjectTable + .selectAll() + .where { SkyObjectTable.id eq id } + .firstOrNull() + ?.let(SkyObjectEntity::from) + } + + fun search( + name: String? = null, constellation: Constellation? = null, + rightAscension: Angle = 0.0, declination: Angle = 0.0, radius: Angle = 0.0, + magnitudeMin: Double = SkyObject.MAGNITUDE_MIN, magnitudeMax: Double = SkyObject.MAGNITUDE_MAX, + type: SkyObjectType? = null, id: Long = 0L, + ) = transaction(connection) { + val findByRegion = radius > 0.0 && radius.toDegrees in 0.016..90.0 // 1 arcmin to 90 deg + + SkyObjectTable + .selectAll() + .also { if (id > 0L) it.andWhere { SkyObjectTable.id eq id } } + .also { if (magnitudeMin > SkyObject.MAGNITUDE_MIN && magnitudeMin < SkyObject.MAGNITUDE_MAX) it.andWhere { SkyObjectTable.magnitude greaterEq magnitudeMin } } + .also { if (magnitudeMax > SkyObject.MAGNITUDE_MIN && magnitudeMax < SkyObject.MAGNITUDE_MAX) it.andWhere { SkyObjectTable.magnitude lessEq magnitudeMax } } + .also { if (type != null) it.andWhere { SkyObjectTable.type eq type } } + .also { if (constellation != null) it.andWhere { SkyObjectTable.constellation eq constellation } } + .also { if (!name.isNullOrBlank()) it.andWhere { SkyObjectTable.name like "%$name%" } } + .also { + if (findByRegion) { + val sinDEC = BigDecimal(declination.sin) + val cosDEC = BigDecimal(declination.cos) + + // acos(sin(o.declinationJ2000) * sinDEC + cos(o.declinationJ2000) * cosDEC * cos(o.rightAscensionJ2000 - rightAscension)) <= radius + it.andWhere { ACosFunction((SinFunction(SkyObjectTable.declination) times sinDEC) plus (CosFunction(SkyObjectTable.declination) times cosDEC times CosFunction(SkyObjectTable.rightAscension minus rightAscension))) lessEq BigDecimal(radius) } + } + } + .orderBy(SkyObjectTable.magnitude) + .limit(5000) + .map(SkyObjectEntity::from) + } + + private val count = Count(SkyObjectTable.id) + + val size + get() = transaction(connection) { SkyObjectTable.select(count).first()[count] } + + val objectTypes + get() = transaction(connection) { + SkyObjectTable + .select(SkyObjectTable.type) + .distinct() + .map { it[SkyObjectTable.type] } + .toSortedSet() + } + + fun add(entity: SkyObjectEntity) = transaction(connection) { + entity.id = SkyObjectTable + .insert { entity.mapTo(it) } get SkyObjectTable.id + entity + } + + fun clear() = transaction(connection) { + SkyObjectTable.deleteAll() > 0 + } +} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectInsideCoordinate.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectInsideCoordinate.kt deleted file mode 100644 index ac2e89655..000000000 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectInsideCoordinate.kt +++ /dev/null @@ -1,23 +0,0 @@ -package nebulosa.api.atlas - -import io.objectbox.query.QueryFilter -import nebulosa.math.Angle -import nebulosa.math.cos -import nebulosa.math.sin -import kotlin.math.acos -import kotlin.math.cos -import kotlin.math.sin - -data class SkyObjectInsideCoordinate( - @JvmField val rightAscension: Angle, - @JvmField val declination: Angle, - @JvmField val radius: Angle, -) : QueryFilter { - - private val sinDEC = declination.sin - private val cosDEC = declination.cos - - override fun keep(o: SimbadEntity): Boolean { - return acos(sin(o.declinationJ2000) * sinDEC + cos(o.declinationJ2000) * cosDEC * cos(o.rightAscensionJ2000 - rightAscension)) <= radius - } -} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectTable.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectTable.kt new file mode 100644 index 000000000..61eb9db4d --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyObjectTable.kt @@ -0,0 +1,22 @@ +package nebulosa.api.atlas + +import nebulosa.nova.astrometry.Constellation +import nebulosa.skycatalog.SkyObjectType +import org.jetbrains.exposed.sql.Table + +object SkyObjectTable : Table("SKY_OBJECTS") { + val id = long("ID").autoIncrement() + val name = text("NAME") + val type = enumeration("TYPE") + val rightAscension = double("RIGHT_ASCENSION") + val declination = double("DECLINATION") + val magnitude = double("MAGNITUDE") + val pmRA = double("PM_RA") + val pmDEC = double("PM_DEC") + val parallax = double("PARALLAX") + val radialVelocity = double("RADIAL_VEL") + val redshift = double("REDSHIFT") + val constellation = enumeration("CONSTELLATION") + + override val primaryKey = PrimaryKey(id) +} diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt index 8c5f7da83..9ed15c143 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt @@ -1,24 +1,19 @@ package nebulosa.api.calibration -import io.objectbox.annotation.Convert -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id -import io.objectbox.annotation.Index -import nebulosa.api.converters.database.FrameTypePropertyConverter -import nebulosa.api.converters.database.PathPropertyConverter -import nebulosa.api.database.BoxEntity import nebulosa.api.validators.Validatable import nebulosa.api.validators.positive import nebulosa.api.validators.positiveOrZero import nebulosa.fits.INVALID_TEMPERATURE import nebulosa.indi.device.camera.FrameType +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.statements.UpdateBuilder import java.nio.file.Path +import kotlin.io.path.Path -@Entity 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 group: String = "", + @JvmField var id: Long = 0L, + @JvmField var type: FrameType = FrameType.LIGHT, + @JvmField var group: String = "", @JvmField var filter: String? = null, @JvmField var exposureTime: Long = 0L, @JvmField var temperature: Double = INVALID_TEMPERATURE, @@ -27,9 +22,9 @@ data class CalibrationFrameEntity( @JvmField var binX: Int = 0, @JvmField var binY: Int = 0, @JvmField var gain: Double = 0.0, - @JvmField @Convert(converter = PathPropertyConverter::class, dbType = String::class) var path: Path? = null, + @JvmField var path: Path? = null, @JvmField var enabled: Boolean = true, -) : BoxEntity, Comparable, Validatable { +) : Comparable, Validatable { override fun validate() { id.positive() @@ -41,6 +36,21 @@ data class CalibrationFrameEntity( gain.positiveOrZero() } + fun mapTo(builder: UpdateBuilder) { + builder[CalibrationFrameTable.type] = type + builder[CalibrationFrameTable.group] = group + builder[CalibrationFrameTable.filter] = filter + builder[CalibrationFrameTable.exposureTime] = exposureTime + builder[CalibrationFrameTable.temperature] = temperature + builder[CalibrationFrameTable.width] = width + builder[CalibrationFrameTable.height] = height + builder[CalibrationFrameTable.binX] = binX + builder[CalibrationFrameTable.binY] = binY + builder[CalibrationFrameTable.gain] = gain + builder[CalibrationFrameTable.path] = "$path" + builder[CalibrationFrameTable.enabled] = enabled + } + override fun compareTo(other: CalibrationFrameEntity): Int { return if (type.ordinal > other.type.ordinal) 1 else if (type.ordinal < other.type.ordinal) -1 @@ -62,4 +72,25 @@ data class CalibrationFrameEntity( else if (filter == null) -1 else 1 } + + companion object { + + fun from(row: ResultRow): CalibrationFrameEntity { + return CalibrationFrameEntity( + row[CalibrationFrameTable.id], + row[CalibrationFrameTable.type], + row[CalibrationFrameTable.group], + row[CalibrationFrameTable.filter], + row[CalibrationFrameTable.exposureTime], + row[CalibrationFrameTable.temperature], + row[CalibrationFrameTable.width], + row[CalibrationFrameTable.height], + row[CalibrationFrameTable.binX], + row[CalibrationFrameTable.binY], + row[CalibrationFrameTable.gain], + Path(row[CalibrationFrameTable.path]), + row[CalibrationFrameTable.enabled], + ) + } + } } diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt index bd40d34f9..2b5e77b31 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt @@ -1,70 +1,95 @@ package nebulosa.api.calibration -import io.objectbox.Box -import io.objectbox.kotlin.equal -import nebulosa.api.repositories.BoxRepository import nebulosa.indi.device.camera.FrameType +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction -class CalibrationFrameRepository(override val box: Box) : - BoxRepository() { +class CalibrationFrameRepository(private val connection: Database) { - fun groups() = box.all.map { it.group }.distinct() + operator fun get(id: Long) = transaction(connection) { + CalibrationFrameTable + .selectAll() + .where { CalibrationFrameTable.id eq id } + .firstOrNull() + ?.let(CalibrationFrameEntity::from) + } + + fun groups() = transaction(connection) { + CalibrationFrameTable + .select(CalibrationFrameTable.group) + .distinct() + .map { it[CalibrationFrameTable.group] } + .toSet() + } - fun findAll(group: String): List { - return box.query(CalibrationFrameEntity_.group equal group) - .build().use { it.find() } + fun findAll(group: String? = null) = transaction(connection) { + CalibrationFrameTable + .selectAll() + .also { if (!group.isNullOrBlank()) it.where { CalibrationFrameTable.group eq group } } + .map(CalibrationFrameEntity::from) } - @Synchronized - 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 delete(group: String, path: String) = transaction(connection) { + CalibrationFrameTable + .deleteWhere { (CalibrationFrameTable.group eq group) and (CalibrationFrameTable.path eq path) } } - 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_.group equal group, - CalibrationFrameEntity_.width equal width, - CalibrationFrameEntity_.height equal height, - CalibrationFrameEntity_.binX equal bin, - CalibrationFrameEntity_.binY equal bin, - if (exposureTime > 0L) CalibrationFrameEntity_.exposureTime equal exposureTime else null, - if (gain > 0L) CalibrationFrameEntity_.gain.equal(gain, 1E-3) else null, - ) + fun darkFrames(group: String, width: Int, height: Int, bin: Int, exposureTime: Long, gain: Double) = transaction(connection) { + CalibrationFrameTable + .selectAll() + .where { CalibrationFrameTable.type eq FrameType.DARK } + .andWhere { CalibrationFrameTable.enabled eq true } + .andWhere { CalibrationFrameTable.group eq group } + .andWhere { CalibrationFrameTable.width eq width } + .andWhere { CalibrationFrameTable.height eq height } + .andWhere { CalibrationFrameTable.binX eq bin } + .andWhere { CalibrationFrameTable.binY eq bin } + .also { if (exposureTime > 0L) it.andWhere { CalibrationFrameTable.exposureTime eq exposureTime } } + .also { if (gain > 0L) it.andWhere { CalibrationFrameTable.gain eq gain } } + .map(CalibrationFrameEntity::from) + } - return box.query(condition).build().use { it.find() } + fun biasFrames(group: String, width: Int, height: Int, bin: Int, gain: Double) = transaction(connection) { + CalibrationFrameTable + .selectAll() + .where { CalibrationFrameTable.type eq FrameType.BIAS } + .andWhere { CalibrationFrameTable.enabled eq true } + .andWhere { CalibrationFrameTable.group eq group } + .andWhere { CalibrationFrameTable.width eq width } + .andWhere { CalibrationFrameTable.height eq height } + .andWhere { CalibrationFrameTable.binX eq bin } + .andWhere { CalibrationFrameTable.binY eq bin } + .also { if (gain > 0L) it.andWhere { CalibrationFrameTable.gain eq gain } } + .map(CalibrationFrameEntity::from) } - 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_.group equal group, - CalibrationFrameEntity_.width equal width, - CalibrationFrameEntity_.height equal height, - CalibrationFrameEntity_.binX equal bin, - CalibrationFrameEntity_.binY equal bin, - if (gain > 0L) CalibrationFrameEntity_.gain.equal(gain, 1E-3) else null, - ) + fun flatFrames(group: String, filter: String?, width: Int, height: Int, bin: Int) = transaction(connection) { + CalibrationFrameTable + .selectAll() + .where { CalibrationFrameTable.type eq FrameType.FLAT } + .andWhere { CalibrationFrameTable.enabled eq true } + .andWhere { CalibrationFrameTable.group eq group } + .andWhere { CalibrationFrameTable.width eq width } + .andWhere { CalibrationFrameTable.height eq height } + .andWhere { CalibrationFrameTable.binX eq bin } + .andWhere { CalibrationFrameTable.binY eq bin } + .also { it.andWhere { if (filter.isNullOrBlank()) CalibrationFrameTable.filter.isNull() else CalibrationFrameTable.filter eq filter } } + .map(CalibrationFrameEntity::from) + } - return box.query(condition).build().use { it.find() } + fun add(entity: CalibrationFrameEntity) = transaction(connection) { + entity.id = CalibrationFrameTable + .insert { entity.mapTo(it) } get CalibrationFrameTable.id + entity } - 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_.group equal group, - CalibrationFrameEntity_.width equal width, - CalibrationFrameEntity_.height equal height, - CalibrationFrameEntity_.binX equal bin, - CalibrationFrameEntity_.binY equal bin, - if (filter.isNullOrBlank()) CalibrationFrameEntity_.filter.isNull - else CalibrationFrameEntity_.filter equalInsensitive filter, - ) + fun update(entity: CalibrationFrameEntity) = transaction(connection) { + CalibrationFrameTable + .update({ CalibrationFrameTable.id eq entity.id }) { entity.mapTo(it) } == 1 + } - return box.query(condition).build().use { it.find() } + fun delete(id: Long) = transaction(connection) { + CalibrationFrameTable.deleteWhere { CalibrationFrameTable.id eq id } == 1 } } diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index f108b73d0..05f17e152 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -79,7 +79,7 @@ class CalibrationFrameService(private val calibrationFrameRepository: Calibratio } } - fun groups(): List { + fun groups(): Collection { return calibrationFrameRepository.groups() } @@ -124,7 +124,7 @@ class CalibrationFrameService(private val calibrationFrameRepository: Calibratio gain, file, ) - calibrationFrameRepository.save(frame) + calibrationFrameRepository.add(frame) .also(frames::add) } } catch (e: Throwable) { @@ -136,7 +136,8 @@ class CalibrationFrameService(private val calibrationFrameRepository: Calibratio } fun edit(frame: CalibrationFrameEntity): CalibrationFrameEntity { - return calibrationFrameRepository.save(frame) + check(calibrationFrameRepository.update(frame)) { "failed to update entity" } + return frame } fun delete(id: Long) { diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameTable.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameTable.kt new file mode 100644 index 000000000..c0b138a1d --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameTable.kt @@ -0,0 +1,22 @@ +package nebulosa.api.calibration + +import nebulosa.indi.device.camera.FrameType +import org.jetbrains.exposed.sql.Table + +object CalibrationFrameTable : Table("CALIBRATION_FRAMES") { + val id = long("ID").autoIncrement() + val type = enumeration("TYPE") + val group = text("GROUP") + val filter = text("FILTER").nullable() + val exposureTime = long("EXPOSURE_TIME") + val temperature = double("TEMPERATURE") + val width = integer("WIDTH") + val height = integer("HEIGHT") + val binX = integer("BIN_X") + val binY = integer("BIN_Y") + val gain = double("GAIN") + val path = text("PATH") + val enabled = bool("ENABLED") + + override val primaryKey = PrimaryKey(id) +} diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt index 6f1d7fc92..8d0a27434 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt @@ -3,6 +3,7 @@ package nebulosa.api.connection import io.javalin.Javalin import io.javalin.http.Context import nebulosa.api.core.Controller +import nebulosa.api.validators.enumOf import nebulosa.api.validators.notNull import nebulosa.api.validators.notNullOrBlank import nebulosa.api.validators.range @@ -22,7 +23,7 @@ class ConnectionController( private fun connect(ctx: Context) { val host = ctx.queryParam("host").notNullOrBlank() val port = ctx.queryParam("port").notNull().toInt().range(1, 65535) - val type = ctx.queryParam("type").notNull().let(ConnectionType::valueOf) + val type = ctx.queryParam("type").notNull().enumOf() connectionService.connect(host, port, type) } diff --git a/api/src/main/kotlin/nebulosa/api/converters/database/ConstellationPropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/converters/database/ConstellationPropertyConverter.kt deleted file mode 100644 index d7ca7b479..000000000 --- a/api/src/main/kotlin/nebulosa/api/converters/database/ConstellationPropertyConverter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package nebulosa.api.converters.database - -import io.objectbox.converter.PropertyConverter -import nebulosa.nova.astrometry.Constellation - -class ConstellationPropertyConverter : PropertyConverter { - - override fun convertToEntityProperty(databaseValue: Int?): Constellation { - return databaseValue?.let(Constellation.entries::get) ?: Constellation.AND - } - - override fun convertToDatabaseValue(entityProperty: Constellation?): Int { - return entityProperty?.ordinal ?: 0 - } -} diff --git a/api/src/main/kotlin/nebulosa/api/converters/database/FrameTypePropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/converters/database/FrameTypePropertyConverter.kt deleted file mode 100644 index 9aba95d6f..000000000 --- a/api/src/main/kotlin/nebulosa/api/converters/database/FrameTypePropertyConverter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package nebulosa.api.converters.database - -import io.objectbox.converter.PropertyConverter -import nebulosa.indi.device.camera.FrameType - -class FrameTypePropertyConverter : PropertyConverter { - - override fun convertToEntityProperty(databaseValue: Int?): FrameType { - return databaseValue?.let(FrameType.entries::get) ?: FrameType.LIGHT - } - - override fun convertToDatabaseValue(entityProperty: FrameType?): Int { - return entityProperty?.ordinal ?: 0 - } -} diff --git a/api/src/main/kotlin/nebulosa/api/converters/database/PathPropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/converters/database/PathPropertyConverter.kt deleted file mode 100644 index bb8b8c017..000000000 --- a/api/src/main/kotlin/nebulosa/api/converters/database/PathPropertyConverter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package nebulosa.api.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/converters/database/SkyObjectTypePropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/converters/database/SkyObjectTypePropertyConverter.kt deleted file mode 100644 index 5378e577d..000000000 --- a/api/src/main/kotlin/nebulosa/api/converters/database/SkyObjectTypePropertyConverter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package nebulosa.api.converters.database - -import io.objectbox.converter.PropertyConverter -import nebulosa.skycatalog.SkyObjectType - -class SkyObjectTypePropertyConverter : PropertyConverter { - - override fun convertToEntityProperty(databaseValue: Int?): SkyObjectType { - return databaseValue?.let(SkyObjectType.entries::get) ?: SkyObjectType.OBJECT_OF_UNKNOWN_NATURE - } - - override fun convertToDatabaseValue(entityProperty: SkyObjectType?): Int { - return entityProperty?.ordinal ?: 0 - } -} diff --git a/api/src/main/kotlin/nebulosa/api/database/BoxEntity.kt b/api/src/main/kotlin/nebulosa/api/database/BoxEntity.kt deleted file mode 100644 index 16091348c..000000000 --- a/api/src/main/kotlin/nebulosa/api/database/BoxEntity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package nebulosa.api.database - -interface BoxEntity { - - val id: Long -} diff --git a/api/src/main/kotlin/nebulosa/api/database/MainDatabaseMigrator.kt b/api/src/main/kotlin/nebulosa/api/database/MainDatabaseMigrator.kt new file mode 100644 index 000000000..11ccfc3cb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/database/MainDatabaseMigrator.kt @@ -0,0 +1,26 @@ +package nebulosa.api.database + +import org.flywaydb.core.Flyway +import java.util.concurrent.CountDownLatch + +data class MainDatabaseMigrator(private val dataSource: String) : Runnable { + + private val latch = CountDownLatch(1) + + override fun run() { + Flyway.configure() + .baselineVersion("0") + .baselineOnMigrate(true) + .table("MIGRATIONS") + .dataSource(dataSource, "root", "") + .locations("classpath:migrations/main") + .load() + .migrate() + + latch.countDown() + } + + fun await() { + latch.await() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/database/Operations.kt b/api/src/main/kotlin/nebulosa/api/database/Operations.kt new file mode 100644 index 000000000..646d8deeb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/database/Operations.kt @@ -0,0 +1,21 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package nebulosa.api.database + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq + +@PublishedApi +internal class ILikeOp(a: Expression<*>, b: Expression<*>) : ComparisonOp(a, b, "ILIKE") + +inline infix fun ExpressionWithColumnType.ilike(pattern: String): Op = ILikeOp(this, stringParam(pattern)) + +@PublishedApi +internal class ArrayContains(a: Expression<*>, b: Expression<*>) : CustomFunction("ARRAY_CONTAINS", BooleanColumnType(), a, b) + +@OptIn(InternalApi::class) +inline infix fun ExpressionWithColumnType>.contains(value: T): Op = + ArrayContains(this, QueryParameter(value, resolveColumnType(T::class))) eq true + +inline infix fun ExpressionWithColumnType>.contains(value: List): Op = + ArrayContains(this, arrayParam(value)) eq true diff --git a/api/src/main/kotlin/nebulosa/api/database/SkyDatabaseMigrator.kt b/api/src/main/kotlin/nebulosa/api/database/SkyDatabaseMigrator.kt new file mode 100644 index 000000000..8379012a6 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/database/SkyDatabaseMigrator.kt @@ -0,0 +1,26 @@ +package nebulosa.api.database + +import org.flywaydb.core.Flyway +import java.util.concurrent.CountDownLatch + +data class SkyDatabaseMigrator(private val dataSource: String) : Runnable { + + private val latch = CountDownLatch(1) + + override fun run() { + Flyway.configure() + .baselineVersion("0") + .baselineOnMigrate(true) + .table("MIGRATIONS") + .dataSource(dataSource, "root", "") + .locations("classpath:migrations/sky") + .load() + .migrate() + + latch.countDown() + } + + fun await() { + latch.await() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt index 8a23eaee4..5668cc73a 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -4,6 +4,7 @@ import io.javalin.Javalin import io.javalin.http.Context import nebulosa.api.connection.ConnectionService import nebulosa.api.core.Controller +import nebulosa.api.validators.enumOf import nebulosa.api.validators.notNull import nebulosa.api.validators.notNullOrBlank import nebulosa.api.validators.range @@ -49,7 +50,7 @@ class GuideOutputController( private fun pulse(ctx: Context) { val id = ctx.pathParam("id") val guideOutput = connectionService.guideOutput(id) ?: return - val direction = ctx.queryParam("direction").notNullOrBlank().let(GuideDirection::valueOf) + val direction = ctx.queryParam("direction").notNullOrBlank().enumOf() val duration = ctx.queryParam("duration").notNull().toLong().range(0L, 1800000000L).times(1000L).let(Duration::ofNanos) guideOutputService.pulse(guideOutput, direction, duration) } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt index 3faf49408..4c641e24f 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageAnnotation.kt @@ -22,7 +22,7 @@ data class ImageAnnotation( data class StarDSO( override val id: Long = 0L, - override val name: String, + override val name: List = emptyList(), override val type: SkyObjectType = SkyObjectType.STAR, @field:JsonSerialize(using = RightAscensionSerializer::class) override val rightAscensionJ2000: Angle = 0.0, @field:JsonSerialize(using = DeclinationSerializer::class) override val declinationJ2000: Angle = 0.0, @@ -44,7 +44,7 @@ data class ImageAnnotation( data class MinorPlanet( override val id: Long = 0L, - override val name: String = "", + override val name: List = emptyList(), @field:JsonSerialize(using = RightAscensionSerializer::class) override val rightAscensionJ2000: Angle = 0.0, @field:JsonSerialize(using = DeclinationSerializer::class) override val declinationJ2000: Angle = 0.0, override val magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index f74629365..1a9c10421 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -3,7 +3,7 @@ package nebulosa.api.image import com.fasterxml.jackson.databind.ObjectMapper import jakarta.servlet.http.HttpServletResponse import nebulosa.api.atlas.Location -import nebulosa.api.atlas.SimbadEntityRepository +import nebulosa.api.atlas.SkyObjectEntityRepository import nebulosa.api.calibration.CalibrationFrameService import nebulosa.api.connection.ConnectionService import nebulosa.api.framing.FramingService @@ -52,7 +52,7 @@ class ImageService( private val framingService: FramingService, private val calibrationFrameService: CalibrationFrameService, private val smallBodyDatabaseService: SmallBodyDatabaseService, - private val simbadEntityRepository: SimbadEntityRepository, + private val skyObjectEntityRepository: SkyObjectEntityRepository, private val simbadService: SimbadService, private val imageBucket: ImageBucket, private val executorService: ExecutorService, @@ -221,7 +221,7 @@ class ImageService( if (x >= 0 && y >= 0 && x < image.width && y < image.height) { val magnitude = it[6].replace(INVALID_MAG_CHARS, "").toDoubleOrNull() ?: SkyObject.UNKNOWN_MAGNITUDE - val minorPlanet = ImageAnnotation.MinorPlanet(0L, it[0], rightAscension, declination, magnitude) + val minorPlanet = ImageAnnotation.MinorPlanet(0L, listOf(it[0]), rightAscension, declination, magnitude) val annotation = ImageAnnotation(x, y, minorPlanet = minorPlanet) annotations.add(annotation) count++ @@ -246,7 +246,7 @@ class ImageService( val catalog = if (request.useSimbad) { simbadService.search(SimbadSearch.Builder().region(rightAscension, declination, radius).build()) } else { - simbadEntityRepository.search(null, null, rightAscension, declination, radius) + skyObjectEntityRepository.search(null, null, rightAscension, declination, radius) } var count = 0 diff --git a/api/src/main/kotlin/nebulosa/api/inject/Inject.kt b/api/src/main/kotlin/nebulosa/api/inject/Inject.kt index f66959c1b..870eed8fa 100644 --- a/api/src/main/kotlin/nebulosa/api/inject/Inject.kt +++ b/api/src/main/kotlin/nebulosa/api/inject/Inject.kt @@ -2,8 +2,6 @@ package nebulosa.api.inject import com.fasterxml.jackson.databind.ObjectMapper import io.javalin.Javalin -import io.objectbox.BoxStore -import io.objectbox.kotlin.boxFor import nebulosa.api.APP_DIR_KEY import nebulosa.api.Nebulosa import nebulosa.api.alignment.polar.PolarAlignmentController @@ -17,7 +15,6 @@ import nebulosa.api.autofocus.AutoFocusController import nebulosa.api.autofocus.AutoFocusExecutor import nebulosa.api.autofocus.AutoFocusService import nebulosa.api.calibration.CalibrationFrameController -import nebulosa.api.calibration.CalibrationFrameEntity import nebulosa.api.calibration.CalibrationFrameRepository import nebulosa.api.calibration.CalibrationFrameService import nebulosa.api.cameras.CameraCaptureExecutor @@ -29,7 +26,8 @@ import nebulosa.api.confirmation.ConfirmationService import nebulosa.api.connection.ConnectionController import nebulosa.api.connection.ConnectionEventHub import nebulosa.api.connection.ConnectionService -import nebulosa.api.database.MyObjectBox +import nebulosa.api.database.MainDatabaseMigrator +import nebulosa.api.database.SkyDatabaseMigrator import nebulosa.api.dustcap.DustCapController import nebulosa.api.dustcap.DustCapEventHub import nebulosa.api.dustcap.DustCapService @@ -56,7 +54,6 @@ import nebulosa.api.mounts.MountEventHub import nebulosa.api.mounts.MountService import nebulosa.api.platesolver.PlateSolverController import nebulosa.api.platesolver.PlateSolverService -import nebulosa.api.preference.PreferenceEntity import nebulosa.api.preference.PreferenceRepository import nebulosa.api.preference.PreferenceService import nebulosa.api.rotators.RotatorController @@ -88,6 +85,7 @@ import okhttp3.ConnectionPool import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.greenrobot.eventbus.EventBus +import org.jetbrains.exposed.sql.Database import org.koin.core.qualifier.named import org.koin.dsl.koinApplication import org.koin.dsl.module @@ -101,8 +99,8 @@ val koinApp = koinApplication { modules(pathModule()) modules(coreModule()) modules(httpModule()) + modules(databaseModule()) modules(eventBusModule()) - modules(boxStoreModule()) modules(repositoriesModule()) modules(phd2Module()) } @@ -119,12 +117,10 @@ object Named { val liveStackingDir = named("liveStackingDir") val defaultHttpClient = named("defaultHttpClient") val alpacaHttpClient = named("alpacaHttpClient") - val mainBoxStore = named("mainBoxStore") - val simbadBoxStore = named("simbadBoxStore") - val calibrationFrameBox = named("calibrationFrameBox") - val preferenceBox = named("preferenceBox") - val satelliteBox = named("satelliteBox") - val simbadBox = named("simbadBox") + val mainConnection = named("mainConnection") + val mainDatasourceUrl = named("mainDatasource") + val skyConnection = named("skyConnection") + val skyDatasourceUrl = named("skyDatasource") } // PATH @@ -212,25 +208,13 @@ fun eventBusModule() = module { } } -// BOX STORE - -fun boxStoreModule() = module { - single(Named.mainBoxStore) { - MyObjectBox.builder() - .baseDirectory(get(Named.dataDir).toFile()) - .name("main") - .build()!! - } - single(Named.simbadBoxStore) { - MyObjectBox.builder() - .baseDirectory(get(Named.dataDir).toFile()) - .name("simbad") - .build()!! - } - single(Named.calibrationFrameBox) { get(Named.mainBoxStore).boxFor() } - single(Named.preferenceBox) { get(Named.mainBoxStore).boxFor() } - single(Named.satelliteBox) { get(Named.mainBoxStore).boxFor() } - single(Named.simbadBox) { get(Named.simbadBoxStore).boxFor() } +fun databaseModule() = module { + single(Named.mainDatasourceUrl) { "jdbc:h2:${get(Named.dataDir)}/main;DB_CLOSE_DELAY=-1" } + single(Named.skyDatasourceUrl) { "jdbc:h2:${get(Named.dataDir)}/sky;DB_CLOSE_DELAY=-1" } + single(Named.mainConnection) { Database.connect(get(Named.mainDatasourceUrl), user = "root", password = "") } + single(Named.skyConnection) { Database.connect(get(Named.skyDatasourceUrl), user = "root", password = "") } + single { MainDatabaseMigrator(get(Named.mainDatasourceUrl)) } + single { SkyDatabaseMigrator(get(Named.skyDatasourceUrl)) } } // OBJECT MAPPER @@ -249,10 +233,10 @@ fun phd2Module() = module { // REPOSITORIES fun repositoriesModule() = module { - single { CalibrationFrameRepository(get(Named.calibrationFrameBox)) } - single { PreferenceRepository(get(Named.preferenceBox)) } - single { SatelliteRepository(get(Named.satelliteBox)) } - single { SimbadEntityRepository(get(Named.simbadBox)) } + single { CalibrationFrameRepository(get(Named.mainConnection)) } + single { PreferenceRepository(get(Named.mainConnection)) } + single { SatelliteRepository(get(Named.skyConnection)) } + single { SkyObjectEntityRepository(get(Named.skyConnection)) } } // SERVICES diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index 7a022907a..8feade719 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -120,7 +120,7 @@ class MountController( private fun trackMode(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val mode = ctx.queryParam("mode").notNullOrBlank().let(TrackMode::valueOf) + val mode = ctx.queryParam("mode").notNullOrBlank().enumOf() mountService.trackMode(mount, mode) } @@ -134,7 +134,7 @@ class MountController( private fun move(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val direction = ctx.queryParam("direction").notNullOrBlank().let(GuideDirection::valueOf) + val direction = ctx.queryParam("direction").notNullOrBlank().enumOf() val enabled = ctx.queryParam("enabled").notNull().toBoolean() mountService.move(mount, direction, enabled) } @@ -173,7 +173,7 @@ class MountController( private fun celestialLocation(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val type = ctx.pathParam("type").notNullOrBlank().let(CelestialLocationType::valueOf) + val type = ctx.pathParam("type").notNullOrBlank().enumOf() val location = when (type) { CelestialLocationType.ZENITH -> mountService.computeZenithLocation(mount) @@ -212,7 +212,7 @@ class MountController( private fun remoteControlStart(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val protocol = ctx.queryParam("protocol").notNullOrBlank().let(MountRemoteControlProtocol::valueOf) + val protocol = ctx.queryParam("protocol").notNullOrBlank().enumOf() val host = ctx.queryParam("host")?.ifBlank { null } ?: "0.0.0.0" val port = ctx.queryParam("port")?.toInt()?.positive() ?: 10001 mountService.remoteControlStart(mount, protocol, host, port) @@ -221,7 +221,7 @@ class MountController( private fun remoteControlStop(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val protocol = ctx.queryParam("protocol").notNullOrBlank().let(MountRemoteControlProtocol::valueOf) + val protocol = ctx.queryParam("protocol").notNullOrBlank().enumOf() mountService.remoteControlStop(mount, protocol) } diff --git a/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt b/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt index 4364d1baa..b8342799d 100644 --- a/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/preference/PreferenceEntity.kt @@ -1,14 +1,23 @@ package nebulosa.api.preference -import io.objectbox.annotation.ConflictStrategy -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id -import io.objectbox.annotation.Unique -import nebulosa.api.database.BoxEntity +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.statements.UpdateBuilder -@Entity data class PreferenceEntity( - @Id override var id: Long = 0L, - @Unique(onConflict = ConflictStrategy.REPLACE) @JvmField var key: String = "", + @JvmField var key: String = "", @JvmField var value: String? = null, -) : BoxEntity +) { + + fun mapTo(builder: UpdateBuilder, update: Boolean = false) { + if (!update) builder[PreferenceTable.key] = key + builder[PreferenceTable.value] = value + } + + companion object { + + fun from(row: ResultRow) = PreferenceEntity( + row[PreferenceTable.key], + row[PreferenceTable.value], + ) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/preference/PreferenceRepository.kt b/api/src/main/kotlin/nebulosa/api/preference/PreferenceRepository.kt index 2222b52b9..cbf76694d 100644 --- a/api/src/main/kotlin/nebulosa/api/preference/PreferenceRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/preference/PreferenceRepository.kt @@ -1,23 +1,47 @@ package nebulosa.api.preference -import io.objectbox.Box -import io.objectbox.kotlin.equal -import nebulosa.api.repositories.BoxRepository +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction -class PreferenceRepository(override val box: Box) : BoxRepository() { +class PreferenceRepository(private val connection: Database) { - fun existsByKey(key: String): Boolean { - return box.query(PreferenceEntity_.key equal key) - .build().use { it.findUnique() != null } + operator fun contains(key: String) = transaction(connection) { + !PreferenceTable + .select(PreferenceTable.key) + .where { PreferenceTable.key eq key } + .empty() } - fun findByKey(key: String): PreferenceEntity? { - return box.query(PreferenceEntity_.key equal key) - .build().use { it.findUnique() } + operator fun get(key: String) = transaction(connection) { + PreferenceTable + .selectAll() + .where { PreferenceTable.key eq key } + .firstOrNull() + ?.let(PreferenceEntity::from) } - fun deleteByKey(key: String) { - return box.query(PreferenceEntity_.key equal key) - .build().use { it.remove() } + private val count = Count(PreferenceTable.key) + + val size + get() = transaction(connection) { PreferenceTable.select(count).first()[count] } + + fun add(entity: PreferenceEntity) = transaction(connection) { + PreferenceTable.insert { entity.mapTo(it) } + entity + } + + fun update(entity: PreferenceEntity) = transaction(connection) { + PreferenceTable.update { entity.mapTo(it, true) } + } + + fun delete(key: String) = transaction(connection) { + PreferenceTable + .deleteWhere { PreferenceTable.key eq key } == 1 + } + + fun clear() = transaction(connection) { + PreferenceTable + .deleteAll() > 0 } } diff --git a/api/src/main/kotlin/nebulosa/api/preference/PreferenceService.kt b/api/src/main/kotlin/nebulosa/api/preference/PreferenceService.kt index c69ac4074..116280175 100644 --- a/api/src/main/kotlin/nebulosa/api/preference/PreferenceService.kt +++ b/api/src/main/kotlin/nebulosa/api/preference/PreferenceService.kt @@ -5,13 +5,19 @@ import com.fasterxml.jackson.databind.ObjectMapper class PreferenceService( private val preferenceRepository: PreferenceRepository, private val objectMapper: ObjectMapper, -) : Collection by preferenceRepository { +) { - operator fun get(key: String) = preferenceRepository.findByKey(key) + operator fun get(key: String) = preferenceRepository[key] - fun put(entity: PreferenceEntity) = preferenceRepository.save(entity) + val size + get() = preferenceRepository.size - operator fun contains(key: String) = preferenceRepository.existsByKey(key) + fun put(entity: PreferenceEntity) { + if (entity.key in this) preferenceRepository.update(entity) + else preferenceRepository.add(entity) + } + + operator fun contains(key: String) = key in preferenceRepository fun getJSON(key: String, type: Class): T? = this[key]?.value?.let { objectMapper.readValue(it, type) } @@ -29,7 +35,7 @@ class PreferenceService( fun getDouble(key: String) = getJSON(key, Double::class.java) - fun putJSON(key: String, value: Any?) = put(PreferenceEntity(0L, key, if (value == null) null else objectMapper.writeValueAsString(value))) + fun putJSON(key: String, value: Any?) = put(PreferenceEntity(key, if (value == null) null else objectMapper.writeValueAsString(value))) fun putBoolean(key: String, value: Boolean) = putJSON(key, value) @@ -43,7 +49,7 @@ class PreferenceService( fun putDouble(key: String, value: Double) = putJSON(key, value) - fun clear() = preferenceRepository.deleteAll() + fun clear() = preferenceRepository.clear() - fun delete(key: String) = preferenceRepository.deleteByKey(key) + fun delete(key: String) = preferenceRepository.delete(key) } diff --git a/api/src/main/kotlin/nebulosa/api/preference/PreferenceTable.kt b/api/src/main/kotlin/nebulosa/api/preference/PreferenceTable.kt new file mode 100644 index 000000000..e0a6818cd --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/preference/PreferenceTable.kt @@ -0,0 +1,10 @@ +package nebulosa.api.preference + +import org.jetbrains.exposed.sql.Table + +object PreferenceTable : Table("PREFERENCES") { + val key = text("KEY") + val value = text("VALUE").nullable() + + override val primaryKey = PrimaryKey(key) +} diff --git a/api/src/main/kotlin/nebulosa/api/repositories/BoxRepository.kt b/api/src/main/kotlin/nebulosa/api/repositories/BoxRepository.kt deleted file mode 100644 index 702837d54..000000000 --- a/api/src/main/kotlin/nebulosa/api/repositories/BoxRepository.kt +++ /dev/null @@ -1,117 +0,0 @@ -package nebulosa.api.repositories - -import io.objectbox.Box -import io.objectbox.Property -import io.objectbox.kotlin.and -import io.objectbox.kotlin.equal -import io.objectbox.kotlin.or -import io.objectbox.query.PropertyQueryCondition -import io.objectbox.query.QueryBuilder.StringOrder -import io.objectbox.query.QueryCondition -import nebulosa.api.database.BoxEntity - -abstract class BoxRepository : Collection { - - protected abstract val box: Box - - @Synchronized - open fun save(entity: T): T { - return entity.also(box::put) - } - - @Synchronized - open fun save(entities: Collection): Collection { - return entities.also(box::put) - } - - @Synchronized - open fun delete(entity: T): Boolean { - return box.remove(entity.id) - } - - @Synchronized - open fun delete(id: Long): Boolean { - return box.remove(id) - } - - @Synchronized - open fun deleteAll() { - return box.removeAll() - } - - open fun findAll(): List { - return box.query().build().use { it.findLazy() } - } - - open fun find(id: Long): T? { - return box.get(id) - } - - override val size - get() = box.count().toInt() - - override fun isEmpty(): Boolean { - return box.isEmpty - } - - override fun iterator(): Iterator { - return findAll().iterator() - } - - override fun containsAll(elements: Collection): Boolean { - return elements.all { it in this } - } - - override fun contains(element: T): Boolean { - return element.id in box - } - - companion object { - - inline val Property.isTrue - get() = this equal true - - inline val Property.isFalse - get() = this equal false - - @Suppress("NOTHING_TO_INLINE") - inline infix fun Property.equalInsensitive(value: String): PropertyQueryCondition { - return equal(value, StringOrder.CASE_INSENSITIVE) - } - - @Suppress("NOTHING_TO_INLINE") - inline infix fun Property.containsInsensitive(value: String): PropertyQueryCondition { - return contains(value, StringOrder.CASE_INSENSITIVE) - } - - @JvmStatic - fun and(condition: QueryCondition, vararg conditions: QueryCondition?): QueryCondition { - return conditions.fold(condition) { a, b -> if (b == null) a else a and b } - } - - @JvmStatic - fun and(vararg conditions: QueryCondition?): QueryCondition? { - return if (conditions.isEmpty()) null else conditions.reduce { a, b -> if (b == null) a else a?.and(b) ?: b } - } - - @JvmStatic - fun and(conditions: Collection?>): QueryCondition? { - return if (conditions.isEmpty()) null else conditions.reduce { a, b -> if (b == null) a else a?.and(b) ?: b } - } - - @JvmStatic - fun or(condition: QueryCondition, vararg conditions: QueryCondition?): QueryCondition { - return conditions.fold(condition) { a, b -> if (b == null) a else a or b } - } - - @JvmStatic - fun or(vararg conditions: QueryCondition?): QueryCondition? { - return if (conditions.isEmpty()) null else conditions.reduce { a, b -> if (b == null) a else a?.or(b) ?: b } - } - - @JvmStatic - fun or(conditions: Collection?>): QueryCondition? { - return if (conditions.isEmpty()) null else conditions.reduce { a, b -> if (b == null) a else a?.or(b) ?: b } - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/validators/Validators.kt b/api/src/main/kotlin/nebulosa/api/validators/Validators.kt index f3aa1d1ee..3a1bb5f8e 100644 --- a/api/src/main/kotlin/nebulosa/api/validators/Validators.kt +++ b/api/src/main/kotlin/nebulosa/api/validators/Validators.kt @@ -85,6 +85,10 @@ inline fun LocalTime.futureOrPresent(message: String? = null) = validate(this >= inline fun LocalTime.past(message: String? = null) = validate(this < LocalTime.now(SystemClock)) { message ?: "must be a past date" } inline fun LocalTime.pastOrPresent(message: String? = null) = validate(this <= LocalTime.now(SystemClock)) { message ?: "must be a time in the past or in the present" } +// ENUM + +inline fun > String.enumOf() = enumValueOf(this) + // BODY inline fun T.valid() = apply(Validatable::validate) diff --git a/api/src/main/resources/migrations/main/V1__initial_schema.sql b/api/src/main/resources/migrations/main/V1__initial_schema.sql new file mode 100644 index 000000000..28b0da6d0 --- /dev/null +++ b/api/src/main/resources/migrations/main/V1__initial_schema.sql @@ -0,0 +1,22 @@ +-- NOTE: Why UpperCase? https://www.h2database.com/html/grammar.html?highlight=DATABASE_TO_UPPER&search=DATABASE_TO_UPPER#firstFound + +CREATE TABLE IF NOT EXISTS CALIBRATION_FRAMES( + ID IDENTITY NOT NULL PRIMARY KEY, + TYPE TINYINT NOT NULL DEFAULT 0, + "GROUP" VARCHAR(512) NOT NULL, + FILTER VARCHAR_IGNORECASE(256), + EXPOSURE_TIME BIGINT NOT NULL DEFAULT 0, + TEMPERATURE DOUBLE PRECISION NOT NULL DEFAULT 0, + WIDTH INTEGER DEFAULT 0, + HEIGHT INTEGER DEFAULT 0, + BIN_X TINYINT DEFAULT 0, + BIN_Y TINYINT DEFAULT 0, + GAIN DOUBLE PRECISION NOT NULL DEFAULT 0, + PATH VARCHAR(4096) NOT NULL, + ENABLED BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS PREFERENCES( + "KEY" VARCHAR(256) NOT NULL PRIMARY KEY, + "VALUE" VARCHAR(4096) +) diff --git a/api/src/main/resources/migrations/sky/V1__initial_schema.sql b/api/src/main/resources/migrations/sky/V1__initial_schema.sql new file mode 100644 index 000000000..29dad8e21 --- /dev/null +++ b/api/src/main/resources/migrations/sky/V1__initial_schema.sql @@ -0,0 +1,32 @@ +-- NOTE: Why UpperCase? https://www.h2database.com/html/grammar.html?highlight=DATABASE_TO_UPPER&search=DATABASE_TO_UPPER#firstFound + +CREATE TABLE IF NOT EXISTS SATELLITES( + ID BIGINT NOT NULL PRIMARY KEY, + NAME VARCHAR_IGNORECASE(256) NOT NULL, + TLE VARCHAR(4096) NOT NULL, + GROUPS TINYINT ARRAY NOT NULL +); + +CREATE INDEX IF NOT EXISTS IDX_SATELLITES_NAME ON SATELLITES(NAME); + +CREATE TABLE IF NOT EXISTS SKY_OBJECTS( + ID IDENTITY NOT NULL PRIMARY KEY, + NAME VARCHAR_IGNORECASE(256) NOT NULL, + TYPE SMALLINT NOT NULL, + RIGHT_ASCENSION DOUBLE PRECISION NOT NULL, + DECLINATION DOUBLE PRECISION NOT NULL, + MAGNITUDE DOUBLE PRECISION NOT NULL, + PM_RA DOUBLE PRECISION NOT NULL, + PM_DEC DOUBLE PRECISION NOT NULL, + PARALLAX DOUBLE PRECISION NOT NULL, + RADIAL_VEL DOUBLE PRECISION NOT NULL, + REDSHIFT DOUBLE PRECISION NOT NULL, + CONSTELLATION TINYINT NOT NULL +); + +CREATE INDEX IF NOT EXISTS IDX_SKY_OBJECTS_MAGNITUDE ON SKY_OBJECTS(MAGNITUDE); +CREATE INDEX IF NOT EXISTS IDX_SKY_OBJECTS_RIGHT_ASCENSION ON SKY_OBJECTS(RIGHT_ASCENSION); +CREATE INDEX IF NOT EXISTS IDX_SKY_OBJECTS_DECLINATION ON SKY_OBJECTS(DECLINATION); +CREATE INDEX IF NOT EXISTS IDX_SKY_OBJECTS_TYPE ON SKY_OBJECTS(TYPE); +CREATE INDEX IF NOT EXISTS IDX_SKY_OBJECTS_CONSTELLATION ON SKY_OBJECTS(CONSTELLATION); +CREATE INDEX IF NOT EXISTS IDX_SKY_OBJECTS_NAME ON SKY_OBJECTS(NAME); diff --git a/api/src/test/kotlin/CalibrationFrameRepositoryTest.kt b/api/src/test/kotlin/CalibrationFrameRepositoryTest.kt index 602571baa..e38768b4b 100644 --- a/api/src/test/kotlin/CalibrationFrameRepositoryTest.kt +++ b/api/src/test/kotlin/CalibrationFrameRepositoryTest.kt @@ -1,13 +1,16 @@ import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize -import io.objectbox.kotlin.boxFor +import io.kotest.matchers.longs.shouldBeExactly +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe import nebulosa.api.calibration.CalibrationFrameEntity import nebulosa.api.calibration.CalibrationFrameRepository -import nebulosa.api.database.MyObjectBox +import nebulosa.api.database.MainDatabaseMigrator import nebulosa.indi.device.camera.FrameType +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Test -import java.util.* class CalibrationFrameRepositoryTest { @@ -16,6 +19,19 @@ class CalibrationFrameRepositoryTest { REPOSITORY.findAll().shouldHaveSize(17) } + @Test + fun finalAllByGroup() { + REPOSITORY.findAll(NAME).shouldHaveSize(17) + } + + @Test + fun getById() { + with(REPOSITORY[1L].shouldNotBeNull()) { + type shouldBe FrameType.DARK + exposureTime shouldBeExactly 1L + } + } + @Test fun findDarks() { REPOSITORY.darkFrames(NAME, 1280, 1024, 1, 1L, 0.0).shouldHaveSize(1) @@ -52,19 +68,21 @@ class CalibrationFrameRepositoryTest { companion object { private const val NAME = "CCD Simulator" + private const val DATASOURCE = "jdbc:h2:mem:cf;DB_CLOSE_DELAY=-1" - @JvmStatic private val BOX_STORE = MyObjectBox.builder() - .inMemory(UUID.randomUUID().toString()) - .build() + private val CONNECTION = Database.connect(DATASOURCE, user = "root", password = "") @AfterAll @JvmStatic - fun closeBoxStore() { - BOX_STORE.close() + fun closeConnection() { + TransactionManager.closeAndUnregister(CONNECTION) } - @JvmStatic private val BOX = BOX_STORE.boxFor() - @JvmStatic private val REPOSITORY = CalibrationFrameRepository(BOX).apply { + init { + MainDatabaseMigrator(DATASOURCE).run() + } + + private val REPOSITORY = CalibrationFrameRepository(CONNECTION).apply { save(FrameType.DARK, 1L) save(FrameType.DARK, 2L) save(FrameType.DARK, 5L) @@ -84,12 +102,11 @@ class CalibrationFrameRepositoryTest { save(FrameType.FLAT, 0L, filter = null) } - @JvmStatic - internal fun CalibrationFrameRepository.save( + @Suppress("NOTHING_TO_INLINE") + internal inline fun CalibrationFrameRepository.save( type: FrameType, exposureTime: Long, temperature: Double = 25.0, width: Int = 1280, height: Int = 1024, - bin: Int = 1, gain: Double = 0.0, - filter: String? = null, - ) = save(CalibrationFrameEntity(0L, type, NAME, filter, exposureTime, temperature, width, height, bin, bin, gain)) + bin: Int = 1, gain: Double = 0.0, filter: String? = null, + ) = add(CalibrationFrameEntity(0L, type, NAME, filter, exposureTime, temperature, width, height, bin, bin, gain)) } } diff --git a/api/src/test/kotlin/PreferenceRepositoryTest.kt b/api/src/test/kotlin/PreferenceServiceTest.kt similarity index 80% rename from api/src/test/kotlin/PreferenceRepositoryTest.kt rename to api/src/test/kotlin/PreferenceServiceTest.kt index 8e68479e9..8f3c2c859 100644 --- a/api/src/test/kotlin/PreferenceRepositoryTest.kt +++ b/api/src/test/kotlin/PreferenceServiceTest.kt @@ -7,18 +7,17 @@ import io.kotest.matchers.longs.shouldBeExactly import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.objectbox.kotlin.boxFor import nebulosa.api.atlas.Location -import nebulosa.api.database.MyObjectBox -import nebulosa.api.preference.PreferenceEntity +import nebulosa.api.database.MainDatabaseMigrator import nebulosa.api.preference.PreferenceRepository import nebulosa.api.preference.PreferenceService import nebulosa.indi.device.camera.FrameType +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Test -import java.util.* -class PreferenceRepositoryTest { +class PreferenceServiceTest { @Test fun boolean() { @@ -28,7 +27,7 @@ class PreferenceRepositoryTest { SERVICE.getBoolean("b").shouldNotBeNull().shouldBeTrue() SERVICE.putBoolean("b", false) SERVICE.getBoolean("b").shouldNotBeNull().shouldBeFalse() - SERVICE.delete("b") + SERVICE.delete("b").shouldBeTrue() SERVICE.contains("b").shouldBeFalse() SERVICE.getBoolean("b").shouldBeNull() } @@ -39,7 +38,7 @@ class PreferenceRepositoryTest { SERVICE.putInt("i", 22) SERVICE.contains("i").shouldBeTrue() SERVICE.getInt("i").shouldNotBeNull() shouldBeExactly 22 - SERVICE.delete("i") + SERVICE.delete("i").shouldBeTrue() SERVICE.contains("i").shouldBeFalse() SERVICE.getInt("i").shouldBeNull() } @@ -50,7 +49,7 @@ class PreferenceRepositoryTest { SERVICE.putLong("l", 22L) SERVICE.contains("l").shouldBeTrue() SERVICE.getLong("l").shouldNotBeNull() shouldBeExactly 22L - SERVICE.delete("l") + SERVICE.delete("l").shouldBeTrue() SERVICE.contains("l").shouldBeFalse() SERVICE.getLong("l").shouldBeNull() } @@ -61,7 +60,7 @@ class PreferenceRepositoryTest { SERVICE.putDouble("d", 22.0) SERVICE.contains("d").shouldBeTrue() SERVICE.getDouble("d").shouldNotBeNull() shouldBeExactly 22.0 - SERVICE.delete("d") + SERVICE.delete("d").shouldBeTrue() SERVICE.contains("d").shouldBeFalse() SERVICE.getDouble("d").shouldBeNull() } @@ -72,7 +71,7 @@ class PreferenceRepositoryTest { SERVICE.putText("s", "Texto") SERVICE.contains("s").shouldBeTrue() SERVICE.getText("s").shouldNotBeNull() shouldBe "Texto" - SERVICE.delete("s") + SERVICE.delete("s").shouldBeTrue() SERVICE.contains("s").shouldBeFalse() SERVICE.getText("s").shouldBeNull() } @@ -83,7 +82,7 @@ class PreferenceRepositoryTest { SERVICE.putEnum("e", FrameType.DARK) SERVICE.contains("e").shouldBeTrue() SERVICE.getEnum("e").shouldNotBeNull() shouldBe FrameType.DARK - SERVICE.delete("e") + SERVICE.delete("e").shouldBeTrue() SERVICE.contains("e").shouldBeFalse() SERVICE.getEnum("e").shouldBeNull() } @@ -94,7 +93,7 @@ class PreferenceRepositoryTest { SERVICE.putJSON("j", Location(longitude = 123.456)) SERVICE.contains("j").shouldBeTrue() SERVICE.getJSON("j").shouldNotBeNull() shouldBe Location(longitude = 123.456) - SERVICE.delete("j") + SERVICE.delete("j").shouldBeTrue() SERVICE.contains("j").shouldBeFalse() SERVICE.getJSON("j").shouldBeNull() } @@ -108,23 +107,26 @@ class PreferenceRepositoryTest { SERVICE.putJSON("j", Location(longitude = 123.456)) SERVICE.size shouldBeExactly 5 SERVICE.clear() - SERVICE.isEmpty().shouldBeTrue() + SERVICE.size shouldBeExactly 0 } companion object { - @JvmStatic private val BOX_STORE = MyObjectBox.builder() - .inMemory(UUID.randomUUID().toString()) - .build() + private const val DATASOURCE = "jdbc:h2:mem:preference;DB_CLOSE_DELAY=-1" + + private val CONNECTION = Database.connect(DATASOURCE, user = "root", password = "") @AfterAll @JvmStatic - fun closeBoxStore() { - BOX_STORE.close() + fun closeConnection() { + TransactionManager.closeAndUnregister(CONNECTION) + } + + init { + MainDatabaseMigrator(DATASOURCE).run() } - @JvmStatic private val BOX = BOX_STORE.boxFor() - @JvmStatic private val REPOSITORY = PreferenceRepository(BOX) + @JvmStatic private val REPOSITORY = PreferenceRepository(CONNECTION) @JvmStatic private val SERVICE = PreferenceService(REPOSITORY, jsonMapper { }) } } diff --git a/api/src/test/kotlin/SatelliteEntityRepositoryTest.kt b/api/src/test/kotlin/SatelliteEntityRepositoryTest.kt index d667bf606..de573a3b4 100644 --- a/api/src/test/kotlin/SatelliteEntityRepositoryTest.kt +++ b/api/src/test/kotlin/SatelliteEntityRepositoryTest.kt @@ -1,13 +1,14 @@ import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize -import io.objectbox.kotlin.boxFor import nebulosa.api.atlas.SatelliteEntity import nebulosa.api.atlas.SatelliteGroupType import nebulosa.api.atlas.SatelliteRepository -import nebulosa.api.database.MyObjectBox +import nebulosa.api.database.SkyDatabaseMigrator +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Test -import java.util.* +import java.util.concurrent.atomic.AtomicLong class SatelliteEntityRepositoryTest { @@ -26,6 +27,8 @@ class SatelliteEntityRepositoryTest { REPOSITORY.search(groups = listOf(SatelliteGroupType.ACTIVE)).shouldHaveSize(2) REPOSITORY.search(groups = listOf(SatelliteGroupType.STARLINK)).shouldHaveSize(1) REPOSITORY.search(groups = listOf(SatelliteGroupType.AMATEUR)).shouldBeEmpty() + REPOSITORY.search(groups = listOf(SatelliteGroupType.AMATEUR, SatelliteGroupType.STARLINK)).shouldHaveSize(1) + REPOSITORY.search(groups = listOf(SatelliteGroupType.EDUCATION, SatelliteGroupType.STARLINK)).shouldHaveSize(2) } @Test @@ -33,35 +36,40 @@ class SatelliteEntityRepositoryTest { REPOSITORY.search(text = "iss", groups = listOf(SatelliteGroupType.ACTIVE)).shouldHaveSize(1) REPOSITORY.search(text = "iss", groups = listOf(SatelliteGroupType.STARLINK)).shouldBeEmpty() REPOSITORY.search(text = "starlink", groups = listOf(SatelliteGroupType.EDUCATION)).shouldBeEmpty() + REPOSITORY.search(text = "starlink", groups = listOf(SatelliteGroupType.ACTIVE, SatelliteGroupType.STARLINK)).shouldHaveSize(1) } companion object { - @JvmStatic internal val ISS_TLE = """ + internal val ISS_TLE = """ ISS (ZARYA) 1 25544U 98067A 24182.23525622 .00026310 00000+0 46754-3 0 9994 2 25544 51.6392 250.6622 0011086 22.0936 34.8107 15.49934787460571 """.trimIndent() - @JvmStatic private val BOX_STORE = MyObjectBox.builder() - .inMemory(UUID.randomUUID().toString()) - .build() + private const val DATASOURCE = "jdbc:h2:mem:sat;DB_CLOSE_DELAY=-1" + + private val CONNECTION = Database.connect(DATASOURCE, user = "root", password = "") + private val ID = AtomicLong(1) @AfterAll @JvmStatic - fun closeBoxStore() { - BOX_STORE.close() + fun closeConnection() { + TransactionManager.closeAndUnregister(CONNECTION) + } + + init { + SkyDatabaseMigrator(DATASOURCE).run() } - @JvmStatic private val BOX = BOX_STORE.boxFor() - @JvmStatic private val REPOSITORY = SatelliteRepository(BOX).apply { + private val REPOSITORY = SatelliteRepository(CONNECTION).apply { save("ISS (ZARYA)", ISS_TLE, SatelliteGroupType.ACTIVE, SatelliteGroupType.EDUCATION) save("StarLink", "", SatelliteGroupType.ACTIVE, SatelliteGroupType.STARLINK) } - @JvmStatic - internal fun SatelliteRepository.save(name: String, tle: String = "", vararg groups: SatelliteGroupType) { - save(SatelliteEntity(0L, name, tle, groups.map { it.name }.toMutableList())) + @Suppress("NOTHING_TO_INLINE") + internal inline fun SatelliteRepository.save(name: String, tle: String = "", vararg groups: SatelliteGroupType) { + add(SatelliteEntity(ID.getAndIncrement(), name, tle, groups.toList())) } } } diff --git a/api/src/test/kotlin/SkyAtlasServiceTest.kt b/api/src/test/kotlin/SkyAtlasServiceTest.kt index 5be0c2ac8..64fad0f23 100644 --- a/api/src/test/kotlin/SkyAtlasServiceTest.kt +++ b/api/src/test/kotlin/SkyAtlasServiceTest.kt @@ -1,6 +1,6 @@ import SatelliteEntityRepositoryTest.Companion.ISS_TLE import SatelliteEntityRepositoryTest.Companion.save -import SimbadEntityRepositoryTest.Companion.save +import SkyObjectEntityRepositoryTest.Companion.save import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jsonMapper import io.kotest.matchers.collections.shouldContainAll @@ -10,22 +10,22 @@ import io.kotest.matchers.collections.shouldNotBeEmpty import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.doubles.shouldBeExactly import io.kotest.matchers.shouldBe -import io.objectbox.kotlin.boxFor import nebulosa.api.atlas.* import nebulosa.api.atlas.ephemeris.BodyEphemerisProvider import nebulosa.api.atlas.ephemeris.HorizonsEphemerisProvider -import nebulosa.api.database.MyObjectBox +import nebulosa.api.database.SkyDatabaseMigrator import nebulosa.horizons.HorizonsService import nebulosa.math.* import nebulosa.nova.astrometry.Constellation import nebulosa.sbd.SmallBodyDatabaseService import nebulosa.skycatalog.SkyObjectType import nebulosa.test.HTTP_CLIENT +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Test import java.time.LocalDate import java.time.LocalDateTime -import java.util.* import java.util.concurrent.Executors class SkyAtlasServiceTest { @@ -229,14 +229,18 @@ class SkyAtlasServiceTest { companion object { - @JvmStatic private val BOX_STORE = MyObjectBox.builder() - .inMemory(UUID.randomUUID().toString()) - .build() + private const val DATASOURCE = "jdbc:h2:mem:skyatlas;DB_CLOSE_DELAY=-1" + + private val CONNECTION = Database.connect(DATASOURCE, user = "root", password = "") @AfterAll @JvmStatic - fun closeBoxStore() { - BOX_STORE.close() + fun closeConnection() { + TransactionManager.closeAndUnregister(CONNECTION) + } + + init { + SkyDatabaseMigrator(DATASOURCE).run() } @JvmStatic private val THREAD_POOL_TASK_EXECUTOR = Executors.newSingleThreadExecutor() @@ -244,8 +248,6 @@ class SkyAtlasServiceTest { @JvmStatic private val HORIZONS_EPHEMERIS_PROVIDER = HorizonsEphemerisProvider(HORIZONS_SERVICE) @JvmStatic private val BODY_EPHEMERIS_PROVIDER = BodyEphemerisProvider(THREAD_POOL_TASK_EXECUTOR) @JvmStatic private val SMALL_BODY_DATABASE_SERVICE = SmallBodyDatabaseService() - @JvmStatic private val SATELLITE_BOX = BOX_STORE.boxFor() - @JvmStatic private val SIMBAD_BOX = BOX_STORE.boxFor() @JvmStatic private val MOON_PHASE_FINDER = MoonPhaseFinder(HORIZONS_SERVICE) @JvmStatic private val OBJECT_MAPPER = jsonMapper { @@ -253,12 +255,12 @@ class SkyAtlasServiceTest { disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) } - @JvmStatic private val SIMBAD_ENTITY_REPOSITORY = SimbadEntityRepository(SIMBAD_BOX).apply { + @JvmStatic private val SIMBAD_ENTITY_REPOSITORY = SkyObjectEntityRepository(CONNECTION).apply { save("Sirius", SkyObjectType.STAR, Constellation.CMA, -1.45, "06 45 06".hours, "-16 43 33".deg) save("75 Tucanae", SkyObjectType.GLOBULAR_CLUSTER, Constellation.TUC, 6.58, "01 03 12".hours, "-70 50 39".deg) } - @JvmStatic private val SATELLITE_REPOSITORY = SatelliteRepository(SATELLITE_BOX).apply { + @JvmStatic private val SATELLITE_REPOSITORY = SatelliteRepository(CONNECTION).apply { save("ISS (ZARYA)", ISS_TLE, SatelliteGroupType.ACTIVE, SatelliteGroupType.EDUCATION) } diff --git a/api/src/test/kotlin/SimbadDatabaseGenerator.kt b/api/src/test/kotlin/SkyDatabaseGenerator.kt similarity index 91% rename from api/src/test/kotlin/SimbadDatabaseGenerator.kt rename to api/src/test/kotlin/SkyDatabaseGenerator.kt index cb18d9609..e33b5028e 100644 --- a/api/src/test/kotlin/SimbadDatabaseGenerator.kt +++ b/api/src/test/kotlin/SkyDatabaseGenerator.kt @@ -2,8 +2,8 @@ import de.siegmar.fastcsv.reader.CommentStrategy import de.siegmar.fastcsv.reader.CsvReader import de.siegmar.fastcsv.reader.NamedCsvRecord import nebulosa.adql.* -import nebulosa.api.atlas.SimbadDatabaseWriter -import nebulosa.api.atlas.SimbadEntity +import nebulosa.api.atlas.SkyDatabaseWriter +import nebulosa.api.atlas.SkyObjectEntity import nebulosa.io.resource import nebulosa.log.i import nebulosa.log.loggerFor @@ -25,10 +25,10 @@ import kotlin.io.path.createDirectories import kotlin.io.path.deleteRecursively import kotlin.math.min -object SimbadDatabaseGenerator { +object SkyDatabaseGenerator { - @JvmStatic private val SIMBAD_DATABASE_PATH = Path.of("data", "simbad") - @JvmStatic private val LOG = loggerFor() + @JvmStatic private val SKY_DATABASE_PATH = Path.of("data", "sky") + @JvmStatic private val LOG = loggerFor() @JvmStatic private val HTTP_CLIENT = OkHttpClient.Builder() .connectTimeout(5L, TimeUnit.MINUTES) @@ -105,8 +105,8 @@ object SimbadDatabaseGenerator { @JvmStatic private val ENTITY_IDS = ConcurrentHashMap.newKeySet(64000) @JvmStatic - fun SimbadEntity.generateNames(): Boolean { - val ids = name.split("|").toMutableList() + fun SkyObjectEntity.generateNames(): Boolean { + val ids = name.toMutableList() val names = LinkedHashSet(ids.size) val moreNames = LinkedHashSet(6) @@ -151,20 +151,12 @@ object SimbadDatabaseGenerator { } } - name = buildString { - var i = 0 - - names.forEach { - if (i > 0) append(SkyObject.NAME_SEPARATOR) - append(it) - i++ - } + name = ArrayList(names.size + moreNames.size).apply { + addAll(names) moreNames.forEach { n -> if (names.none { it.equals(n, true) }) { - if (i > 0) append(SkyObject.NAME_SEPARATOR) - append(n) - i++ + add(n) } } } @@ -174,10 +166,10 @@ object SimbadDatabaseGenerator { @JvmStatic fun main(args: Array) { - SIMBAD_DATABASE_PATH.deleteRecursively() - SIMBAD_DATABASE_PATH.createDirectories() + SKY_DATABASE_PATH.deleteRecursively() + SKY_DATABASE_PATH.createDirectories() - val tasks = ArrayList>>() + val tasks = ArrayList>>() tasks.add(EXECUTOR_SERVICE.submit(NgcDownloadTask)) tasks.add(EXECUTOR_SERVICE.submit(IcDownloadTask)) @@ -204,8 +196,8 @@ object SimbadDatabaseGenerator { var count = 0 var index = 0 - var writer: SimbadDatabaseWriter? = null - val entities = HashMap() + var writer: SkyDatabaseWriter? = null + val entities = HashMap() for (task in tasks) { with(task.get()) { @@ -216,7 +208,7 @@ object SimbadDatabaseGenerator { for ((_, entity) in entities) { if (writer == null || count > 10000) { writer?.close() - writer = SimbadDatabaseWriter(SIMBAD_DATABASE_PATH.concat("simbad.%02d.dat".format(index++)).sink()) + writer = SkyDatabaseWriter(SKY_DATABASE_PATH.concat("simbad.%02d.dat".format(index++)).sink()) count = 0 } @@ -234,11 +226,11 @@ object SimbadDatabaseGenerator { } @JvmStatic - private fun List.parse(entities: MutableList): List { + private fun List.parse(entities: MutableList): List { var writeCount = 0 for (row in this) { - val name = row.getField("ids") + val name = row.getField("ids").split("|") val id = row.getField("oid").toLong() val type = SkyObjectType.parse(row.getField("otype")) ?: continue // Save using the original units to optmize file size. @@ -263,7 +255,7 @@ object SimbadDatabaseGenerator { magnitude = min(magnitude, row.getField("K").toDoubleOrNull() ?: SkyObject.UNKNOWN_MAGNITUDE) } - val entity = SimbadEntity( + val entity = SkyObjectEntity( id, name, type, rightAscensionJ2000, declinationJ2000, magnitude, pmRA, pmDEC, @@ -284,7 +276,7 @@ object SimbadDatabaseGenerator { private sealed class DownloadTask( private val name: String, private val magnitudeMax: Double = Double.NaN, - ) : Callable> { + ) : Callable> { protected val log by lazy { LoggerFactory.getLogger(javaClass)!! } @@ -314,8 +306,8 @@ object SimbadDatabaseGenerator { add(SortBy(IDENT_TABLE.column("oidref"))) } - override fun call(): List { - val entities = ArrayList() + override fun call(): List { + val entities = ArrayList() var lastID = 0L log.info("Task started") diff --git a/api/src/test/kotlin/SimbadEntityRepositoryTest.kt b/api/src/test/kotlin/SkyObjectEntityRepositoryTest.kt similarity index 68% rename from api/src/test/kotlin/SimbadEntityRepositoryTest.kt rename to api/src/test/kotlin/SkyObjectEntityRepositoryTest.kt index 10df681ce..55b1234bd 100644 --- a/api/src/test/kotlin/SimbadEntityRepositoryTest.kt +++ b/api/src/test/kotlin/SkyObjectEntityRepositoryTest.kt @@ -2,20 +2,20 @@ import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.doubles.shouldBeExactly import io.kotest.matchers.shouldBe -import io.objectbox.kotlin.boxFor -import nebulosa.api.atlas.SimbadEntity -import nebulosa.api.atlas.SimbadEntityRepository -import nebulosa.api.database.MyObjectBox +import nebulosa.api.atlas.SkyObjectEntity +import nebulosa.api.atlas.SkyObjectEntityRepository +import nebulosa.api.database.SkyDatabaseMigrator import nebulosa.math.Angle import nebulosa.math.deg import nebulosa.math.hours import nebulosa.nova.astrometry.Constellation import nebulosa.skycatalog.SkyObjectType +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Test -import java.util.* -class SimbadEntityRepositoryTest { +class SkyObjectEntityRepositoryTest { @Test fun findAll() { @@ -24,7 +24,8 @@ class SimbadEntityRepositoryTest { @Test fun findByName() { - REPOSITORY.search(name = "dolphin").shouldHaveSize(1).first().name shouldBe "Dolphin Nebula" + REPOSITORY.search(name = "dolphin").shouldHaveSize(1).first().name shouldBe listOf("Dolphin Nebula") + REPOSITORY.search(name = "In nE").shouldHaveSize(1).first().name shouldBe listOf("Dolphin Nebula") REPOSITORY.search(name = "andromeda").shouldBeEmpty() REPOSITORY.search(name = "nebula").shouldHaveSize(2).first().magnitude shouldBeExactly 5.0 } @@ -40,15 +41,16 @@ class SimbadEntityRepositoryTest { REPOSITORY.search(rightAscension = "06 45 59".hours, declination = "-20 45 29".deg, radius = 4.5.deg).shouldHaveSize(2) .first().magnitude shouldBeExactly -1.45 REPOSITORY.search(rightAscension = "06 45 59".hours, declination = "-20 45 29".deg, radius = 4.0.deg).shouldHaveSize(1) - .first().name shouldBe "Dolphin Nebula" + .first().name shouldBe listOf("Dolphin Nebula") + REPOSITORY.search(rightAscension = "06 45 59".hours, declination = "-20 45 29".deg, radius = 1.0.deg).shouldBeEmpty() REPOSITORY.search(rightAscension = "00 42 43".hours, declination = "41 15 53".deg, radius = 10.deg).shouldBeEmpty() } @Test fun findByMagnitude() { REPOSITORY.search(magnitudeMin = 5.0).shouldHaveSize(3) - REPOSITORY.search(magnitudeMax = 4.9).shouldHaveSize(1).first().name shouldBe "Sirius" - REPOSITORY.search(magnitudeMin = 6.6, magnitudeMax = 6.99).shouldHaveSize(1).first().name shouldBe "Dolphin Nebula" + REPOSITORY.search(magnitudeMax = 4.9).shouldHaveSize(1).first().name shouldBe listOf("Sirius") + REPOSITORY.search(magnitudeMin = 6.6, magnitudeMax = 6.99).shouldHaveSize(1).first().name shouldBe listOf("Dolphin Nebula") REPOSITORY.search(magnitudeMax = -2.0).shouldBeEmpty() REPOSITORY.search(magnitudeMin = 7.0).shouldBeEmpty() REPOSITORY.search(magnitudeMin = 5.1, magnitudeMax = 6.0).shouldBeEmpty() @@ -62,18 +64,21 @@ class SimbadEntityRepositoryTest { companion object { - @JvmStatic private val BOX_STORE = MyObjectBox.builder() - .inMemory(UUID.randomUUID().toString()) - .build() + private const val DATASOURCE = "jdbc:h2:mem:sky;DB_CLOSE_DELAY=-1" + + private val CONNECTION = Database.connect(DATASOURCE, user = "root", password = "") @AfterAll @JvmStatic - fun closeBoxStore() { - BOX_STORE.close() + fun closeConnection() { + TransactionManager.closeAndUnregister(CONNECTION) + } + + init { + SkyDatabaseMigrator(DATASOURCE).run() } - @JvmStatic private val BOX = BOX_STORE.boxFor() - @JvmStatic private val REPOSITORY = SimbadEntityRepository(BOX).apply { + @JvmStatic private val REPOSITORY = SkyObjectEntityRepository(CONNECTION).apply { save("Sirius", SkyObjectType.STAR, Constellation.CMA, -1.45, "06 45 06".hours, "-16 43 33".deg) save("Dolphin Nebula", SkyObjectType.NEBULA, Constellation.CMA, 6.91, "06 54 11".hours, "-23 55 47".deg) save("75 Tucanae", SkyObjectType.GLOBULAR_CLUSTER, Constellation.TUC, 6.58, "01 03 12".hours, "-70 50 39".deg) @@ -81,11 +86,9 @@ class SimbadEntityRepositoryTest { } @JvmStatic - internal fun SimbadEntityRepository.save( + internal fun SkyObjectEntityRepository.save( name: String, type: SkyObjectType, constellation: Constellation, magnitude: Double, rightAscension: Angle, declination: Angle, - ) { - save(SimbadEntity(0L, name, type, rightAscension, declination, magnitude, constellation = constellation)) - } + ) = add(SkyObjectEntity(0L, listOf(name), type, rightAscension, declination, magnitude, constellation = constellation)) } } diff --git a/build.gradle.kts b/build.gradle.kts index d8b61c18f..e8e5097bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21") - classpath("io.objectbox:objectbox-gradle-plugin:4.0.3") classpath("com.gradleup.shadow:shadow-gradle-plugin:8.3.3") } diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 08a73bc71..2d53caee5 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -74,7 +74,6 @@ 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' import { AlignmentComponent } from './alignment/alignment.component' @@ -155,7 +154,6 @@ import { SettingsComponent } from './settings/settings.component' RotatorComponent, SequencerComponent, SettingsComponent, - SkyObjectPipe, SlideMenuComponent, StopPropagationDirective, WinPipe, @@ -217,7 +215,6 @@ import { SettingsComponent } from './settings/settings.component' EnvPipe, ExposureTimePipe, MessageService, - SkyObjectPipe, provideHttpClient(withInterceptorsFromDi()), WinPipe, { diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index 4e6fb831e..c88559119 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -385,7 +385,7 @@ pTemplate="body" let-item> - {{ item | skyObject: 'name' }} + {{ item.name.join(' · ') }} {{ item.type | enum }} {{ item.magnitude < 30 ? item.magnitude.toFixed(1) : '-' }} {{ item.constellation }} diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 1077494b8..3c12d9fc1 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -547,7 +547,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, protected async skyObjectChanged() { if (this.skyObject.search.selected) { - this.skyObject.name = this.skyObject.search.selected.name.replaceAll('|', ' · ') + this.skyObject.name = this.skyObject.search.selected.name.join(' · ') await this.refreshTab(false, true) } } diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index 4d6a7b1e5..c4fb27c4c 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -41,7 +41,7 @@ fill="#00897B" style="font-size: 5px" class="pointer-events-none select-none"> - {{ a.star ?? a.dso ?? a.minorPlanet | skyObject: 'name' }} + {{ (a.star ?? a.dso ?? a.minorPlanet)?.name?.join(' · ') }} @@ -260,7 +260,7 @@ style="height: 36px" (click)="annotationSelected(item)"> {{ item.star?.type ?? item.dso?.type }} - {{ item.star ?? item.dso ?? item.minorPlanet | skyObject: 'name' }} + {{ (item.star ?? item.dso ?? item.minorPlanet)?.name?.join(' · ') }} @@ -291,7 +291,7 @@ @@ -370,7 +370,7 @@ *ngIf="astronomicalObject.info?.type"> + href="{{ 'https://simbad.cds.unistra.fr/simbad/sim-id?Ident=' + astronomicalObject.info!.name[0] }}"> Simbad diff --git a/desktop/src/shared/pipes/skyObject.pipe.ts b/desktop/src/shared/pipes/skyObject.pipe.ts deleted file mode 100644 index 98f2693cf..000000000 --- a/desktop/src/shared/pipes/skyObject.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { AstronomicalObject } from '../types/atlas.types' - -export type SkyObjectPart = 'name' | 'firstName' - -@Pipe({ name: 'skyObject' }) -export class SkyObjectPipe implements PipeTransform { - transform(value: AstronomicalObject | undefined, what: SkyObjectPart) { - switch (what) { - case 'name': - return value?.name.replaceAll('|', ' · ') - case 'firstName': - return value?.name.split(/\[([^\]]+)\]/g).find(Boolean) - default: - return `${value}` - } - } -} diff --git a/desktop/src/shared/types/atlas.types.ts b/desktop/src/shared/types/atlas.types.ts index bbb43b438..7cdb1e11f 100644 --- a/desktop/src/shared/types/atlas.types.ts +++ b/desktop/src/shared/types/atlas.types.ts @@ -254,7 +254,7 @@ export interface MoonPhase { export interface AstronomicalObject extends EquatorialCoordinateJ2000 { id: number - name: string + name: string[] magnitude: number } @@ -891,5 +891,5 @@ export function skyAtlasPreferenceWithDefault(preference?: Partial e.toLocaleUpperCase().includes(text)) >= 0 || (!!o.type && o.type.includes(text)) || (!!o.constellation && o.constellation === text) } diff --git a/desktop/src/shared/utils/comparators.ts b/desktop/src/shared/utils/comparators.ts index 06a3af46e..25d187b8b 100644 --- a/desktop/src/shared/utils/comparators.ts +++ b/desktop/src/shared/utils/comparators.ts @@ -2,7 +2,7 @@ import type { Device } from '../types/device.types' export type Comparator = (a: T, b: T) => number -export const textComparator: Comparator = (a: string, b: string) => a.localeCompare(b) +export const textComparator: Comparator = (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: 'base' }) export const numberComparator: Comparator = (a: number, b: number) => a - b export const deviceComparator: Comparator = (a: Device, b: Device) => textComparator(a.name, b.name) export const numericTextComparator: Comparator = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }) diff --git a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt index e967497e2..3a81cc346 100644 --- a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt +++ b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt @@ -13,7 +13,7 @@ import nebulosa.time.InstantOfTime data class SimbadEntry( override var id: Long = 0L, - override var name: String = "", + override val name: List = emptyList(), override var magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, override var rightAscensionJ2000: Angle = 0.0, override var declinationJ2000: Angle = 0.0, diff --git a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt index 4c196839f..d5bfed91e 100644 --- a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt +++ b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadService.kt @@ -95,7 +95,7 @@ class SimbadService( val constellation = SkyObject.constellationFor(rightAscensionJ2000, declinationJ2000) val entity = SimbadEntry( - id, name.joinToString("|"), magnitude, + id, name, magnitude, rightAscensionJ2000, declinationJ2000, type, spType, majorAxis, minorAxis, orientation, pmRA, pmDEC, parallax.mas, radialVelocity, redshift, diff --git a/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt b/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt index 98cb77fbf..6a240b333 100644 --- a/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt +++ b/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygDatabase.kt @@ -9,7 +9,6 @@ import nebulosa.math.mas import nebulosa.nova.astrometry.Constellation import nebulosa.skycatalog.SkyCatalog import nebulosa.skycatalog.SkyObject -import nebulosa.skycatalog.SkyObject.Companion.NAME_SEPARATOR import java.io.InputStream import java.io.InputStreamReader @@ -62,7 +61,7 @@ class HygDatabase : SkyCatalog(118005) { if (names.isEmpty()) continue val star = HygEntry( - id, names.joinToString(NAME_SEPARATOR).trim(), + id, names, magnitude, rightAscension, declination, spType, pmRA, pmDEC, diff --git a/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygEntry.kt b/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygEntry.kt index 4422d0f24..6c4d8bd9c 100644 --- a/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygEntry.kt +++ b/nebulosa-skycatalog-hyg/src/main/kotlin/nebulosa/skycatalog/hyg/HygEntry.kt @@ -10,7 +10,7 @@ import nebulosa.skycatalog.SpectralSkyObject data class HygEntry( override val id: Long = 0L, - override val name: String = "", + override val name: List = emptyList(), override val magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, override val rightAscensionJ2000: Angle = 0.0, override val declinationJ2000: Angle = 0.0, diff --git a/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoCatalog.kt b/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoCatalog.kt index df14d2164..d0413e404 100644 --- a/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoCatalog.kt +++ b/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoCatalog.kt @@ -41,7 +41,7 @@ class SaoCatalog : SkyCatalog(258997) { val (rightAscensionJ2000, declinationJ2000) = icrf.equatorialAtDate() val star = SaoEntry( - id, "SAO $id", + id, listOf("SAO $id"), magnitude, rightAscensionJ2000, declinationJ2000, spType, diff --git a/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoEntry.kt b/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoEntry.kt index 0c965a0bb..98feed651 100644 --- a/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoEntry.kt +++ b/nebulosa-skycatalog-sao/src/main/kotlin/nebulosa/skycatalog/sao/SaoEntry.kt @@ -10,7 +10,7 @@ import nebulosa.skycatalog.SpectralSkyObject data class SaoEntry( override val id: Long = 0L, - override val name: String = "", + override val name: List = emptyList(), override val magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, override val rightAscensionJ2000: Angle = 0.0, override val declinationJ2000: Angle = 0.0, diff --git a/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt b/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt index 7969817ad..eb4876ba8 100644 --- a/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt +++ b/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/Nebula.kt @@ -6,7 +6,6 @@ import nebulosa.math.mas import nebulosa.math.rad import nebulosa.skycatalog.SkyCatalog import nebulosa.skycatalog.SkyObject -import nebulosa.skycatalog.SkyObject.Companion.NAME_SEPARATOR import okio.BufferedSource import okio.Source import okio.buffer @@ -113,8 +112,7 @@ class Nebula : SkyCatalog(94661) { if (vdbha > 0) "VdBHA $vdbha".findNames() val nebula = NebulaEntry( - id, - names.joinToString(NAME_SEPARATOR).trim(), + id, names, ra, dec, min(mB, mV), NebulaType.entries[type].type, majorAxis, minorAxis, orientation, diff --git a/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/NebulaEntry.kt b/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/NebulaEntry.kt index 517824f7b..ca7cd7f90 100644 --- a/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/NebulaEntry.kt +++ b/nebulosa-skycatalog-stellarium/src/main/kotlin/nebulosa/skycatalog/stellarium/NebulaEntry.kt @@ -10,7 +10,7 @@ import nebulosa.skycatalog.SkyObjectType data class NebulaEntry( override val id: Long = 0L, - override val name: String = "", + override val name: List = emptyList(), override val rightAscensionJ2000: Angle = 0.0, override val declinationJ2000: Angle = 0.0, override val magnitude: Double = SkyObject.UNKNOWN_MAGNITUDE, diff --git a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyCatalog.kt b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyCatalog.kt index 66b6f0157..c9e694378 100644 --- a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyCatalog.kt +++ b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyCatalog.kt @@ -11,7 +11,7 @@ abstract class SkyCatalog(estimatedSize: Int = 0) : Collection private val data = ArrayList(max(32, estimatedSize)) fun withText(text: String): List { - return data.filter { it.name.contains(text, true) } + return data.filter { e -> e.name.any { it.contains(text, true) } } } fun searchAround(rightAscension: Angle, declination: Angle, limitFOV: Angle): List { diff --git a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt index 124dc2a4f..11ee528b2 100644 --- a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt +++ b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/SkyObject.kt @@ -11,7 +11,7 @@ interface SkyObject { val id: Long - val name: String + val name: List val magnitude: Double @@ -24,7 +24,6 @@ interface SkyObject { const val UNKNOWN_MAGNITUDE = 30.0 const val MAGNITUDE_MIN = -UNKNOWN_MAGNITUDE const val MAGNITUDE_MAX = UNKNOWN_MAGNITUDE - const val NAME_SEPARATOR = "|" @JvmStatic val MAGNITUDE_RANGE = MAGNITUDE_MIN..MAGNITUDE_MAX diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f7837ecd..0805f61a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,11 +38,16 @@ dependencyResolutionManagement { library("javalin", "io.javalin:javalin:6.3.0") library("koin", "io.insert-koin:koin-core:4.0.0") library("airline", "com.github.rvesse:airline:3.0.0") + library("h2", "com.h2database:h2:2.3.232") + library("flyway", "org.flywaydb:flyway-core:10.20.0") + library("exposed-core", "org.jetbrains.exposed:exposed-core:0.55.0") + library("exposed-jdbc", "org.jetbrains.exposed:exposed-jdbc:0.55.0") library("kotest", "io.kotest:kotest-assertions-core:5.9.1") library("junit-api", "org.junit.jupiter:junit-jupiter-api:5.11.2") library("junit-engine", "org.junit.jupiter:junit-jupiter-engine:5.11.2") bundle("netty", listOf("netty-transport", "netty-codec")) bundle("jackson", listOf("jackson-core", "jackson-jsr310", "jackson-kt")) + bundle("exposed", listOf("exposed-core", "exposed-jdbc")) } } }