Showing posts with label Geolocator. Show all posts
Showing posts with label Geolocator. Show all posts

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.beginPath();
      planet.path.context(context)(circle);
      context.fill();
      context.stroke();

      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)();
        context.beginPath();
        planet.path.context(context)(circle);
        context.fill();
      }
    };

    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.remove("*");
    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.ephemeris.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.

Saturday, December 28, 2019

D3-Celestial Geolocator: Markers

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

A new feature for my d3-celestial star map: Celestial.addCallback allows to define a callback function that will be executed in the local client context every time the map is redrawn. This enables other display elements to react to changes, for example the marker position in the geolocator-globe above. Usage is pretty simple: add an anonymous function as the parameter for addCallback:

  Celestial.addCallback(function () {
    var loc = Celestial.location();
    globe.plugins.markers.remove("*");
    globe.plugins.markers.add(loc[1], loc[0]);
  });

It also demonstrates the other new feature, a geo-marker, implemented as a plugin for the geolocator-globe. This takes the position of the last mouse position and puts a marker on it. These can be of different colors and sizes, so can be added in any form or number you like. The remove function takes an asterisk to remove all, an arry index or the exact position to remove one.

  function markers(config) {
    var marks = [];
    config = config || {};

    var addMark = function(lng, lat, options) {
      options = options || {};
      options.color = options.color || config.color || 'white';
      options.size = options.size || config.size || 5;
      var mark = { options: options };
      if (config.latitudeFirst) {
        mark.lat = lng;
        mark.lng = lat;
      } else {
        mark.lng = lng;
        mark.lat = lat;
      }
      marks.push(mark);
    };

    var removeMark = function(lng, lat) {
      if (lng === "*") {
        marks = [];
        return;
      }    
      if (arguments.length === 1 && lng < marks.length) {
        marks.splice(lng, 1);
        return;
      }
      if (arguments.length === 2) {
        for (var i=0; i <= marks.length; i++) {
          if (marks[i].lng === lng && marks[i].lat === lat) {
            marks.splice(i, 1);
            return;  
          }
        }
      }
    }

    var drawMarks = function(planet, context) {
      for (var i = 0; i < marks.length; i++) {
        var mark = marks[i];
        drawMark(planet, context, mark);
      }
    };

    var drawMark = function(planet, context, mark) {
      var color = mark.options.color,
          size = mark.options.size * 5,
          pos = planet.projection([mark.lng, mark.lat]);
          
      context.fillStyle = color;
      context.beginPath();
      context.moveTo(pos[0], pos[1]);
      context.arc(pos[0], pos[1] - size, size/2, Math.PI, 0);
      context.fill();
      context.fillStyle = "#fff";
      context.beginPath();
      context.arc(pos[0], pos[1]- size, size/4, 0, 2 * Math.PI);
      context.fill();

    };

    return function (planet) {
      planet.plugins.markers = {
        add: addMark,
        remove: removeMark
      };

      planet.onDraw(function() {
        planet.withSavedContext(function(context) {
          drawMarks(planet, context);
        });
      });
    };
  };

[Next: part IV Night sky.]

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

Monday, November 18, 2019

D3-Celestial Geolocator

I was looking for a simple graphical location-selector, a globe that can be spun, zoomed in and clicked on to get an approximate location. I found none so obviously I had to make my own. A good starting point was planetary.js which already takes care of the spinnable and zoomable globe and is pretty easy to extend with plugins. See my fork for details about the mouse-actions plugin.

All the work is done in a callback function for the mouse actions mousedown and mouseup. We could just use click, but that would also cause a positioning update on dragging actions on the globe. Only updating when the mouse coordinates don't change between down and up takes care of that. The rest is pretty straight forward, take mouse coordinates, calculate geographic position with the d3.js function projection.invert, and if that is valid (i.e. inside the globe) update the geolocation of the sky view.

  globe.loadPlugin(mouse({
    onMousedown: function() {
      var x = d3.event.offsetX,
          y = d3.event.offsetY;
      position = [x, y];
    },
    onMouseup: function() {
      var x = d3.event.offsetX,
          y = d3.event.offsetY,
          format = d3.format("-.3f");
      if (position[0] !== x || position[1] !== y) return;

      var pos = this.projection.invert([x,y]);
      if (!isNaN(pos[0])) {
        // latitude, longitude convention is the opposite for sky coordinates
        Celestial.skyview({"location": [format(pos[1]), format(pos[0])]});
      }   
      return pos;
    }
  }));

Time zones are not taken into account yet, so the result is not necessarily valid. That will be fixed later, as well as putting a position marker on the globe, showing the current terminator between day and night, and optionally also show the current sky state on the current view changing between blue and transparent.

[Next: part II Sky color.]