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
This commit is contained in:
120
source/Solar.mc
Normal file
120
source/Solar.mc
Normal file
@@ -0,0 +1,120 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user