/** * BangleJS ASTROCALC * * Inspired by: https://www.timeanddate.com * * Original Author: Paul Cockrell https://github.com/paulcockrell * Created: April 2020 * * Calculate the Sun and Moon positions based on watch GPS and display graphically */ const SunCalc = require("suncalc.js"); const storage = require("Storage"); const LAST_GPS_FILE = "astrocalc.gps.json"; let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null); function drawMoon(phase, x, y) { const moonImgFiles = [ "new", "waxing-crescent", "first-quarter", "waxing-gibbous", "full", "waning-gibbous", "last-quarter", "waning-crescent", ]; img = require("Storage").read(`${moonImgFiles[phase]}.img`); // image width & height = 92px g.drawImage(img, x - parseInt(92 / 2), y); } // linear interpolation between two values a and b // u controls amount of a/b and is in range [0.0,1.0] function lerp(a,b,u) { return (1-u) * a + u * b; } function titlizeKey(key) { return (key[0].toUpperCase() + key.slice(1)).match(/[A-Z][a-z]+/g).join(" "); } function dateToTimeString(date) { const hrs = ("0" + date.getHours()).substr(-2); const mins = ("0" + date.getMinutes()).substr(-2); const secs = ("0" + date.getMinutes()).substr(-2); return `${hrs}:${mins}:${secs}`; } function drawTitle(key) { const fontHeight = 16; const x = 0; const x2 = g.getWidth() - 1; const y = fontHeight + 26; const y2 = g.getHeight() - 1; const title = titlizeKey(key); g.setFont("6x8", 2); g.setFontAlign(0,-1); g.drawString(title,(x+x2)/2,y-fontHeight-2); g.drawLine(x,y-2,x2,y-2); } /** * @params {Number} angle Angle of point around a radius * @params {Number} radius Radius of the point to be drawn, default 2 * @params {Object} color Color of the point * @params {Number} color.r Red 0-1 * @params {Number} color.g Green 0-1 * @params {Number} color.b Blue 0-1 */ function drawPoint(angle, radius, color) { const pRad = Math.PI / 180; const faceWidth = 80; // watch face radius const centerPx = g.getWidth() / 2; const a = angle * pRad; const x = centerPx + Math.sin(a) * faceWidth; const y = centerPx - Math.cos(a) * faceWidth; if (!radius) radius = 2; g.setColor(color.r, color.g, color.b); g.fillCircle(x, y + 20, radius); } function drawPoints() { const startColor = {r: 140, g: 255, b: 255}; // light blue const endColor = {r: 0, g: 0, b: 140}; // dark turquoise const steps = 60; const step_u = 1.0 / (steps / 2); let u = 0.0; for (let i = 0; i < steps; i++) { const colR = lerp(startColor.r, endColor.r, u) / 255; const colG = lerp(startColor.g, endColor.g, u) / 255; const colB = lerp(startColor.b, endColor.b, u) / 255; const col = {r: colR, g: colG, b: colB}; if (i >= 0 && i <= 30) { u += step_u; } else { u -= step_u; } drawPoint((360 * i) / steps, 2, col); } } function drawData(title, obj, startX, startY) { g.clear(); drawTitle(title); let xPos, yPos; if (typeof(startX) === "undefined" || startX === null) { // Center text g.setFontAlign(0,-1); xPos = (0 + g.getWidth() - 2) / 2; } else { xPos = startX; } if (typeof(startY) === "undefined") { yPos = 5; } else { yPos = startY; } g.setFont("6x8", 1); Object.keys(obj).forEach((key) => { g.drawString(`${key}: ${obj[key]}`, xPos, yPos += 20); }); g.flip(); } function drawMoonPositionPage(gps, title) { const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon); const pageData = { Azimuth: pos.azimuth.toFixed(2), Altitude: pos.altitude.toFixed(2), Distance: `${pos.distance.toFixed(0)} km`, "Parallactic Ang": pos.parallacticAngle.toFixed(2), }; const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); drawData(title, pageData, null, 80); drawPoints(); drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1}); let m = setWatch(() => { let m = moonIndexPageMenu(gps); }, BTN3, {repeat: false, edge: "falling"}); } function drawMoonIlluminationPage(gps, title) { const phaseNames = [ "New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent", ]; const phase = SunCalc.getMoonIllumination(new Date()); const pageData = { Phase: phaseNames[phase.phase], }; drawData(title, pageData, null, 35); drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2); let m = setWatch(() => { let m = moonIndexPageMenu(gps); }, BTN3, {repease: false, edge: "falling"}); } function drawMoonTimesPage(gps, title) { const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon); const pageData = { Rise: dateToTimeString(times.rise), Set: dateToTimeString(times.set), }; drawData(title, pageData, null, 105); drawPoints(); // Draw the moon rise position const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon); const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI); drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); // Draw the moon set position const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon); const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); let m = setWatch(() => { let m = moonIndexPageMenu(gps); }, BTN3, {repease: false, edge: "falling"}); } function drawSunShowPage(gps, key, date) { const pos = SunCalc.getPosition(date, gps.lat, gps.lon); const hrs = ("0" + date.getHours()).substr(-2); const mins = ("0" + date.getMinutes()).substr(-2); const secs = ("0" + date.getMinutes()).substr(-2); const time = `${hrs}:${mins}:${secs}`; const azimuth = Number(pos.azimuth.toFixed(2)); const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); const altitude = Number(pos.altitude.toFixed(2)); const pageData = { Time: time, Altitude: altitude, Azimumth: azimuth, Degrees: azimuthDegrees }; drawData(key, pageData, null, 85); drawPoints(); // Draw the suns position drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0}); m = setWatch(() => { m = sunIndexPageMenu(gps); }, BTN3, {repeat: false, edge: "falling"}); return null; } function sunIndexPageMenu(gps) { const sunTimes = SunCalc.getTimes(new Date(), gps.lat, gps.lon); const sunMenu = { "": { "title": "-- Sun --", }, "Current Pos": () => { m = E.showMenu(); drawSunShowPage(gps, "Current Pos", new Date()); }, }; Object.keys(sunTimes).sort().reduce((menu, key) => { const title = titlizeKey(key); menu[title] = () => { m = E.showMenu(); drawSunShowPage(gps, key, sunTimes[key]); }; return menu; }, sunMenu); sunMenu["< Back"] = () => m = indexPageMenu(gps); return E.showMenu(sunMenu); } function moonIndexPageMenu(gps) { const moonMenu = { "": { "title": "-- Moon --", }, "Times": () => { m = E.showMenu(); drawMoonTimesPage(gps, "Times"); }, "Position": () => { m = E.showMenu(); drawMoonPositionPage(gps, "Position"); }, "Illumination": () => { m = E.showMenu(); drawMoonIlluminationPage(gps, "Illumination"); }, "< Back": () => m = indexPageMenu(gps), }; return E.showMenu(moonMenu); } function indexPageMenu(gps) { const menu = { "": { "title": "Select", }, "Sun": () => { m = sunIndexPageMenu(gps); }, "Moon": () => { m = moonIndexPageMenu(gps); }, "< Exit": () => { load(); } }; return E.showMenu(menu); } function getCenterStringX(str) { return (g.getWidth() - g.stringWidth(str)) / 2; } /** * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page */ function drawGPSWaitPage() { const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")); const str1 = "Astrocalc v0.02"; const str2 = "Locating GPS"; const str3 = "Please wait..."; g.clear(); g.drawImage(img, 100, 50); g.setFont("6x8", 1); g.drawString(str1, getCenterStringX(str1), 105); g.drawString(str2, getCenterStringX(str2), 140); g.drawString(str3, getCenterStringX(str3), 155); if (lastGPS) { lastGPS = JSON.parse(lastGPS); lastGPS.time = new Date(); const str4 = "Press Button 3 to use last GPS"; g.setColor("#d32e29"); g.fillRect(0, 190, g.getWidth(), 215); g.setColor("#ffffff"); g.drawString(str4, getCenterStringX(str4), 200); setWatch(() => { clearWatch(); Bangle.setGPSPower(0); m = indexPageMenu(lastGPS); }, BTN3, {repeat: false}); } g.flip(); const DEBUG = false; if (DEBUG) { clearWatch(); const gps = { "lat": 56.45783133333, "lon": -3.02188583333, "alt": 75.3, "speed": 0.070376, "course": NaN, "time":new Date(), "satellites": 4, "fix": 1 }; m = indexPageMenu(gps); return; } Bangle.on('GPS', (gps) => { if (gps.fix === 0) return; clearWatch(); if (isNaN(gps.course)) gps.course = 0; require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps)); Bangle.setGPSPower(0); Bangle.buzz(); Bangle.setLCDPower(true); m = indexPageMenu(gps); }); } function init() { Bangle.setGPSPower(1); drawGPSWaitPage(); } let m; init();