Files
fenix8v3-watchface/source/Solar.mc
ericwyuan 4d96ab0912 Update watch face: modular refactor + OWM/Solar/background + codegen tools
- 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
2026-09-10 07:10:55 +08:00

121 lines
5.8 KiB
MonkeyC
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 交叉验证过,差 13 分钟)。
// 极圈内某天太阳压根到不了要求的高度时返回 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;
}
}