Kastro is a Kotlin multiplatform library for calculating astronomical events for the Moon and Sun (Luna and Sol). This includes events like Sunset, Sunrise, Noon, Nadir, and so many more. What makes Kastro special is the lazily evaluated Sequence
-based implementation, which allows you to use the data in powerful ways. Kastro builds on the work of Richard Körber (shred) in his library commons-suncalc. Much of the math in Kastro comes from commons-suncalc but was ported to common Kotlin.
Note
For a pure Java API be sure to check out Richard Körber's project!.
Like commons-suncalc, this library strives on getting "close enough" without using a lot of computational resources. Most calculations should be accurate to within a minute. Moon phase events may be off by as much as five minutes.
https://yoxjames.github.io/kastro-demo/
This is a static site which embeds Kastro as a Javascript library. A simple GUI has been created to allow you to easily try Kastro in your browser.
This project is deployed on Maven Central. Coordinates are as follows:
groupId: dev.jamesyox
artifactId: kastro
version: 0.3.0
If you use Gradle you should be able to add the following to your dependencies to use Kastro:
implementation("dev.jamesyox:kastro:0.3.0")
You can calculate the following solar phases:
Phase | Description |
---|---|
Astronomical twlight | During dawn, the sun is increaing in brightness and transitioning from night toward nautical twilight. During dusk, the sun is decreasing in brightness and is transitioning from nautical twilight toward night |
Blue hour | Happens during both dawn and dusk, when the sun is below the horizon, causing the light to look mostly blue. |
Civil twilight | During dawn, the sun is increasing in brightness and is transitioning from nautical twilight toward day. During dusk, the sun is decreasing in brightness and transitioning from day toward nautical dusk |
Day | The sun is above the horizon |
Golden hour | Happens during both dawn and dusk, when the sun is close to the horizon, causing the light to look golden. Ideal time to take pictures. |
Nadir | Opposite of noon. The sun is at its lowest point below the horizon |
Nautical twilight | During dawn, the sun is increasing in brightness and is transitioning from night toward civil twilight. During dusk, the sun is decreasing in brightness and is transitioning from civil twilight toward astronomical twilight |
Night | The sun is below the horizon |
Noon | Opposite of nadir. The sun is at its highest point above the horizon |
Sunrise | The sun's top edge first rises above the horizon |
Sunset | The sun's top edge completely disappears below the horizon |
You can calculate the following lunar phases:
Phase | Description |
---|---|
First quarter | Moon is increasing in brightness and is transitioning toward full moon |
Full moon | Moon is fully illuminated |
Last quarter | Moon is decreasing in brightness and is transitioning toward new moon |
New moon | Moon is not illuminated |
Waning crescent | The moon is decreasing in brightness and is transitioning between last quarter and new moon |
Waning gibbous | The moon is decreasing in brightness and is transitioning between full moon and last quarter |
Waxing crescent | The moon is increasing in brightness and is transitioning between new moon and first quarter |
Waxing gibbous | The moon is increasing in brightness and is transitioning between first quarter and full moon |
Kastro leverages Kotlin Sequences to lazily evaluate the algorithms contained within so you can determine anything in a single call, from what time the sun will rise tomorrow, to every sunrise time for the next 10 years. with a single call.
See ReadmeExamples.kt
for a compiled full text version of all of the examples.
Warning
first() will throw if your sequence is empty! Kastro can return empty sequences. For example if you set a limit for 1 minute there may not be any solar events in that timeframe and an empty Sequence
will be returned.
This use case uses Sequence
's first()
method. Start can be any Instant
in the past or future.
requestedSolarEvents
is an optional field that can make calculations more efficient however you are free
to use any Sequence
methods to achieve the same results.
val nextSunset = SolarEventSequence(
start = clock.now(),
latitude = 39.7348,
longitude = -104.9653,
requestedSolarEvents = listOf(SolarEvent.Sunset) // Not required but makes calculations more efficient
).first() // This example is safe, but first() can throw on empty sequences!
This use case uses the reverse
parameter to return a Sequence
that goes backwards in time. All sequences in Kastro support going in reverse chronological order!
val lastSunset = SolarEventSequence(
start = clock.now(),
latitude = latitude,
longitude = longitude,
requestedSolarEvents = listOf(SolarEvent.Sunset), // Not required but makes calculations more efficient,
reverse = true // Sequence goes backwards in time
).first() // This example is safe, but first() can throw on empty sequences!
val solarNoon = SolarEventSequence(
start = LocalDate(2023, 12, 31).atStartOfDayIn(timeZone),
latitude = 39.7348,
longitude = -104.9653,
requestedSolarEvents = listOf(SolarEvent.Noon)
).first() // This example is safe, but first() can throw on empty sequences!
val fullWeek = SolarEventSequence(
start = clock.now(),
latitude = 39.7348,
longitude = -104.9653,
requestedSolarEvents = listOf(SolarEvent.Sunrise, SolarEvent.Sunset),
limit = 7.days
).toList()
val goldenHour = SolarEventSequence(
start = clock.now(),
latitude = 39.7348,
longitude = -104.9653,
requestedSolarEvents = listOf(SolarEvent.GoldenHourDusk, SolarEvent.GoldenHourDusk)
).firstOrNull()
Note
SolarEvent.GoldenHourDusk
and SolarEvent.GoldenHourDawn
are not added by default and must be included in requestedSolarEvents
if you want that information.
val sunState = clock.now().calculateSolarState(
latitude = 39.7348,
longitude = -104.9653,
)
val tuesdaySunrises = SolarEventSequence(
start = clock.now(),
latitude = 39.7348,
longitude = -104.9653,
requestedSolarEvents = listOf(SolarEvent.Sunrise),
limit = 365.days // Omitting leap year shenanigans
).filter {
it.time.toLocalDateTime(timeZone).dayOfWeek == DayOfWeek.TUESDAY
}.toList()
val doesItSet = SolarEventSequence(
start = clock.now(),
latitude = 39.7348,
longitude = -104.9653,
requestedSolarEvents = listOf(SolarEvent.Sunrise),
limit = 1.hours
).any() // Returns true if anything is in the sequence
val nextMoonrise = LunarHorizonEventSequence(
start = clock.now(),
latitude = 39.7348,
longitude = -104.9653,
requestedHorizonEvents = listOf(LunarEvent.HorizonEvent.Moonrise)
).first()
val lastMoonrise = LunarHorizonEventSequence(
start = clock.now(),
latitude = latitude,
longitude = longitude,
requestedHorizonEvents = listOf(LunarEvent.HorizonEvent.Moonrise),
reverse = true
).first()
val nextFullMoon = LunarPhaseSequence(
start = clock.now(),
requestedLunarPhases = listOf(LunarEvent.PhaseEvent.FullMoon)
).first()
Note
This example does not require location because moon phases are the same across Earth.
val lastFullMoon = LunarPhaseSequence(
start = clock.now(),
requestedLunarPhases = listOf(LunarEvent.PhaseEvent.FullMoon),
reverse = true
).first()
Note
This example does not require location because moon phases are the same across Earth.
val moonList = LunarEventSequence(
start = clock.now(),
latitude = 39.7348,
longitude = -104.9653,
requestedLunarEvents = LunarEvent.all, // Show us everything!
limit = 30.days
).toList()
val fridayFullMoon = LunarPhaseSequence(
start = clock.now(),
requestedLunarPhases = listOf(LunarEvent.PhaseEvent.FullMoon),
limit = 365.days // Omitting leap year shenanigans
).filter {
it.time.toLocalDateTime(timeZone).dayOfWeek == DayOfWeek.FRIDAY
}.any()
Kastro guarantees all sequences are ordered by time. This means that events closer to the start
time come before later
events. This means that if you wanted to execute some code on each sunset (maybe to turn off your lights?) the following
would work.
Please note you will need to add kotlinx-coroutines as a dependency to do this. Kastro strives to include as few dependencies as possible.
SolarEventSequence(
start = clock.now(),
latitude = latitude,
longitude = longitude,
requestedSolarEvents = listOf(SolarEvent.Sunset),
limit = Duration.INFINITE
).asFlow()
.onEach { delay(it.time) - clock.now() }
.collect { doSomething() } // The world is your oyster!
Pull requests are welcome. Feel free to fork and open a PR. Beforeopening a PR, make sure that both tests detekt static analysis pass.
./gradlew allTest allDetekt
Should return successfully
You may run into issues depending on your host OS. At the very least ensure that the following returns successfully:
./gradlew jvmTest allDetekt
This project is still alpha so API changes are possible, but we strive for no breaking changes.
Run ./gradlew apiCheck
to see if your changes maintain binary compatibility!
Enhancements to the overall shape of the API are welcome though as this has not yet reached the 1.0
milestone.
I ran into some difficulties getting the height offset calculation to work correctly for SolarEventSequence
. I hope to eventually resolve that but didn't think it should block an alpha release. It's something I want for the future 1.0
release.
I am curious to potentially make the library usable for other languages like Javascript or Swift. This library is a Kotlin Multiplatform project, but it would be cool to also have it be on npm for use in Javascript/Typescript projects or even be a Swift package (SPM) for use on iOS/Apple targets. There are some challenges to doing that (such as how the exposed API could be adapted to better fit those languages) but I plan to actively look into this as it's something I am generally curious about.
- “Blue Hour – Magic Hour.” Timeanddate.com, 2019, www.timeanddate.com/astronomy/blue-hour.html.
- “Golden Hour – When Sunlight Turns Magical.” Www.timeanddate.com, www.timeanddate.com/astronomy/golden-hour.html.
- Körber, Richard. “Shred/Commons-Suncalc.” GitHub, 16 Jan. 2024, github.com/shred/commons-suncalc.
- Meeus, Jean. Astronomical Algorithms. Richmond, Va., Willmann-Bell, 1998.
- Montenbruck, Oliver, and Thomas Pfleger. Astronomy on the Personal Computer. Springer, 14 Mar. 2013.
- NASA. “Moon Phases | Phases, Eclipses & Supermoons.” Moon: NASA Science, https://moon.nasa.gov/moon-in-motion/phases-eclipses-supermoons/moon-phases/
- US Department of Commerce, NOAA. “Definitions of Twilight.” www.weather.gov, www.weather.gov/fsd/twilight.