Skip to content

Instantly share code, notes, and snippets.

@001101
Forked from HendrikRunte/dawn2dusk.js
Created August 8, 2021 13:49
Show Gist options
  • Save 001101/8db5c674747d1fb0ec05fe2f42bff90e to your computer and use it in GitHub Desktop.
Save 001101/8db5c674747d1fb0ec05fe2f42bff90e 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 …
// 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();
@sfksuperman
Copy link

sfksuperman commented Mar 26, 2024

Every few min, it shows "The widget is loading" (Das Widget wird geladen). Why? I have to refresh it every time I have to check it. Can you please fix this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment