xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
background: #fcfcfa;
width: 1200px;
}
.hex {
stroke: #FF0000;
stroke-width: .2px;
fill: transparent;
opacity: 1;
}
.hex:hover {
fill: red;
opacity: 0.5;
}
.hex.selected:hover {
fill: green;
opacity: 0.5;
}
.hex.selected {
fill: green;
opacity: 0.5;
}
h1 {
color: rgb(51, 102, 0)
font-size: 2.5em;
margin: 0;
}
.author a, .meta a {
color: #000;
}
.author, .meta {
color: #666;
font-style: italic;
font-size: small;
}
.sea {
stroke: none;
fill: #ccf;
}
.outline {
stroke: #000;
stroke-width: 1px;
fill: none;
}
.graticule {
stroke: #000;
stroke-width: .25px;
stroke-opacity: .5;
fill: none;
}
.land {
stroke: #000;
stroke-width: .5px;
fill: #9f9;
}
.face {
stroke: #FF0000;
stroke-width: .5px;
stroke-dasharray: 8,3;
fill: transparent;
opacity: 1;
cursor: pointer;
}
.face:hover {
stroke: #FF0000;
stroke-width: .5px;
stroke-dasharray: 8,3;
fill: red;
opacity: 0.5;
cursor: pointer;
}
.face.selected:hover {
fill: green;
opacity: 0.5;
}
.face.selected {
fill: green;
opacity: 0.5;
}
#faceselector {
position: absolute;
top: 80px;
left: 20px;
}
#HexagonSizeSelector {
position: absolute;
top: 380px;
left: 20px;
}
#renderer {
background: beige;
position: absolute;
top: 80px;
left: 400px;
}
#hexjsonscreen {
background: beige;
position: absolute;
top: 650px;
left: 400px;
}
</style>
<body>
<h1>Gnomonic projection on faces of an icosahedron and associated hexagonal regular grid</h1>
<p class="author">created with <a href="https://d3js.org/">D3.js</a></p>
<div id="faceselector">
<label tyle="display: inline-block; width: 240px; text-align: right">Select a face of the icosahedron foldout : </label>
<div id="foldout"> </div>
</div>
<div id="HexagonSizeSelector">
<label for="GridSize" style="display: inline-block; width: 240px; text-align: left">subdivision = </label>
<input type="number" min="0" max="96" id="GridSize" value=0>
<p>
<label style="display: inline-block; width: 540px; text-align: left">-> Number of hexagon on the face = <span id="Hex-value"></span></label>
<br>
<label style="display: inline-block; width: 540px; text-align: left">-> Characteristic dimension of one hexagon</label>
<ul>
<li><span id="Arc-value"></span> rad</li>
<li><span id="Size-value"></span> miles</li>
<li><span id="Size2-value"></span> km</li>
</ul>
</p>
</div>
<div id="renderer"></div>
<div>
<textarea id="hexjsonscreen" rows="6" cols="87">
</textarea>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="d3-geo-projection.js"></script>
<script src="d3-geo.js"></script>
<script src="platonic_solids.js"></script>
<script>
var ε = 1e-2;
var id_face = 3;
var sub = 0;
var sel_hex = '+0 +0'
var width1 = 300;
var height1 = width1/ d3.icosahedron.sizeratio;
var margin1 = 1;
var width2 = 12*width1; // sert à définir la taille du foldout complet d'où en découle la taille d'une face
var height2 = width2/ d3.icosahedron.sizeratio;
var margin2 = 1;
var svg1 = d3.select("#foldout").append("svg");
var renderer = d3.select("#renderer");
var hexjsonscreen = d3.select("#hexjsonscreen");
var svg2;
var svg3;
var wface;
var hface;
var localHexagons;
// Add an event listener to the #GridSize input created in the html part
d3.select("#GridSize").on("input", changeSize )
// Define here how to project the globe on each face of an icosahedron
// We choose a gnomonic projection centered on the face's centroid
var faceProjection = function(face) {
var c = d3.geoCentroid({type: "MultiPoint", coordinates: face});
if (Math.abs(Math.abs(c[1]) - 90) < 1e-6) c[0] = 0;
return d3.geoGnomonic().scale(1).translate([0, 0]).rotate([-c[0], -c[1]]);
};
var projection1 = d3.icosahedron.projection(faceProjection);
projection1.fitExtent([ [margin1, margin1], [width1-margin1, (width1-margin1)/ d3.icosahedron.sizeratio] ], {type:"Sphere"});
projection1.rotate([162,0,0]);
var projection2 = d3.icosahedron.projection(faceProjection);
projection2.fitExtent([ [margin2, margin2], [width2-margin2, (width2-margin2)/ d3.icosahedron.sizeratio] ], {type:"Sphere"});
projection2.rotate([162,0,0]);
var path1 = d3.geoPath().projection(projection1);
var path2 = d3.geoPath().projection(projection2);
var graticule = d3.geoGraticule();
var rot = d3.geoRotation([-162,0,0]);
var geofaces = d3.icosahedron.faces.map(function(d) {
var P0 = d3.geoInterpolate(d.centroid, d[0])(1-ε);
var P1 = d3.geoInterpolate(d.centroid, d[1])(1-ε);
var P2 = d3.geoInterpolate(d.centroid, d[2])(1-ε);
var poly = {id: d3.icosahedron.faces.indexOf(d), type: "Polygon", coordinates: [[rot(P0), rot(P1), rot(P2), rot(P0)]], geocentroid:rot(d.centroid)};
return poly;
})
svg1
.attr("width", width1)
.attr("height", height1);
var definitions1 = svg1.append("defs");
definitions1.append("clipPath")
.attr("id", "sphereclip")
.append("path")
.datum({type: "Sphere"})
.attr("d", path1);
svg1.append("path")
.datum({type: "Sphere"})
.attr("class", "sea")
.attr("d", path1);
svg1.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path1);
d3.json("world-110m.json", function(error, world) {
var land = topojson.feature(world, world.objects.land);
svg1
.insert("path", ".graticule")
.datum(land)
.attr("class", "land")
.attr("clip-path", "url(#sphereclip)")
.attr("d", path1);
});
svg1.selectAll('face')
.data(geofaces)
.enter()
.append('path')
.attr("class", function(d){
if (d.id==id_face) {
return "face selected";
} else {
return "face";
}
})
.attr("d", path1)
.on("click", function(d) {
svg1.selectAll(".selected").classed("selected", false);
d3.select(this).classed("selected", true);
id_face = d.id;
renderer.selectAll("svg").remove();
renderface();
changeselection();
});
svg1.selectAll('faceindex')
.data(geofaces)
.enter()
.append('text')
.attr("class", "faceindex")
.attr("x", function(d){return projection1(d.geocentroid)[0];})
.attr("y", function(d){return projection1(d.geocentroid)[1];})
.style("fill", "black")
.style("font-size", '10px')
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("pointer-events", "none")
.style("text-align", "left")
.text(function(d){return d.id;});
svg1.append("path")
.datum({type: "Sphere"})
.attr("class", "outline")
.attr("d", path1);
svg1.selectAll('.symbol')
.data([projection1(projection1.center())])
.enter().append('path')
.attr("class", "symbol")
.attr('d', d3.symbol().type(d3.symbols[1]) )
.attr('transform',function(d){ return 'translate('+ d +')';});
renderface();
changeselection();
//////////////////////// functions ////////////////////////////////////////////
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function round(value, precision) {
var multiplier = Math.pow(10, precision || 0);
return Math.round(value * multiplier) / multiplier;
}
function dist(a,b){
var x = a[0] - b[0];
var y = a[1] - b[1];
return Math.sqrt( x*x + y*y );
}
function translate(pts, V1, V2){
V=[V2[0]-V1[0],V2[1]-V1[1]]
return pts.map(function(P){return [P[0]+V[0],P[1]+V[1]];});
}
function pair(chiffre){
chiffre=parseInt(chiffre);
return ((chiffre & 1)=='0')?true:false;
}
function sign(i){
return [0,1,2,3,4,6,8,10,12,14].indexOf(i)==-1 ? -1 : 1;
}
function subdivide(tri, n) {
var side01 = interpolate(tri[0], tri[1]),
side12 = interpolate(tri[1], tri[2]),
side20 = interpolate(tri[2], tri[0]),
axe0 = interpolate(tri[0],side12(1 / 2)),
axe1 = interpolate(tri[1],side20(1 / 2)),
axe2 = interpolate(tri[2],side01(1 / 2));
var hex = [];
var r = dist(tri[0], tri[1])/(3*(n+1));
var sens = '+';
if (sign(id_face)==-1){sens='-';}
for (var i = 0; i <= (2*n+1); ++i) {
if (pair(n)){offset=0;}
else {offset=1/(3*(n+1));}
c = axe0(offset+((3*n + 2)/(3*(n+1)))-((2*i)/(3*(n+1))))
p0=[c[0]+r,c[1]]
p1=[c[0]+r/2,c[1]-r*(Math.sqrt(3)/2)]
p2=[c[0]-r/2,c[1]-r*(Math.sqrt(3)/2)]
p3=[c[0]-r,c[1]]
p4=[c[0]-r/2,c[1]+r*(Math.sqrt(3)/2)]
p5=[c[0]+r/2,c[1]+r*(Math.sqrt(3)/2)]
hex.push({'id':sens+i+' +0','type':'Polygon','coordinates':[p0,p1,p2,p3,p4,p5,p0]});
var cp=c,pp0=p0,pp1=p1,pp2=p2,pp3=p3,pp4=p4,pp5=p5;
var cm=c,pm0=p0,pm1=p1,pm2=p2,pm3=p3,pm4=p4,pm5=p5;
for (var j = 1; j <= (n+1); ++j) {
if (pair(j)){
a = interpolate(pp0,pp1)(1/2);
b = interpolate(cp,a)(2);
d = interpolate(pm3,pm2)(1/2);
e = interpolate(cm,d)(2);
}
else {
a = interpolate(pp0,pp5)(1/2);
b = interpolate(cp,a)(2);
d = interpolate(pm3,pm4)(1/2);
e = interpolate(cm,d)(2);
}
[cp,pp0,pp1,pp2,pp3,pp4,pp5] = translate([cp,pp0,pp1,pp2,pp3,pp4,pp5], cp, b);
hex.push({'id':sens+i+' +'+j,'type':'Polygon','coordinates':[pp0,pp1,pp2,pp3,pp4,pp5,pp0]});
[cm,pm0,pm1,pm2,pm3,pm4,pm5] = translate([cm,pm0,pm1,pm2,pm3,pm4,pm5], cm, e);
hex.push({'id':sens+i+' -'+j,'type':'Polygon','coordinates':[pm0,pm1,pm2,pm3,pm4,pm5,pm0]});
}
}
return hex;
}
function interpolate(p0, p1) { return function(t) { return [p0[0] + t * (p1[0] - p0[0]), p0[1] + t * (p1[1] - p0[1])]; };}
function renderface(){
var geoface = geofaces.filter(function(d){ return d.id == id_face; })[0];
//var centroid = projection2(geoface.geocentroid);
var side = dist(projection2(geoface.coordinates[0][0]), projection2(geoface.coordinates[0][1]))
wface = margin2 + (Math.round(side)+1);
hface = margin2 + (Math.round((Math.sqrt(3)*side/2))+1);
var centre = [wface/2 - projection2(geoface.geocentroid)[0], hface/2 + sign(id_face)*(Math.sqrt(3)/12)*side - projection2(geoface.geocentroid)[1]];
// on définit les subdivisions sur la face (plane) de l'icosaèdre
localHexagons = subdivide([projection2(geoface.coordinates[0][0]), projection2(geoface.coordinates[0][1]), projection2(geoface.coordinates[0][2])], sub);
svg2 = renderer.append('svg').attr("id", "svg");
svg2
.attr("width", wface)
.attr("height", hface);
var definitions = svg2.append("defs");
definitions.append("clipPath")
.attr("id", "faceclip"+id_face)
.append("path")
.datum(geoface)
.attr("d", path2);
var maplayer = svg2.append("g");
maplayer.append("path")
.datum({type: "Sphere"})
.attr("class", "sea")
.attr("id", "sphere"+id_face)
.attr("d", path2)
.attr("clip-path", "url(#faceclip"+id_face+")")
.attr("transform", "translate(" + centre[0] + "," + centre[1] + ")");
maplayer.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("clip-path", "url(#faceclip"+id_face+")")
.attr("transform", "translate(" + centre[0] + "," + centre[1] + ")")
.attr("d", path2);
maplayer.append("path")
.datum(geoface)
.attr("class", "face")
.attr("transform", "translate(" + centre[0] + "," + centre[1] + ")")
.attr("d", path2);
d3.json("world-110m.json", function(error, world) {
maplayer.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("clip-path", "url(#faceclip"+id_face+")")
.attr("transform", "translate(" + centre[0] + "," + centre[1] + ")")
.attr("d", path2);
});
var hexgridlayer = svg2.append("g")
.attr("id", "hex");
hexgridlayer.selectAll("hex")
.data(localHexagons)
.enter()
.append("path")
.attr("class", function(d){
if (d.id==sel_hex) {
return "hex selected";
} else {
return "hex";
}
})
.attr("clip-path", "url(#faceclip"+id_face+")")
.attr("d", function(d){return polygon(d['coordinates']);})
.attr("transform", "translate(" + centre[0] + "," + centre[1] + ")")
.on("click", function(d) {
svg2.selectAll(".selected").classed("selected", false);
d3.select(this).classed("selected", true);
sel_hex = d.id;
changeselection();
});
if (sub<=24) {
var indexlayer = svg2.append("g")
.attr("id", "index");
indexlayer.selectAll("index")
.data(localHexagons)
.enter()
.append("text")
.attr("class", "index")
.attr("clip-path", "url(#faceclip"+id_face+")")
.attr("x", function(d){return d3.polygonCentroid(d['coordinates'])[0];})
.attr("y", function(d){return d3.polygonCentroid(d['coordinates'])[1];})
.style("fill", "black")
.style("font-size", function(d){
if (sub<=6){return '10px';}
else if (sub<=12){return '8px';}
else if (sub<=18){return '6px';}
else {return '4px';};
})
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("pointer-events", "none")
.style("text-align", "left")
.text(function(d){return d['id'];})
.attr("transform", "translate(" + centre[0] + "," + centre[1] + ")");
}
d3.select("#Hex-value").text((1.5*sub**2+4.5*sub+4) + ' ( including ' + (1.5*sub**2+1.5*sub+1) + ' complete)');
d3.select("#Arc-value").text(2*d3.geoDistance(projection2.invert(localHexagons[0].coordinates[0]),projection2.invert(localHexagons[0].coordinates[1])));
d3.select("#Size-value").text(Math.round(2*3064*d3.geoDistance(projection2.invert(localHexagons[0].coordinates[0]),projection2.invert(localHexagons[0].coordinates[1]))));
d3.select("#Size2-value").text(Math.round(1.60934*2*3064*d3.geoDistance(projection2.invert(localHexagons[0].coordinates[0]),projection2.invert(localHexagons[0].coordinates[1]))));
}
function changeSize() {
sub=parseInt(this.value);
renderer.selectAll("svg").remove();
renderface();
changeselection();
}
function changeselection() {
document.getElementById("hexjsonscreen").value = get_geojson();
//hexjsonscreen
// .datum(get_geojson())
// .property('value', d);
}
function get_geojson() {
var sel = localHexagons.filter(function(d){ return d.id == sel_hex; })[0];
var obj_hex = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"id": sel.id},
"geometry": {
"type": "Polygon",
"coordinates": (sel.coordinates.map(projection2.invert)).map(d3.geoRotation([360,0,0]))
}
}
]
}
return JSON.stringify(obj_hex)
}
// Mystara
// R = 3095 miles = 4980,92 km
//Earth
// R = 6378 miles = 10264,4 km
// 1 miles = 1,60934 km
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://d3js.org/topojson.v2.min.js