UNPKG

d3-geo-polygon

Version:

Clipping and geometric operations for spherical polygons.

343 lines (295 loc) 8.44 kB
/* * Imago projection, by Justin Kunimune * * Inspired by Hajime Narukawa’s AuthaGraph * */ import { abs, acos, asin, atan, atan2, cos, degrees, epsilon, floor, halfPi, hypot, pi, pow, sign, sin, sqrt, tan, } from "./math.js"; import { geoProjectionMutator as projectionMutator } from "d3-geo"; import clipPolygon from "./clip/polygon.js"; import { solve } from "./newton.js"; const ASIN_ONE_THD = asin(1 / 3), centrums = [ [halfPi, 0, 0, -halfPi, 0, sqrt(3)], [-ASIN_ONE_THD, 0, pi, halfPi, 0, -sqrt(3)], [-ASIN_ONE_THD, (2 * pi) / 3, pi, (5 * pi) / 6, 3, 0], [-ASIN_ONE_THD, (-2 * pi) / 3, pi, pi / 6, -3, 0], ], TETRAHEDRON_WIDE_VERTEX = { sphereSym: 3, planarSym: 6, width: 6, height: 2 * sqrt(3), centrums, rotateOOB: function (x, y, xCen, yCen) { yCen * 0; if (abs(x) > this.width / 2) return [2 * xCen - x, -y]; else return [-x, this.height * sign(y) - y]; }, inBounds: () => true, }, configuration = TETRAHEDRON_WIDE_VERTEX; export function imagoRaw(k) { function faceProject(lon, lat) { const tht = atan(((lon - asin(sin(lon) / sqrt(3))) / pi) * sqrt(12)), p = (halfPi - lat) / atan(sqrt(2) / cos(lon)); return [(pow(p, k) * sqrt(3)) / cos(tht), tht]; } function faceInverse(r, th) { const l = solve( (l) => atan(((l - asin(sin(l) / sqrt(3))) / pi) * sqrt(12)), th, th / 2 ), R = r / (sqrt(3) / cos(th)); return [halfPi - pow(R, 1 / k) * atan(sqrt(2) / cos(l)), l]; } function obliquifySphc(latF, lonF, pole) { if (pole == null) // null pole indicates that this procedure should be bypassed return [latF, lonF]; const lat0 = pole[0], lon0 = pole[1], tht0 = pole[2]; let lat1, lon1; if (lat0 == halfPi) lat1 = latF; else lat1 = asin( sin(lat0) * sin(latF) + cos(lat0) * cos(latF) * cos(lon0 - lonF) ); // relative latitude if (lat0 == halfPi) // accounts for all the 0/0 errors at the poles lon1 = lonF - lon0; else if (lat0 == -halfPi) lon1 = lon0 - lonF - pi; else { lon1 = acos( (cos(lat0) * sin(latF) - sin(lat0) * cos(latF) * cos(lon0 - lonF)) / cos(lat1) ) - pi; // relative longitude if (isNaN(lon1)) { if ( (cos(lon0 - lonF) >= 0 && latF < lat0) || (cos(lon0 - lonF) < 0 && latF < -lat0) ) lon1 = 0; else lon1 = -pi; } else if (sin(lonF - lon0) > 0) // it's a plus-or-minus arccos. lon1 = -lon1; } lon1 = lon1 - tht0; return [lat1, lon1]; } function obliquifyPlnr(coords, pole) { if (pole == null) //this indicates that you just shouldn't do this calculation return coords; let lat1 = coords[0], lon1 = coords[1]; const lat0 = pole[0], lon0 = pole[1], tht0 = pole[2]; lon1 += tht0; let latf = asin(sin(lat0) * sin(lat1) - cos(lat0) * cos(lon1) * cos(lat1)), lonf, innerFunc = sin(lat1) / cos(lat0) / cos(latf) - tan(lat0) * tan(latf); if (lat0 == halfPi) // accounts for special case when lat0 = pi/2 lonf = lon1 + lon0; else if (lat0 == -halfPi) // accounts for special case when lat0 = -pi/2 lonf = -lon1 + lon0 + pi; else if (abs(innerFunc) > 1) { // accounts for special case when cos(lat1) -> 0 if ((lon1 == 0 && lat1 < -lat0) || (lon1 != 0 && lat1 < lat0)) lonf = lon0 + pi; else lonf = lon0; } else if (sin(lon1) > 0) lonf = lon0 + acos(innerFunc); else lonf = lon0 - acos(innerFunc); let thtf = pole[2]; return [latf, lonf, thtf]; } function forward(lon, lat) { const width = configuration.width, height = configuration.height; const numSym = configuration.sphereSym; //we're about to be using this variable a lot let latR = -Infinity; let lonR = -Infinity; let centrum = null; for (const testCentrum of centrums) { //iterate through the centrums to see which goes here const relCoords = obliquifySphc(lat, lon, testCentrum); if (relCoords[0] > latR) { latR = relCoords[0]; lonR = relCoords[1]; centrum = testCentrum; } } const lonR0 = floor((lonR + pi / numSym) / ((2 * pi) / numSym)) * ((2 * pi) / numSym); const rth = faceProject(lonR - lonR0, latR); const r = rth[0]; const th = rth[1] + centrum[3] + (lonR0 * numSym) / configuration.planarSym; const x0 = centrum[4]; const y0 = centrum[5]; let output = [r * cos(th) + x0, r * sin(th) + y0]; if (abs(output[0]) > width / 2 || abs(output[1]) > height / 2) { output = configuration.rotateOOB(output[0], output[1], x0, y0); } return output; } function invert(x, y) { if (isNaN(x) || isNaN(y)) return null; if (!configuration.inBounds(x, y)) return null; const numSym = configuration.planarSym; let rM = +Infinity; let centrum = null; //iterate to see which centrum we get for (const testCentrum of centrums) { const rR = hypot(x - testCentrum[4], y - testCentrum[5]); if (rR < rM) { //pick the centrum that minimises r rM = rR; centrum = testCentrum; } } const th0 = centrum[3], x0 = centrum[4], y0 = centrum[5], r = hypot(x - x0, y - y0), th = atan2(y - y0, x - x0) - th0, thBase = floor((th + pi / numSym) / ((2 * pi) / numSym)) * ((2 * pi) / numSym); let relCoords = faceInverse(r, th - thBase); if (relCoords == null) return null; relCoords[1] = (thBase * numSym) / configuration.sphereSym + relCoords[1]; let absCoords = obliquifyPlnr(relCoords, centrum); return [absCoords[1], absCoords[0]]; } forward.invert = invert; return forward; } export function imagoBlock() { let k = 0.68; const m = projectionMutator(imagoRaw); const p = m(k); p.k = function (_) { return arguments.length ? m((k = +_)) : k; }; const a = -atan(1 / sqrt(2)) * degrees, border = [ [-180 + epsilon, a + epsilon], [0, 90], [180 - epsilon, a + epsilon], [180 - epsilon, a - epsilon], [-180 + epsilon, a - epsilon], [-180 + epsilon, a + epsilon], ]; return p .preclip( clipPolygon({ type: "Polygon", coordinates: [border], }) ) .scale(144.04) .rotate([18, -12.5, 3.5]) .center([0, 35.2644]); } function imagoWideRaw(k, shift) { const imago = imagoRaw(k); const height = configuration.height; function forward(lon, lat) { const p = imago(lon, lat), q = [p[1], -p[0]]; if (q[1] > 0) { q[0] = height - q[0]; q[1] *= -1; } q[0] += shift; if (q[0] < 0) q[0] += height * 2; return q; } function invert(x, y) { x = (x - shift) / height; if (x > 1.5) x -= 2; if (x > 0.5) { x = 1 - x; y *= -1; } return imago.invert(-y, x * height); } forward.invert = invert; return forward; } export default function () { let k = 0.59; let shift = 1.16; const m = projectionMutator(imagoWideRaw); const p = m(k, shift); p.shift = function (_) { return arguments.length ? clipped(m(k, (shift = +_))) : shift; }; p.k = function (_) { return arguments.length ? clipped(m((k = +_), shift)) : k; }; function clipped(p) { const N = 100 + 2 * epsilon, border = [], e = 3e-3; const scale = p.scale(), center = p.center(), translate = p.translate(), rotate = p.rotate(); p.scale(1).center([0, 90]).rotate([0, 0]).translate([shift, 0]); for (let i = N - epsilon; i > 0; i--) { border.unshift( p.invert([ 1.5 * configuration.height - e, ((configuration.width / 2) * i) / N, ]) ); border.push( p.invert([ -0.5 * configuration.height + e, ((configuration.width / 2) * i) / N, ]) ); } border.push(border[0]); return p .scale(scale) .center(center) .translate(translate) .rotate(rotate) .preclip( clipPolygon({ type: "Polygon", coordinates: [border], }) ); } return clipped(p) .rotate([18, -12.5, 3.5]) .scale(138.42) .translate([480, 250]) .center([-139.405, 40.5844]); }