1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import {
|
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";
|
25 | import { geoProjectionMutator as projectionMutator } from "d3-geo";
|
26 | import { default as clipPolygon } from "./clip/polygon.js";
|
27 | import { solve } from "./newton.js";
|
28 |
|
29 | var hypot = Math.hypot;
|
30 |
|
31 | var 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 |
|
53 | export 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 |
|
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 | );
|
86 |
|
87 | if (lat0 == halfPi)
|
88 |
|
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;
|
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 |
|
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 |
|
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 |
|
130 | lonf = lon1 + lon0;
|
131 | else if (lat0 == -halfPi)
|
132 |
|
133 | lonf = -lon1 + lon0 + pi;
|
134 | else if (abs(innerFunc) > 1) {
|
135 |
|
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;
|
151 | let latR = -Infinity;
|
152 | let lonR = -Infinity;
|
153 | let centrum = null;
|
154 | for (const testCentrum of centrums) {
|
155 |
|
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;
|
189 | for (const testCentrum of centrums) {
|
190 | const rR = hypot(x - testCentrum[4], y - testCentrum[5]);
|
191 | if (rR < rM) {
|
192 |
|
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 |
|
219 | export 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 |
|
248 | function 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 |
|
286 | export 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 | }
|