block by fil cb7930254c9e7062114678d62d9be5ac

Furuti cubic projection #3

Full Screen

Carlos Furuti’s cubic globe #3 - http://www.progonos.com/furuti/MapProj/Normal/ProjPoly/projPoly2.html

Research for d3-geo-projection/pull/86 and d3-geo/issues/46.

Re-incorporating Jason Davies’ clipPolygon() code into d3v4.

Code base at Fil/d3-geo:clip-polygon.

See also https://bl.ocks.org/Fil/694ba0d0bc1fc4c24eb257dc210eb01a

index.html

<!DOCTYPE html>   
<html>
<head>
  <meta charset="utf-8">
  <style>
  .poly {
  fill: #999;
  fill-opacity: 0.4;
  stroke: black;
  stroke-width: .5px;
  }
  .graticule {
  fill: none;
  stroke: #777;
  stroke-width: 0.3px;
  stroke-opacity: 0.5;
  }
  .sphere {
  fill: none;
  stroke: black;
  stroke-width: 2px;
  }
  .face {
   fill: lightblue;
  }
  </style>
  <title>Furuti 3</title>
</head>
<body>
  <script src="https://d3js.org/d3.v4.min.js"></script> 
  <script src="https://d3js.org/d3-geo-projection.v2.min.js"></script> 
<script src="https://unpkg.com/d3-geo"></script>
<script src="https://unpkg.com/d3-geo-polygon"></script>
<script src="https://recifs.neocities.org/d3-geo-projection-clip-polyhedral.js"></script>
  <script src="versor.js"></script>
  <script>

  var width = 960, height = 500;
  var scaleProj = Math.min(width/2, height)/Math.PI;

    // imports
    var atan = Math.atan, sqrt1_2 = Math.sqrt(1/2), pi = Math.PI, degrees = 180 / Math.PI;
    var d3Geo = d3, polyhedral = d3.geoPolyhedral;
    
var phi1 = atan(sqrt1_2) * degrees;

var cube = [
  [0, phi1], [90, phi1], [180, phi1], [-90, phi1],
  [0, -phi1], [90, -phi1], [180, -phi1], [-90, -phi1]
];

var cube$1 = [
  [0, 3, 2, 1], // N
  [0, 1, 5, 4],
  [1, 2, 6, 5],
  [2, 3, 7, 6],
  [3, 0, 4, 7],
  [4, 5, 6, 7] // S
].map(function(face) {
  return face.map(function(i) {
    return cube[i];
  });
});

var f3 = function(faceProjection) {

  // it is possible to pass a specific projection on each face
  // by default is is a gnomonic projection centered on the face's centroid
  // scale 1 by convention
  faceProjection = faceProjection || function(face) {
    var c = d3Geo.geoCentroid({type: "MultiPoint", coordinates: face});
    return d3Geo.geoGnomonic().scale(1).translate([0, 0]).rotate([-c[0], -c[1]]);
  };

  // the faces from the cube each yield
  // - face: its four vertices
  // - contains: does this face contain a point?
  // - project: local projection on this face
  var faces = cube$1.map(function(face) {
    var polygon = face.slice();
    polygon.push(polygon[0]);
    return {
      face: face,
      contains: function(lambda, phi) {
        return d3Geo.geoContains({ type: "Polygon", coordinates: [ polygon ] },
          [lambda * degrees, phi * degrees]);
      },
      project: faceProjection(face)
    };
  });

  // Build a tree of the faces, starting with face 0 (North Pole)
  // which has no parent (-1); the next four faces around the equator
  // are attached to the north face (0); the face containing the South Pole
  // is attached to South America (4)
  [-1, 0, 0, 0, 0, 4].forEach(function(d, i) {
    var node = faces[d];
    node && (node.children || (node.children = [])).push(faces[i]);
  });

  // Polyhedral projection
  var proj = polyhedral(faces[0], function(lambda, phi) {
      for (var i = 0; i < faces.length; i++) {
        if (faces[i].contains(lambda, phi)) return faces[i];
      }
    },
    -pi/4,    // rotation of the root face in the projected (pixel) space
    true // clipPolygon
  )
  .rotate([28,-4,0]) // See that California touches the corner, and Australia
  //.clipAngle(1) // no antimeridian clipping on the Sphere
  .fitExtent([[20,20], [width-20,height-20]], {type:"Sphere"});
   
  proj.faces = faces;
  return proj;
}

    d3.geoPolyhedralFuruti3 = f3;
    
  var projection = d3.geoPolyhedralFuruti3();
  var path = d3.geoPath().projection(projection);
  var graticule = d3.geoGraticule();

  var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);
    
  svg.call(d3.drag().on("start", dragstarted).on("drag", dragged));

  var movable = svg.append("g");
  var countries = movable.append("g")
    .attr("class", "poly");
  
  d3.json('countries.geo.json', function(err, world) {

    countries
    .selectAll('path')
    .data(world.features)
    .enter()
    .append('path')
    .attr("d", path);
  });

  movable.selectAll(".graticule")
    .data(graticule.lines)
    .enter().append("path")
    .attr("class", "graticule")
    .attr("d", path);


var rot = projection.rotate();
projection.rotate([]);
  svg.append("path")
    .datum({type: "MultiPoint", coordinates: projection.faces.map(function(face) {
      return d3.geoCentroid({type: "MultiPoint", coordinates: face.face});
    })})
    .attr("class", "face")
    .attr("d", path);
projection.rotate(rot);

  movable.append('path')
  .datum({type: "Point", coordinates: [0,90]})
  .attr('d', path);
  movable.append('path')
  .datum({type: "Point", coordinates: [0,-90]})
  .attr('d', path);

  svg.append('path')
  .datum({type: "Sphere"})
    .attr("class", "sphere")
  .attr('d', path);


var render = function() {
	movable.selectAll('path').attr('d', path);
},
  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();
  q0 = versor(r0);
}

function dragged() {
  var inv = projection.rotate(r0).invert(d3.mouse(this));
  if (!inv || isNaN(inv[0])) return;
  var v1 = versor.cartesian(inv),
    q1 = versor.multiply(q0, versor.delta(v0, v1)),
    r1 = versor.rotation(q1);
  projection.rotate(r1);
  render();
}

// projection.rotate([29, -11, 12]) && render()

  </script>
</body>
</html>

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;
})));