Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Last active September 4, 2018 14:47
Show Gist options
  • Save Sphinxxxx/d472d3ebc660bc5d7d29ac0f8b13147e to your computer and use it in GitHub Desktop.
Save Sphinxxxx/d472d3ebc660bc5d7d29ac0f8b13147e to your computer and use it in GitHub Desktop.
Moonrise, Moonset, and Moon Phases
<script src="//unpkg.com/[email protected]"></script>
<script src="//unpkg.com/[email protected]"></script>
<script src="//unpkg.com/[email protected]"></script>
<header>
<h2>Moon calendar</h2>
<aside>(All data from <a target="_blank" href="https://www.timeanddate.com/moon/norway/hamar?month=9&year=2018">timeanddate.com</a> )</aside>
</header>
<form>
<div class="location">
<label>
<span class="caption">Country</span>
<input type="text" name="country" value="Norway" />
</label>
<label>
<span class="caption">City</span>
<input type="text" name="city" value="Hamar" />
</label>
</div>
<div class="month-picker">
<div class="year">
<label>
<span class="caption">Year</span>
<input type="number" name="year" value="2018" />
</label>
</div>
<div class="month">
<span class="caption">Month</span>
<div class="inputs">
<label><input type="radio" name="month" value="1" /><span>Jan</span></label>
<label><input type="radio" name="month" value="2" /><span>Feb</span></label>
<label><input type="radio" name="month" value="3" /><span>Mar</span></label>
<label><input type="radio" name="month" value="4" /><span>Apr</span></label>
<label><input type="radio" name="month" value="5" /><span>May</span></label>
<label><input type="radio" name="month" value="6" /><span>Jun</span></label>
<label><input type="radio" name="month" value="7" /><span>Jul</span></label>
<label><input type="radio" name="month" value="8" /><span>Aug</span></label>
<label><input type="radio" name="month" value="9" /><span>Sep</span></label>
<label><input type="radio" name="month" value="10"/><span>Oct</span></label>
<label><input type="radio" name="month" value="11"/><span>Nov</span></label>
<label><input type="radio" name="month" value="12"/><span>Dec</span></label>
</div>
</div>
</div>
<div>
<button type="submit">Update</button>
</div>
</form>
<div id="container"></div>
<div id="shortcuts">
<a id="start" href="#">First day of month</a>
<a id="now" href="#" style="display:none;">Today</a>
<a id="end" href="#">Last day of month</a>
</div>
<div id="loader"></div>
console.clear();
/**
* localForage + lz-string
* https://github.com/localForage/localForage
* https://github.com/pieroxy/lz-string
*/
class CompressedForage {
constructor(key) {
this._key = key;
this.key = 'ABO_CompressedForage_' + key;
}
_onError(err) {
console.error(err);
}
_containerWork(work, callback) {
const mainKey = this.key,
errHandler = this._onError;
localforage.getItem(mainKey)
.then(function(cont) {
if(!cont) { cont = {}; }
const result = work(cont);
localforage.setItem(mainKey, cont)
.then(function(cont) {
if(callback) { callback(result); }
})
.catch(errHandler);
})
.catch(errHandler);
}
_normKey(key) {
//See points 2 and 3 here (avoid overriding standard functions and properties on Object):
//http://2ality.com/2012/01/objects-as-maps.html
return '_@' + key;
}
setItem(key, data, callback) {
const k = this._normKey(key);
this._containerWork(cont => {
const buffer = data ? LZString.compressToUint8Array(data) : data;
cont[k] = buffer;
}, callback);
}
getKeys(callback) {
this._containerWork(cont => {
const keys = Object.keys(cont).map(k => {
//Keep this code synced with _normKey()..
return k.substring(2);
});
return keys;
}, callback);
}
getItem(key, callback) {
const k = this._normKey(key);
this._containerWork(cont => {
const buffer = cont[k],
data = buffer ? LZString.decompressFromUint8Array(buffer) : buffer;
return data;
}, callback);
}
removeItem(key, callback) {
const k = this._normKey(key);
this._containerWork(cont => {
//https://stackoverflow.com/questions/208105/how-do-i-remove-a-property-from-a-javascript-object
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete
delete cont[k];
}, callback);
}
clear(callback) {
localforage.removeItem(this.key)
.then(callback)
.catch(this._onError);
}
}
function createTimeline(table) {
const ud = ABOUtils.DOM,
[$, $$] = ud.selectors();
let events = [];
function parseEvent(type, date, time, deg) {
if(!time) { return null; }
function extractNumbers(str) {
return str.match(/(\d+)/g).map(Number);
}
function parseTime(time) {
time = time.trim();
let [h, m] = extractNumbers(time);
//12:13 am -> 00:13
// 1:16 am -> 01:16
//..
//12:42 pm -> 12:42
// 1:52 pm -> 13:52
if(time.indexOf('am') > 0) {
if(h === 12) { h = 0; }
}
else if(time.indexOf('pm') > 0) {
if(h !== 12) { h += 12; }
}
return [h, m];
}
//Example: "↑ (296°)", "13:14"
const degr = extractNumbers(deg)[0],
[h, m] = parseTime(time),
dayFraction = (h + (m/60))/24,
monthIndex = date-1 + dayFraction;
const event = createEvent(type, monthIndex, degr);
event.raw = { date, time, deg };
return event;
}
function createEvent(type, time, degr) {
return {
type,
time,
degr,
};
}
$$('tr[data-day]', table).forEach(tr => {
const date = Number(tr.dataset.day),
tds = $$('td', tr);
//Account for empty padder cells:
const cells = [];
tds.forEach(td => {
let span = td.colSpan;
if(span === 1) {
cells.push(td.textContent.trim());
}
else {
while((span--) > 0) { cells.push(''); }
}
});
let up1 = parseEvent('up', date, cells[0], cells[1]),
down = parseEvent('down', date, cells[2], cells[3]),
up2 = parseEvent('up', date, cells[4], cells[5]),
max = parseEvent('max', date, cells[6], cells[7]);
if(max) {
max.illum = parseFloat(cells[9]);
}
events.push(up1, down, up2, max);
});
//Order the events into a moon's rise, max and set:
events = events.filter(x => x).sort((a, b) => a.time - b.time);
//Fake first moonrise and last moonset if we have a max:
const e1 = events[0],
e2 = events[1],
e99 = events[events.length - 2],
e100 = events[events.length - 1];
if(e1.type === 'max') {
const up0 = createEvent('up', e1.time - (e2.time - e1.time));
events.unshift(up0);
}
if(e100.type === 'max') {
const down101 = createEvent('down', e100.time + (e100.time - e99.time));
}
while(events[0].type !== 'up') { events.shift(); }
//console.log('Events: ', events);
//Group the events into a moon's rise, max and set:
const moons = [];
for(let i = 2; i < events.length; i += 3) {
if(events[i].type !== 'down') { throw 'Order WTF?! - ' + i; }
moons.push({
up: events[i-2],
max: events[i-1],
down: events[i],
});
}
console.log('Moons:', moons);
/* Render the "calendar" */
const chart = ud.createElement('.moon-chart'),
base = ud.createElement('.day-base', chart);
function toBasePct(x) {
return (x * 100).toFixed(1) + '%';
}
function toBase(x) {
//console.log(x, base.clientWidth);
return Math.round(x * base.clientWidth);
}
/* Not quite reliable..
function moonPos(m) {
function isEast (degr) { return (degr < 180); }
function isSouth(degr) { return (degr > 90) && (degr < 270); }
function posInfo(degr) {
if(!degr) { return null; }
return {
degr: degr,
isEast: isEast(degr),
isSouth: isSouth(degr),
};
}
//The moon rotates very slowly around the earth (.56 degrees/hr),
//so its perceived motion across the sky is caused by earth's rotation around itself (15 degrees/hr).
//We see it moving from east to west. Far north, it is always to the south, and vice versa.
//https://www.quora.com/Does-the-moon-orbit-the-earth-at-the-equator-Does-the-orbit-vary-much
//https://cseligman.com/text/sky/moonmotion.htm
const p1 = posInfo(m.up.degr),
p2 = posInfo(m.down.degr);
if(!p1 || !p2) { return '?'; }
let makesSense = true;
if(p2.isEast || !p1.isEast) {
makesSense = false;
}
if(p1.isSouth !== p2.isSouth) {
makesSense = (m.max.degr > 80);
}
if(!makesSense) { throw 'Pos WTF?!'; }
const hoursUp = (m.down.time - m.up.time) * 24,
toSouth = (hoursUp < 12) ? p1.isSouth : !p1.isSouth;
const degsTraveled = toSouth ? (p2.degr - p1.degr) : (p1.degr + 360-p2.degr),
speed = degsTraveled/hoursUp,
centerDegr = toSouth ? (p2.degr + p1.degr)/2 : p1.degr - (p1.degr + 360-p2.degr)/2;
console.log(`pos ${m.max.raw.date} ${toSouth ? 'S' : 'N'} ${hoursUp.toFixed(1)}h\t(${centerDegr}°\t${speed.toFixed(1)}°/h)`);
return centerDegr;
}
*/
moons.forEach((m, i) => {
const illum1 = i ? moons[i-1].max.illum : m.max.illum,
illum2 = i ? m.max.illum : moons[i+1].max.illum;
m.wane = illum1 > illum2;
const path = ud.createElement('svg.moon-path', base, {
style: `left:${toBasePct(m.up.time)};`, //width:${toBasePct(m.down.time - m.up.time)}; height:${toBasePct(2 * m.max.degr/90)};`
width: toBasePct(m.down.time - m.up.time),
height: toBasePct(m.max.degr/90),
viewBox: '0,0 100,50',
preserveAspectRatio: 'none',
});
ud.createElement('path', path, {
d: 'M0,50 Q50,-50 100,50',
});
ud.createElement('.east-west-marker', base, {
style: `left:${toBasePct(m.max.time)}; height:${toBasePct(m.max.degr/90)};`
});
const moon = ud.createElement('.moon', base, {
title: `${m.max.raw.time} ${m.max.illum}%`, //@${moonPos(m)}`,
style: `left:${toBasePct(m.max.time)}; bottom:${toBasePct(m.max.degr/90)};`
});
if(m.wane) { moon.classList.add('wane'); }
const pct = m.max.illum/100,
small = (pct < .5),
half = small ? (1 - 2*pct) : 2 * (pct-.5);
moon.innerHTML = `<svg width="10" height="10" viewBox="0,0 2,2">
<circle cx="1" cy="1" r="1" fill="currentColor" />
<path d="M1,0 a1,1 0 1,0 0,2 a${half},1 0 1,${small ? 0 : 1} 0,-2" />
</svg>`;
});
for(let d = 1; d <= e100.raw.date; d++) {
const day = ud.createElement('.day', chart, {
'data-date': d,
}),
hours = ud.createElement('.hours', day);
['03', '06', '09', '12', '15', '18', '21'].forEach(h => ud.createElement('span', hours, { 'data-hour': h }));
}
return chart;
}
(function() {
const ud = ABOUtils.DOM,
[$, $$] = ud.selectors();
const form = $('form'),
container = $('#container'),
shortcuts = {},
loader = $('#loader');
$$('#shortcuts a').forEach(a => shortcuts[a.id] = a);
//Stored (html) pages from timeanddate.com
const htmlStore = new CompressedForage('moon-data');
function onError(err) {
alert(err);
toggleLoader(false);
}
function toggleLoader(show) {
//loader.style.display = show ? 'flex' : 'none';
show ? loader.classList.add('show') : loader.classList.remove('show');
}
function formSerialize(form) {
//https://stackoverflow.com/a/44033425/1869660
const data = new FormData(form);
return new URLSearchParams(data).toString();
}
function formDeserialize(form, data) {
const entries = (new URLSearchParams(data)).entries();
let entry;
while(!(entry = entries.next()).done) {
const [key, val] = entry.value,
//http://javascript-coder.com/javascript-form/javascript-form-value.phtml
input = form.elements[key];
switch(input.type) {
case 'checkbox': input.checked = !!val; break;
default: input.value = val; break;
}
}
}
function currDate() {
const date = new Date(),
year = date.getFullYear(),
month = date.getMonth() + 1,
day = date.getDate();
return {
year,
month,
day,
};
}
function htmlKey(year, month, url) {
month = ('0' + month).slice(-2);
return `${year}-${month}-${url}`;
}
function init(callback) {
//Form values (stored + default):
const stored = localStorage.ABO_moonPhases;
if(stored) {
console.log('Stored:', stored);
formDeserialize(form, stored);
}
const now = currDate();
form.elements.year.value = now.year;
form.elements.month.value = now.month;
function updateChart() {
localStorage.ABO_moonPhases = formSerialize(form);
doIt();
}
ud.addEvent(form, 'submit', e => {
e.preventDefault();
updateChart();
});
//For convenience, also update the chart when we click on a new month:
ud.addEvent(form, 'change', e => {
if(e.target.type === 'radio') {
updateChart();
}
});
//Scrolling the chart:
ud.addEvent($('#shortcuts'), 'click', e => {
e.preventDefault();
const chart = container.firstElementChild;
switch(e.target.id) {
case 'start': chart.scrollLeft = 0; break;
case 'end': chart.scrollLeft = chart.scrollWidth; break;
case 'now': chart.scrollLeft = $(`[data-date="${now.day}"]`, chart).offsetLeft; break;
}
})
//Delete stored pages that are more than a month old:
htmlStore._onError = onError;
function deleteHtml(keys, callback) {
if(keys && keys.length) {
const next = keys.shift();
console.log('Deleting ' + next);
htmlStore.removeItem(next, () => deleteHtml(keys, callback));
}
else {
callback();
}
}
const prevMonthKey = htmlKey(now.year, now.month-1, ''),
storedKeys = htmlStore.getKeys(keys => {
const toDelete = keys.filter(k => k < prevMonthKey);
deleteHtml(toDelete, callback);
});
}
function doIt() {
toggleLoader(true);
function getParam(key, toLower) {
let val = form.elements[key].value;
if(toLower) { val = val.toLowerCase(); }
return val;
}
function parseHtml(html) {
//console.log('html\n' + html);
let table = html.substring(html.indexOf('<table id=tb-7dmn'),
html.indexOf('</table>')) + '</table>';
//console.log('table\n' + table);
const parser = ud.createElement('div');
parser.innerHTML = table;
const tableElm = parser.firstElementChild;
container.innerHTML = '';
container.appendChild(createTimeline(tableElm));
const nowLink = shortcuts.now;
if(isNow) {
nowLink.textContent = now.day;
nowLink.style.display = 'inline-block';
nowLink.click();
}
else {
nowLink.style.display = 'none';
}
toggleLoader(false);
}
//https://www.timeanddate.com/moon/norway/hamar?month=9&year=2018
const y = Number(getParam('year')),
m = Number(getParam('month')),
now = currDate(),
isNow = (y === now.year) && (m === now.month);
//We store the moon data (scraped html pages) locally,
//as the moon's path and phases are unlikely to change:
const url = `https://www.timeanddate.com/moon/${getParam('country', true)}/${getParam('city', true)}?month=${m}&year=${y}`,
storeKey = htmlKey(y, m, url);
htmlStore.getItem(storeKey, html => {
if(html) {
console.log('Found stored ' + url);
parseHtml(html);
}
else {
//*/
console.log('Fetching ' + url);
fetch('https://cors-anywhere.herokuapp.com/' + url)
.then(r => r.text())
.then(html => {
htmlStore.setItem(storeKey, html, () => parseHtml(html));
})
.catch(onError);
//*/
}
});
}
init(doIt);
})();
body {
font-family: Georgia, sans-serif;
header h2 {
margin-bottom: 0;
}
button, input {
font: inherit;
box-sizing: border-box;
}
}
#loader {
position: absolute;
top:0;left:0;bottom:0;right:0;
display: flex;
justify-content: center;
align-items: center;
background: rgba(white, .5);
opacity: 0;
pointer-events: none;
transition: opacity .5s;
&.show {
opacity: 1;
pointer-events: auto;
}
&::before {
content: 'Loading...';
display: inline-block;
font-size: 1.5em;
width: 7em;
height: 7em;
line-height: 6;
text-align: center;
border-radius: 100%;
background: palegreen;
//color: white;
}
}
form {
margin: 1em 0;
> div {
}
label {
cursor: pointer;
white-space: nowrap;
}
.caption {
padding-right: .5em;
}
.location, .month-picker {
display: inline-block;
}
.location {
//display: table;
margin: 0 .5em .5em 0;
label {
display: table-row;
> * { display: table-cell; }
}
input {
width: 100%;
min-width: 5em;
}
}
@supports (display: grid) {
& {
display: grid;
grid-template-areas: "loc month"
". month";
//grid-template-columns: minmax(1fr, 15em) auto;
grid-template-rows: 1fr auto;
grid-column-gap: 1em;
justify-content: start;
justify-items: stretch;
}
.location {
grid-area: loc;
margin: 0;
}
.month-picker {
grid-area: month;
}
button[type="submit"] {
width: 100%;
}
}
}
.month-picker {
@mixin hide() {
position: absolute;
z-index: -1;
opacity: 0;
}
display: inline-flex;
flex-flow: column nowrap;
//align-items: center;
border: 1px solid gainsboro;
.year {
//background: gainsboro;
padding: .2em .4em;
input {
width: 4em;
}
}
.month {
.caption {
@include hide();
}
.inputs {
display: flex;
flex-flow: row wrap;
width: 9em;
label {
flex: 1 1 30%;
margin: 1px;
input {
@include hide();
}
span {
display: inline-block;
width: 100%;
padding: .5em 0;
text-align: center;
background: aliceblue;
border: 1px solid lightskyblue;
box-sizing: border-box;
}
span:hover,
input:checked + span {
background: gold;
border-color: orange;
}
}
}
}
}
#container {
margin: 1em 0;
}
#shortcuts {
a {
display: inline-block;
width: 2.5em;
height: 2.5em;
white-space: nowrap;
text-indent: 100%;
overflow: hidden;
background: url("data:image/svg+xml,%3Csvg style='color:tomato;fill:black' xmlns='http://www.w3.org/2000/svg' width='1200' height='400' viewBox='0 0 555 185'%3E %3Cdefs%3E %3Cpath id='cal' d='M22 143h29v-29H22v29zm35 0h32v-29H57v29zm-35-36h29V75H22v32zm35 0h32V75H57v32zM22 69h29V40H22v29zm74 74h32v-29H96v29zM57 69h32V40H57v29zm77 74h29v-29h-29v29zm-38-36h32V75H96v32zM60 21V-8l-1-2-2-1h-6l-2 1-1 2v29l1 2 2 1h6l2-1 1-2zm74 86h29V75h-29v32zM96 69h32V40H96v29zm38 0h29V40h-29v29zm3-48V-8l-1-2-2-1h-7l-2 1-1 2v29l1 2 2 1h7l2-1 1-2zm38-6v128q1 5-3 9t-9 3H22q-5 0-9-3t-4-9V15q0-6 4-9t9-4h13V-8q0-6 4-11t12-5h6q7 0 11 5t5 11V2h39V-8q0-6 4-11t12-5h6q7 0 11 5t5 11V2h13q6 0 9 4t3 9z'/%3E %3C/defs%3E %3Csvg x='0' width='185' viewBox='0 -26 185 185'%3E %3Ccircle id='start' cx='36' cy='54' r='30' fill='currentColor' stroke='none' /%3E %3Cuse href='%23cal' /%3E %3C/svg%3E %3Csvg x='185' width='185' viewBox='0 -26 185 185'%3E %3Ccircle id='now' cx='112' cy='91' r='30' fill='currentColor' stroke='none' /%3E %3Cuse href='%23cal' /%3E %3C/svg%3E %3Csvg x='370' width='185' viewBox='0 -26 185 185'%3E %3Ccircle id='end' cx='149' cy='128' r='30' fill='currentColor' stroke='none' /%3E %3Cuse href='%23cal' /%3E %3C/svg%3E %3C/svg%3E") center/300% no-repeat;
&#start {
background-position: 0;
}
&#now {
background-position: 50%;
}
&#end {
background-position: 100%;
}
}
}
$day-width: 200px;
$day-height: 100px;
.moon-chart {
position: relative;
display: flex;
align-items: flex-end;
overflow-x: auto;
overflow-y: hidden;
$altitude: royalblue;
background: dodgerblue;
background-image:
repeating-linear-gradient(90deg, white 0, white 1px, transparent 0, transparent $day-width),
repeating-linear-gradient(180deg, transparent 0, transparent $day-height*.33, $altitude 0, $altitude $day-height*.34);
background-attachment: local;
.day-base {
position: absolute;
bottom: 0;
display: block;
width: $day-width;
height: $day-height;
.moon-path {
position: absolute;
bottom: 0;
path {
fill: none;
stroke: white;
stroke-width: 2;
stroke-dasharray: 6;
}
}
.east-west-marker {
position: absolute;
bottom: 0;
//52% instead of 50%:
//Because of the moon's own orbit speed, it takes a little longer than 24hrs to circle around to the same point on the horizon:
//https://cseligman.com/text/sky/moonmotion.htm
width: 52%;
box-sizing: border-box;
border: 1px dashed yellow;
border-top: none;
transform: translateX(-50%);
&::before, &::after {
display: block;
position: absolute;
top: -1em;
color: white;
}
&::before {
content: 'E';
left: -.3em;
}
&::after {
content: 'W';
right: -.4em;
}
}
.moon {
position: absolute;
display: block;
width: 1.5em;
height: 1.5em;
margin: -.75em;
//background: rgba(white, .8);
//border-radius: 100%;
svg {
width: 100%;
height: 100%;
color: white;
fill: royalblue;
}
&.wane {
transform: rotate(180deg);
}
}
}
.day {
flex: 0 0 auto;
position: relative;
display: block;
width: $day-width;
height: $day-height;
$dark: rgba(black, 0.6);
background-image:
//url("data:image/svg+xml,%3Csvg fill='none' stroke='white' stroke-width='.2' xmlns='http://www.w3.org/2000/svg' width='120' height='10' viewBox='0,0, 24,2'%3E %3Cpath d='M0,2v-2 M12,2v-2 M24,2v-2' /%3E %3Cpath d='M6,2v-1.5 M18,2v-1.5'/%3E %3Cpath d='M3,2v-1 M9,2v-1 M15,2v-1 M21,2v-1'/%3E %3C/svg%3E"),
linear-gradient(90deg, $dark 0, $dark $day-width*.2, rgba(0,0,0,0) $day-width*.3, rgba(0,0,0,0) $day-width*.7, $dark $day-width*.8, $dark $day-width),
linear-gradient(180deg, transparent $day-height*.95, green 0);
//background-repeat: no-repeat;
//background-size: 100%, auto, auto;
//background-position: center 100%;
pointer-events: none;
box-sizing: border-box;
border-right: 1px dashed white;
&::after {
content: attr(data-date);
display: block;
width: 1.5em;
height: 1.5em;
margin: auto;
text-align: center;
line-height: 1.5;
background: #ff9;
border-radius: 100%;
}
.hours {
position: absolute;
bottom:0; left:0; width: 100%;
display: flex;
justify-content: space-evenly;
span {
position: relative;
display: inline-block;
//width: 2px;
height: 1.4em;
font-family: sans-serif;
font-size: .7em;
color: white;
&::before {
content: '';
display: block;
position: absolute;
width: 2px;
height: 2px;
transform: translateX(-50%);
background: currentColor;
}
&::after {
content: attr(data-hour);
position: absolute;
bottom:0;
transform: translateX(-50%);
}
}
}
}
} //.moon-chart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment