Last active
September 9, 2024 18:10
-
-
Save HendrikRunte/4b5d03cb26e31508bc96553ad3c10f47 to your computer and use it in GitHub Desktop.
Scriptable.app widget displaying the exact time of today's sunrise and sunset. Which comes in handy in the wintertime …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: orange; icon-glyph: sun; | |
/////////////////////////////////////////////////////////////////////// | |
// dawn2dusk.js | |
// Origin: | |
// https://gist.github.com/HendrikRunte/4b5d03cb26e31508bc96553ad3c10f47 | |
// Take it and have fun. | |
// Hendrik Runte, Nov 12, 2020, 17:33. | |
/////////////////////////////////////////////////////////////////////// | |
// Extending the JavaScritp Date object. | |
// Usage: | |
// const sunriseDateObject = new Date().sunrise(lat, long); | |
// const sunsetDateObject = new Date().sunrise(lat, long); | |
// All the other methods just help. | |
Date.prototype.sunrise = function (latitude, longitude, zenith) { | |
return this.setSun(latitude, longitude, true, zenith); | |
}; | |
Date.prototype.sunset = function (latitude, longitude, zenith) { | |
return this.setSun(latitude, longitude, false, zenith); | |
}; | |
Date.prototype.setSun = function (latitude, longitude, isSunrise, zenith) { | |
zenith = zenith || 90.8333; | |
const DEGREES_PER_HOUR = 360 / 24; | |
const hoursFromMeridian = longitude / DEGREES_PER_HOUR; | |
const dayOfYear = this.getDayOfYear(); | |
const approxTimeOfEventInDays = isSunrise | |
? dayOfYear + (6 - hoursFromMeridian) / 24 | |
: dayOfYear + (18 - hoursFromMeridian) / 24; | |
const sunMeanAnomaly = 0.9856 * approxTimeOfEventInDays - 3.289; | |
const sunTrueLongitude = Math.mod( | |
sunMeanAnomaly + | |
1.916 * Math.sinDeg(sunMeanAnomaly) + | |
0.02 * Math.sinDeg(2 * sunMeanAnomaly) + | |
282.634, | |
360 | |
); | |
const ascension = 0.91764 * Math.tanDeg(sunTrueLongitude); | |
let rightAscension = (360 / (2 * Math.PI)) * Math.atan(ascension); | |
rightAscension = Math.mod((360 / (2 * Math.PI)) * Math.atan(ascension), 360); | |
const lQuadrant = Math.floor(sunTrueLongitude / 90) * 90; | |
const raQuadrant = Math.floor(rightAscension / 90) * 90; | |
rightAscension = rightAscension + (lQuadrant - raQuadrant); | |
rightAscension /= DEGREES_PER_HOUR; | |
const sinDec = 0.39782 * Math.sinDeg(sunTrueLongitude); | |
const cosDec = Math.cosDeg(Math.asinDeg(sinDec)); | |
const cosLocalHourAngle = | |
(Math.cosDeg(zenith) - sinDec * Math.sinDeg(latitude)) / | |
(cosDec * Math.cosDeg(latitude)); | |
const localHourAngle = Math.acosDeg(cosLocalHourAngle); | |
const localHour = isSunrise | |
? (360 - localHourAngle) / DEGREES_PER_HOUR | |
: localHourAngle / DEGREES_PER_HOUR; | |
const localMeanTime = | |
localHour + rightAscension - 0.06571 * approxTimeOfEventInDays - 6.622; | |
let time = localMeanTime - longitude / DEGREES_PER_HOUR; | |
time = Math.mod(time, 24); | |
const midnight = new Date(0); | |
midnight.setUTCFullYear(this.getUTCFullYear()); | |
midnight.setUTCMonth(this.getUTCMonth()); | |
midnight.setUTCDate(this.getUTCDate()); | |
const milli = midnight.getTime() + time * 60 * 60 * 1000; | |
return new Date(milli); | |
}; | |
// Utility functions | |
Date.prototype.getDayOfYear = function () { | |
return Math.ceil((this - new Date(this.getFullYear(), 0, 1)) / 86400000); | |
}; | |
Math.degToRad = function (num) { | |
return (num * Math.PI) / 180; | |
}; | |
Math.radToDeg = function (radians) { | |
return (radians * 180.0) / Math.PI; | |
}; | |
Math.sinDeg = function (deg) { | |
return Math.sin((deg * 2.0 * Math.PI) / 360.0); | |
}; | |
Math.acosDeg = function (x) { | |
return (Math.acos(x) * 360.0) / (2 * Math.PI); | |
}; | |
Math.asinDeg = function (x) { | |
return (Math.asin(x) * 360.0) / (2 * Math.PI); | |
}; | |
Math.tanDeg = function (deg) { | |
return Math.tan((deg * 2.0 * Math.PI) / 360.0); | |
}; | |
Math.cosDeg = function (deg) { | |
return Math.cos((deg * 2.0 * Math.PI) / 360.0); | |
}; | |
Math.mod = function (a, b) { | |
let result = a % b; | |
if (result < 0) { | |
result += b; | |
} | |
return result; | |
}; | |
/////////////////////////////////////////////////////////////////////// | |
// Here comes the actual Scriptable widget stuff. | |
/////////////////////////////////////////////////////////////////////// | |
function getMoonphase(dateObj) { | |
// Bluntly copied from https://gist.github.com/endel/dfe6bb2fbe679781948c | |
let c = 0; | |
let e = 0; | |
let jd = 0; | |
let b = 0; | |
let year = dateObj.getFullYear(); | |
let month = dateObj.getMonth() + 1; | |
let day = dateObj.getDate(); | |
if (month < 3) { | |
year--; | |
month += 12; | |
} | |
++month; | |
c = 365.25 * year; | |
e = 30.6 * month; | |
jd = c + e + day - 694039.09; // jd is total days elapsed | |
jd /= 29.5305882; // divide by the moon cycle | |
b = parseInt(jd); // int(jd) -> b, take integer part of jd | |
jd -= b; // subtract integer part to leave fractional part of original jd | |
b = Math.round(jd * 8); // scale fraction from 0-8 and round | |
if (b >= 8) { | |
b = 0; // 0 and 8 are the same so turn 8 into 0 | |
} | |
return b; | |
} | |
// Helps adding icons from SF Symbols. | |
function addSymbol({ | |
symbolName = 'applelogo', | |
stack, | |
color = Color.white(), | |
size = 20, | |
}) { | |
const icon = stack.addImage(SFSymbol.named(symbolName).image); | |
icon.tintColor = color; | |
icon.imageSize = new Size(size, size); | |
} | |
function getSunriseAndSunset(date, location) { | |
return { | |
location: location, | |
todaysSunrise: date.sunrise(location.latitude, location.longitude), | |
todaysSunset: date.sunset(location.latitude, location.longitude), | |
}; | |
} | |
function displayLoadingIndicator() { | |
const listWidget = new ListWidget(); | |
const gradient = new LinearGradient(); | |
gradient.locations = [0, 1]; | |
gradient.colors = [new Color('#000618'), new Color('#121A34')]; | |
listWidget.backgroundGradient = gradient; | |
const iconStack = listWidget.addStack(); | |
addSymbol({ | |
symbolName: 'text.bubble', | |
stack: iconStack, | |
color: Color.white(), | |
size: 32, | |
}); | |
listWidget.addSpacer(10); | |
const header = listWidget.addText('Das Widget'); | |
header.font = Font.regularRoundedSystemFont(FONTSETTINGS.medium); | |
header.textColor = Color.white(); | |
listWidget.addSpacer(2); | |
const footer = listWidget.addText('wird geladen …'); | |
footer.font = Font.regularRoundedSystemFont(FONTSETTINGS.medium); | |
footer.textColor = Color.white(); | |
return listWidget; | |
} | |
async function displaySunriseAndSunset( | |
{ location, todaysSunrise, todaysSunset }, | |
locality = null | |
) { | |
const listWidget = new ListWidget(); | |
let todaysDate = new Date(NOW); | |
let headerText = 'Sonnenlauf, heute'; | |
let headerColor = Color.white(); | |
const gradient = new LinearGradient(); | |
const gradientByTime = | |
NOW >= todaysSunrise.getTime() - 900000 && | |
NOW < todaysSunset.getTime() + 900000 | |
? { gradientStart: '#093199', gradientStop: '#4C95FE' } // day | |
: { gradientStart: '#000618', gradientStop: '#121A34' }; // night | |
gradient.locations = [0, 1]; | |
gradient.colors = [ | |
new Color(gradientByTime.gradientStart), | |
new Color(gradientByTime.gradientStop), | |
]; | |
listWidget.backgroundGradient = gradient; | |
// Is it before midnight but later than today's sunset | |
// we'll look at tomorrow: | |
if ( | |
NOW <= todaysDate.setHours(23, 59, 59, 999) && | |
NOW > todaysSunset.getTime() | |
) { | |
todaysDate = new Date(new Date().setDate(todaysDate.getDate() + 1)); // tomorrow | |
headerText = 'Sonnenlauf, morgen'; | |
todaysSunrise = todaysDate.sunrise(location.latitude, location.longitude); | |
todaysSunset = todaysDate.sunset(location.latitude, location.longitude); | |
headerColor = Color.white(); | |
} | |
const header = listWidget.addText(headerText.toUpperCase()); | |
header.font = Font.regularRoundedSystemFont(FONTSETTINGS.small); | |
header.textColor = headerColor; | |
listWidget.addSpacer(12); | |
// Sunrise | |
const sunriseStack = listWidget.addStack(); | |
const sunriseStackColor = | |
todaysSunrise.getTime() < NOW ? new Color('#ffffff99') : Color.white(); | |
addSymbol({ | |
symbolName: 'sunrise.fill', | |
stack: sunriseStack, | |
color: sunriseStackColor, | |
size: 26, | |
}); | |
sunriseStack.addSpacer(); | |
const sunriseLabel = sunriseStack.addText( | |
` ${todaysSunrise | |
.getHours() | |
.toString() | |
.replace(/^0(?:0:0?)?/, '')}:${('0' + todaysSunrise.getMinutes()).slice( | |
-2 | |
)}` | |
); | |
sunriseLabel.font = Font.mediumRoundedSystemFont(FONTSETTINGS.big); | |
sunriseLabel.textColor = sunriseStackColor; | |
// Sunset | |
const sunsetStack = listWidget.addStack(); | |
const sunsetStackColor = | |
todaysSunset.getTime() < NOW ? new Color('#ffffff99') : Color.white(); | |
addSymbol({ | |
symbolName: 'sunset.fill', | |
stack: sunsetStack, | |
color: sunsetStackColor, | |
size: 26, | |
}); | |
sunsetStack.addSpacer(); | |
const sunsetLabel = sunsetStack.addText( | |
` ${todaysSunset | |
.getHours() | |
.toString() | |
.replace(/^0(?:0:0?)?/, '')}:${('0' + todaysSunset.getMinutes()).slice( | |
-2 | |
)}` | |
); | |
sunsetLabel.font = Font.mediumRoundedSystemFont(FONTSETTINGS.big); | |
sunsetLabel.textColor = sunsetStackColor; | |
listWidget.addSpacer(12); | |
// Footer: | |
const footerStack = listWidget.addStack(); | |
addSymbol({ | |
symbolName: locality ? 'location.fill' : 'arrowtriangle.right.circle', | |
stack: footerStack, | |
color: Color.white(), | |
size: 12, | |
}); | |
const footerLabel = locality | |
? footerStack.addText(` ${locality.toUpperCase()}`) | |
: footerStack.addText( | |
` ${todaysDate.toLocaleDateString(undefined, { | |
weekday: 'short', | |
})}., ${todaysDate.toLocaleDateString(undefined, { | |
year: 'numeric', | |
month: 'numeric', | |
day: 'numeric', | |
})}` | |
); | |
footerStack.addSpacer(); | |
footerStack.addText(MOONICONS[getMoonphase(new Date())]); | |
footerLabel.font = Font.regularRoundedSystemFont(FONTSETTINGS.small); | |
footerLabel.textColor = Color.white(); | |
// render | |
return listWidget; | |
} | |
// Locate yourself or use params. | |
async function getLocation() { | |
try { | |
if (args.widgetParameter) { | |
const fixedCoordinates = args.widgetParameter.split(',').map(parseFloat); | |
return { latitude: fixedCoordinates[0], longitude: fixedCoordinates[1] }; | |
} else { | |
Location.setAccuracyToThreeKilometers(); | |
return await Location.current(); | |
} | |
} catch (e) { | |
return null; | |
} | |
} | |
async function getLocality(geolocation) { | |
let locality = null; | |
try { | |
// Location.reverseGeocode returns an array with object properties. | |
// Uses Apple CLLocation. | |
const address = await Location.reverseGeocode( | |
geolocation.latitude, | |
geolocation.longitude | |
); | |
// The order is relevant for processing the | |
// address properties. | |
const cascade = [ | |
'ocean', | |
'inlandWater', | |
'administrativeArea', | |
'subAdministrativeArea', | |
'locality', | |
'subLocality', | |
]; | |
if (address.length) { | |
cascade.forEach((prop) => { | |
locality = address[0][prop] ? address[0][prop] : locality; | |
}); | |
} | |
return locality; | |
} catch (e) { | |
return null; | |
} | |
} | |
/////////////////////////////////////////////////////////////////////// | |
let widget = {}; | |
const FONTSETTINGS = { | |
big: 30, | |
medium: 16, | |
small: 9, | |
}; | |
const NOW = +new Date(); | |
const MOONICONS = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘']; | |
const location = await getLocation(); | |
const locality = await getLocality(location); | |
if (location) { | |
const sunriseAndSunset = getSunriseAndSunset(new Date(NOW), location); | |
widget = await displaySunriseAndSunset(sunriseAndSunset, locality); | |
} else { | |
console.error(location); | |
console.error(locality); | |
widget = await displayLoadingIndicator(); | |
} | |
if (!config.runsInWidget) { | |
await widget.presentSmall(); | |
} | |
Script.setWidget(widget); | |
Script.complete(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Are you sure bro? Have you checked the widget let say after few hours? I hope you could reproduce it.
Also, apart from this, could you please align the time and sun icons for sunrise and sunset (i think they are not properly aligned)?