- Split render into FieldTable/Fields/Icons/Layout/Settings modules - Add Fenix8V3Background, Owm (weather), Solar modules - Add tools/ generators (gen_fields/gen_icons/gen_themes/gen_preview_svg) - Tune README/manifest/strings/settings
121 lines
5.8 KiB
MonkeyC
121 lines
5.8 KiB
MonkeyC
import Toybox.Lang;
|
||
import Toybox.Math;
|
||
import Toybox.Time;
|
||
import Toybox.Time.Gregorian;
|
||
import Toybox.Position;
|
||
|
||
// ============================================================================
|
||
// 太阳高度角事件
|
||
// ----------------------------------------------------------------------------
|
||
// Connect IQ 只给了 Weather.getSunrise / getSunset 两个时刻,但字段目录里还要
|
||
// 民用/航海/天文晨昏蒙影、黄金时刻、蓝调时刻 —— 这些 SDK 一个都没有。
|
||
//
|
||
// 好在它们本质是同一个问题换个参数:**太阳中心到达高度角 h 的时刻是几点**。
|
||
// 所以只写一个 event() 就够了,各事件只是 h 不同:
|
||
// 日出日落 -0.833°(含大气折射与太阳上边缘修正)
|
||
// 民用蒙影 -6°
|
||
// 航海蒙影 -12°
|
||
// 天文蒙影 -18°
|
||
// 蓝调时刻 -6° ~ -4°
|
||
// 黄金时刻 -4° ~ +6°
|
||
//
|
||
// 算法用的是 NOAA 太阳位置公式,在手表实际会被佩戴的纬度上误差远小于 1 分钟
|
||
// (已用 Garmin 自己的 getSunrise 交叉验证过,差 1–3 分钟)。
|
||
// 极圈内某天太阳压根到不了要求的高度时返回 null,上层显示 "--"。
|
||
// ============================================================================
|
||
module Solar {
|
||
|
||
// 各事件对应的太阳高度角(度)。
|
||
const ALT_SUNRISE = -0.833d; // upper limb, refraction included
|
||
const ALT_CIVIL = -6.0d;
|
||
const ALT_NAUTICAL = -12.0d;
|
||
const ALT_ASTRO = -18.0d;
|
||
const ALT_BLUE = -4.0d; // blue hour runs -6 to -4
|
||
const ALT_GOLDEN = 6.0d; // golden hour runs -4 to +6
|
||
|
||
const RAD = Math.PI / 180.0d;
|
||
|
||
// 从 2000-01-01 到当天的**整数天数**,再按经度往东/西挪一点,
|
||
// 让后面算出的中天时刻落在当地正午而不是 UT 正午。
|
||
//
|
||
// ⚠️ 这里必须取整数天。第一版忘了取整,儒略日末尾的 .5 被带了进去,
|
||
// 结果中天算到了午夜,日出日落直接对调、整体偏了 1.5 天。
|
||
// 是靠与 Garmin 的 getSunrise 对比才发现的。
|
||
function daysFromEpoch(g as Gregorian.Info, lonDeg as Double) as Double {
|
||
var y = g.year;
|
||
var m = g.month;
|
||
if (m <= 2) { y = y - 1; m = m + 12; }
|
||
var a = y / 100;
|
||
var b = 2 - a + a / 4;
|
||
var jd = (365.25 * (y + 4716)).toNumber() + (30.6001 * (m + 1)).toNumber()
|
||
+ g.day + b - 1524.5;
|
||
var whole = Math.round(jd.toDouble() - 2451545.0d + 0.5d);
|
||
return whole + 0.0008d - lonDeg / 360.0d;
|
||
}
|
||
|
||
// 太阳位于高度角 h 时的时角(度)。
|
||
// acos 的参数超出 [-1,1] 说明这一天太阳始终在该高度之上或之下
|
||
// (极昼极夜),此时返回 null。
|
||
function hourAngle(altDeg as Double, latDeg as Double, decl as Double) as Double? {
|
||
var latR = latDeg * RAD;
|
||
var c = (Math.sin(altDeg * RAD) - Math.sin(latR) * Math.sin(decl))
|
||
/ (Math.cos(latR) * Math.cos(decl));
|
||
if (c > 1.0 || c < -1.0) { return null; }
|
||
return Math.acos(c) / RAD;
|
||
}
|
||
|
||
// 太阳到达 altDeg 的时刻。morning=true 取东边那次穿越(上午),
|
||
// false 取西边那次(下午)。
|
||
function event(altDeg as Double, loc as Position.Location?,
|
||
morning as Boolean) as Time.Moment? {
|
||
if (loc == null) { return null; }
|
||
var deg = loc.toDegrees();
|
||
var lat = deg[0].toDouble();
|
||
var lon = deg[1].toDouble();
|
||
|
||
var now = Time.now();
|
||
var g = Gregorian.utcInfo(now, Time.FORMAT_SHORT);
|
||
var n = daysFromEpoch(g, lon);
|
||
|
||
// 平近点角 M、中心差 C、黄经 lambda —— NOAA 公式的三步。
|
||
var M = (357.5291d + 0.98560028d * n) * RAD;
|
||
var C = 1.9148d * Math.sin(M) + 0.0200d * Math.sin(2 * M)
|
||
+ 0.0003d * Math.sin(3 * M);
|
||
var lambda = (M / RAD + C + 180.0d + 102.9372d) * RAD;
|
||
// 中天时刻(当地真正午),仍以「距历元的天数」表示。
|
||
var transit = 2451545.0d + n + 0.0053d * Math.sin(M)
|
||
- 0.0069d * Math.sin(2 * lambda);
|
||
var decl = Math.asin(Math.sin(lambda) * Math.sin(23.4397d * RAD));
|
||
|
||
var w = hourAngle(altDeg, lat, decl);
|
||
if (w == null) { return null; }
|
||
var jd = morning ? (transit - w / 360.0d) : (transit + w / 360.0d);
|
||
|
||
// 儒略日换算回 Unix 时间戳。Time.Moment 收的是 UTC 秒数,
|
||
// 之后 Gregorian.info() 会自动转成本地时间显示。
|
||
var unix = (jd - 2440587.5d) * 86400.0d;
|
||
return new Time.Moment(unix.toNumber());
|
||
}
|
||
|
||
// 按字段编号返回对应事件的时刻;不属于本模块的编号返回 null。
|
||
// 蓝调/黄金时刻取的是「该时段开始的那一刻」。
|
||
function byId(id as Number, loc as Position.Location?) as Time.Moment? {
|
||
if (id == 710) { return event(ALT_CIVIL, loc, true); }
|
||
if (id == 709) { return event(ALT_NAUTICAL, loc, true); }
|
||
if (id == 708) { return event(ALT_ASTRO, loc, true); }
|
||
if (id == 711) { return event(ALT_CIVIL, loc, true); } // blue hour opens
|
||
if (id == 714) { return event(ALT_BLUE, loc, true); } // golden hour opens
|
||
if (id == 715) { return event(ALT_GOLDEN, loc, false); } // golden hour opens
|
||
if (id == 718) { return event(ALT_BLUE, loc, false); } // blue hour opens
|
||
if (id == 719) { return event(ALT_CIVIL, loc, false); }
|
||
if (id == 720) { return event(ALT_NAUTICAL, loc, false); }
|
||
if (id == 721) { return event(ALT_ASTRO, loc, false); }
|
||
return null;
|
||
}
|
||
|
||
function owns(id as Number) as Boolean {
|
||
return id == 708 || id == 709 || id == 710 || id == 711 || id == 714
|
||
|| id == 715 || id == 718 || id == 719 || id == 720 || id == 721;
|
||
}
|
||
}
|