Tuesday, January 14, 2020

D3 Celestial Gelolocator: Night Sky

[Previously: part I Geolocator, part II Sky color, part III Geomarker]

The next feature for the geolocator-globe, a plugin to display the current night hemisphere with increasing levels of darkness, depending on the state of twilight. The distinction is between civil twilight, which lasts from sunset until the sun is 6 degrees below the horizon, followed by nautical twilight until 12 degrees and finally astronomical twilight lasting to 18 degrees below. In reverse order the same is true for dawn. The subsolar point, where the Sun is directly above, is market with a small yellow circle.

[Update: javascript Date object only knows local time, therefore the timezone offset needs to be calculated from that. For this Celestial also gets a new function: Celestial.timezone(tz) for setting and with no argument getting current timezone.]

A small update for the Geomarker plugin as well, where the marker is invisible when the current position on the globe is rotated out of view.

  // This plugin shows concentric hemispheres with increasing opacities,
  // specifically to show the dark side with increasing levels of twilight.
  function hemisphere(options) {
    var pos = {},
        options = options || {};
    options.color = options.color || 'black';
    options.alpha = options.alpha || 0.12;

    // Current antisolar point, directly opposite of the Sun
    var setOrigin = function(lng, lat) {
      pos.lng = lng;
      pos.lat = lat;

    var drawHemisphere = function(context, planet, pos) {
      // First, the subsolar point as a small yellow circle with black border
      context.fillStyle = "#ff0";
      context.lineStyle = "#000";
      var circle = d3.geo.circle().origin([pos.lng + 180, -pos.lat]).angle(1.5)();

      context.fillStyle = options.color;
      context.globalAlpha = options.alpha;

      // Draw the concentric circles of darkness with the Sun at 0°, 6°, 12° and 18° below the horizon 
      for (var i = 0; i <= 3; i++) {
        circle = d3.geo.circle().origin([pos.lng, pos.lat]).angle(90 - i*6)();

    return function(planet) {
      planet.plugins.hemisphere = {
        origin: setOrigin
      planet.onInit(function() {});
      planet.onDraw(function() {
        if (!pos.hasOwnProperty("lat")) return;
        planet.withSavedContext(function(context) {
          drawHemisphere(context, planet, pos);

  // callback funtion, where the celestial map data is used to update the geolocator globe
  Celestial.addCallback(function () {
    // put the marker on the current location
    var loc = Celestial.location();
    globe.plugins.markers.add(loc[1], loc[0]);
    // Sun location, current date and timezone offset 
    var sol = Celestial.getPlanet("sol"),
         dt = Celestial.date(),
         tz = Celestial.timezone() - dt.getTimezoneOffset();
    if (sol) {
      // lat & lng of current nadir point directly opposite the solar position
      var lat = -sol.pos[1], 
          // Simple assumption: UTC time equals sun angle from Greenwich meridian, trap: dt still is local 
          lng = -(dt.getUTCHours() * 3600 + dt.getUTCMinutes() * 60 + dt.getUTCSeconds() + tz * 60) / 3600 * 15;
      var antisol = [lat, lng];
      globe.plugins.hemisphere.origin(antisol[1], antisol[0]);

Time zone still needs to be set manually and horizontal refraction isn't considered yet, so the result need not be entirely accurate.

No comments:

Post a Comment