UNPKG

8.45 kBJavaScriptView Raw
1/*
2 * Imago projection, by Justin Kunimune
3 *
4 * Inspired by Hajime Narukawa’s AuthaGraph
5 *
6 */
7import {
8 abs,
9 acos,
10 asin,
11 atan,
12 atan2,
13 cos,
14 degrees,
15 epsilon,
16 floor,
17 halfPi,
18 pi,
19 pow,
20 sign,
21 sin,
22 sqrt,
23 tan
24} from "./math";
25import { geoProjectionMutator as projectionMutator } from "d3-geo";
26import { default as clipPolygon } from "./clip/polygon.js";
27import { solve } from "./newton.js";
28
29var hypot = Math.hypot;
30
31var ASIN_ONE_THD = asin(1 / 3),
32 centrums = [
33 [halfPi, 0, 0, -halfPi, 0, sqrt(3)],
34 [-ASIN_ONE_THD, 0, pi, halfPi, 0, -sqrt(3)],
35 [-ASIN_ONE_THD, (2 * pi) / 3, pi, (5 * pi) / 6, 3, 0],
36 [-ASIN_ONE_THD, (-2 * pi) / 3, pi, pi / 6, -3, 0]
37 ],
38 TETRAHEDRON_WIDE_VERTEX = {
39 sphereSym: 3,
40 planarSym: 6,
41 width: 6,
42 height: 2 * sqrt(3),
43 centrums,
44 rotateOOB: function(x, y, xCen, yCen) {
45 yCen * 0;
46 if (abs(x) > this.width / 2) return [2 * xCen - x, -y];
47 else return [-x, this.height * sign(y) - y];
48 },
49 inBounds: () => true
50 },
51 configuration = TETRAHEDRON_WIDE_VERTEX;
52
53export function imagoRaw(k) {
54 function faceProject(lon, lat) {
55 const tht = atan(((lon - asin(sin(lon) / sqrt(3))) / pi) * sqrt(12)),
56 p = (halfPi - lat) / atan(sqrt(2) / cos(lon));
57
58 return [(pow(p, k) * sqrt(3)) / cos(tht), tht];
59 }
60
61 function faceInverse(r, th) {
62 const l = solve(
63 l => atan(((l - asin(sin(l) / sqrt(3))) / pi) * sqrt(12)),
64 th,
65 th / 2
66 ),
67 R = r / (sqrt(3) / cos(th));
68 return [halfPi - pow(R, 1 / k) * atan(sqrt(2) / cos(l)), l];
69 }
70
71 function obliquifySphc(latF, lonF, pole) {
72 if (pole == null)
73 // null pole indicates that this procedure should be bypassed
74 return [latF, lonF];
75
76 const lat0 = pole[0],
77 lon0 = pole[1],
78 tht0 = pole[2];
79
80 let lat1, lon1;
81 if (lat0 == halfPi) lat1 = latF;
82 else
83 lat1 = asin(
84 sin(lat0) * sin(latF) + cos(lat0) * cos(latF) * cos(lon0 - lonF)
85 ); // relative latitude
86
87 if (lat0 == halfPi)
88 // accounts for all the 0/0 errors at the poles
89 lon1 = lonF - lon0;
90 else if (lat0 == -halfPi) lon1 = lon0 - lonF - pi;
91 else {
92 lon1 =
93 acos(
94 (cos(lat0) * sin(latF) - sin(lat0) * cos(latF) * cos(lon0 - lonF)) /
95 cos(lat1)
96 ) - pi; // relative longitude
97 if (isNaN(lon1)) {
98 if (
99 (cos(lon0 - lonF) >= 0 && latF < lat0) ||
100 (cos(lon0 - lonF) < 0 && latF < -lat0)
101 )
102 lon1 = 0;
103 else lon1 = -pi;
104 } else if (sin(lonF - lon0) > 0)
105 // it's a plus-or-minus arccos.
106 lon1 = -lon1;
107 }
108 lon1 = lon1 - tht0;
109
110 return [lat1, lon1];
111 }
112
113 function obliquifyPlnr(coords, pole) {
114 if (pole == null)
115 //this indicates that you just shouldn't do this calculation
116 return coords;
117
118 let lat1 = coords[0],
119 lon1 = coords[1];
120 const lat0 = pole[0],
121 lon0 = pole[1],
122 tht0 = pole[2];
123
124 lon1 += tht0;
125 let latf = asin(sin(lat0) * sin(lat1) - cos(lat0) * cos(lon1) * cos(lat1)),
126 lonf,
127 innerFunc = sin(lat1) / cos(lat0) / cos(latf) - tan(lat0) * tan(latf);
128 if (lat0 == halfPi)
129 // accounts for special case when lat0 = pi/2
130 lonf = lon1 + lon0;
131 else if (lat0 == -halfPi)
132 // accounts for special case when lat0 = -pi/2
133 lonf = -lon1 + lon0 + pi;
134 else if (abs(innerFunc) > 1) {
135 // accounts for special case when cos(lat1) -> 0
136 if ((lon1 == 0 && lat1 < -lat0) || (lon1 != 0 && lat1 < lat0))
137 lonf = lon0 + pi;
138 else lonf = lon0;
139 } else if (sin(lon1) > 0) lonf = lon0 + acos(innerFunc);
140 else lonf = lon0 - acos(innerFunc);
141
142 let thtf = pole[2];
143
144 return [latf, lonf, thtf];
145 }
146
147 function forward(lon, lat) {
148 const width = configuration.width,
149 height = configuration.height;
150 const numSym = configuration.sphereSym; //we're about to be using this variable a lot
151 let latR = -Infinity;
152 let lonR = -Infinity;
153 let centrum = null;
154 for (const testCentrum of centrums) {
155 //iterate through the centrums to see which goes here
156 const relCoords = obliquifySphc(lat, lon, testCentrum);
157 if (relCoords[0] > latR) {
158 latR = relCoords[0];
159 lonR = relCoords[1];
160 centrum = testCentrum;
161 }
162 }
163
164 const lonR0 =
165 floor((lonR + pi / numSym) / ((2 * pi) / numSym)) * ((2 * pi) / numSym);
166
167 const rth = faceProject(lonR - lonR0, latR);
168 const r = rth[0];
169 const th = rth[1] + centrum[3] + (lonR0 * numSym) / configuration.planarSym;
170 const x0 = centrum[4];
171 const y0 = centrum[5];
172
173 let output = [r * cos(th) + x0, r * sin(th) + y0];
174 if (abs(output[0]) > width / 2 || abs(output[1]) > height / 2) {
175 output = configuration.rotateOOB(output[0], output[1], x0, y0);
176 }
177 return output;
178 }
179
180 function invert(x, y) {
181 if (isNaN(x) || isNaN(y)) return null;
182
183 if (!configuration.inBounds(x, y)) return null;
184
185 const numSym = configuration.planarSym;
186
187 let rM = +Infinity;
188 let centrum = null; //iterate to see which centrum we get
189 for (const testCentrum of centrums) {
190 const rR = hypot(x - testCentrum[4], y - testCentrum[5]);
191 if (rR < rM) {
192 //pick the centrum that minimises r
193 rM = rR;
194 centrum = testCentrum;
195 }
196 }
197 const th0 = centrum[3],
198 x0 = centrum[4],
199 y0 = centrum[5],
200 r = hypot(x - x0, y - y0),
201 th = atan2(y - y0, x - x0) - th0,
202 thBase =
203 floor((th + pi / numSym) / ((2 * pi) / numSym)) * ((2 * pi) / numSym);
204
205 let relCoords = faceInverse(r, th - thBase);
206
207 if (relCoords == null) return null;
208
209 relCoords[1] = (thBase * numSym) / configuration.sphereSym + relCoords[1];
210 let absCoords = obliquifyPlnr(relCoords, centrum);
211 return [absCoords[1], absCoords[0]];
212 }
213
214 forward.invert = invert;
215
216 return forward;
217}
218
219export function imagoBlock() {
220 var k = 0.68,
221 m = projectionMutator(imagoRaw),
222 p = m(k);
223
224 p.k = function(_) {
225 return arguments.length ? m((k = +_)) : k;
226 };
227
228 var a = -atan(1 / sqrt(2)) * degrees,
229 border = [
230 [-180 + epsilon, a + epsilon],
231 [0, 90],
232 [180 - epsilon, a + epsilon],
233 [180 - epsilon, a - epsilon],
234 [-180 + epsilon, a - epsilon],
235 [-180 + epsilon, a + epsilon]
236 ];
237
238 return p
239 .preclip(clipPolygon({
240 type: "Polygon",
241 coordinates: [border]
242 }))
243 .scale(144.04)
244 .rotate([18, -12.5, 3.5])
245 .center([0, 35.2644]);
246}
247
248function imagoWideRaw(k, shift) {
249 var imago = imagoRaw(k);
250 const height = configuration.height;
251
252 function forward(lon, lat) {
253 const p = imago(lon, lat),
254 q = [p[1], -p[0]];
255
256 if (q[1] > 0) {
257 q[0] = height - q[0];
258 q[1] *= -1;
259 }
260
261 q[0] += shift;
262 if (q[0] < 0) q[0] += height * 2;
263
264 return q;
265 }
266
267 function invert(x, y) {
268 x = (x - shift) / height;
269
270 if (x > 1.5) {
271 x -= 2;
272 }
273
274 if (x > 0.5) {
275 x = 1 - x;
276 y *= -1;
277 }
278
279 return imago.invert(-y, x * height);
280 }
281
282 forward.invert = invert;
283 return forward;
284}
285
286export default function() {
287 var k = 0.59,
288 shift = 1.16,
289 m = projectionMutator(imagoWideRaw),
290 p = m(k, shift);
291
292 p.shift = function(_) {
293 return arguments.length ? clipped(m(k, (shift = +_))) : shift;
294 };
295 p.k = function(_) {
296 return arguments.length ? clipped(m((k = +_), shift)) : k;
297 };
298
299 function clipped(p) {
300 const N = 100 + 2 * epsilon,
301 border = [],
302 e = 3e-3;
303
304 const scale = p.scale(),
305 center = p.center(),
306 translate = p.translate(),
307 rotate = p.rotate();
308 p.scale(1)
309 .center([0, 90])
310 .rotate([0, 0])
311 .translate([shift, 0]);
312 for (let i = N - epsilon; i > 0; i--) {
313 border.unshift(
314 p.invert([
315 1.5 * configuration.height - e,
316 ((configuration.width / 2) * i) / N
317 ])
318 );
319 border.push(
320 p.invert([
321 -0.5 * configuration.height + e,
322 ((configuration.width / 2) * i) / N
323 ])
324 );
325 }
326 border.push(border[0]);
327
328 return p
329 .scale(scale)
330 .center(center)
331 .translate(translate)
332 .rotate(rotate)
333 .preclip(
334 clipPolygon({
335 type: "Polygon",
336 coordinates: [border]
337 })
338 );
339 }
340
341 return clipped(p)
342 .rotate([18, -12.5, 3.5])
343 .scale(138.42)
344 .translate([480, 250])
345 .center([-139.405, 40.5844]);
346}