|
1 (function(){d3.geo = {}; |
|
2 |
|
3 var d3_geo_radians = Math.PI / 180; |
|
4 // TODO clip input coordinates on opposite hemisphere |
|
5 d3.geo.azimuthal = function() { |
|
6 var mode = "orthographic", // or stereographic, gnomonic, equidistant or equalarea |
|
7 origin, |
|
8 scale = 200, |
|
9 translate = [480, 250], |
|
10 x0, |
|
11 y0, |
|
12 cy0, |
|
13 sy0; |
|
14 |
|
15 function azimuthal(coordinates) { |
|
16 var x1 = coordinates[0] * d3_geo_radians - x0, |
|
17 y1 = coordinates[1] * d3_geo_radians, |
|
18 cx1 = Math.cos(x1), |
|
19 sx1 = Math.sin(x1), |
|
20 cy1 = Math.cos(y1), |
|
21 sy1 = Math.sin(y1), |
|
22 cc = mode !== "orthographic" ? sy0 * sy1 + cy0 * cy1 * cx1 : null, |
|
23 c, |
|
24 k = mode === "stereographic" ? 1 / (1 + cc) |
|
25 : mode === "gnomonic" ? 1 / cc |
|
26 : mode === "equidistant" ? (c = Math.acos(cc), c ? c / Math.sin(c) : 0) |
|
27 : mode === "equalarea" ? Math.sqrt(2 / (1 + cc)) |
|
28 : 1, |
|
29 x = k * cy1 * sx1, |
|
30 y = k * (sy0 * cy1 * cx1 - cy0 * sy1); |
|
31 return [ |
|
32 scale * x + translate[0], |
|
33 scale * y + translate[1] |
|
34 ]; |
|
35 } |
|
36 |
|
37 azimuthal.invert = function(coordinates) { |
|
38 var x = (coordinates[0] - translate[0]) / scale, |
|
39 y = (coordinates[1] - translate[1]) / scale, |
|
40 p = Math.sqrt(x * x + y * y), |
|
41 c = mode === "stereographic" ? 2 * Math.atan(p) |
|
42 : mode === "gnomonic" ? Math.atan(p) |
|
43 : mode === "equidistant" ? p |
|
44 : mode === "equalarea" ? 2 * Math.asin(.5 * p) |
|
45 : Math.asin(p), |
|
46 sc = Math.sin(c), |
|
47 cc = Math.cos(c); |
|
48 return [ |
|
49 (x0 + Math.atan2(x * sc, p * cy0 * cc + y * sy0 * sc)) / d3_geo_radians, |
|
50 Math.asin(cc * sy0 - (p ? (y * sc * cy0) / p : 0)) / d3_geo_radians |
|
51 ]; |
|
52 }; |
|
53 |
|
54 azimuthal.mode = function(x) { |
|
55 if (!arguments.length) return mode; |
|
56 mode = x + ""; |
|
57 return azimuthal; |
|
58 }; |
|
59 |
|
60 azimuthal.origin = function(x) { |
|
61 if (!arguments.length) return origin; |
|
62 origin = x; |
|
63 x0 = origin[0] * d3_geo_radians; |
|
64 y0 = origin[1] * d3_geo_radians; |
|
65 cy0 = Math.cos(y0); |
|
66 sy0 = Math.sin(y0); |
|
67 return azimuthal; |
|
68 }; |
|
69 |
|
70 azimuthal.scale = function(x) { |
|
71 if (!arguments.length) return scale; |
|
72 scale = +x; |
|
73 return azimuthal; |
|
74 }; |
|
75 |
|
76 azimuthal.translate = function(x) { |
|
77 if (!arguments.length) return translate; |
|
78 translate = [+x[0], +x[1]]; |
|
79 return azimuthal; |
|
80 }; |
|
81 |
|
82 return azimuthal.origin([0, 0]); |
|
83 }; |
|
84 // Derived from Tom Carden's Albers implementation for Protovis. |
|
85 // http://gist.github.com/476238 |
|
86 // http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html |
|
87 |
|
88 d3.geo.albers = function() { |
|
89 var origin = [-98, 38], |
|
90 parallels = [29.5, 45.5], |
|
91 scale = 1000, |
|
92 translate = [480, 250], |
|
93 lng0, // d3_geo_radians * origin[0] |
|
94 n, |
|
95 C, |
|
96 p0; |
|
97 |
|
98 function albers(coordinates) { |
|
99 var t = n * (d3_geo_radians * coordinates[0] - lng0), |
|
100 p = Math.sqrt(C - 2 * n * Math.sin(d3_geo_radians * coordinates[1])) / n; |
|
101 return [ |
|
102 scale * p * Math.sin(t) + translate[0], |
|
103 scale * (p * Math.cos(t) - p0) + translate[1] |
|
104 ]; |
|
105 } |
|
106 |
|
107 albers.invert = function(coordinates) { |
|
108 var x = (coordinates[0] - translate[0]) / scale, |
|
109 y = (coordinates[1] - translate[1]) / scale, |
|
110 p0y = p0 + y, |
|
111 t = Math.atan2(x, p0y), |
|
112 p = Math.sqrt(x * x + p0y * p0y); |
|
113 return [ |
|
114 (lng0 + t / n) / d3_geo_radians, |
|
115 Math.asin((C - p * p * n * n) / (2 * n)) / d3_geo_radians |
|
116 ]; |
|
117 }; |
|
118 |
|
119 function reload() { |
|
120 var phi1 = d3_geo_radians * parallels[0], |
|
121 phi2 = d3_geo_radians * parallels[1], |
|
122 lat0 = d3_geo_radians * origin[1], |
|
123 s = Math.sin(phi1), |
|
124 c = Math.cos(phi1); |
|
125 lng0 = d3_geo_radians * origin[0]; |
|
126 n = .5 * (s + Math.sin(phi2)); |
|
127 C = c * c + 2 * n * s; |
|
128 p0 = Math.sqrt(C - 2 * n * Math.sin(lat0)) / n; |
|
129 return albers; |
|
130 } |
|
131 |
|
132 albers.origin = function(x) { |
|
133 if (!arguments.length) return origin; |
|
134 origin = [+x[0], +x[1]]; |
|
135 return reload(); |
|
136 }; |
|
137 |
|
138 albers.parallels = function(x) { |
|
139 if (!arguments.length) return parallels; |
|
140 parallels = [+x[0], +x[1]]; |
|
141 return reload(); |
|
142 }; |
|
143 |
|
144 albers.scale = function(x) { |
|
145 if (!arguments.length) return scale; |
|
146 scale = +x; |
|
147 return albers; |
|
148 }; |
|
149 |
|
150 albers.translate = function(x) { |
|
151 if (!arguments.length) return translate; |
|
152 translate = [+x[0], +x[1]]; |
|
153 return albers; |
|
154 }; |
|
155 |
|
156 return reload(); |
|
157 }; |
|
158 |
|
159 // A composite projection for the United States, 960x500. The set of standard |
|
160 // parallels for each region comes from USGS, which is published here: |
|
161 // http://egsc.usgs.gov/isb/pubs/MapProjections/projections.html#albers |
|
162 // TODO allow the composite projection to be rescaled? |
|
163 d3.geo.albersUsa = function() { |
|
164 var lower48 = d3.geo.albers(); |
|
165 |
|
166 var alaska = d3.geo.albers() |
|
167 .origin([-160, 60]) |
|
168 .parallels([55, 65]); |
|
169 |
|
170 var hawaii = d3.geo.albers() |
|
171 .origin([-160, 20]) |
|
172 .parallels([8, 18]); |
|
173 |
|
174 var puertoRico = d3.geo.albers() |
|
175 .origin([-60, 10]) |
|
176 .parallels([8, 18]); |
|
177 |
|
178 function albersUsa(coordinates) { |
|
179 var lon = coordinates[0], |
|
180 lat = coordinates[1]; |
|
181 return (lat > 50 ? alaska |
|
182 : lon < -140 ? hawaii |
|
183 : lat < 21 ? puertoRico |
|
184 : lower48)(coordinates); |
|
185 } |
|
186 |
|
187 albersUsa.scale = function(x) { |
|
188 if (!arguments.length) return lower48.scale(); |
|
189 lower48.scale(x); |
|
190 alaska.scale(x * .6); |
|
191 hawaii.scale(x); |
|
192 puertoRico.scale(x * 1.5); |
|
193 return albersUsa.translate(lower48.translate()); |
|
194 }; |
|
195 |
|
196 albersUsa.translate = function(x) { |
|
197 if (!arguments.length) return lower48.translate(); |
|
198 var dz = lower48.scale() / 1000, |
|
199 dx = x[0], |
|
200 dy = x[1]; |
|
201 lower48.translate(x); |
|
202 alaska.translate([dx - 400 * dz, dy + 170 * dz]); |
|
203 hawaii.translate([dx - 190 * dz, dy + 200 * dz]); |
|
204 puertoRico.translate([dx + 580 * dz, dy + 430 * dz]); |
|
205 return albersUsa; |
|
206 }; |
|
207 |
|
208 return albersUsa.scale(lower48.scale()); |
|
209 }; |
|
210 d3.geo.bonne = function() { |
|
211 var scale = 200, |
|
212 translate = [480, 250], |
|
213 x0, // origin longitude in radians |
|
214 y0, // origin latitude in radians |
|
215 y1, // parallel latitude in radians |
|
216 c1; // cot(y1) |
|
217 |
|
218 function bonne(coordinates) { |
|
219 var x = coordinates[0] * d3_geo_radians - x0, |
|
220 y = coordinates[1] * d3_geo_radians - y0; |
|
221 if (y1) { |
|
222 var p = c1 + y1 - y, E = x * Math.cos(y) / p; |
|
223 x = p * Math.sin(E); |
|
224 y = p * Math.cos(E) - c1; |
|
225 } else { |
|
226 x *= Math.cos(y); |
|
227 y *= -1; |
|
228 } |
|
229 return [ |
|
230 scale * x + translate[0], |
|
231 scale * y + translate[1] |
|
232 ]; |
|
233 } |
|
234 |
|
235 bonne.invert = function(coordinates) { |
|
236 var x = (coordinates[0] - translate[0]) / scale, |
|
237 y = (coordinates[1] - translate[1]) / scale; |
|
238 if (y1) { |
|
239 var c = c1 + y, p = Math.sqrt(x * x + c * c); |
|
240 y = c1 + y1 - p; |
|
241 x = x0 + p * Math.atan2(x, c) / Math.cos(y); |
|
242 } else { |
|
243 y *= -1; |
|
244 x /= Math.cos(y); |
|
245 } |
|
246 return [ |
|
247 x / d3_geo_radians, |
|
248 y / d3_geo_radians |
|
249 ]; |
|
250 }; |
|
251 |
|
252 // 90° for Werner, 0° for Sinusoidal |
|
253 bonne.parallel = function(x) { |
|
254 if (!arguments.length) return y1 / d3_geo_radians; |
|
255 c1 = 1 / Math.tan(y1 = x * d3_geo_radians); |
|
256 return bonne; |
|
257 }; |
|
258 |
|
259 bonne.origin = function(x) { |
|
260 if (!arguments.length) return [x0 / d3_geo_radians, y0 / d3_geo_radians]; |
|
261 x0 = x[0] * d3_geo_radians; |
|
262 y0 = x[1] * d3_geo_radians; |
|
263 return bonne; |
|
264 }; |
|
265 |
|
266 bonne.scale = function(x) { |
|
267 if (!arguments.length) return scale; |
|
268 scale = +x; |
|
269 return bonne; |
|
270 }; |
|
271 |
|
272 bonne.translate = function(x) { |
|
273 if (!arguments.length) return translate; |
|
274 translate = [+x[0], +x[1]]; |
|
275 return bonne; |
|
276 }; |
|
277 |
|
278 return bonne.origin([0, 0]).parallel(45); |
|
279 }; |
|
280 d3.geo.equirectangular = function() { |
|
281 var scale = 500, |
|
282 translate = [480, 250]; |
|
283 |
|
284 function equirectangular(coordinates) { |
|
285 var x = coordinates[0] / 360, |
|
286 y = -coordinates[1] / 360; |
|
287 return [ |
|
288 scale * x + translate[0], |
|
289 scale * y + translate[1] |
|
290 ]; |
|
291 } |
|
292 |
|
293 equirectangular.invert = function(coordinates) { |
|
294 var x = (coordinates[0] - translate[0]) / scale, |
|
295 y = (coordinates[1] - translate[1]) / scale; |
|
296 return [ |
|
297 360 * x, |
|
298 -360 * y |
|
299 ]; |
|
300 }; |
|
301 |
|
302 equirectangular.scale = function(x) { |
|
303 if (!arguments.length) return scale; |
|
304 scale = +x; |
|
305 return equirectangular; |
|
306 }; |
|
307 |
|
308 equirectangular.translate = function(x) { |
|
309 if (!arguments.length) return translate; |
|
310 translate = [+x[0], +x[1]]; |
|
311 return equirectangular; |
|
312 }; |
|
313 |
|
314 return equirectangular; |
|
315 }; |
|
316 d3.geo.mercator = function() { |
|
317 var scale = 500, |
|
318 translate = [480, 250]; |
|
319 |
|
320 function mercator(coordinates) { |
|
321 var x = coordinates[0] / 360, |
|
322 y = -(Math.log(Math.tan(Math.PI / 4 + coordinates[1] * d3_geo_radians / 2)) / d3_geo_radians) / 360; |
|
323 return [ |
|
324 scale * x + translate[0], |
|
325 scale * Math.max(-.5, Math.min(.5, y)) + translate[1] |
|
326 ]; |
|
327 } |
|
328 |
|
329 mercator.invert = function(coordinates) { |
|
330 var x = (coordinates[0] - translate[0]) / scale, |
|
331 y = (coordinates[1] - translate[1]) / scale; |
|
332 return [ |
|
333 360 * x, |
|
334 2 * Math.atan(Math.exp(-360 * y * d3_geo_radians)) / d3_geo_radians - 90 |
|
335 ]; |
|
336 }; |
|
337 |
|
338 mercator.scale = function(x) { |
|
339 if (!arguments.length) return scale; |
|
340 scale = +x; |
|
341 return mercator; |
|
342 }; |
|
343 |
|
344 mercator.translate = function(x) { |
|
345 if (!arguments.length) return translate; |
|
346 translate = [+x[0], +x[1]]; |
|
347 return mercator; |
|
348 }; |
|
349 |
|
350 return mercator; |
|
351 }; |
|
352 function d3_geo_type(types, defaultValue) { |
|
353 return function(object) { |
|
354 return object && object.type in types ? types[object.type](object) : defaultValue; |
|
355 }; |
|
356 } |
|
357 /** |
|
358 * Returns a function that, given a GeoJSON object (e.g., a feature), returns |
|
359 * the corresponding SVG path. The function can be customized by overriding the |
|
360 * projection. Point features are mapped to circles with a default radius of |
|
361 * 4.5px; the radius can be specified either as a constant or a function that |
|
362 * is evaluated per object. |
|
363 */ |
|
364 d3.geo.path = function() { |
|
365 var pointRadius = 4.5, |
|
366 pointCircle = d3_path_circle(pointRadius), |
|
367 projection = d3.geo.albersUsa(); |
|
368 |
|
369 function path(d, i) { |
|
370 if (typeof pointRadius === "function") { |
|
371 pointCircle = d3_path_circle(pointRadius.apply(this, arguments)); |
|
372 } |
|
373 return pathType(d) || null; |
|
374 } |
|
375 |
|
376 function project(coordinates) { |
|
377 return projection(coordinates).join(","); |
|
378 } |
|
379 |
|
380 var pathType = d3_geo_type({ |
|
381 |
|
382 FeatureCollection: function(o) { |
|
383 var path = [], |
|
384 features = o.features, |
|
385 i = -1, // features.index |
|
386 n = features.length; |
|
387 while (++i < n) path.push(pathType(features[i].geometry)); |
|
388 return path.join(""); |
|
389 }, |
|
390 |
|
391 Feature: function(o) { |
|
392 return pathType(o.geometry); |
|
393 }, |
|
394 |
|
395 Point: function(o) { |
|
396 return "M" + project(o.coordinates) + pointCircle; |
|
397 }, |
|
398 |
|
399 MultiPoint: function(o) { |
|
400 var path = [], |
|
401 coordinates = o.coordinates, |
|
402 i = -1, // coordinates.index |
|
403 n = coordinates.length; |
|
404 while (++i < n) path.push("M", project(coordinates[i]), pointCircle); |
|
405 return path.join(""); |
|
406 }, |
|
407 |
|
408 LineString: function(o) { |
|
409 var path = ["M"], |
|
410 coordinates = o.coordinates, |
|
411 i = -1, // coordinates.index |
|
412 n = coordinates.length; |
|
413 while (++i < n) path.push(project(coordinates[i]), "L"); |
|
414 path.pop(); |
|
415 return path.join(""); |
|
416 }, |
|
417 |
|
418 MultiLineString: function(o) { |
|
419 var path = [], |
|
420 coordinates = o.coordinates, |
|
421 i = -1, // coordinates.index |
|
422 n = coordinates.length, |
|
423 subcoordinates, // coordinates[i] |
|
424 j, // subcoordinates.index |
|
425 m; // subcoordinates.length |
|
426 while (++i < n) { |
|
427 subcoordinates = coordinates[i]; |
|
428 j = -1; |
|
429 m = subcoordinates.length; |
|
430 path.push("M"); |
|
431 while (++j < m) path.push(project(subcoordinates[j]), "L"); |
|
432 path.pop(); |
|
433 } |
|
434 return path.join(""); |
|
435 }, |
|
436 |
|
437 Polygon: function(o) { |
|
438 var path = [], |
|
439 coordinates = o.coordinates, |
|
440 i = -1, // coordinates.index |
|
441 n = coordinates.length, |
|
442 subcoordinates, // coordinates[i] |
|
443 j, // subcoordinates.index |
|
444 m; // subcoordinates.length |
|
445 while (++i < n) { |
|
446 subcoordinates = coordinates[i]; |
|
447 j = -1; |
|
448 if ((m = subcoordinates.length - 1) > 0) { |
|
449 path.push("M"); |
|
450 while (++j < m) path.push(project(subcoordinates[j]), "L"); |
|
451 path[path.length - 1] = "Z"; |
|
452 } |
|
453 } |
|
454 return path.join(""); |
|
455 }, |
|
456 |
|
457 MultiPolygon: function(o) { |
|
458 var path = [], |
|
459 coordinates = o.coordinates, |
|
460 i = -1, // coordinates index |
|
461 n = coordinates.length, |
|
462 subcoordinates, // coordinates[i] |
|
463 j, // subcoordinates index |
|
464 m, // subcoordinates.length |
|
465 subsubcoordinates, // subcoordinates[j] |
|
466 k, // subsubcoordinates index |
|
467 p; // subsubcoordinates.length |
|
468 while (++i < n) { |
|
469 subcoordinates = coordinates[i]; |
|
470 j = -1; |
|
471 m = subcoordinates.length; |
|
472 while (++j < m) { |
|
473 subsubcoordinates = subcoordinates[j]; |
|
474 k = -1; |
|
475 if ((p = subsubcoordinates.length - 1) > 0) { |
|
476 path.push("M"); |
|
477 while (++k < p) path.push(project(subsubcoordinates[k]), "L"); |
|
478 path[path.length - 1] = "Z"; |
|
479 } |
|
480 } |
|
481 } |
|
482 return path.join(""); |
|
483 }, |
|
484 |
|
485 GeometryCollection: function(o) { |
|
486 var path = [], |
|
487 geometries = o.geometries, |
|
488 i = -1, // geometries index |
|
489 n = geometries.length; |
|
490 while (++i < n) path.push(pathType(geometries[i])); |
|
491 return path.join(""); |
|
492 } |
|
493 |
|
494 }); |
|
495 |
|
496 var areaType = path.area = d3_geo_type({ |
|
497 |
|
498 FeatureCollection: function(o) { |
|
499 var area = 0, |
|
500 features = o.features, |
|
501 i = -1, // features.index |
|
502 n = features.length; |
|
503 while (++i < n) area += areaType(features[i]); |
|
504 return area; |
|
505 }, |
|
506 |
|
507 Feature: function(o) { |
|
508 return areaType(o.geometry); |
|
509 }, |
|
510 |
|
511 Polygon: function(o) { |
|
512 return polygonArea(o.coordinates); |
|
513 }, |
|
514 |
|
515 MultiPolygon: function(o) { |
|
516 var sum = 0, |
|
517 coordinates = o.coordinates, |
|
518 i = -1, // coordinates index |
|
519 n = coordinates.length; |
|
520 while (++i < n) sum += polygonArea(coordinates[i]); |
|
521 return sum; |
|
522 }, |
|
523 |
|
524 GeometryCollection: function(o) { |
|
525 var sum = 0, |
|
526 geometries = o.geometries, |
|
527 i = -1, // geometries index |
|
528 n = geometries.length; |
|
529 while (++i < n) sum += areaType(geometries[i]); |
|
530 return sum; |
|
531 } |
|
532 |
|
533 }, 0); |
|
534 |
|
535 function polygonArea(coordinates) { |
|
536 var sum = area(coordinates[0]), // exterior ring |
|
537 i = 0, // coordinates.index |
|
538 n = coordinates.length; |
|
539 while (++i < n) sum -= area(coordinates[i]); // holes |
|
540 return sum; |
|
541 } |
|
542 |
|
543 function polygonCentroid(coordinates) { |
|
544 var polygon = d3.geom.polygon(coordinates[0].map(projection)), // exterior ring |
|
545 area = polygon.area(), |
|
546 centroid = polygon.centroid(area < 0 ? (area *= -1, 1) : -1), |
|
547 x = centroid[0], |
|
548 y = centroid[1], |
|
549 z = area, |
|
550 i = 0, // coordinates index |
|
551 n = coordinates.length; |
|
552 while (++i < n) { |
|
553 polygon = d3.geom.polygon(coordinates[i].map(projection)); // holes |
|
554 area = polygon.area(); |
|
555 centroid = polygon.centroid(area < 0 ? (area *= -1, 1) : -1); |
|
556 x -= centroid[0]; |
|
557 y -= centroid[1]; |
|
558 z -= area; |
|
559 } |
|
560 return [x, y, 6 * z]; // weighted centroid |
|
561 } |
|
562 |
|
563 var centroidType = path.centroid = d3_geo_type({ |
|
564 |
|
565 // TODO FeatureCollection |
|
566 // TODO Point |
|
567 // TODO MultiPoint |
|
568 // TODO LineString |
|
569 // TODO MultiLineString |
|
570 // TODO GeometryCollection |
|
571 |
|
572 Feature: function(o) { |
|
573 return centroidType(o.geometry); |
|
574 }, |
|
575 |
|
576 Polygon: function(o) { |
|
577 var centroid = polygonCentroid(o.coordinates); |
|
578 return [centroid[0] / centroid[2], centroid[1] / centroid[2]]; |
|
579 }, |
|
580 |
|
581 MultiPolygon: function(o) { |
|
582 var area = 0, |
|
583 coordinates = o.coordinates, |
|
584 centroid, |
|
585 x = 0, |
|
586 y = 0, |
|
587 z = 0, |
|
588 i = -1, // coordinates index |
|
589 n = coordinates.length; |
|
590 while (++i < n) { |
|
591 centroid = polygonCentroid(coordinates[i]); |
|
592 x += centroid[0]; |
|
593 y += centroid[1]; |
|
594 z += centroid[2]; |
|
595 } |
|
596 return [x / z, y / z]; |
|
597 } |
|
598 |
|
599 }); |
|
600 |
|
601 function area(coordinates) { |
|
602 return Math.abs(d3.geom.polygon(coordinates.map(projection)).area()); |
|
603 } |
|
604 |
|
605 path.projection = function(x) { |
|
606 projection = x; |
|
607 return path; |
|
608 }; |
|
609 |
|
610 path.pointRadius = function(x) { |
|
611 if (typeof x === "function") pointRadius = x; |
|
612 else { |
|
613 pointRadius = +x; |
|
614 pointCircle = d3_path_circle(pointRadius); |
|
615 } |
|
616 return path; |
|
617 }; |
|
618 |
|
619 return path; |
|
620 }; |
|
621 |
|
622 function d3_path_circle(radius) { |
|
623 return "m0," + radius |
|
624 + "a" + radius + "," + radius + " 0 1,1 0," + (-2 * radius) |
|
625 + "a" + radius + "," + radius + " 0 1,1 0," + (+2 * radius) |
|
626 + "z"; |
|
627 } |
|
628 /** |
|
629 * Given a GeoJSON object, returns the corresponding bounding box. The bounding |
|
630 * box is represented by a two-dimensional array: [[left, bottom], [right, |
|
631 * top]], where left is the minimum longitude, bottom is the minimum latitude, |
|
632 * right is maximum longitude, and top is the maximum latitude. |
|
633 */ |
|
634 d3.geo.bounds = function(feature) { |
|
635 var left = Infinity, |
|
636 bottom = Infinity, |
|
637 right = -Infinity, |
|
638 top = -Infinity; |
|
639 d3_geo_bounds(feature, function(x, y) { |
|
640 if (x < left) left = x; |
|
641 if (x > right) right = x; |
|
642 if (y < bottom) bottom = y; |
|
643 if (y > top) top = y; |
|
644 }); |
|
645 return [[left, bottom], [right, top]]; |
|
646 }; |
|
647 |
|
648 function d3_geo_bounds(o, f) { |
|
649 if (o.type in d3_geo_boundsTypes) d3_geo_boundsTypes[o.type](o, f); |
|
650 } |
|
651 |
|
652 var d3_geo_boundsTypes = { |
|
653 Feature: d3_geo_boundsFeature, |
|
654 FeatureCollection: d3_geo_boundsFeatureCollection, |
|
655 LineString: d3_geo_boundsLineString, |
|
656 MultiLineString: d3_geo_boundsMultiLineString, |
|
657 MultiPoint: d3_geo_boundsLineString, |
|
658 MultiPolygon: d3_geo_boundsMultiPolygon, |
|
659 Point: d3_geo_boundsPoint, |
|
660 Polygon: d3_geo_boundsPolygon |
|
661 }; |
|
662 |
|
663 function d3_geo_boundsFeature(o, f) { |
|
664 d3_geo_bounds(o.geometry, f); |
|
665 } |
|
666 |
|
667 function d3_geo_boundsFeatureCollection(o, f) { |
|
668 for (var a = o.features, i = 0, n = a.length; i < n; i++) { |
|
669 d3_geo_bounds(a[i].geometry, f); |
|
670 } |
|
671 } |
|
672 |
|
673 function d3_geo_boundsLineString(o, f) { |
|
674 for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) { |
|
675 f.apply(null, a[i]); |
|
676 } |
|
677 } |
|
678 |
|
679 function d3_geo_boundsMultiLineString(o, f) { |
|
680 for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) { |
|
681 for (var b = a[i], j = 0, m = b.length; j < m; j++) { |
|
682 f.apply(null, b[j]); |
|
683 } |
|
684 } |
|
685 } |
|
686 |
|
687 function d3_geo_boundsMultiPolygon(o, f) { |
|
688 for (var a = o.coordinates, i = 0, n = a.length; i < n; i++) { |
|
689 for (var b = a[i][0], j = 0, m = b.length; j < m; j++) { |
|
690 f.apply(null, b[j]); |
|
691 } |
|
692 } |
|
693 } |
|
694 |
|
695 function d3_geo_boundsPoint(o, f) { |
|
696 f.apply(null, o.coordinates); |
|
697 } |
|
698 |
|
699 function d3_geo_boundsPolygon(o, f) { |
|
700 for (var a = o.coordinates[0], i = 0, n = a.length; i < n; i++) { |
|
701 f.apply(null, a[i]); |
|
702 } |
|
703 } |
|
704 // TODO breakAtDateLine? |
|
705 |
|
706 d3.geo.circle = function() { |
|
707 var origin = [0, 0], |
|
708 degrees = 90 - 1e-2, |
|
709 radians = degrees * d3_geo_radians, |
|
710 arc = d3.geo.greatArc().target(Object); |
|
711 |
|
712 function circle() { |
|
713 // TODO render a circle as a Polygon |
|
714 } |
|
715 |
|
716 function visible(point) { |
|
717 return arc.distance(point) < radians; |
|
718 } |
|
719 |
|
720 circle.clip = function(d) { |
|
721 arc.source(typeof origin === "function" ? origin.apply(this, arguments) : origin); |
|
722 return clipType(d); |
|
723 }; |
|
724 |
|
725 var clipType = d3_geo_type({ |
|
726 |
|
727 FeatureCollection: function(o) { |
|
728 var features = o.features.map(clipType).filter(Object); |
|
729 return features && (o = Object.create(o), o.features = features, o); |
|
730 }, |
|
731 |
|
732 Feature: function(o) { |
|
733 var geometry = clipType(o.geometry); |
|
734 return geometry && (o = Object.create(o), o.geometry = geometry, o); |
|
735 }, |
|
736 |
|
737 Point: function(o) { |
|
738 return visible(o.coordinates) && o; |
|
739 }, |
|
740 |
|
741 MultiPoint: function(o) { |
|
742 var coordinates = o.coordinates.filter(visible); |
|
743 return coordinates.length && { |
|
744 type: o.type, |
|
745 coordinates: coordinates |
|
746 }; |
|
747 }, |
|
748 |
|
749 LineString: function(o) { |
|
750 var coordinates = clip(o.coordinates); |
|
751 return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o); |
|
752 }, |
|
753 |
|
754 MultiLineString: function(o) { |
|
755 var coordinates = o.coordinates.map(clip).filter(function(d) { return d.length; }); |
|
756 return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o); |
|
757 }, |
|
758 |
|
759 Polygon: function(o) { |
|
760 var coordinates = o.coordinates.map(clip); |
|
761 return coordinates[0].length && (o = Object.create(o), o.coordinates = coordinates, o); |
|
762 }, |
|
763 |
|
764 MultiPolygon: function(o) { |
|
765 var coordinates = o.coordinates.map(function(d) { return d.map(clip); }).filter(function(d) { return d[0].length; }); |
|
766 return coordinates.length && (o = Object.create(o), o.coordinates = coordinates, o); |
|
767 }, |
|
768 |
|
769 GeometryCollection: function(o) { |
|
770 var geometries = o.geometries.map(clipType).filter(Object); |
|
771 return geometries.length && (o = Object.create(o), o.geometries = geometries, o); |
|
772 } |
|
773 |
|
774 }); |
|
775 |
|
776 function clip(coordinates) { |
|
777 var i = -1, |
|
778 n = coordinates.length, |
|
779 clipped = [], |
|
780 p0, |
|
781 p1, |
|
782 p2, |
|
783 d0, |
|
784 d1; |
|
785 |
|
786 while (++i < n) { |
|
787 d1 = arc.distance(p2 = coordinates[i]); |
|
788 if (d1 < radians) { |
|
789 if (p1) clipped.push(d3_geo_greatArcInterpolate(p1, p2)((d0 - radians) / (d0 - d1))); |
|
790 clipped.push(p2); |
|
791 p0 = p1 = null; |
|
792 } else { |
|
793 p1 = p2; |
|
794 if (!p0 && clipped.length) { |
|
795 clipped.push(d3_geo_greatArcInterpolate(clipped[clipped.length - 1], p1)((radians - d0) / (d1 - d0))); |
|
796 p0 = p1; |
|
797 } |
|
798 } |
|
799 d0 = d1; |
|
800 } |
|
801 |
|
802 if (p1 && clipped.length) { |
|
803 d1 = arc.distance(p2 = clipped[0]); |
|
804 clipped.push(d3_geo_greatArcInterpolate(p1, p2)((d0 - radians) / (d0 - d1))); |
|
805 } |
|
806 |
|
807 return resample(clipped); |
|
808 } |
|
809 |
|
810 // Resample coordinates, creating great arcs between each. |
|
811 function resample(coordinates) { |
|
812 var i = 0, |
|
813 n = coordinates.length, |
|
814 j, |
|
815 m, |
|
816 resampled = n ? [coordinates[0]] : coordinates, |
|
817 resamples, |
|
818 origin = arc.source(); |
|
819 |
|
820 while (++i < n) { |
|
821 resamples = arc.source(coordinates[i - 1])(coordinates[i]).coordinates; |
|
822 for (j = 0, m = resamples.length; ++j < m;) resampled.push(resamples[j]); |
|
823 } |
|
824 |
|
825 arc.source(origin); |
|
826 return resampled; |
|
827 } |
|
828 |
|
829 circle.origin = function(x) { |
|
830 if (!arguments.length) return origin; |
|
831 origin = x; |
|
832 return circle; |
|
833 }; |
|
834 |
|
835 circle.angle = function(x) { |
|
836 if (!arguments.length) return degrees; |
|
837 radians = (degrees = +x) * d3_geo_radians; |
|
838 return circle; |
|
839 }; |
|
840 |
|
841 // Precision is specified in degrees. |
|
842 circle.precision = function(x) { |
|
843 if (!arguments.length) return arc.precision(); |
|
844 arc.precision(x); |
|
845 return circle; |
|
846 }; |
|
847 |
|
848 return circle; |
|
849 } |
|
850 d3.geo.greatArc = function() { |
|
851 var source = d3_geo_greatArcSource, |
|
852 target = d3_geo_greatArcTarget, |
|
853 precision = 6 * d3_geo_radians; |
|
854 |
|
855 function greatArc() { |
|
856 var a = typeof source === "function" ? source.apply(this, arguments) : source, |
|
857 b = typeof target === "function" ? target.apply(this, arguments) : target, |
|
858 i = d3_geo_greatArcInterpolate(a, b), |
|
859 dt = precision / i.d, |
|
860 t = 0, |
|
861 coordinates = [a]; |
|
862 while ((t += dt) < 1) coordinates.push(i(t)); |
|
863 coordinates.push(b); |
|
864 return { |
|
865 type: "LineString", |
|
866 coordinates: coordinates |
|
867 }; |
|
868 } |
|
869 |
|
870 // Length returned in radians; multiply by radius for distance. |
|
871 greatArc.distance = function() { |
|
872 var a = typeof source === "function" ? source.apply(this, arguments) : source, |
|
873 b = typeof target === "function" ? target.apply(this, arguments) : target; |
|
874 return d3_geo_greatArcInterpolate(a, b).d; |
|
875 }; |
|
876 |
|
877 greatArc.source = function(x) { |
|
878 if (!arguments.length) return source; |
|
879 source = x; |
|
880 return greatArc; |
|
881 }; |
|
882 |
|
883 greatArc.target = function(x) { |
|
884 if (!arguments.length) return target; |
|
885 target = x; |
|
886 return greatArc; |
|
887 }; |
|
888 |
|
889 // Precision is specified in degrees. |
|
890 greatArc.precision = function(x) { |
|
891 if (!arguments.length) return precision / d3_geo_radians; |
|
892 precision = x * d3_geo_radians; |
|
893 return greatArc; |
|
894 }; |
|
895 |
|
896 return greatArc; |
|
897 }; |
|
898 |
|
899 function d3_geo_greatArcSource(d) { |
|
900 return d.source; |
|
901 } |
|
902 |
|
903 function d3_geo_greatArcTarget(d) { |
|
904 return d.target; |
|
905 } |
|
906 |
|
907 function d3_geo_greatArcInterpolate(a, b) { |
|
908 var x0 = a[0] * d3_geo_radians, cx0 = Math.cos(x0), sx0 = Math.sin(x0), |
|
909 y0 = a[1] * d3_geo_radians, cy0 = Math.cos(y0), sy0 = Math.sin(y0), |
|
910 x1 = b[0] * d3_geo_radians, cx1 = Math.cos(x1), sx1 = Math.sin(x1), |
|
911 y1 = b[1] * d3_geo_radians, cy1 = Math.cos(y1), sy1 = Math.sin(y1), |
|
912 d = interpolate.d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)))), |
|
913 sd = Math.sin(d); |
|
914 |
|
915 // From http://williams.best.vwh.net/avform.htm#Intermediate |
|
916 function interpolate(t) { |
|
917 var A = Math.sin(d - (t *= d)) / sd, |
|
918 B = Math.sin(t) / sd, |
|
919 x = A * cy0 * cx0 + B * cy1 * cx1, |
|
920 y = A * cy0 * sx0 + B * cy1 * sx1, |
|
921 z = A * sy0 + B * sy1; |
|
922 return [ |
|
923 Math.atan2(y, x) / d3_geo_radians, |
|
924 Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_geo_radians |
|
925 ]; |
|
926 } |
|
927 |
|
928 return interpolate; |
|
929 } |
|
930 d3.geo.greatCircle = d3.geo.circle; |
|
931 })(); |