Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagohm committed Aug 22, 2024
2 parents 7f40a0c + ba84104 commit 71e794f
Show file tree
Hide file tree
Showing 33 changed files with 8,573 additions and 8,647 deletions.
8 changes: 8 additions & 0 deletions api/src/main/kotlin/nebulosa/api/atlas/MoonPhaseDateTime.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nebulosa.api.atlas

import java.time.LocalDateTime

data class MoonPhaseDateTime(
@JvmField val dateTime: LocalDateTime = LocalDateTime.MIN,
@JvmField val name: MoonPhaseName = MoonPhaseName.NEW_MOON,
)
88 changes: 88 additions & 0 deletions api/src/main/kotlin/nebulosa/api/atlas/MoonPhaseFinder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package nebulosa.api.atlas

import nebulosa.constants.PIOVERTWO
import nebulosa.horizons.HorizonsElement
import nebulosa.horizons.HorizonsQuantity
import nebulosa.horizons.HorizonsService
import nebulosa.horizons.ObservingSite
import nebulosa.math.Angle
import nebulosa.math.Distance
import nebulosa.math.deg
import nebulosa.nova.almanac.computeDiffAndReduceToIndices
import org.springframework.stereotype.Component
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.temporal.TemporalAdjusters
import kotlin.math.floor

@Component
class MoonPhaseFinder(private val horizonsService: HorizonsService) {

fun find(
date: LocalDate,
longitude: Angle, latitude: Angle, elevation: Distance = 0.0,
offsetInMinutes: Long = 0L,
): List<MoonPhaseDateTime> {
return find(date, ObservingSite.Geographic(longitude, latitude, elevation), offsetInMinutes)
}

fun find(date: LocalDate, offsetInMinutes: Long = 0L): List<MoonPhaseDateTime> {
return find(date, ObservingSite.Geocentric.EARTH, offsetInMinutes)
}

private fun find(date: LocalDate, site: ObservingSite, offsetInMinutes: Long = 0L): List<MoonPhaseDateTime> {
val startTime = LocalDateTime.of(date.withDayOfMonth(1), LocalTime.MIN).minusMinutes(offsetInMinutes)
val endTime = LocalDateTime.of(date.with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX).minusMinutes(offsetInMinutes)
return compute(60, startTime, endTime, site, offsetInMinutes)
}

private fun compute(
stepSizeInMinutes: Int,
startTime: LocalDateTime, endTime: LocalDateTime,
site: ObservingSite, offsetInMinutes: Long,
): List<MoonPhaseDateTime> {
val sun = horizonsService.observer(
"10", site,
startTime, endTime,
stepSizeInMinutes,
extraPrecision = false,
quantities = QUANTITIES,
).execute().body()!!

val moon = horizonsService.observer(
"301", site,
startTime, endTime,
stepSizeInMinutes,
extraPrecision = false,
quantities = QUANTITIES,
).execute().body()!!

val phases = IntArray(sun.size) { floor((moon[it].eclipticLongitude() - sun[it].eclipticLongitude()) / PIOVERTWO).mod(4.0).toInt() }
val indices = phases.computeDiffAndReduceToIndices()

if (stepSizeInMinutes == 1) {
return indices.map { MoonPhaseDateTime(moon[it].dateTime.plusMinutes(offsetInMinutes), MoonPhaseName.entries[phases[it + 1]]) }
} else {
val res = ArrayList<MoonPhaseDateTime>(5)

for (i in indices) {
val dateTime = moon[i].dateTime
val a = dateTime.minusMinutes(stepSizeInMinutes.toLong())
val b = dateTime.plusMinutes(stepSizeInMinutes.toLong())
res.addAll(compute(1, a, b, site, offsetInMinutes))
}

return res
}
}

companion object {

@JvmStatic private val QUANTITIES = arrayOf(HorizonsQuantity.OBSERVER_ECLIPTIC_LONGITUDE)

private fun HorizonsElement.eclipticLongitude(): Double {
return this[HorizonsQuantity.OBSERVER_ECLIPTIC_LONGITUDE]!!.toDouble().deg
}
}
}
8 changes: 8 additions & 0 deletions api/src/main/kotlin/nebulosa/api/atlas/MoonPhaseName.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nebulosa.api.atlas

enum class MoonPhaseName {
NEW_MOON,
FIRST_QUARTER,
FULL_MOON,
LAST_QUARTER,
}
24 changes: 12 additions & 12 deletions api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ class SkyAtlasController(
@GetMapping("sun/altitude-points")
fun altitudePointsOfSun(
@LocationParam location: Location,
@DateAndTimeParam date: LocalDate,
@DateAndTimeParam dateTime: LocalDateTime,
@RequestParam(required = false, defaultValue = "1") @Valid @Min(1) stepSize: Int,
@RequestParam(required = false, defaultValue = "false") fast: Boolean,
) = skyAtlasService.altitudePointsOfSun(location, date, stepSize, fast)
) = skyAtlasService.altitudePointsOfSun(location, dateTime, stepSize, fast)

@GetMapping("moon/position")
fun positionOfMoon(
Expand All @@ -53,10 +53,10 @@ class SkyAtlasController(
@GetMapping("moon/altitude-points")
fun altitudePointsOfMoon(
@LocationParam location: Location,
@DateAndTimeParam date: LocalDate,
@DateAndTimeParam dateTime: LocalDateTime,
@RequestParam(required = false, defaultValue = "1") @Valid @Min(1) stepSize: Int,
@RequestParam(required = false, defaultValue = "false") fast: Boolean,
) = skyAtlasService.altitudePointsOfMoon(location, date, stepSize, fast)
) = skyAtlasService.altitudePointsOfMoon(location, dateTime, stepSize, fast)

@GetMapping("planets/{code}/position")
fun positionOfPlanet(
Expand All @@ -70,10 +70,10 @@ class SkyAtlasController(
fun altitudePointsOfPlanet(
@PathVariable code: String,
@LocationParam location: Location,
@DateAndTimeParam date: LocalDate,
@DateAndTimeParam dateTime: LocalDateTime,
@RequestParam(required = false, defaultValue = "1") @Valid @Min(1) stepSize: Int,
@RequestParam(required = false, defaultValue = "false") fast: Boolean,
) = skyAtlasService.altitudePointsOfPlanet(location, code, date, stepSize, fast)
) = skyAtlasService.altitudePointsOfPlanet(location, code, dateTime, stepSize, fast)

@GetMapping("minor-planets")
fun searchMinorPlanet(@RequestParam @Valid @NotBlank text: String) = skyAtlasService.searchMinorPlanet(text)
Expand All @@ -96,9 +96,9 @@ class SkyAtlasController(
fun altitudePointsOfSkyObject(
@PathVariable id: Long,
@LocationParam location: Location,
@DateAndTimeParam date: LocalDate,
@DateAndTimeParam dateTime: LocalDateTime,
@RequestParam(required = false, defaultValue = "1") @Valid @Min(1) stepSize: Int,
) = skyAtlasService.altitudePointsOfSkyObject(location, id, date, stepSize)
) = skyAtlasService.altitudePointsOfSkyObject(location, id, dateTime, stepSize)

@GetMapping("sky-objects")
fun searchSkyObject(
Expand Down Expand Up @@ -130,9 +130,9 @@ class SkyAtlasController(
fun altitudePointsOfSatellite(
satellite: SatelliteEntity,
@LocationParam location: Location,
@DateAndTimeParam date: LocalDate,
@DateAndTimeParam dateTime: LocalDateTime,
@RequestParam(required = false, defaultValue = "1") @Valid @Min(1) stepSize: Int,
) = skyAtlasService.altitudePointsOfSatellite(location, satellite, date, stepSize)
) = skyAtlasService.altitudePointsOfSatellite(location, satellite, dateTime, stepSize)

@GetMapping("satellites")
fun searchSatellites(
Expand All @@ -144,9 +144,9 @@ class SkyAtlasController(
@GetMapping("twilight")
fun twilight(
@LocationParam location: Location,
@DateAndTimeParam date: LocalDate,
@DateAndTimeParam dateTime: LocalDateTime,
@RequestParam(required = false, defaultValue = "false") fast: Boolean,
) = skyAtlasService.twilight(location, date, fast)
) = skyAtlasService.twilight(location, dateTime, fast)

@GetMapping("moon/phase")
fun moonPhase(
Expand Down
82 changes: 48 additions & 34 deletions api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import nebulosa.api.atlas.ephemeris.HorizonsEphemerisProvider
import nebulosa.horizons.HorizonsElement
import nebulosa.horizons.HorizonsQuantity
import nebulosa.math.Angle
import nebulosa.math.evenlySpacedNumbers
import nebulosa.math.toLightYears
import nebulosa.math.toMas
import nebulosa.nova.almanac.findDiscrete
Expand Down Expand Up @@ -51,11 +52,13 @@ class SkyAtlasService(
private val simbadEntityRepository: SimbadEntityRepository,
private val httpClient: OkHttpClient,
private val objectMapper: ObjectMapper,
private val moonPhaseFinder: MoonPhaseFinder,
) {

private val positions = HashMap<GeographicCoordinate, GeographicPosition>()
private val cachedSimbadEntities = HashMap<Long, SimbadEntity>()
private val targetLocks = HashMap<Any, Any>()
private val cachedMoonPhases = HashMap<LocalDate, List<MoonPhaseDateTime>>()

@Volatile private var sunImage = ByteArray(0)
@Volatile private var moonPhase: Pair<LocalDateTime, MoonPhase>? = null
Expand Down Expand Up @@ -121,7 +124,7 @@ class SkyAtlasService(
return satelliteRepository.search(text.ifBlank { null }, groups, id)
}

fun twilight(location: GeographicCoordinate, date: LocalDate, fast: Boolean = false): Twilight {
fun twilight(location: GeographicCoordinate, dateTime: LocalDateTime, fast: Boolean = false): Twilight {
val civilDusk = doubleArrayOf(0.0, 0.0)
val nauticalDusk = doubleArrayOf(0.0, 0.0)
val astronomicalDusk = doubleArrayOf(0.0, 0.0)
Expand All @@ -130,59 +133,60 @@ class SkyAtlasService(
val nauticalDawn = doubleArrayOf(0.0, 0.0)
val civilDawn = doubleArrayOf(0.0, 0.0)

val ephemeris = bodyEphemeris(if (fast) VSOP87E.SUN else SUN, location, LocalDateTime.of(date, LocalTime.now(SystemClock)), true)
val (a) = findDiscrete(0.0, (ephemeris.size - 1).toDouble(), TwilightDiscreteFunction(ephemeris), 1.0)

civilDusk[0] = a[0] / 60.0
civilDusk[1] = a[1] / 60.0
nauticalDusk[0] = a[1] / 60.0
nauticalDusk[1] = a[2] / 60.0
astronomicalDusk[0] = a[2] / 60.0
astronomicalDusk[1] = a[3] / 60.0
night[0] = a[3] / 60.0
night[1] = a[4] / 60.0
astronomicalDawn[0] = a[4] / 60.0
astronomicalDawn[1] = a[5] / 60.0
nauticalDawn[0] = a[5] / 60.0
nauticalDawn[1] = a[6] / 60.0
civilDawn[0] = a[6] / 60.0
civilDawn[1] = a[7] / 60.0
val ephemeris = bodyEphemeris(if (fast) VSOP87E.SUN else SUN, location, dateTime, true)
val range = evenlySpacedNumbers(0.0, (ephemeris.size - 1).toDouble(), ephemeris.size)
val result = findDiscrete(range, TwilightDiscreteFunction(ephemeris), 1.0)

civilDusk[0] = result.x(0) / 60.0
civilDusk[1] = result.x(1) / 60.0
nauticalDusk[0] = result.x(1) / 60.0
nauticalDusk[1] = result.x(2) / 60.0
astronomicalDusk[0] = result.x(2) / 60.0
astronomicalDusk[1] = result.x(3) / 60.0
night[0] = result.x(3) / 60.0
night[1] = result.x(4) / 60.0
astronomicalDawn[0] = result.x(4) / 60.0
astronomicalDawn[1] = result.x(5) / 60.0
nauticalDawn[0] = result.x(5) / 60.0
nauticalDawn[1] = result.x(6) / 60.0
civilDawn[0] = result.x(6) / 60.0
civilDawn[1] = result.x(7) / 60.0

return Twilight(
civilDusk, nauticalDusk, astronomicalDusk, night,
astronomicalDawn, nauticalDawn, civilDawn,
)
}

fun altitudePointsOfSun(location: GeographicCoordinate, date: LocalDate, stepSize: Int, fast: Boolean = false): List<DoubleArray> {
val ephemeris = bodyEphemeris(if (fast) VSOP87E.SUN else SUN, location, LocalDateTime.of(date, LocalTime.now(SystemClock)), true)
fun altitudePointsOfSun(location: GeographicCoordinate, dateTime: LocalDateTime, stepSize: Int, fast: Boolean = false): List<DoubleArray> {
val ephemeris = bodyEphemeris(if (fast) VSOP87E.SUN else SUN, location, dateTime, true)
return altitudePointsOfBody(ephemeris, stepSize)
}

fun altitudePointsOfMoon(location: GeographicCoordinate, date: LocalDate, stepSize: Int, fast: Boolean = false): List<DoubleArray> {
val ephemeris = bodyEphemeris(if (fast) FAST_MOON else MOON, location, LocalDateTime.of(date, LocalTime.now(SystemClock)), true)
fun altitudePointsOfMoon(location: GeographicCoordinate, dateTime: LocalDateTime, stepSize: Int, fast: Boolean = false): List<DoubleArray> {
val ephemeris = bodyEphemeris(if (fast) FAST_MOON else MOON, location, dateTime, true)
return altitudePointsOfBody(ephemeris, stepSize)
}

fun altitudePointsOfPlanet(
location: GeographicCoordinate, code: String, date: LocalDate,
location: GeographicCoordinate, code: String, dateTime: LocalDateTime,
stepSize: Int, fast: Boolean = false
): List<DoubleArray> {
val target: Any = VSOP87E.entries.takeIf { fast }?.find { "${it.target}" == code } ?: code
val ephemeris = bodyEphemeris(target, location, LocalDateTime.of(date, LocalTime.now(SystemClock)), true)
val ephemeris = bodyEphemeris(target, location, dateTime, true)
return altitudePointsOfBody(ephemeris, stepSize)
}

fun altitudePointsOfSkyObject(location: GeographicCoordinate, id: Long, date: LocalDate, stepSize: Int): List<DoubleArray> {
fun altitudePointsOfSkyObject(location: GeographicCoordinate, id: Long, dateTime: LocalDateTime, stepSize: Int): List<DoubleArray> {
val target = cachedSimbadEntities[id] ?: simbadEntityRepository.find(id)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Cannot found sky object: [$id]")
cachedSimbadEntities[id] = target
val ephemeris = bodyEphemeris(target, location, LocalDateTime.of(date, LocalTime.now(SystemClock)), true)
val ephemeris = bodyEphemeris(target, location, dateTime, true)
return altitudePointsOfBody(ephemeris, stepSize)
}

fun altitudePointsOfSatellite(location: GeographicCoordinate, satellite: SatelliteEntity, date: LocalDate, stepSize: Int): List<DoubleArray> {
val ephemeris = bodyEphemeris("TLE@${satellite.tle}", location, LocalDateTime.of(date, LocalTime.now(SystemClock)), true)
fun altitudePointsOfSatellite(location: GeographicCoordinate, satellite: SatelliteEntity, dateTime: LocalDateTime, stepSize: Int): List<DoubleArray> {
val ephemeris = bodyEphemeris("TLE@${satellite.tle}", location, dateTime, true)
return altitudePointsOfBody(ephemeris, stepSize)
}

Expand Down Expand Up @@ -232,10 +236,9 @@ class SkyAtlasService(
sunImage = bytes.toByteArray()
}

fun moonPhase(location: GeographicCoordinate, dateTime: LocalDateTime): MoonPhase? {
val now = LocalDateTime.now(SystemClock)

if (moonPhase == null || abs(ChronoUnit.HOURS.between(moonPhase!!.first, now)) >= 1) {
@Synchronized
fun moonPhase(location: GeographicCoordinate, dateTime: LocalDateTime): Map<String, Any?>? {
if (moonPhase == null || abs(ChronoUnit.HOURS.between(moonPhase!!.first, dateTime)) >= 1) {
val request = Request.Builder()
.url(MOON_PHASE_URL.format(dateTime.minusMinutes(location.offsetInMinutes().toLong()).format(MOON_PHASE_DATE_TIME_FORMAT)))
.build()
Expand All @@ -248,10 +251,21 @@ class SkyAtlasService(
null
}

moonPhase = now.withMinute(0).withSecond(0).withNano(0) to (body ?: return null)
moonPhase = dateTime.withMinute(0).withSecond(0).withNano(0) to (body ?: return null)
}

return moonPhase?.second
val date = dateTime.toLocalDate().withDayOfMonth(1)
val phases = if (date in cachedMoonPhases) {
cachedMoonPhases[date]!!
} else {
val offsetInMinutes = location.offsetInMinutes().toLong()
moonPhaseFinder.find(date, offsetInMinutes).also { cachedMoonPhases[date] = it }
}

return mapOf(
"current" to moonPhase?.second,
"phases" to phases,
)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ data class TwilightDiscreteFunction(private val ephemeris: List<HorizonsElement>

private val cached = DoubleArray(ephemeris.size) { Double.NaN }

override val stepSize = 1.0

override fun compute(x: Double): Int {
override fun invoke(x: Double): Int {
val index = x.toInt()

val altitude = if (cached[index].isNaN()) ephemeris[index].asDouble(HorizonsQuantity.APPARENT_ALT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import nebulosa.horizons.HorizonsElement
import nebulosa.horizons.HorizonsQuantity
import nebulosa.horizons.HorizonsService
import nebulosa.horizons.NonUniqueObjectException
import nebulosa.horizons.ObservingSite
import nebulosa.log.loggerFor
import nebulosa.nova.position.GeographicPosition
import nebulosa.sbd.SmallBody
Expand Down Expand Up @@ -61,7 +62,7 @@ class HorizonsEphemerisProvider(private val horizonsService: HorizonsService) :
horizonsService
.observer(
target,
position.longitude, position.latitude, position.elevation,
ObservingSite.Geographic(position.longitude, position.latitude, position.elevation),
startTime, endTime,
extraPrecision = true,
quantities = QUANTITIES,
Expand All @@ -72,7 +73,7 @@ class HorizonsEphemerisProvider(private val horizonsService: HorizonsService) :
horizonsService
.observer(
"$target;CAP;NOFRAG".replace(";;", ";"),
position.longitude, position.latitude, position.elevation,
ObservingSite.Geographic(position.longitude, position.latitude, position.elevation),
startTime, endTime,
extraPrecision = true,
quantities = QUANTITIES,
Expand Down
11 changes: 9 additions & 2 deletions api/src/main/kotlin/nebulosa/api/image/ImageService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,17 @@ class ImageService(
}

fun saveImageAs(path: Path, save: SaveImage, camera: Camera?) {
val (image) = imageBucket.open(path).image?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE)
require(save.path != null)

var (image) = imageBucket.open(path).image?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")

require(save.path != null)
var (x, y, width, height) = save.subFrame.constrained(image.width, image.height)

if (width > 0 && height > 0 && (x > 0 || y > 0 || width != image.width || height != image.height)) {
LOG.debug { "image subframed. x=$x, y=$y, width=$width, height=$height" }
image = image.transform(SubFrame(x, y, width, height))
}

val modifier = ImageModifier
.bitpix(save.bitpix)
Expand Down
Loading

0 comments on commit 71e794f

Please sign in to comment.