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; } }