557 lines
23 KiB
Go
557 lines
23 KiB
Go
package sundial
|
||
|
||
import (
|
||
"math"
|
||
"time"
|
||
|
||
"b612.me/astro/sun"
|
||
)
|
||
|
||
// PlanarDial 平面日晷参数 / planar-sundial parameters.
|
||
//
|
||
// Latitude 为地理纬度(北正南负);PlaneNormalAzimuth 为日晷盘面法线的方位角,
|
||
// 按正北为 0°、向东增加;PlaneNormalZenithDistance 为盘面法线的天顶距,0° 表示水平日晷,
|
||
// 90° 表示垂直日晷;StylusLength 为垂直于盘面的直晷针长度。
|
||
//
|
||
// 坐标系沿本章通用约定:x 轴位于盘面内且保持水平,y 轴沿盘面最大坡度向上,
|
||
// x 正向为右侧,y 正向为上坡方向。
|
||
// Latitude is geographic latitude, north positive. PlaneNormalAzimuth is the azimuth of the dial-plane normal,
|
||
// measured from north toward east. PlaneNormalZenithDistance is the zenith distance of the plane normal:
|
||
// 0 degrees for a horizontal dial and 90 degrees for a vertical dial. StylusLength is the length of the direct stylus normal to the plane.
|
||
//
|
||
// The plane coordinates follow the chapter convention: the x axis lies in the plane and remains horizontal,
|
||
// the y axis follows the steepest upward direction in the plane, x positive points to the right, and y positive points upslope.
|
||
type PlanarDial struct {
|
||
Latitude float64
|
||
PlaneNormalAzimuth float64
|
||
PlaneNormalZenithDistance float64
|
||
StylusLength float64
|
||
}
|
||
|
||
// PlanarShadowPoint 平面日晷影尖位置 / shadow-tip position on a planar sundial.
|
||
//
|
||
// X/Y 为影尖在盘面坐标系中的坐标;DenominatorQ 对应书中公式里的 Q;
|
||
// SunAboveHorizon 表示太阳在地平线上方;PlaneIlluminated 表示盘面被太阳照亮;
|
||
// Illuminated 为前两者同时满足。
|
||
// X/Y are the shadow-tip coordinates in the dial-plane coordinate system. DenominatorQ is the Q term from the book formula.
|
||
// SunAboveHorizon reports whether the Sun is above the horizon. PlaneIlluminated reports whether sunlight reaches the dial plane.
|
||
// Illuminated is true only when both conditions are satisfied.
|
||
type PlanarShadowPoint struct {
|
||
X float64
|
||
Y float64
|
||
DenominatorQ float64
|
||
SunAboveHorizon bool
|
||
PlaneIlluminated bool
|
||
Illuminated bool
|
||
}
|
||
|
||
// PlanarGeometry 平面日晷几何量 / derived planar-sundial geometry.
|
||
//
|
||
// CenterX/CenterY 为日晷中心(极轴晷针固定点)坐标;PolarStylusLength 为极轴晷针长度;
|
||
// PolarStylusPlaneAngle 为极轴晷针与盘面的夹角。HasFiniteCenter 为 false 时,
|
||
// 表示极轴晷针与盘面平行,中心退化到无穷远处。
|
||
// CenterX/CenterY are the coordinates of the dial center, where the polar stylus is fixed. PolarStylusLength is the polar-stylus length.
|
||
// PolarStylusPlaneAngle is the angle between the polar stylus and the dial plane. When HasFiniteCenter is false,
|
||
// the polar stylus is parallel to the plane and the center degenerates to infinity.
|
||
type PlanarGeometry struct {
|
||
CenterX float64
|
||
CenterY float64
|
||
PolarStylusLength float64
|
||
PolarStylusPlaneAngle float64
|
||
HasFiniteCenter bool
|
||
}
|
||
|
||
// HourAngleInterval 时角区间 / hour-angle interval.
|
||
//
|
||
// Start/End 均为有符号太阳时角,单位度,满足 Start <= End。
|
||
// 约定取值范围为 [-180, 180],用于表达一天中的一段连续时角。
|
||
// Start and End are signed solar hour angles in degrees, with Start <= End.
|
||
// The intended range is [-180, 180], representing one continuous hour-angle span within a day.
|
||
type HourAngleInterval struct {
|
||
Start float64
|
||
End float64
|
||
}
|
||
|
||
// DeclinationCurveSample 赤纬曲线采样点 / sampled point on a declination curve.
|
||
//
|
||
// HourAngle 为采样点的太阳时角;Point 为该时角下的影尖位置与照明状态。
|
||
// HourAngle is the solar hour angle of the sample. Point gives the shadow-tip position and illumination state at that hour angle.
|
||
type DeclinationCurveSample struct {
|
||
HourAngle float64
|
||
Point PlanarShadowPoint
|
||
}
|
||
|
||
// DeclinationCurveSegment 赤纬曲线分段 / one illuminated segment of a declination curve.
|
||
//
|
||
// Interval 给出该段对应的连续受光时角范围;Samples 为该段内部的采样点。
|
||
// Interval gives the continuous illuminated hour-angle span for the segment. Samples contains the sampled points within that segment.
|
||
type DeclinationCurveSegment struct {
|
||
Declination float64
|
||
Interval HourAngleInterval
|
||
Samples []DeclinationCurveSample
|
||
}
|
||
|
||
// TimeLineSample 时间线采样点 / sampled point on a mean-time or zone-time line.
|
||
//
|
||
// Date 为该采样点对应的绝对时刻;其日期来自输入 date,钟面时间由调用参数指定。
|
||
// Declination 为该采样瞬时的太阳赤纬;HourAngle 为换算后的视太阳时角;
|
||
// Point 为该时角下的影尖位置与照明状态。
|
||
// Date is the absolute instant of the sample. Its date comes from the input date, while the clock reading comes from the call parameters.
|
||
// Declination is the solar declination at that instant. HourAngle is the derived apparent-solar hour angle.
|
||
// Point gives the shadow-tip position and illumination state at that hour angle.
|
||
type TimeLineSample struct {
|
||
Date time.Time
|
||
Declination float64
|
||
HourAngle float64
|
||
Point PlanarShadowPoint
|
||
}
|
||
|
||
// EquatorialNorthDial 北面赤道日晷 / north-face equatorial dial.
|
||
//
|
||
// 北半球时用于春夏半年(太阳赤纬为正);南半球也可直接按公式使用。
|
||
// In the northern hemisphere this face is used during the spring and summer half-year, when solar declination is positive. The same formula can also be used directly in the southern hemisphere.
|
||
func EquatorialNorthDial(latitude, stylusLength float64) PlanarDial {
|
||
return PlanarDial{
|
||
Latitude: latitude,
|
||
PlaneNormalAzimuth: 0,
|
||
PlaneNormalZenithDistance: 90 - latitude,
|
||
StylusLength: stylusLength,
|
||
}
|
||
}
|
||
|
||
// EquatorialSouthDial 南面赤道日晷 / south-face equatorial dial.
|
||
//
|
||
// 北半球时用于秋冬半年(太阳赤纬为负);南半球也可直接按公式使用。
|
||
// In the northern hemisphere this face is used during the autumn and winter half-year, when solar declination is negative. The same formula can also be used directly in the southern hemisphere.
|
||
func EquatorialSouthDial(latitude, stylusLength float64) PlanarDial {
|
||
return PlanarDial{
|
||
Latitude: latitude,
|
||
PlaneNormalAzimuth: 180,
|
||
PlaneNormalZenithDistance: 90 + latitude,
|
||
StylusLength: stylusLength,
|
||
}
|
||
}
|
||
|
||
// HorizontalDial 水平日晷 / horizontal dial.
|
||
//
|
||
// 该构造器采用经典水平日晷的坐标约定:x 轴向东,y 轴向北。
|
||
// This constructor follows the classical horizontal-dial coordinate convention: the x axis points east and the y axis points north.
|
||
func HorizontalDial(latitude, stylusLength float64) PlanarDial {
|
||
return PlanarDial{
|
||
Latitude: latitude,
|
||
PlaneNormalAzimuth: 180,
|
||
PlaneNormalZenithDistance: 0,
|
||
StylusLength: stylusLength,
|
||
}
|
||
}
|
||
|
||
// VerticalDial 垂直日晷 / vertical dial.
|
||
//
|
||
// planeNormalAzimuth 为盘面法线方位角,按正北为 0°、向东增加。
|
||
// 例如:朝南墙面取 180°,朝东墙面取 90°。
|
||
// planeNormalAzimuth is the azimuth of the plane normal, measured from north toward east.
|
||
// For example, use 180 degrees for a south-facing wall and 90 degrees for an east-facing wall.
|
||
func VerticalDial(latitude, planeNormalAzimuth, stylusLength float64) PlanarDial {
|
||
return PlanarDial{
|
||
Latitude: latitude,
|
||
PlaneNormalAzimuth: normalize360(planeNormalAzimuth),
|
||
PlaneNormalZenithDistance: 90,
|
||
StylusLength: stylusLength,
|
||
}
|
||
}
|
||
|
||
// Geometry 平面日晷中心与极轴晷针几何量 / derived planar-dial center and polar-stylus geometry.
|
||
//
|
||
// 返回平面日晷的中心与极轴晷针几何量。
|
||
// Returns the derived center and polar-stylus geometry of the planar dial.
|
||
func (dial PlanarDial) Geometry() PlanarGeometry {
|
||
geometry := PlanarGeometry{
|
||
CenterX: math.NaN(),
|
||
CenterY: math.NaN(),
|
||
PolarStylusLength: math.NaN(),
|
||
PolarStylusPlaneAngle: math.NaN(),
|
||
}
|
||
if !dial.isFinite() {
|
||
return geometry
|
||
}
|
||
|
||
P, _, _, _ := dial.baseCoefficients(0, 0)
|
||
geometry.PolarStylusPlaneAngle = math.Asin(clampUnit(math.Abs(P))) * 180 / math.Pi
|
||
if nearZero(P) {
|
||
return geometry
|
||
}
|
||
|
||
latSin, latCos := sinCosDeg(dial.Latitude)
|
||
zedSin, zedCos := sinCosDeg(dial.PlaneNormalZenithDistance)
|
||
declinationSin, declinationCos := sinCosDeg(dial.bookPlaneNormalAzimuth())
|
||
geometry.CenterX = dial.StylusLength / P * latCos * declinationSin
|
||
geometry.CenterY = -dial.StylusLength / P * (latSin*zedSin + latCos*zedCos*declinationCos)
|
||
geometry.PolarStylusLength = dial.StylusLength / math.Abs(P)
|
||
geometry.HasFiniteCenter = true
|
||
return geometry
|
||
}
|
||
|
||
// ShadowPointByHourAngleDeclination 影尖坐标(按时角与赤纬) / shadow point from hour angle and declination.
|
||
//
|
||
// hourAngle 为有符号太阳时角,上午为负,下午为正;declination 为太阳赤纬,单位度。
|
||
// hourAngle is the signed solar hour angle, negative in the morning and positive in the afternoon. declination is solar declination in degrees.
|
||
func (dial PlanarDial) ShadowPointByHourAngleDeclination(hourAngle, declination float64) PlanarShadowPoint {
|
||
point := PlanarShadowPoint{
|
||
X: math.NaN(),
|
||
Y: math.NaN(),
|
||
DenominatorQ: math.NaN(),
|
||
}
|
||
if !dial.isFinite() || !isFinite(hourAngle) || !isFinite(declination) {
|
||
return point
|
||
}
|
||
|
||
_, Q, Nx, Ny := dial.baseCoefficients(hourAngle, declination)
|
||
point.DenominatorQ = Q
|
||
point.SunAboveHorizon = sunAboveHorizon(hourAngle, declination, dial.Latitude)
|
||
point.PlaneIlluminated = Q > 0
|
||
point.Illuminated = point.SunAboveHorizon && point.PlaneIlluminated
|
||
if nearZero(Q) {
|
||
return point
|
||
}
|
||
|
||
point.X = dial.StylusLength * Nx / Q
|
||
point.Y = dial.StylusLength * Ny / Q
|
||
return point
|
||
}
|
||
|
||
// ShadowPointAt 影尖坐标(按绝对时刻) / shadow point at an instant.
|
||
//
|
||
// 直接读取该时刻对应的视太阳时角和瞬时太阳赤纬,并返回平面日晷上的影尖位置。
|
||
// Uses the apparent-solar hour angle and instantaneous solar declination at the supplied instant, and returns the shadow-tip position on the planar dial.
|
||
func (dial PlanarDial) ShadowPointAt(date time.Time, lon float64) PlanarShadowPoint {
|
||
return dial.ShadowPointByHourAngleDeclination(HourAngle(date, lon), sun.ApparentDec(date))
|
||
}
|
||
|
||
// MeanSolarTimePoint 平太阳时影尖位置 / shadow point for local mean solar time.
|
||
//
|
||
// date 应处于目标地点的地方平太阳时区,例如 `MeanSolarTime` 的返回值;其原有钟面时间会被忽略。
|
||
// meanSolarHours 为地方平太阳时钟面读数,单位小时,例如 9.5 表示 09:30。
|
||
// date should already be expressed in the local mean-solar timezone of the site, for example a value returned by `MeanSolarTime`; its original clock fields are ignored.
|
||
// meanSolarHours is the local mean-solar clock reading in hours, for example 9.5 for 09:30.
|
||
func (dial PlanarDial) MeanSolarTimePoint(date time.Time, meanSolarHours float64) PlanarShadowPoint {
|
||
sampleTime := dateWithClockHours(date, meanSolarHours)
|
||
declination := sun.ApparentDec(sampleTime)
|
||
return dial.ShadowPointByHourAngleDeclination(MeanSolarHourAngle(sampleTime, meanSolarHours), declination)
|
||
}
|
||
|
||
// ZoneTimePoint 区时影尖位置 / shadow point for zone time.
|
||
//
|
||
// date 提供民用日期和时区,原有钟面时间会被忽略;zoneTimeHours 为该时区下的区时钟面读数。
|
||
// date provides the civil date and timezone; its original clock fields are ignored. zoneTimeHours is the civil clock reading in that timezone.
|
||
func (dial PlanarDial) ZoneTimePoint(date time.Time, lon, zoneTimeHours float64) PlanarShadowPoint {
|
||
sampleTime := dateWithClockHours(date, zoneTimeHours)
|
||
declination := sun.ApparentDec(sampleTime)
|
||
return dial.ShadowPointByHourAngleDeclination(HourAngle(sampleTime, lon), declination)
|
||
}
|
||
|
||
// MeanSolarTimeLine 平太阳时时间线 / local mean solar time line.
|
||
//
|
||
// dates 由调用者自行决定取样日期密度,例如每月或每日;每个 date 都应处于目标地点的地方平太阳时区,
|
||
// 例如先通过 `MeanSolarTime` 得到对应地点的地方平太阳时再取其年月日。meanSolarHours 为地方平太阳时钟面读数。
|
||
// dates define the sampling cadence, for example monthly or daily. Each date should already be in the site's local mean-solar timezone,
|
||
// for example by first calling `MeanSolarTime` and then keeping its year, month, and day. meanSolarHours is the local mean-solar clock reading.
|
||
func (dial PlanarDial) MeanSolarTimeLine(dates []time.Time, meanSolarHours float64) []TimeLineSample {
|
||
if !isFinite(meanSolarHours) {
|
||
return nil
|
||
}
|
||
samples := make([]TimeLineSample, 0, len(dates))
|
||
for _, date := range dates {
|
||
sampleTime := dateWithClockHours(date, meanSolarHours)
|
||
declination := sun.ApparentDec(sampleTime)
|
||
hourAngle := MeanSolarHourAngle(sampleTime, meanSolarHours)
|
||
samples = append(samples, TimeLineSample{
|
||
Date: sampleTime,
|
||
Declination: declination,
|
||
HourAngle: hourAngle,
|
||
Point: dial.ShadowPointByHourAngleDeclination(hourAngle, declination),
|
||
})
|
||
}
|
||
return samples
|
||
}
|
||
|
||
// ZoneTimeLine 区时时间线 / zone-time line.
|
||
//
|
||
// dates 由调用者自行决定取样日期密度;zoneTimeHours 为 date 所在时区的区时钟面读数。
|
||
// 每个 date 的原有钟面时间都会被 zoneTimeHours 替换。
|
||
// dates define the sampling cadence. zoneTimeHours is the civil clock reading in the timezone carried by each date.
|
||
// The original clock fields of every date are replaced by zoneTimeHours.
|
||
func (dial PlanarDial) ZoneTimeLine(dates []time.Time, lon, zoneTimeHours float64) []TimeLineSample {
|
||
if !isFinite(zoneTimeHours) || !isFinite(lon) {
|
||
return nil
|
||
}
|
||
samples := make([]TimeLineSample, 0, len(dates))
|
||
for _, date := range dates {
|
||
sampleTime := dateWithClockHours(date, zoneTimeHours)
|
||
declination := sun.ApparentDec(sampleTime)
|
||
hourAngle := HourAngle(sampleTime, lon)
|
||
samples = append(samples, TimeLineSample{
|
||
Date: sampleTime,
|
||
Declination: declination,
|
||
HourAngle: hourAngle,
|
||
Point: dial.ShadowPointByHourAngleDeclination(hourAngle, declination),
|
||
})
|
||
}
|
||
return samples
|
||
}
|
||
|
||
// PlaneIlluminatedHourAngleIntervals 盘面受光时角区间 / plane-illuminated hour-angle intervals.
|
||
//
|
||
// declination 为太阳赤纬,单位度。返回的区间只考虑盘面受光,不判断太阳是否在地平线上方。
|
||
// declination is solar declination in degrees. The returned intervals consider only whether the dial plane is illuminated and do not test whether the Sun is above the horizon.
|
||
func (dial PlanarDial) PlaneIlluminatedHourAngleIntervals(declination float64) []HourAngleInterval {
|
||
if !dial.isFinite() || !isFinite(declination) {
|
||
return nil
|
||
}
|
||
sinCoeff, cosCoeff, constant := dial.qCoefficients(declination)
|
||
return positiveHourAngleIntervals(sinCoeff, cosCoeff, constant)
|
||
}
|
||
|
||
// IlluminatedHourAngleIntervals 可见且受光时角区间 / illuminated hour-angle intervals.
|
||
//
|
||
// declination 为太阳赤纬,单位度。结果可直接用于日晷绘图时筛掉无效时线。
|
||
// declination is solar declination in degrees. The result can be used directly to discard invalid hour-line segments in sundial plotting.
|
||
func (dial PlanarDial) IlluminatedHourAngleIntervals(declination float64) []HourAngleInterval {
|
||
aboveHorizon := SunAboveHorizonHourAngleIntervals(dial.Latitude, declination)
|
||
planeLit := dial.PlaneIlluminatedHourAngleIntervals(declination)
|
||
return intersectHourAngleIntervals(aboveHorizon, planeLit)
|
||
}
|
||
|
||
// DeclinationCurve 赤纬曲线采样 / declination-curve samples.
|
||
//
|
||
// declination 为太阳赤纬,单位度;hourAngleStep 为采样步长,单位度,常用值是 15°(每小时一格)。
|
||
// 返回值按受光区间分段,每段都带有精确的时角范围;Samples 只包含区间内部的有效采样点。
|
||
// declination is solar declination in degrees. hourAngleStep is the sampling step in degrees; 15 degrees is a common one-hour spacing.
|
||
// The return value is split by illuminated intervals. Each segment carries the exact hour-angle bounds, and Samples contains only valid interior sample points.
|
||
func (dial PlanarDial) DeclinationCurve(declination, hourAngleStep float64) []DeclinationCurveSegment {
|
||
if !dial.isFinite() || !isFinite(declination) || !isFinite(hourAngleStep) || hourAngleStep <= 0 {
|
||
return nil
|
||
}
|
||
|
||
intervals := dial.IlluminatedHourAngleIntervals(declination)
|
||
segments := make([]DeclinationCurveSegment, 0, len(intervals))
|
||
for _, interval := range intervals {
|
||
sampleAngles := intervalInteriorSampleAngles(interval, hourAngleStep)
|
||
samples := make([]DeclinationCurveSample, 0, len(sampleAngles))
|
||
for _, hourAngle := range sampleAngles {
|
||
samples = append(samples, DeclinationCurveSample{
|
||
HourAngle: hourAngle,
|
||
Point: dial.ShadowPointByHourAngleDeclination(hourAngle, declination),
|
||
})
|
||
}
|
||
segments = append(segments, DeclinationCurveSegment{
|
||
Declination: declination,
|
||
Interval: interval,
|
||
Samples: samples,
|
||
})
|
||
}
|
||
return segments
|
||
}
|
||
|
||
// DeclinationCurveAt 瞬时赤纬曲线采样 / declination-curve samples at an instant.
|
||
//
|
||
// 用 date 对应瞬时太阳赤纬生成日晷分段曲线采样。
|
||
// Builds the segmented declination-curve samples from the instantaneous solar declination at date.
|
||
func (dial PlanarDial) DeclinationCurveAt(date time.Time, hourAngleStep float64) []DeclinationCurveSegment {
|
||
return dial.DeclinationCurve(sun.ApparentDec(date), hourAngleStep)
|
||
}
|
||
|
||
// SunAboveHorizonHourAngleIntervals 地平线上方时角区间 / above-horizon hour-angle intervals.
|
||
//
|
||
// latitude 为地理纬度,declination 为太阳赤纬,单位度。结果只反映太阳是否升到地平线上方,
|
||
// 不包含盘面朝向的影响。
|
||
// latitude is geographic latitude and declination is solar declination, both in degrees. The result reflects only whether the Sun is above the horizon and does not include dial-plane orientation.
|
||
func SunAboveHorizonHourAngleIntervals(latitude, declination float64) []HourAngleInterval {
|
||
if !isFinite(latitude) || !isFinite(declination) {
|
||
return nil
|
||
}
|
||
|
||
latRad := latitude * math.Pi / 180
|
||
declinationRad := declination * math.Pi / 180
|
||
threshold := -math.Tan(latRad) * math.Tan(declinationRad)
|
||
if threshold <= -1 {
|
||
return fullDayHourAngleIntervals()
|
||
}
|
||
if threshold >= 1 {
|
||
return nil
|
||
}
|
||
halfWidth := math.Acos(clampUnit(threshold)) * 180 / math.Pi
|
||
if nearZero(halfWidth) {
|
||
return nil
|
||
}
|
||
return []HourAngleInterval{{Start: -halfWidth, End: halfWidth}}
|
||
}
|
||
|
||
func (dial PlanarDial) baseCoefficients(hourAngle, declination float64) (P, Q, Nx, Ny float64) {
|
||
latSin, latCos := sinCosDeg(dial.Latitude)
|
||
zedSin, zedCos := sinCosDeg(dial.PlaneNormalZenithDistance)
|
||
declinationSin, declinationCos := sinCosDeg(dial.bookPlaneNormalAzimuth())
|
||
hourAngleSin, hourAngleCos := sinCosDeg(hourAngle)
|
||
declinationTan := math.Tan(declination * math.Pi / 180)
|
||
|
||
P = latSin*zedCos - latCos*zedSin*declinationCos
|
||
Q = declinationSin*zedSin*hourAngleSin +
|
||
(latCos*zedCos+latSin*zedSin*declinationCos)*hourAngleCos +
|
||
P*declinationTan
|
||
Nx = declinationCos*hourAngleSin -
|
||
declinationSin*(latSin*hourAngleCos-latCos*declinationTan)
|
||
Ny = zedCos*declinationSin*hourAngleSin -
|
||
(latCos*zedSin-latSin*zedCos*declinationCos)*hourAngleCos -
|
||
(latSin*zedSin+latCos*zedCos*declinationCos)*declinationTan
|
||
return P, Q, Nx, Ny
|
||
}
|
||
|
||
func (dial PlanarDial) bookPlaneNormalAzimuth() float64 {
|
||
return normalize360(dial.PlaneNormalAzimuth - 180)
|
||
}
|
||
|
||
func (dial PlanarDial) qCoefficients(declination float64) (sinCoeff, cosCoeff, constant float64) {
|
||
latSin, latCos := sinCosDeg(dial.Latitude)
|
||
zedSin, zedCos := sinCosDeg(dial.PlaneNormalZenithDistance)
|
||
declinationSin, declinationCos := sinCosDeg(dial.bookPlaneNormalAzimuth())
|
||
P := latSin*zedCos - latCos*zedSin*declinationCos
|
||
return declinationSin * zedSin,
|
||
latCos*zedCos + latSin*zedSin*declinationCos,
|
||
P * math.Tan(declination*math.Pi/180)
|
||
}
|
||
|
||
func (dial PlanarDial) isFinite() bool {
|
||
return isFinite(dial.Latitude) &&
|
||
isFinite(dial.PlaneNormalAzimuth) &&
|
||
isFinite(dial.PlaneNormalZenithDistance) &&
|
||
isFinite(dial.StylusLength)
|
||
}
|
||
|
||
func sunAboveHorizon(hourAngle, declination, latitude float64) bool {
|
||
latSin, latCos := sinCosDeg(latitude)
|
||
decSin, decCos := sinCosDeg(declination)
|
||
_, hourAngleCos := sinCosDeg(hourAngle)
|
||
return latSin*decSin+latCos*decCos*hourAngleCos > 0
|
||
}
|
||
|
||
func sinCosDeg(value float64) (sinValue, cosValue float64) {
|
||
rad := value * math.Pi / 180
|
||
return math.Sin(rad), math.Cos(rad)
|
||
}
|
||
|
||
func normalize360(value float64) float64 {
|
||
value = math.Mod(value, 360)
|
||
if value < 0 {
|
||
value += 360
|
||
}
|
||
return value
|
||
}
|
||
|
||
func clampUnit(value float64) float64 {
|
||
if value > 1 {
|
||
return 1
|
||
}
|
||
if value < -1 {
|
||
return -1
|
||
}
|
||
return value
|
||
}
|
||
|
||
func nearZero(value float64) bool {
|
||
return math.Abs(value) <= 1e-15
|
||
}
|
||
|
||
func positiveHourAngleIntervals(sinCoeff, cosCoeff, constant float64) []HourAngleInterval {
|
||
radius := math.Hypot(sinCoeff, cosCoeff)
|
||
if nearZero(radius) {
|
||
if constant > 0 {
|
||
return fullDayHourAngleIntervals()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
threshold := -constant / radius
|
||
if threshold <= -1 {
|
||
return fullDayHourAngleIntervals()
|
||
}
|
||
if threshold >= 1 {
|
||
return nil
|
||
}
|
||
|
||
center := math.Atan2(sinCoeff, cosCoeff) * 180 / math.Pi
|
||
halfWidth := math.Acos(clampUnit(threshold)) * 180 / math.Pi
|
||
if nearZero(halfWidth) {
|
||
return nil
|
||
}
|
||
return splitWrappedSignedInterval(center-halfWidth, center+halfWidth)
|
||
}
|
||
|
||
func splitWrappedSignedInterval(start, end float64) []HourAngleInterval {
|
||
if end-start >= 360-negligibleHourAngle {
|
||
return fullDayHourAngleIntervals()
|
||
}
|
||
for start < -180 {
|
||
start += 360
|
||
end += 360
|
||
}
|
||
for start >= 180 {
|
||
start -= 360
|
||
end -= 360
|
||
}
|
||
if end <= 180 {
|
||
return []HourAngleInterval{{Start: start, End: end}}
|
||
}
|
||
return []HourAngleInterval{
|
||
{Start: -180, End: end - 360},
|
||
{Start: start, End: 180},
|
||
}
|
||
}
|
||
|
||
func intersectHourAngleIntervals(a, b []HourAngleInterval) []HourAngleInterval {
|
||
if len(a) == 0 || len(b) == 0 {
|
||
return nil
|
||
}
|
||
intersections := make([]HourAngleInterval, 0, len(a)+len(b))
|
||
for i, j := 0, 0; i < len(a) && j < len(b); {
|
||
start := math.Max(a[i].Start, b[j].Start)
|
||
end := math.Min(a[i].End, b[j].End)
|
||
if end-start > negligibleHourAngle {
|
||
intersections = append(intersections, HourAngleInterval{Start: start, End: end})
|
||
}
|
||
switch {
|
||
case a[i].End < b[j].End-negligibleHourAngle:
|
||
i++
|
||
case b[j].End < a[i].End-negligibleHourAngle:
|
||
j++
|
||
default:
|
||
i++
|
||
j++
|
||
}
|
||
}
|
||
return intersections
|
||
}
|
||
|
||
func intervalInteriorSampleAngles(interval HourAngleInterval, step float64) []float64 {
|
||
if !isFinite(interval.Start) || !isFinite(interval.End) || !isFinite(step) || step <= 0 {
|
||
return nil
|
||
}
|
||
if interval.End-interval.Start <= negligibleHourAngle {
|
||
return nil
|
||
}
|
||
|
||
samples := make([]float64, 0, int(math.Ceil((interval.End-interval.Start)/step)))
|
||
first := math.Ceil((interval.Start+negligibleHourAngle)/step) * step
|
||
for hourAngle := first; hourAngle < interval.End-negligibleHourAngle; hourAngle += step {
|
||
samples = append(samples, hourAngle)
|
||
}
|
||
if len(samples) == 0 {
|
||
samples = append(samples, (interval.Start+interval.End)/2)
|
||
}
|
||
return samples
|
||
}
|
||
|
||
func fullDayHourAngleIntervals() []HourAngleInterval {
|
||
return []HourAngleInterval{{Start: -180, End: 180}}
|
||
}
|
||
|
||
const negligibleHourAngle = 1e-12
|