Last active
January 5, 2025 17:46
-
-
Save mourner/09abba95f274480b6eb38a5388df8848 to your computer and use it in GitHub Desktop.
Mapbox GL JS + SunCalc hack Friday
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
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; | |
} |
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
<!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> |
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
'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