d3-geo-polygon
Version:
Clipping and geometric operations for spherical polygons.
594 lines (525 loc) • 17.1 kB
JavaScript
/*
* Cahill-Keyes projection
*
* Implemented in Perl by Mary Jo Graça (2011)
*
* Ported to D3.js by Enrico Spinielli (2013)
*
*/
import { abs, cos, degrees, pi, radians, sin, sign, sqrt, tan } from "./math.js";
import { cartesianCross, cartesianDegrees, cartesianDot, sphericalDegrees } from "./cartesian.js";
import polyhedral from "./polyhedral/index.js";
import { geoProjectionMutator as projectionMutator } from "d3-geo";
import {solve2d} from "./newton.js";
export default function(faceProjection) {
faceProjection =
faceProjection ||
(() => cahillKeyesProjection().scale(1));
const octa = [[0, 90], [-90, 0], [0, 0], [90, 0], [180, 0], [0, -90]];
const octahedron = [
[0, 2, 1],
[0, 3, 2],
[5, 1, 2],
[5, 2, 3],
[0, 1, 4],
[0, 4, 3],
[5, 4, 1],
[5, 3, 4]
].map((face) => face.map((i) => octa[i]));
const ck = octahedron.map((face) => {
const xyz = face.map(cartesianDegrees),
n = xyz.length,
theta = 17 * radians,
cosTheta = cos(theta),
sinTheta = sin(theta),
hexagon = [];
let a = xyz[n - 1];
let b;
for (let i = 0; i < n; ++i) {
b = xyz[i];
hexagon.push(
sphericalDegrees([
a[0] * cosTheta + b[0] * sinTheta,
a[1] * cosTheta + b[1] * sinTheta,
a[2] * cosTheta + b[2] * sinTheta
]),
sphericalDegrees([
b[0] * cosTheta + a[0] * sinTheta,
b[1] * cosTheta + a[1] * sinTheta,
b[2] * cosTheta + a[2] * sinTheta
])
);
a = b;
}
return hexagon;
});
const cornerNormals = [];
const parents = [-1, 3, 0, 2, 0, 1, 4, 5];
ck.forEach((hexagon, j) => {
const face = octahedron[j],
n = face.length,
normals = (cornerNormals[j] = []);
for (let i = 0; i < n; ++i) {
ck.push([
face[i],
hexagon[(i * 2 + 2) % (2 * n)],
hexagon[(i * 2 + 1) % (2 * n)]
]);
parents.push(j);
normals.push(
cartesianCross(
cartesianDegrees(hexagon[(i * 2 + 2) % (2 * n)]),
cartesianDegrees(hexagon[(i * 2 + 1) % (2 * n)])
)
);
}
});
const faces = ck.map((face) => ({project: faceProjection(face), face}));
parents.forEach((d, i) => {
const parent = faces[d];
parent && (parent.children || (parent.children = [])).push(faces[i]);
});
return polyhedral(faces[0], face, 0, true)
.scale(0.023975)
.rotate([20, 0])
.center([0,-17]);
function face(lambda, phi) {
const cosPhi = cos(phi);
const p = [cosPhi * cos(lambda), cosPhi * sin(lambda), sin(phi)];
const hexagon =
lambda < -pi / 2
? phi < 0 ? 6 : 4
: lambda < 0
? phi < 0 ? 2 : 0
: lambda < pi / 2 ? (phi < 0 ? 3 : 1) : phi < 0 ? 7 : 5;
const n = cornerNormals[hexagon];
return faces[
cartesianDot(n[0], p) < 0
? 8 + 3 * hexagon
: cartesianDot(n[1], p) < 0
? 8 + 3 * hexagon + 1
: cartesianDot(n[2], p) < 0 ? 8 + 3 * hexagon + 2 : hexagon
];
}
}
// all names of reference points, A, B, D, ... , G, P75
// or zones, A-L, are detailed fully in Gene Keyes'
// web site http://www.genekeyes.com/CKOG-OOo/7-CKOG-illus-&-coastline.html
export function cahillKeyesRaw(mg) {
const CK = {
lengthMG: mg // magic scaling length
};
preliminaries();
function preliminaries() {
let pointN, lengthMB, lengthMN, lengthNG, pointU;
let m = 29, // meridian
p = 15, // parallel
p73a,
lF,
lT,
lM,
l,
pointV,
k = sqrt(3);
CK.lengthMA = 940 / 10000 * CK.lengthMG;
CK.lengthParallel0to73At0 = CK.lengthMG / 100;
CK.lengthParallel73to90At0 =
(CK.lengthMG - CK.lengthMA - CK.lengthParallel0to73At0 * 73) / (90 - 73);
CK.sin60 = k / 2; // √3/2
CK.cos60 = 0.5;
CK.pointM = [0, 0];
CK.pointG = [CK.lengthMG, 0];
pointN = [CK.lengthMG, CK.lengthMG * tan(30 * radians)];
CK.pointA = [CK.lengthMA, 0];
CK.pointB = lineIntersection(CK.pointM, 30, CK.pointA, 45);
CK.lengthAG = distance(CK.pointA, CK.pointG);
CK.lengthAB = distance(CK.pointA, CK.pointB);
lengthMB = distance(CK.pointM, CK.pointB);
lengthMN = distance(CK.pointM, pointN);
lengthNG = distance(pointN, CK.pointG);
CK.pointD = interpolate(lengthMB, lengthMN, pointN, CK.pointM);
CK.pointF = [CK.lengthMG, lengthNG - lengthMB];
CK.pointE = [
pointN[0] - CK.lengthMA * sin(30 * radians),
pointN[1] - CK.lengthMA * cos(30 * radians)
];
CK.lengthGF = distance(CK.pointG, CK.pointF);
CK.lengthBD = distance(CK.pointB, CK.pointD);
CK.lengthBDE = CK.lengthBD + CK.lengthAB; // lengthAB = lengthDE
CK.lengthGFE = CK.lengthGF + CK.lengthAB; // lengthAB = lengthFE
CK.deltaMEq = CK.lengthGFE / 45;
CK.lengthAP75 = (90 - 75) * CK.lengthParallel73to90At0;
CK.lengthAP73 = CK.lengthMG - CK.lengthMA - CK.lengthParallel0to73At0 * 73;
pointU = [
CK.pointA[0] + CK.lengthAP73 * cos(30 * radians),
CK.pointA[1] + CK.lengthAP73 * sin(30 * radians)
];
CK.pointT = lineIntersection(pointU, -60, CK.pointB, 30);
p73a = parallel73(m);
lF = p73a.lengthParallel73;
lT = lengthTorridSegment(m);
lM = lengthMiddleSegment(m);
l = p * (lT + lM + lF) / 73;
pointV = [0, 0];
CK.pointC = [0, 0];
CK.radius = 0;
l = l - lT;
pointV = interpolate(l, lM, jointT(m), jointF(m));
CK.pointC[1] =
(pointV[0] * pointV[0] +
pointV[1] * pointV[1] -
CK.pointD[0] * CK.pointD[0] -
CK.pointD[1] * CK.pointD[1]) /
(2 * (k * pointV[0] + pointV[1] - k * CK.pointD[0] - CK.pointD[1]));
CK.pointC[0] = k * CK.pointC[1];
CK.radius = distance(CK.pointC, CK.pointD);
return CK;
}
//**** helper functions ****//
// distance between two 2D coordinates
function distance(p1, p2) {
return Math.hypot(p1[0] - p2[0], p1[1] - p2[1]);
}
// return 2D point at position length/totallength of the line
// defined by two 2D points, start and end.
function interpolate(length, totalLength, start, end) {
return [
start[0] + (end[0] - start[0]) * length / totalLength,
start[1] + (end[1] - start[1]) * length / totalLength
];
}
// return the 2D point intersection between two lines defined
// by one 2D point and a slope each.
function lineIntersection(point1, slope1, point2, slope2) {
// s1/s2 = slope in degrees
const m1 = tan(slope1 * radians);
const m2 = tan(slope2 * radians);
const x = (m1 * point1[0] - m2 * point2[0] - point1[1] + point2[1]) / (m1 - m2);
return [x, m1 * (x - point1[0]) + point1[1]];
}
// return the 2D point intercepting a circumference centered
// at cc and of radius rn and a line defined by 2 points, p1 and p2:
// First element of the returned array is a flag to state whether there is
// an intersection, a value of zero (0) means NO INTERSECTION.
// The following array is the 2D point of the intersection.
// Equations from "Intersection of a Line and a Sphere (or circle)/Line Segment"
// at http://paulbourke.net/geometry/circlesphere/
function circleLineIntersection(cc, r, p1, p2) {
let x1 = p1[0],
y1 = p1[1],
x2 = p2[0],
y2 = p2[1],
xc = cc[0],
yc = cc[1],
a = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1),
b = 2 * ((x2 - x1) * (x1 - xc) + (y2 - y1) * (y1 - yc)),
c =
xc * xc + yc * yc + x1 * x1 + y1 * y1 - 2 * (xc * x1 + yc * y1) - r * r,
d = b * b - 4 * a * c,
u1 = 0,
u2 = 0,
x = 0,
y = 0;
if (a === 0) {
return [0, [0, 0]];
} else if (d < 0) {
return [0, [0, 0]];
}
u1 = (-b + sqrt(d)) / (2 * a);
u2 = (-b - sqrt(d)) / (2 * a);
if (0 <= u1 && u1 <= 1) {
x = x1 + u1 * (x2 - x1);
y = y1 + u1 * (y2 - y1);
return [1, [x, y]];
} else if (0 <= u2 && u2 <= 1) {
x = x1 + u2 * (x2 - x1);
y = y1 + u2 * (y2 - y1);
return [1, [x, y]];
} else {
return [0, [0, 0]];
}
}
// counterclockwise rotate 2D vector, xy, by angle (in degrees)
// [original CKOG uses clockwise rotation]
function rotate(xy, angle) {
const xynew = [0, 0];
if (angle === -60) {
xynew[0] = xy[0] * CK.cos60 + xy[1] * CK.sin60;
xynew[1] = -xy[0] * CK.sin60 + xy[1] * CK.cos60;
} else if (angle === -120) {
xynew[0] = -xy[0] * CK.cos60 + xy[1] * CK.sin60;
xynew[1] = -xy[0] * CK.sin60 - xy[1] * CK.cos60;
} else {
// !!!!! This should not happen for this projection!!!!
// the general algorithm: cos(angle) * xy + sin(angle) * perpendicular(xy)
// return cos(angle * radians) * xy + sin(angle * radians) * perpendicular(xy);
//console.log("rotate: angle " + angle + " different than -60 or -120!");
// counterclockwise
xynew[0] = xy[0] * cos(angle * radians) - xy[1] * sin(angle * radians);
xynew[1] = xy[0] * sin(angle * radians) + xy[1] * cos(angle * radians);
}
return xynew;
}
// truncate towards zero like int() in Perl
function truncate(n) {
return Math[n > 0 ? "floor" : "ceil"](n);
}
function equator(m) {
const l = CK.deltaMEq * m;
return (l <= CK.lengthGF)
? [CK.pointG[0], l]
: interpolate(l - CK.lengthGF, CK.lengthAB, CK.pointF, CK.pointE);
}
function jointE(m) {
return equator(m);
}
function jointT(m) {
return lineIntersection(CK.pointM, 2 * m / 3, jointE(m), m / 3);
}
function jointF(m) {
if (m === 0) {
return [CK.pointA + CK.lengthAB, 0];
}
return lineIntersection(CK.pointA, m, CK.pointM, 2 * m / 3);
}
function lengthTorridSegment(m) {
return distance(jointE(m), jointT(m));
}
function lengthMiddleSegment(m) {
return distance(jointT(m), jointF(m));
}
function parallel73(m) {
let p73 = [0, 0],
jF = jointF(m),
lF = 0,
xy = [0, 0];
if (m <= 30) {
p73[0] = CK.pointA[0] + CK.lengthAP73 * cos(m * radians);
p73[1] = CK.pointA[1] + CK.lengthAP73 * sin(m * radians);
lF = distance(jF, p73);
} else {
p73 = lineIntersection(CK.pointT, -60, jF, m);
lF = distance(jF, p73);
if (m > 44) {
xy = lineIntersection(CK.pointT, -60, jF, 2 / 3 * m);
if (xy[0] > p73[0]) {
p73 = xy;
lF = -distance(jF, p73);
}
}
}
return {
parallel73: p73,
lengthParallel73: lF
};
}
function parallel75(m) {
return [
CK.pointA[0] + CK.lengthAP75 * cos(m * radians),
CK.pointA[1] + CK.lengthAP75 * sin(m * radians)
];
}
// special functions to transform lon/lat to x/y
function ll2mp(lon, lat) {
const south = [0, 6, 7, 8, 5];
let o = truncate((lon + 180) / 90 + 1);
let m = (lon + 720) % 90 - 45; // meridian
const s = sign(m);
m = abs(m);
if (o === 5) o = 1;
if (lat < 0) o = south[o];
return [m, abs(lat), s, o];
}
function zoneA(m, p) {
return [CK.pointA[0] + (90 - p) * 104, 0];
}
function zoneB(m, p) {
return [CK.pointG[0] - p * 100, 0];
}
function zoneC(m, p) {
const l = 104 * (90 - p);
return [
CK.pointA[0] + l * cos(m * radians),
CK.pointA[1] + l * sin(m * radians)
];
}
function zoneD(m /*, p */) {
// p = p; // just keep it for symmetry in signature
return equator(m);
}
function zoneE(m, p) {
const l = 1560 + (75 - p) * 100;
return [
CK.pointA[0] + l * cos(m * radians),
CK.pointA[1] + l * sin(m * radians)
];
}
function zoneF(m, p) {
return interpolate(p, 15, CK.pointE, CK.pointD);
}
function zoneG(m, p) {
const l = p - 15;
return interpolate(l, 58, CK.pointD, CK.pointT);
}
function zoneH(m, p) {
const p75 = parallel75(45),
p73a = parallel73(m),
p73 = p73a.parallel73,
lF = distance(CK.pointT, CK.pointB),
lF75 = distance(CK.pointB, p75),
l = (75 - p) * (lF75 + lF) / 2;
return (l <= lF75)
? interpolate(l, lF75, p75, CK.pointB)
: interpolate(l - lF75, lF, CK.pointB, p73);
}
function zoneI(m, p) {
const p73a = parallel73(m),
lT = lengthTorridSegment(m),
lM = lengthMiddleSegment(m),
l = p * (lT + lM + p73a.lengthParallel73) / 73;
return (l <= lT)
? interpolate(l, lT, jointE(m), jointT(m))
: (l <= lT + lM)
? interpolate(l - lT, lM, jointT(m), jointF(m))
: interpolate(l - lT - lM, p73a.lengthParallel73, jointF(m), p73a.parallel73);
}
function zoneJ(m, p) {
const p75 = parallel75(m),
lF75 = distance(jointF(m), p75),
p73a = parallel73(m),
p73 = p73a.parallel73,
lF = p73a.lengthParallel73,
l = (75 - p) * (lF75 - lF) / 2;
return (l <= lF75)
? interpolate(l, lF75, p75, jointF(m))
: interpolate(l - lF75, -lF, jointF(m), p73);
}
function zoneK(m, p, l15) {
const l = p * l15 / 15,
lT = lengthTorridSegment(m),
lM = lengthMiddleSegment(m);
return (l <= lT)
// point is in torrid segment
? interpolate(l, lT, jointE(m), jointT(m))
// point is in middle segment
: interpolate(l - lT, lM, jointT(m), jointF(m));
}
function zoneL(m, p, l15) {
const p73a = parallel73(m),
p73 = p73a.parallel73,
lT = lengthTorridSegment(m),
lM = lengthMiddleSegment(m),
lF = p73a.lengthParallel73,
l = l15 + (p - 15) * (lT + lM + lF - l15) / 58;
return (l <= lT)
// on torrid segment
? interpolate(l, lT, jointE(m), jointF(m))
: (l <= lT + lM)
// on middle segment
? interpolate(l - lT, lM, jointT(m), jointF(m))
// on frigid segment
: interpolate(l - lT - lM, lF, jointF(m), p73);
}
// convert half-octant meridian,parallel to x,y coordinates.
// arguments are meridian, parallel
function mp2xy(m, p) {
// zones (a) and (b)
if (m === 0) return (p >= 75) ? zoneA(m, p) : zoneB(m, p);
else if (p >= 75) return zoneC(m, p);
else if (p === 0) return zoneD(m, p);
else if (p >= 73 && m <= 30) return zoneE(m, p);
else if (m === 45) return (p <= 15) ? zoneF(m, p) : (p <= 73) ? zoneG(m, p) : zoneH(m, p);
else {
if (m <= 29) return zoneI(m, p);
else {
// supple zones (j), (k) and (l)
if (p >= 73) return zoneJ(m, p);
else {
const lT = lengthTorridSegment(m);
let l15;
//zones (k) and (l)
let p15a = circleLineIntersection(
CK.pointC,
CK.radius,
jointT(m),
jointF(m)
);
let flag15 = p15a[0];
let p15 = p15a[1];
if (flag15 === 1) {
// intersection is in middle segment
l15 = lT + distance(jointT(m), p15);
} else {
// intersection is in torrid segment
p15a = circleLineIntersection(
CK.pointC,
CK.radius,
jointE(m),
jointT(m)
);
flag15 = p15a[0];
p15 = p15a[1];
if (flag15 === 0) {
//console.log("Something weird!");
// TODO: Trap this! Something odd happened!
}
l15 = lT - distance(jointT(m), p15);
}
return (p <= 15) ? zoneK(m, p, l15) : zoneL(m, p, l15);
}
}
}
}
// from half-octant to megamap (single rotated octant)
function mj2g(xy, octant) {
let xynew;
if (octant === 0) {
xynew = rotate(xy, -60);
} else if (octant === 1) {
xynew = rotate(xy, -120);
xynew[0] -= CK.lengthMG;
} else if (octant === 2) {
xynew = rotate(xy, -60);
xynew[0] -= CK.lengthMG;
} else if (octant === 3) {
xynew = rotate(xy, -120);
xynew[0] += CK.lengthMG;
} else if (octant === 4) {
xynew = rotate(xy, -60);
xynew[0] += CK.lengthMG;
} else if (octant === 5) {
xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -60);
xynew[0] += CK.lengthMG;
} else if (octant === 6) {
xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -120);
xynew[0] -= CK.lengthMG;
} else if (octant === 7) {
xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -60);
xynew[0] -= CK.lengthMG;
} else if (octant === 8) {
xynew = rotate([2 * CK.lengthMG - xy[0], xy[1]], -120);
xynew[0] += CK.lengthMG;
}
return xynew;
}
// general CK map projection
function forward(lambda, phi) {
// lambda, phi are in radians.
const lon = lambda * degrees,
lat = phi * degrees,
res = ll2mp(lon, lat),
m = res[0], // 0 ≤ m ≤ 45
p = res[1], // 0 ≤ p ≤ 90
s = res[2], // -1 / 1 = side of m
o = res[3], // octant
xy = mp2xy(m, p);
return mj2g([xy[0], s * xy[1]], o);
}
forward.invert = solve2d(forward);
return forward;
}
function cahillKeyesProjection() {
const mg = 10000;
const m = projectionMutator(cahillKeyesRaw);
return m(mg);
}