|
<!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 1</title> |
|
</head> |
|
<body> |
|
<script src="https://d3js.org/d3.v4.min.js"></script><!-- https://github.com/d3/d3/releases/tag/v4.11.0 minimum, for projection.preclip() --> |
|
<script src="https://recifs.neocities.org/d3-geo-projection-clip-polyhedral.js"> </script><!-- this unpublished version uses d3-geo-polygon if available --> |
|
<script src="https://unpkg.com/d3-geo-polygon"></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 f1 = 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, 4, 5, 2, 0, 1].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/2 // rotation of the root face in the projected (pixel) space |
|
) |
|
//.clipAngle(1) // no antimeridian clipping on the Sphere |
|
.fitExtent([[20,20],[width-20, height-20]], {type:"Sphere"}) |
|
|
|
proj.faces = faces; |
|
return proj; |
|
}; |
|
|
|
d3.geoPolyhedralFuruti1 = f1; |
|
|
|
|
|
|
|
var projection = d3.geoPolyhedralFuruti1() |
|
.rotate([30,0]); |
|
// var preclip = projection.preclip(); |
|
//projection.preclip(s => d3.geoClipRectangle(-0.6,-0.2, 0.5,0.5)(preclip(s))) |
|
//projection.postclip(d3.geoClipRectangle(150,100, 250, 200)) |
|
|
|
//projection.postclip(d3.geoClipRectangle(0,100, 250, 200)) |
|
|
|
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 rotate = projection.rotate(); |
|
projection.rotate([0,0]); |
|
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(rotate); |
|
|
|
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> |