package tools
import (
"fmt"
"strings"
"time"
"github.com/sixdouglas/suncalc"
"skraak/utils"
)
type IsNightInput struct {
FilePath string `json:"file_path"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
Timezone string `json:"timezone,omitempty"`
}
type IsNightOutput struct {
FilePath string `json:"file_path"`
TimestampUTC string `json:"timestamp_utc"`
SolarNight bool `json:"solar_night"`
CivilNight bool `json:"civil_night"`
DiurnalActive bool `json:"diurnal_active"`
MoonPhase float64 `json:"moon_phase"`
DurationSec float64 `json:"duration_seconds"`
TimestampSrc string `json:"timestamp_source"`
MidpointUTC string `json:"midpoint_utc"`
SunriseUTC string `json:"sunrise_utc,omitempty"`
SunsetUTC string `json:"sunset_utc,omitempty"`
DawnUTC string `json:"dawn_utc,omitempty"`
DuskUTC string `json:"dusk_utc,omitempty"`
}
func IsNight(input IsNightInput) (IsNightOutput, error) {
var output IsNightOutput
metadata, err := utils.ParseWAVHeader(input.FilePath)
if err != nil {
return output, fmt.Errorf("WAV header parsing failed: %w", err)
}
output.DurationSec = metadata.Duration
tsResult, err := utils.ResolveTimestamp(metadata, input.FilePath, input.Timezone, true, nil)
if err != nil {
return output, fmt.Errorf("cannot determine recording timestamp: %w", err)
}
tsSource := "file_mod_time"
if tsResult.IsAudioMoth {
tsSource = "audiomoth_comment"
} else if utils.HasTimestampFilename(input.FilePath) {
tsSource = "filename"
}
astroData := utils.CalculateAstronomicalData(
tsResult.Timestamp.UTC(),
metadata.Duration,
input.Lat,
input.Lng,
)
midpoint := utils.CalculateMidpointTime(tsResult.Timestamp.UTC(), metadata.Duration)
sunTimes := suncalc.GetTimes(midpoint, input.Lat, input.Lng)
output.FilePath = input.FilePath
output.TimestampUTC = tsResult.Timestamp.UTC().Format(time.RFC3339)
output.SolarNight = astroData.SolarNight
output.CivilNight = astroData.CivilNight
output.MoonPhase = astroData.MoonPhase
output.TimestampSrc = tsSource
output.MidpointUTC = midpoint.Format(time.RFC3339)
populateSunTimes(&output, sunTimes, midpoint)
return output, nil
}
func sunTimeUTC(sunTimes map[suncalc.DayTimeName]suncalc.DayTime, name suncalc.DayTimeName) string {
if entry, ok := sunTimes[name]; ok && !entry.Value.IsZero() {
return entry.Value.UTC().Format(time.RFC3339)
}
return ""
}
func populateSunTimes(output *IsNightOutput, sunTimes map[suncalc.DayTimeName]suncalc.DayTime, midpoint time.Time) {
if dawn, ok := sunTimes[suncalc.Dawn]; ok && !dawn.Value.IsZero() {
if sunset, ok := sunTimes[suncalc.Sunset]; ok && !sunset.Value.IsZero() {
output.DiurnalActive = !midpoint.Before(dawn.Value) && !midpoint.After(sunset.Value)
}
}
output.SunriseUTC = sunTimeUTC(sunTimes, suncalc.Sunrise)
output.SunsetUTC = sunTimeUTC(sunTimes, suncalc.Sunset)
output.DawnUTC = sunTimeUTC(sunTimes, suncalc.Dawn)
output.DuskUTC = sunTimeUTC(sunTimes, suncalc.Dusk)
}
func (o IsNightOutput) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "File: %s\n", o.FilePath)
fmt.Fprintf(&sb, "Timestamp (UTC): %s\n", o.TimestampUTC)
fmt.Fprintf(&sb, "Midpoint (UTC): %s\n", o.MidpointUTC)
fmt.Fprintf(&sb, "Duration: %.1f seconds\n", o.DurationSec)
fmt.Fprintf(&sb, "Source: %s\n", o.TimestampSrc)
fmt.Fprintf(&sb, "Solar night: %v\n", o.SolarNight)
fmt.Fprintf(&sb, "Civil night: %v\n", o.CivilNight)
fmt.Fprintf(&sb, "Moon phase: %.2f\n", o.MoonPhase)
if o.SunriseUTC != "" {
fmt.Fprintf(&sb, "Sunrise (UTC): %s\n", o.SunriseUTC)
}
if o.SunsetUTC != "" {
fmt.Fprintf(&sb, "Sunset (UTC): %s\n", o.SunsetUTC)
}
if o.DawnUTC != "" {
fmt.Fprintf(&sb, "Dawn (UTC): %s\n", o.DawnUTC)
}
if o.DuskUTC != "" {
fmt.Fprintf(&sb, "Dusk (UTC): %s\n", o.DuskUTC)
}
return sb.String()
}