Skip to content

Instantly share code, notes, and snippets.

@mourner
Last active January 5, 2025 17:46
Show Gist options
  • Save mourner/09abba95f274480b6eb38a5388df8848 to your computer and use it in GitHub Desktop.
Save mourner/09abba95f274480b6eb38a5388df8848 to your computer and use it in GitHub Desktop.
Mapbox GL JS + SunCalc hack Friday
html, body {
margin: 0;
font-family: sans-serif;
}
#map {
position: absolute;
top: 45px;
bottom: 0;
left: 0;
right: 0;
}
input {
width: calc(100% - 6em);
}
#viz {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
pointer-events: none;
z-index: 1;
}
#circle {
fill: none;
stroke: black;
stroke-width: 2px;
}
#sun-dir {
stroke: #aaa;
stroke-width: 7px;
}
#sun-pos {
stroke: orange;
stroke-width: 7px;
}
#sunrise {
stroke: yellow;
stroke-width: 4px;
}
#sunset {
stroke: #f73;
stroke-width: 4px;
}
#sun, #eye, #sunset-icon, #sunrise-icon {
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
#sun {
stroke: orange;
fill: yellow;
}
#sunrise-icon, #sunset-icon {
fill: none;
stroke: #777;
}
#eye {
stroke: black;
fill: white;
}
<!DOCTYPE html>
<html>
<head>
<title>SunCalc — sun position, sunlight phases, sunrise, sunset, dusk and dawn times</title>
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css" rel="stylesheet" />
<link href="index.css" rel="stylesheet" />
</head>
<body>
<input id="day" type="range" min="1" max="365" /> <span id="day-text"></span>
<input id="time" type="range" min="0" max="86400" /> <span id="time-text"></span>
<div id="map">
<svg id="viz" xmlns="http://www.w3.org/2000/svg" version="1.1">
<!-- <circle id="circle" /> -->
<line id="sun-dir" />
<line id="sunrise" />
<line id="sunset" />
<line id="sun-pos" />
<svg id="eye" width="24" height="24" viewBox="0 0 24 24">
<path d="M1,12S5,4,12,4s11,8,11,8-4,8-11,8S1,12,1,12Z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg id="sun" width="32" height="32" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg id="sunrise-icon" width="24" height="24" viewBox="0 0 24 24">
<path d="M17,18A5,5,0,0,0,7,18"/>
<line x1="12" y1="2" x2="12" y2="9"/>
<line x1="4.22" y1="10.22" x2="5.64" y2="11.64"/>
<line x1="1" y1="18" x2="3" y2="18"/>
<line x1="21" y1="18" x2="23" y2="18"/>
<line x1="18.36" y1="11.64" x2="19.78" y2="10.22"/>
<line x1="23" y1="22" x2="1" y2="22"/>
<polyline points="8 6 12 2 16 6"/>
</svg>
<svg id="sunset-icon" width="24" height="24" viewBox="0 0 24 24">
<path d="M17,18A5,5,0,0,0,7,18"/>
<line x1="12" y1="9" x2="12" y2="2"/>
<line x1="4.22" y1="10.22" x2="5.64" y2="11.64"/>
<line x1="1" y1="18" x2="3" y2="18"/>
<line x1="21" y1="18" x2="23" y2="18"/>
<line x1="18.36" y1="11.64" x2="19.78" y2="10.22"/>
<line x1="23" y1="22" x2="1" y2="22"/>
<polyline points="16 5 12 9 8 5"/>
</svg>
</svg>
</div>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://unpkg.com/[email protected]/dist/gl-matrix-min.js"></script>
<script src="index.js"></script>
</body>
</html>
'use strict';
mapboxgl.accessToken = 'pk.eyJ1IjoibW91cm5lciIsImEiOiJWWnRiWG1VIn0.j6eccFHpE3Q04XPLI7JxbA';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v10',
center: [20, 40],
zoom: 1.6,
scrollZoom: {around: 'center'},
hash: true
});
map.addControl(new mapboxgl.NavigationControl());
map.addControl(new mapboxgl.GeolocateControl());
map.on('load', function() {
map.addLayer({
'id': '3d-buildings',
'source': 'composite',
'source-layer': 'building',
'filter': ['==', 'extrude', 'true'],
'type': 'fill-extrusion',
'minzoom': 15,
'paint': {
'fill-extrusion-color': '#afa7a0',
'fill-extrusion-height': {
'type': 'identity',
'property': 'height'
},
'fill-extrusion-base': {
'type': 'identity',
'property': 'min_height'
},
'fill-extrusion-opacity': .6
}
});
});
var date = new Date();
var start = new Date(date.getFullYear(), 0, 1);
var diff = date - start;
var day = getDOY(date);
var time = date.getHours() * 60 * 60 + date.getMinutes() * 60 + date.getSeconds();
// var circle = document.getElementById('circle');
var sunDirLine = document.getElementById('sun-dir');
var sunPosLine = document.getElementById('sun-pos');
var sunriseLine = document.getElementById('sunrise');
var sunsetLine = document.getElementById('sunset');
var sunIcon = document.getElementById('sun');
var eyeIcon = document.getElementById('eye');
var sunriseIcon = document.getElementById('sunrise-icon');
var sunsetIcon = document.getElementById('sunset-icon');
var timeInput = document.getElementById('time');
var dayInput = document.getElementById('day');
var timeText = document.getElementById('time-text');
var dayText = document.getElementById('day-text');
dayInput.value = day;
timeInput.value = time;
dayInput.oninput = function () {
day = +dayInput.value;
date = new Date(start.valueOf());
date.setDate(day);
date.setHours(Math.floor(time / 60 / 60));
date.setMinutes(Math.floor(time / 60) % 60);
date.setSeconds(time % 60);
draw();
}
timeInput.oninput = function () {
time = +timeInput.value;
date.setHours(Math.floor(time / 60 / 60));
date.setMinutes(Math.floor(time / 60) % 60);
date.setSeconds(time % 60);
draw();
}
function draw() {
var tr = map.transform;
var cx = tr.width / 2;
var cy = tr.height / 2;
// var r = Math.min(cx, cy) - 5;
var loc = map.getCenter();
var sunPos = SunCalc.getPosition(date, loc.lat, loc.lng);
var sunTimes = SunCalc.getTimes(date, loc.lat, loc.lng);
var sunAngle = Math.PI / 2 + sunPos.azimuth + tr.angle;
var sunriseAngle = SunCalc.getPosition(sunTimes.sunrise, loc.lat, loc.lng).azimuth + Math.PI / 2 + tr.angle;
var sunsetAngle = SunCalc.getPosition(sunTimes.sunset, loc.lat, loc.lng).azimuth + Math.PI / 2 + tr.angle;
var pitchCos = Math.cos(tr.pitch * Math.PI / 180);
var m = new Float64Array(16);
mat4.perspective(m, tr._fov, tr.width / tr.height, 1, 3000);
mat4.scale(m, m, [1, -1, 1]);
mat4.translate(m, m, [0, 0, -tr.cameraToCenterDistance]);
mat4.rotateX(m, m, tr._pitch);
mat4.rotateZ(m, m, tr.angle);
mat4.translate(m, m, [-tr.x, -tr.y, 0]);
var m2 = mat4.create();
mat4.scale(m2, m2, [tr.width / 2, -tr.height / 2, 1]);
mat4.translate(m2, m2, [1, -1, 0]);
mat4.multiply(m, m2, m);
var coord = tr.pointCoordinate(tr.centerPoint, tr.zoom);
var p = [
coord.column * tr.tileSize + Math.sin(-sunPos.azimuth),
coord.row * tr.tileSize + Math.cos(-sunPos.azimuth), Math.sin(sunPos.altitude), 1];
vec4.transformMat4(p, p, m);
var dx = p[0] / p[3] - tr.centerPoint.x;
var dy = p[1] / p[3] - tr.centerPoint.y;
var r2 = Math.min(Math.abs(cx / dx), Math.abs(cy / dy)) - 30;
val(eyeIcon.x, cx - val(eyeIcon.width) / 2);
val(eyeIcon.y, cy - val(eyeIcon.height) / 2);
var isDay = sunPos.altitude > -0.833 * Math.PI / 180;
if (isDay) {
drawGroundLine(sunDirLine, null, sunAngle, pitchCos, cx, cy, 5);
} else {
hideLine(sunDirLine, null);
}
drawGroundLine(sunriseLine, sunriseIcon, sunriseAngle, pitchCos, cx, cy, -30);
drawGroundLine(sunsetLine, sunsetIcon, sunsetAngle, pitchCos, cx, cy, -30);
if (isDay) {
drawLine(sunPosLine, sunIcon, cx, cy, r2 * dx, r2 * dy);
} else {
hideLine(sunPosLine, sunIcon);
}
timeText.innerHTML = pad(Math.floor(time / 60 / 60)) + ':' + pad(Math.floor(time / 60 % 60));
dayText.innerHTML = pad(date.getMonth() + 1) + '/' + pad(date.getDate());
map.setLight({
anchor: 'map',
position: [1.5, 180 + sunPos.azimuth * 180 / Math.PI, 90 - sunPos.altitude * 180 / Math.PI],
'position-transition': {duration: 0}
}, {duration: 0});
}
function drawGroundLine(line, icon, angle, pitchCos, cx, cy, pad) {
var sunSin = Math.cos(angle);
var sunCos = Math.sin(angle);
var r = Math.min(Math.abs(cx / sunSin), Math.abs(cy / sunCos / pitchCos)) + pad;
drawLine(line, icon, cx, cy, r * sunSin, r * sunCos * pitchCos);
}
function hideLine(line, icon) {
line.style.display = 'none';
if (icon) icon.style.display = 'none';
}
function drawLine(line, icon, cx, cy, dx, dy) {
line.style.display = '';
val(line.x1, cx);
val(line.y1, cy);
val(line.x2, cx + dx);
val(line.y2, cy + dy);
if (icon) {
icon.style.display = '';
val(icon.x, val(line.x2) - val(icon.width) / 2);
val(icon.y, val(line.y2) - val(icon.height) / 2);
}
}
function val(attr, value) {
if (value !== undefined) {
attr.baseVal.value = value;
} else {
return attr.baseVal.value;
}
}
map.on('load', draw);
map.on('resize', draw);
map.on('move', draw);
function isLeapYear(date) {
var year = date.getFullYear();
if((year & 3) != 0) return false;
return ((year % 100) != 0 || (year % 400) == 0);
};
function getDOY(date) {
var dayCount = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
var mn = date.getMonth();
var dn = date.getDate();
var dayOfYear = dayCount[mn] + dn;
if(mn > 1 && isLeapYear(date)) dayOfYear++;
return dayOfYear;
};
function pad(num) {
return num <= 9 ? '0' + num : num;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment