UNPKG

18.3 kBJavaScriptView Raw
1/*
2 * Cahill-Keyes projection
3 *
4 * Implemented in Perl by Mary Jo Graça (2011)
5 *
6 * Ported to D3.js by Enrico Spinielli (2013)
7 *
8 */
9import { abs, cos, degrees, pi, radians, sin, sign, sqrt, tan } from "./math.js";
10import { cartesianCross, cartesianDegrees, cartesianDot, sphericalDegrees } from "./cartesian.js";
11import polyhedral from "./polyhedral/index.js";
12import { geoProjectionMutator as projectionMutator } from "d3-geo";
13import {solve2d} from "./newton.js";
14
15export default function(faceProjection) {
16 faceProjection =
17 faceProjection ||
18 function() {
19 return cahillKeyesProjection().scale(1);
20 };
21
22 var octahedron = [[0, 90], [-90, 0], [0, 0], [90, 0], [180, 0], [0, -90]];
23
24 octahedron = [
25 [0, 2, 1],
26 [0, 3, 2],
27 [5, 1, 2],
28 [5, 2, 3],
29 [0, 1, 4],
30 [0, 4, 3],
31 [5, 4, 1],
32 [5, 3, 4]
33 ].map(function(face) {
34 return face.map(function(i) {
35 return octahedron[i];
36 });
37 });
38
39 var ck = octahedron.map(function(face) {
40 var xyz = face.map(cartesianDegrees),
41 n = xyz.length,
42 a = xyz[n - 1],
43 b,
44 theta = 17 * radians,
45 cosTheta = cos(theta),
46 sinTheta = sin(theta),
47 hexagon = [];
48 for (var i = 0; i < n; ++i) {
49 b = xyz[i];
50 hexagon.push(
51 sphericalDegrees([
52 a[0] * cosTheta + b[0] * sinTheta,
53 a[1] * cosTheta + b[1] * sinTheta,
54 a[2] * cosTheta + b[2] * sinTheta
55 ]),
56 sphericalDegrees([
57 b[0] * cosTheta + a[0] * sinTheta,
58 b[1] * cosTheta + a[1] * sinTheta,
59 b[2] * cosTheta + a[2] * sinTheta
60 ])
61 );
62 a = b;
63 }
64 return hexagon;
65 });
66
67 var cornerNormals = [];
68
69 var parents = [-1, 3, 0, 2, 0, 1, 4, 5];
70
71 ck.forEach(function(hexagon, j) {
72 var face = octahedron[j],
73 n = face.length,
74 normals = (cornerNormals[j] = []);
75 for (var i = 0; i < n; ++i) {
76 ck.push([
77 face[i],
78 hexagon[(i * 2 + 2) % (2 * n)],
79 hexagon[(i * 2 + 1) % (2 * n)]
80 ]);
81 parents.push(j);
82 normals.push(
83 cartesianCross(
84 cartesianDegrees(hexagon[(i * 2 + 2) % (2 * n)]),
85 cartesianDegrees(hexagon[(i * 2 + 1) % (2 * n)])
86 )
87 );
88 }
89 });
90
91 var faces = ck.map(function(face) {
92 return {
93 project: faceProjection(face),
94 face: face
95 };
96 });
97
98 parents.forEach(function(d, i) {
99 var parent = faces[d];
100 parent && (parent.children || (parent.children = [])).push(faces[i]);
101 });
102 return polyhedral(faces[0], face, 0, true)
103 .scale(0.023975)
104 .rotate([20, 0])
105 .center([0,-17]);
106
107 function face(lambda, phi) {
108 var cosPhi = cos(phi),
109 p = [cosPhi * cos(lambda), cosPhi * sin(lambda), sin(phi)];
110
111 var hexagon =
112 lambda < -pi / 2
113 ? phi < 0 ? 6 : 4
114 : lambda < 0
115 ? phi < 0 ? 2 : 0
116 : lambda < pi / 2 ? (phi < 0 ? 3 : 1) : phi < 0 ? 7 : 5;
117
118 var n = cornerNormals[hexagon];
119
120 return faces[
121 cartesianDot(n[0], p) < 0
122 ? 8 + 3 * hexagon
123 : cartesianDot(n[1], p) < 0
124 ? 8 + 3 * hexagon + 1
125 : cartesianDot(n[2], p) < 0 ? 8 + 3 * hexagon + 2 : hexagon
126 ];
127 }
128}
129
130// all names of reference points, A, B, D, ... , G, P75
131// or zones, A-L, are detailed fully in Gene Keyes'
132// web site http://www.genekeyes.com/CKOG-OOo/7-CKOG-illus-&-coastline.html
133
134export function cahillKeyesRaw(mg) {
135 var CK = {
136 lengthMG: mg // magic scaling length
137 };
138
139 preliminaries();
140
141 function preliminaries() {
142 var pointN, lengthMB, lengthMN, lengthNG, pointU;
143 var m = 29, // meridian
144 p = 15, // parallel
145 p73a,
146 lF,
147 lT,
148 lM,
149 l,
150 pointV,
151 k = sqrt(3);
152
153 CK.lengthMA = 940 / 10000 * CK.lengthMG;
154 CK.lengthParallel0to73At0 = CK.lengthMG / 100;
155 CK.lengthParallel73to90At0 =
156 (CK.lengthMG - CK.lengthMA - CK.lengthParallel0to73At0 * 73) / (90 - 73);
157 CK.sin60 = k / 2; // √3/2
158 CK.cos60 = 0.5;
159 CK.pointM = [0, 0];
160 CK.pointG = [CK.lengthMG, 0];
161 pointN = [CK.lengthMG, CK.lengthMG * tan(30 * radians)];
162 CK.pointA = [CK.lengthMA, 0];
163 CK.pointB = lineIntersection(CK.pointM, 30, CK.pointA, 45);
164 CK.lengthAG = distance(CK.pointA, CK.pointG);
165 CK.lengthAB = distance(CK.pointA, CK.pointB);
166 lengthMB = distance(CK.pointM, CK.pointB);
167 lengthMN = distance(CK.pointM, pointN);
168 lengthNG = distance(pointN, CK.pointG);
169 CK.pointD = interpolate(lengthMB, lengthMN, pointN, CK.pointM);
170 CK.pointF = [CK.lengthMG, lengthNG - lengthMB];
171 CK.pointE = [
172 pointN[0] - CK.lengthMA * sin(30 * radians),
173 pointN[1] - CK.lengthMA * cos(30 * radians)
174 ];
175 CK.lengthGF = distance(CK.pointG, CK.pointF);
176 CK.lengthBD = distance(CK.pointB, CK.pointD);
177 CK.lengthBDE = CK.lengthBD + CK.lengthAB; // lengthAB = lengthDE
178 CK.lengthGFE = CK.lengthGF + CK.lengthAB; // lengthAB = lengthFE
179 CK.deltaMEq = CK.lengthGFE / 45;
180 CK.lengthAP75 = (90 - 75) * CK.lengthParallel73to90At0;
181 CK.lengthAP73 = CK.lengthMG - CK.lengthMA - CK.lengthParallel0to73At0 * 73;
182 pointU = [
183 CK.pointA[0] + CK.lengthAP73 * cos(30 * radians),
184 CK.pointA[1] + CK.lengthAP73 * sin(30 * radians)
185 ];
186 CK.pointT = lineIntersection(pointU, -60, CK.pointB, 30);
187
188 p73a = parallel73(m);
189 lF = p73a.lengthParallel73;
190 lT = lengthTorridSegment(m);
191 lM = lengthMiddleSegment(m);
192 l = p * (lT + lM + lF) / 73;
193 pointV = [0, 0];
194 CK.pointC = [0, 0];
195 CK.radius = 0;
196
197 l = l - lT;
198 pointV = interpolate(l, lM, jointT(m), jointF(m));
199 CK.pointC[1] =
200 (pointV[0] * pointV[0] +
201 pointV[1] * pointV[1] -
202 CK.pointD[0] * CK.pointD[0] -
203 CK.pointD[1] * CK.pointD[1]) /
204 (2 * (k * pointV[0] + pointV[1] - k * CK.pointD[0] - CK.pointD[1]));
205 CK.pointC[0] = k * CK.pointC[1];
206 CK.radius = distance(CK.pointC, CK.pointD);
207
208 return CK;
209 }
210
211 //**** helper functions ****//
212
213 // distance between two 2D coordinates
214
215 function distance(p1, p2) {
216 var deltaX = p1[0] - p2[0],
217 deltaY = p1[1] - p2[1];
218 return sqrt(deltaX * deltaX + deltaY * deltaY);
219 }
220
221 // return 2D point at position length/totallength of the line
222 // defined by two 2D points, start and end.
223
224 function interpolate(length, totalLength, start, end) {
225 var xy = [
226 start[0] + (end[0] - start[0]) * length / totalLength,
227 start[1] + (end[1] - start[1]) * length / totalLength
228 ];
229 return xy;
230 }
231
232 // return the 2D point intersection between two lines defined
233 // by one 2D point and a slope each.
234
235 function lineIntersection(point1, slope1, point2, slope2) {
236 // s1/s2 = slope in degrees
237 var m1 = tan(slope1 * radians),
238 m2 = tan(slope2 * radians),
239 p = [0, 0];
240 p[0] =
241 (m1 * point1[0] - m2 * point2[0] - point1[1] + point2[1]) / (m1 - m2);
242 p[1] = m1 * (p[0] - point1[0]) + point1[1];
243 return p;
244 }
245
246 // return the 2D point intercepting a circumference centered
247 // at cc and of radius rn and a line defined by 2 points, p1 and p2:
248 // First element of the returned array is a flag to state whether there is
249 // an intersection, a value of zero (0) means NO INTERSECTION.
250 // The following array is the 2D point of the intersection.
251 // Equations from "Intersection of a Line and a Sphere (or circle)/Line Segment"
252 // at http://paulbourke.net/geometry/circlesphere/
253 function circleLineIntersection(cc, r, p1, p2) {
254 var x1 = p1[0],
255 y1 = p1[1],
256 x2 = p2[0],
257 y2 = p2[1],
258 xc = cc[0],
259 yc = cc[1],
260 a = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1),
261 b = 2 * ((x2 - x1) * (x1 - xc) + (y2 - y1) * (y1 - yc)),
262 c =
263 xc * xc + yc * yc + x1 * x1 + y1 * y1 - 2 * (xc * x1 + yc * y1) - r * r,
264 d = b * b - 4 * a * c,
265 u1 = 0,
266 u2 = 0,
267 x = 0,
268 y = 0;
269 if (a === 0) {
270 return [0, [0, 0]];
271 } else if (d < 0) {
272 return [0, [0, 0]];
273 }
274 u1 = (-b + sqrt(d)) / (2 * a);
275 u2 = (-b - sqrt(d)) / (2 * a);
276 if (0 <= u1 && u1 <= 1) {
277 x = x1 + u1 * (x2 - x1);
278 y = y1 + u1 * (y2 - y1);
279 return [1, [x, y]];
280 } else if (0 <= u2 && u2 <= 1) {
281 x = x1 + u2 * (x2 - x1);
282 y = y1 + u2 * (y2 - y1);
283 return [1, [x, y]];
284 } else {
285 return [0, [0, 0]];
286 }
287 }
288
289 // counterclockwise rotate 2D vector, xy, by angle (in degrees)
290 // [original CKOG uses clockwise rotation]
291
292 function rotate(xy, angle) {
293 var xynew = [0, 0];
294
295 if (angle === -60) {
296 xynew[0] = xy[0] * CK.cos60 + xy[1] * CK.sin60;
297 xynew[1] = -xy[0] * CK.sin60 + xy[1] * CK.cos60;
298 } else if (angle === -120) {
299 xynew[0] = -xy[0] * CK.cos60 + xy[1] * CK.sin60;
300 xynew[1] = -xy[0] * CK.sin60 - xy[1] * CK.cos60;
301 } else {
302 // !!!!! This should not happen for this projection!!!!
303 // the general algorith: cos(angle) * xy + sin(angle) * perpendicular(xy)
304 // return cos(angle * radians) * xy + sin(angle * radians) * perpendicular(xy);
305 //console.log("rotate: angle " + angle + " different than -60 or -120!");
306 // counterclockwise
307 xynew[0] = xy[0] * cos(angle * radians) - xy[1] * sin(angle * radians);
308 xynew[1] = xy[0] * sin(angle * radians) + xy[1] * cos(angle * radians);
309 }
310
311 return xynew;
312 }
313
314 // truncate towards zero like int() in Perl
315 function truncate(n) {
316 return Math[n > 0 ? "floor" : "ceil"](n);
317 }
318
319 function equator(m) {
320 var l = CK.deltaMEq * m,
321 jointE = [0, 0];
322 if (l <= CK.lengthGF) {
323 jointE = [CK.pointG[0], l];
324 } else {
325 l = l - CK.lengthGF;
326 jointE = interpolate(l, CK.lengthAB, CK.pointF, CK.pointE);
327 }
328 return jointE;
329 }
330
331 function jointE(m) {
332 return equator(m);
333 }
334
335 function jointT(m) {
336 return lineIntersection(CK.pointM, 2 * m / 3, jointE(m), m / 3);
337 }
338
339 function jointF(m) {
340 if (m === 0) {
341 return [CK.pointA + CK.lengthAB, 0];
342 }
343 var xy = lineIntersection(CK.pointA, m, CK.pointM, 2 * m / 3);
344 return xy;
345 }
346
347 function lengthTorridSegment(m) {
348 return distance(jointE(m), jointT(m));
349 }
350
351 function lengthMiddleSegment(m) {
352 return distance(jointT(m), jointF(m));
353 }
354
355 function parallel73(m) {
356 var p73 = [0, 0],
357 jF = jointF(m),
358 lF = 0,
359 xy = [0, 0];
360 if (m <= 30) {
361 p73[0] = CK.pointA[0] + CK.lengthAP73 * cos(m * radians);
362 p73[1] = CK.pointA[1] + CK.lengthAP73 * sin(m * radians);
363 lF = distance(jF, p73);
364 } else {
365 p73 = lineIntersection(CK.pointT, -60, jF, m);
366 lF = distance(jF, p73);
367 if (m > 44) {
368 xy = lineIntersection(CK.pointT, -60, jF, 2 / 3 * m);
369 if (xy[0] > p73[0]) {
370 p73 = xy;
371 lF = -distance(jF, p73);
372 }
373 }
374 }
375 return {
376 parallel73: p73,
377 lengthParallel73: lF
378 };
379 }
380
381 function parallel75(m) {
382 return [
383 CK.pointA[0] + CK.lengthAP75 * cos(m * radians),
384 CK.pointA[1] + CK.lengthAP75 * sin(m * radians)
385 ];
386 }
387
388 // special functions to transform lon/lat to x/y
389 function ll2mp(lon, lat) {
390 var south = [0, 6, 7, 8, 5],
391 o = truncate((lon + 180) / 90 + 1),
392 p, // parallel
393 m = (lon + 720) % 90 - 45, // meridian
394 s = sign(m);
395
396 m = abs(m);
397 if (o === 5) o = 1;
398 if (lat < 0) o = south[o];
399 p = abs(lat);
400 return [m, p, s, o];
401 }
402
403 function zoneA(m, p) {
404 return [CK.pointA[0] + (90 - p) * 104, 0];
405 }
406
407 function zoneB(m, p) {
408 return [CK.pointG[0] - p * 100, 0];
409 }
410
411 function zoneC(m, p) {
412 var l = 104 * (90 - p);
413 return [
414 CK.pointA[0] + l * cos(m * radians),
415 CK.pointA[1] + l * sin(m * radians)
416 ];
417 }
418
419 function zoneD(m /*, p */) {
420 // p = p; // just keep it for symmetry in signature
421 return equator(m);
422 }
423
424 function zoneE(m, p) {
425 var l = 1560 + (75 - p) * 100;
426 return [
427 CK.pointA[0] + l * cos(m * radians),
428 CK.pointA[1] + l * sin(m * radians)
429 ];
430 }
431
432 function zoneF(m, p) {
433 return interpolate(p, 15, CK.pointE, CK.pointD);
434 }
435
436 function zoneG(m, p) {
437 var l = p - 15;
438 return interpolate(l, 58, CK.pointD, CK.pointT);
439 }
440
441 function zoneH(m, p) {
442 var p75 = parallel75(45),
443 p73a = parallel73(m),
444 p73 = p73a.parallel73,
445 lF = distance(CK.pointT, CK.pointB),
446 lF75 = distance(CK.pointB, p75),
447 l = (75 - p) * (lF75 + lF) / 2,
448 xy = [0, 0];
449 if (l <= lF75) {
450 xy = interpolate(l, lF75, p75, CK.pointB);
451 } else {
452 l = l - lF75;
453 xy = interpolate(l, lF, CK.pointB, p73);
454 }
455 return xy;
456 }
457
458 function zoneI(m, p) {
459 var p73a = parallel73(m),
460 lT = lengthTorridSegment(m),
461 lM = lengthMiddleSegment(m),
462 l = p * (lT + lM + p73a.lengthParallel73) / 73,
463 xy;
464 if (l <= lT) {
465 xy = interpolate(l, lT, jointE(m), jointT(m));
466 } else if (l <= lT + lM) {
467 l = l - lT;
468 xy = interpolate(l, lM, jointT(m), jointF(m));
469 } else {
470 l = l - lT - lM;
471 xy = interpolate(l, p73a.lengthParallel73, jointF(m), p73a.parallel73);
472 }
473 return xy;
474 }
475
476 function zoneJ(m, p) {
477 var p75 = parallel75(m),
478 lF75 = distance(jointF(m), p75),
479 p73a = parallel73(m),
480 p73 = p73a.parallel73,
481 lF = p73a.lengthParallel73,
482 l = (75 - p) * (lF75 - lF) / 2,
483 xy = [0, 0];
484
485 if (l <= lF75) {
486 xy = interpolate(l, lF75, p75, jointF(m));
487 } else {
488 l = l - lF75;
489 xy = interpolate(l, -lF, jointF(m), p73);
490 }
491 return xy;
492 }
493
494 function zoneK(m, p, l15) {
495 var l = p * l15 / 15,
496 lT = lengthTorridSegment(m),
497 lM = lengthMiddleSegment(m),
498 xy = [0, 0];
499 if (l <= lT) {
500 // point is in torrid segment
501 xy = interpolate(l, lT, jointE(m), jointT(m));
502 } else {
503 // point is in middle segment
504 l = l - lT;
505 xy = interpolate(l, lM, jointT(m), jointF(m));
506 }
507 return xy;
508 }
509
510 function zoneL(m, p, l15) {
511 var p73a = parallel73(m),
512 p73 = p73a.parallel73,
513 lT = lengthTorridSegment(m),
514 lM = lengthMiddleSegment(m),
515 xy,
516 lF = p73a.lengthParallel73,
517 l = l15 + (p - 15) * (lT + lM + lF - l15) / 58;
518 if (l <= lT) {
519 //on torrid segment
520 xy = interpolate(l, lT, jointE(m), jointF(m));
521 } else if (l <= lT + lM) {
522 //on middle segment
523 l = l - lT;
524 xy = interpolate(l, lM, jointT(m), jointF(m));
525 } else {
526 //on frigid segment
527 l = l - lT - lM;
528 xy = interpolate(l, lF, jointF(m), p73);
529 }
530 return xy;
531 }
532
533 // convert half-octant meridian,parallel to x,y coordinates.
534 // arguments are meridian, parallel
535
536 function mp2xy(m, p) {
537 var xy = [0, 0],
538 lT,
539 p15a,
540 p15,
541 flag15,
542 l15;
543
544 if (m === 0) {
545 // zones (a) and (b)
546 if (p >= 75) {
547 xy = zoneA(m, p);
548 } else {
549 xy = zoneB(m, p);
550 }
551 } else if (p >= 75) {
552 xy = zoneC(m, p);
553 } else if (p === 0) {
554 xy = zoneD(m, p);
555 } else if (p >= 73 && m <= 30) {
556 xy = zoneE(m, p);
557 } else if (m === 45) {
558 if (p <= 15) {
559 xy = zoneF(m, p);
560 } else if (p <= 73) {
561 xy = zoneG(m, p);
562 } else {
563 xy = zoneH(m, p);
564 }
565 } else {
566 if (m <= 29) {
567 xy = zoneI(m, p);
568 } else {
569 // supple zones (j), (k) and (l)
570 if (p >= 73) {
571 xy = zoneJ(m, p);
572 } else {
573 //zones (k) and (l)
574 p15a = circleLineIntersection(
575 CK.pointC,
576 CK.radius,
577 jointT(m),
578 jointF(m)
579 );
580 flag15 = p15a[0];
581 p15 = p15a[1];
582 lT = lengthTorridSegment(m);
583 if (flag15 === 1) {
584 // intersection is in middle segment
585 l15 = lT + distance(jointT(m), p15);
586 } else {
587 // intersection is in torrid segment
588 p15a = circleLineIntersection(
589 CK.pointC,
590 CK.radius,
591 jointE(m),
592 jointT(m)
593 );
594 flag15 = p15a[0];
595 p15 = p15a[1];
596 if (flag15 === 0) {
597 //console.log("Something weird!");
598 // TODO: Trap this! Something odd happened!
599 }
600 l15 = lT - distance(jointT(m), p15);
601 }
602 if (p <= 15) {
603 xy = zoneK(m, p, l15);
604 } else {
605 //zone (l)
606 xy = zoneL(m, p, l15);
607 }
608 }
609 }
610 }
611 return xy;
612 }
613
614 // from half-octant to megamap (single rotated octant)
615
616 function mj2g(xy, octant) {
617 var xynew = [0, 0];
618
619 if (octant === 0) {
620 xynew = rotate(xy, -60);
621 } else if (octant === 1) {
622 xynew = rotate(xy, -120);
623 xynew[0] -= CK.lengthMG;
624 } else if (octant === 2) {
625 xynew = rotate(xy, -60);
626 xynew[0] -= CK.lengthMG;
627 } else if (octant === 3) {
628 xynew = rotate(xy, -120);
629 xynew[0] += CK.lengthMG;
630 } else if (octant === 4) {
631 xynew = rotate(xy, -60);
632 xynew[0] += CK.lengthMG;
633 } else if (octant === 5) {
634 xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -60);
635 xynew[0] += CK.lengthMG;
636 } else if (octant === 6) {
637 xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -120);
638 xynew[0] -= CK.lengthMG;
639 } else if (octant === 7) {
640 xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -60);
641 xynew[0] -= CK.lengthMG;
642 } else if (octant === 8) {
643 xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -120);
644 xynew[0] += CK.lengthMG;
645 } else {
646 // TODO trap this some way.
647 // ERROR!
648 // print "Error converting to M-map coordinates; there is no Octant octant!\n";
649 //console.log("mj2g: something weird happened!");
650 return xynew;
651 }
652
653 return xynew;
654 }
655
656 // general CK map projection
657
658 function forward(lambda, phi) {
659 // lambda, phi are in radians.
660 var lon = lambda * degrees,
661 lat = phi * degrees,
662 res = ll2mp(lon, lat),
663 m = res[0], // 0 ≤ m ≤ 45
664 p = res[1], // 0 ≤ p ≤ 90
665 s = res[2], // -1 / 1 = side of m
666 o = res[3], // octant
667 xy = mp2xy(m, p),
668 mm = mj2g([xy[0], s * xy[1]], o);
669
670 return mm;
671 }
672
673 forward.invert = solve2d(forward);
674
675 return forward;
676}
677
678function cahillKeyesProjection() {
679 var mg = 10000,
680 m = projectionMutator(cahillKeyesRaw);
681 return m(mg);
682}