UNPKG

@fxi/d3-geo-zoom

Version:

Zoom and Pan D3 Geo projections (Ported from vasturiano/d3-geo-zoom)

165 lines (139 loc) 4.98 kB
/** * A modern TypeScript implementation for handling geographic rotations using quaternions. * This replaces the older versor library with a more type-safe and maintainable solution. */ const EPSILON = 1e-6; export type Quaternion = [number, number, number, number]; export type Vector3 = [number, number, number]; export type Angles = [number, number, number]; export class GeoRotation { private static readonly RAD_TO_DEG = 180 / Math.PI; private static readonly DEG_TO_RAD = Math.PI / 180; /** * Convert geographic angles (lambda, phi, gamma) to a quaternion * @param angles [lambda, phi, gamma] in degrees */ static fromAngles(angles: Angles): Quaternion { if(!Array.isArray(angles)){ throw new Error('fromAngles requires an array'); } const [lambda, phi, gamma] = angles.map(a => a * GeoRotation.DEG_TO_RAD); const halfLambda = lambda / 2; const halfPhi = phi / 2; const halfGamma = gamma / 2; const cl = Math.cos(halfLambda); const sl = Math.sin(halfLambda); const cp = Math.cos(halfPhi); const sp = Math.sin(halfPhi); const cg = Math.cos(halfGamma); const sg = Math.sin(halfGamma); return [ cl * cp * cg + sl * sp * sg, sl * cp * cg - cl * sp * sg, cl * sp * cg + sl * cp * sg, cl * cp * sg - sl * sp * cg ]; } /** * Convert a quaternion back to geographic angles * @param q quaternion * @returns [lambda, phi, gamma] in degrees */ static toAngles(q: Quaternion): Angles { const [a, b, c, d] = q; return [ Math.atan2(2 * (a * b + c * d), 1 - 2 * (b * b + c * c)) * GeoRotation.RAD_TO_DEG, Math.asin(Math.max(-1, Math.min(1, 2 * (a * c - d * b)))) * GeoRotation.RAD_TO_DEG, Math.atan2(2 * (a * d + b * c), 1 - 2 * (c * c + d * d)) * GeoRotation.RAD_TO_DEG ]; } /** * Multiply two quaternions */ static multiply(a: Quaternion, b: Quaternion): Quaternion { const [a1, b1, c1, d1] = a; const [a2, b2, c2, d2] = b; return [ a1 * a2 - b1 * b2 - c1 * c2 - d1 * d2, a1 * b2 + b1 * a2 + c1 * d2 - d1 * c2, a1 * c2 - b1 * d2 + c1 * a2 + d1 * b2, a1 * d2 + b1 * c2 - c1 * b2 + d1 * a2 ]; } /** * Convert spherical coordinates to cartesian coordinates */ static cartesian([lambda, phi]: [number, number]): Vector3 { const l = lambda * GeoRotation.DEG_TO_RAD; const p = phi * GeoRotation.DEG_TO_RAD; const cp = Math.cos(p); return [cp * Math.cos(l), cp * Math.sin(l), Math.sin(p)]; } /** * Calculate the quaternion to rotate between two cartesian points on the sphere */ static delta(v0: Vector3, v1: Vector3, alpha: number = 1): Quaternion { function cross([x0, y0, z0]: Vector3, [x1, y1, z1]: Vector3): Vector3 { return [ y0 * z1 - z0 * y1, z0 * x1 - x0 * z1, x0 * y1 - y0 * x1 ]; } function dot([x0, y0, z0]: Vector3, [x1, y1, z1]: Vector3): number { return x0 * x1 + y0 * y1 + z0 * z1; } const w = cross(v0, v1); const l = Math.sqrt(dot(w, w)); if (l < EPSILON) return [1, 0, 0, 0]; const t = alpha * Math.acos(Math.max(-1, Math.min(1, dot(v0, v1)))) / 2; const s = Math.sin(t); return [Math.cos(t), w[2] / l * s, -w[1] / l * s, w[0] / l * s]; } /** * Interpolate between two sets of angles * @param a Starting angles [lambda, phi, gamma] in degrees * @param b Ending angles [lambda, phi, gamma] in degrees * @param t Interpolation parameter [0-1] * @returns Interpolated angles [lambda, phi, gamma] in degrees */ static interpolateAngles(a: Angles, b: Angles, t: number): Angles { // Convert angles to quaternions const qa = GeoRotation.fromAngles(a); const qb = GeoRotation.fromAngles(b); // Calculate dot product to determine shortest path let dot = qa[0] * qb[0] + qa[1] * qb[1] + qa[2] * qb[2] + qa[3] * qb[3]; // If dot is negative, we need to negate one quaternion to take shortest path if (dot < 0) { qb[0] = -qb[0]; qb[1] = -qb[1]; qb[2] = -qb[2]; qb[3] = -qb[3]; dot = -dot; } // Use linear interpolation for very close quaternions if (dot > 0.9995) { return a.map((start, i) => { let end = b[i]; const diff = ((end - start + 180) % 360) - 180; return start + diff * t; }) as Angles; } // Use spherical interpolation (slerp) for other cases const theta0 = Math.acos(Math.max(-1, Math.min(1, dot))); const theta = theta0 * t; const s = Math.sin(theta); const c = Math.cos(theta); // Calculate interpolated quaternion const scale1 = c; const scale2 = s / Math.sin(theta0); const qi: Quaternion = [ qa[0] * scale1 + qb[0] * scale2, qa[1] * scale1 + qb[1] * scale2, qa[2] * scale1 + qb[2] * scale2, qa[3] * scale1 + qb[3] * scale2 ]; // Convert back to angles return GeoRotation.toAngles(qi); } }