block by fil e5b449606ca1e3e120cda8d08a7f3351

Tetrahedral Gnomonic projection Antarctic

Full Screen

The Tetrahedral Gnomonic projection, published by A. J. Potter in The Geographical Teacher, Vol. 13, No. 1 (SPRING, 1925), pp. 52-56.


Research by Philippe Rivière for d3-geo-projection issue 107.


[](https://github.com/Fil/) Questions and comments welcome on [gitter.im/d3](https://gitter.im/d3/d3), [twitter](https://twitter.com/@recifs) or [slack](https://d3js.slack.com).

index.html

<!DOCTYPE html>
<canvas width="960" height="600"></canvas>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="versor.js"></script>

<style>
path {fill: none; stroke: #444; }
</style>

<script>
var canvas = d3.select("canvas"),
  width = canvas.property("width"),
  height = canvas.property("height"),
  context = canvas.node().getContext("2d");

  
  // retina display
  var devicePixelRatio = window.devicePixelRatio || 1;
  canvas.style('width', canvas.attr('width')+'px');
  canvas.style('height', canvas.attr('height')+'px');
  canvas.attr('width', canvas.attr('width') * devicePixelRatio);
  canvas.attr('height', canvas.attr('height') * devicePixelRatio);
  context.scale(devicePixelRatio,devicePixelRatio);

  
var pi = Math.PI, degrees = 180 / pi, asin1_3 = Math.asin(1 / 3);

var centers = [
  [0, 90],
  [-180, -asin1_3 * degrees],
  [-60, -asin1_3 * degrees],
  [60, -asin1_3 * degrees]
];

d3.geoTetrahedralGnomonic = function(faceProjection) {
  var tetrahedron = [[1, 2, 3], [0, 2, 1], [0, 3, 2], [0, 1, 3]].map(function(
    face
  ) {
    return face.map(function(i) {
      return centers[i];
    });
  });

  faceProjection =
    faceProjection ||
    function(face) {
      var c = d3.geoCentroid({ type: "MultiPoint", coordinates: face });
      return d3
        .geoGnomonic()
        .scale(1)
        .translate([0, 0])
        .rotate([-c[0] * (Math.abs(c[1]) != 90), -c[1]]);
    };

  var faces = tetrahedron.map(function(face) {
    return { face: face, project: faceProjection(face) };
  });

  [-1, 0, 0, 0].forEach(function(d, i) {
    var node = faces[d];
    node && (node.children || (node.children = [])).push(faces[i]);
  });

  return d3
    .geoPolyhedral(
      faces[0],
      function(lambda, phi) {
        lambda *= degrees;
        phi *= degrees;
        for (var i = 0; i < faces.length; i++) {
          if (
            d3.geoContains(
              {
                type: "Polygon",
                coordinates: [[...tetrahedron[i], tetrahedron[i][0]]]
              },
              [lambda, phi]
            )
          ) {
            return faces[i];
          }
        }
      },
      pi / 3
    )
    .clipAngle(180)
    .rotate([-30, 0])
    .fitExtent([[0, 0], [width, height]], { type: "Sphere" });
};

projection = d3.geoTetrahedralGnomonic();

var init_scale = projection.scale(),
  path = d3.geoPath().projection(projection).context(context);

d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(
  error,
  world
) {
  if (error) throw error;

  var land = topojson.feature(world, world.objects.countries);

  render = function() {
    context.fillStyle = "#fff";
    context.fillRect(0, 0, width, height);

    context.beginPath();
    path({type:"Sphere"});
    context.strokeStyle = "black";
    context.stroke(), context.clip(), context.closePath();

    context.beginPath();
    path(land);
    context.fillStyle = "#000";
    context.fill(), context.closePath();

    context.beginPath();
    path(d3.geoGraticule()());
    context.strokeStyle = "#777";
    context.stroke(), context.closePath();

  };

  render();
});

canvas.call(d3.drag().on("start", dragstarted).on("drag", dragged));

var render = function() {},
  v0, // Mouse position in Cartesian coordinates at start of drag gesture.
  r0, // Projection rotation as Euler angles at start.
  q0; // Projection rotation as versor at start.

function dragstarted() {
  v0 = versor.cartesian(projection.invert(d3.mouse(this)));
  r0 = projection.rotate();
  console.log("r0", r0);
  q0 = versor(r0);
}

function dragged() {
  var v1 = versor.cartesian(projection.rotate(r0).invert(d3.mouse(this))),
    q1 = versor.multiply(q0, versor.delta(v0, v1)),
    r1 = versor.rotation(q1);
  projection.rotate(r1);
  render();
}
</script>

versor.js

// Version 0.0.0. Copyright 2017 Mike Bostock.
(function(global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global.versor = factory());
}(this, (function() {'use strict';

var acos = Math.acos,
    asin = Math.asin,
    atan2 = Math.atan2,
    cos = Math.cos,
    max = Math.max,
    min = Math.min,
    PI = Math.PI,
    sin = Math.sin,
    sqrt = Math.sqrt,
    radians = PI / 180,
    degrees = 180 / PI;

// Returns the unit quaternion for the given Euler rotation angles [λ, φ, γ].
function versor(e) {
  var l = e[0] / 2 * radians, sl = sin(l), cl = cos(l), // λ / 2
      p = e[1] / 2 * radians, sp = sin(p), cp = cos(p), // φ / 2
      g = e[2] / 2 * radians, sg = sin(g), cg = cos(g); // γ / 2
  return [
    cl * cp * cg + sl * sp * sg,
    sl * cp * cg - cl * sp * sg,
    cl * sp * cg + sl * cp * sg,
    cl * cp * sg - sl * sp * cg
  ];
}

// Returns Cartesian coordinates [x, y, z] given spherical coordinates [λ, φ].
versor.cartesian = function(e) {
  var l = e[0] * radians, p = e[1] * radians, cp = cos(p);
  return [cp * cos(l), cp * sin(l), sin(p)];
};

// Returns the Euler rotation angles [λ, φ, γ] for the given quaternion.
versor.rotation = function(q) {
  return [
    atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees,
    asin(max(-1, min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees,
    atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees
  ];
};

// Returns the quaternion to rotate between two cartesian points on the sphere.
versor.delta = function(v0, v1) {
  var w = cross(v0, v1), l = sqrt(dot(w, w));
  if (!l) return [1, 0, 0, 0];
  var t = acos(max(-1, min(1, dot(v0, v1)))) / 2, s = sin(t); // t = θ / 2
  return [cos(t), w[2] / l * s, -w[1] / l * s, w[0] / l * s];
};

// Returns the quaternion that represents q0 * q1.
versor.multiply = function(q0, q1) {
  return [
    q0[0] * q1[0] - q0[1] * q1[1] - q0[2] * q1[2] - q0[3] * q1[3],
    q0[1] * q1[0] + q0[0] * q1[1] + q0[2] * q1[3] - q0[3] * q1[2],
    q0[0] * q1[2] - q0[1] * q1[3] + q0[2] * q1[0] + q0[3] * q1[1],
    q0[0] * q1[3] + q0[1] * q1[2] - q0[2] * q1[1] + q0[3] * q1[0]
  ];
};

function cross(v0, v1) {
  return [
    v0[1] * v1[2] - v0[2] * v1[1],
    v0[2] * v1[0] - v0[0] * v1[2],
    v0[0] * v1[1] - v0[1] * v1[0]
  ];
}

function dot(v0, v1) {
  return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2];
}

return versor;
})));