Add d3.geo.greatCircle.polyline and precision.

This can be used to process output from d3.geo.clip to ensure clipped
polygons are correctly curved.

The "n" option has been replaced with precision, which denotes the
approximate angular length of great-circle segments.  This is much
faster than using a fixed number of segments, particularly when
processing a large number of polygons, only a few of which may have
edges long enough to warrant being converted into a geodesic.
This commit is contained in:
Jason Davies 2011-09-12 11:53:37 +01:00
Родитель 0a1af36dcc
Коммит e284f87b0d
7 изменённых файлов: 163 добавлений и 119 удалений

135
d3.geo.js
Просмотреть файл

@ -624,59 +624,25 @@ function d3_geo_boundsPolygon(o, f) {
d3.geo.greatCircle = function() {
var source = d3_geo_greatCircleSource,
target = d3_geo_greatCircleTarget,
n = 100,
coordinates = Object,
precision = 1,
radius = d3_geo_earthRadius;
// TODO: breakAtDateLine?
function greatCircle(d, i) {
var from = source.call(this, d, i),
to = target.call(this, d, i),
x0 = from[0] * d3_radians,
y0 = from[1] * d3_radians,
x1 = to[0] * d3_radians,
y1 = to[1] * d3_radians,
cx0 = Math.cos(x0), sx0 = Math.sin(x0),
cy0 = Math.cos(y0), sy0 = Math.sin(y0),
cx1 = Math.cos(x1), sx1 = Math.sin(x1),
cy1 = Math.cos(y1), sy1 = Math.sin(y1),
d = Math.acos(sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)),
sd = Math.sin(d),
f = d / (n - 1),
e = -f,
path = [],
i = -1;
while (++i < n) {
e += f;
var A = Math.sin(d - e) / sd,
B = Math.sin(e) / sd,
x = A * cy0 * cx0 + B * cy1 * cx1,
y = A * cy0 * sx0 + B * cy1 * sx1,
z = A * sy0 + B * sy1;
path[i] = [
Math.atan2(y, x) / d3_radians,
Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians
];
}
return path;
return d3_geo_greatCirclePath([
source.call(this, d, i), target.call(this, d, i)], precision);
}
greatCircle.source = function(x) {
if (!arguments.length) return source;
source = x;
greatCircle.coordinates = function(x) {
if (!arguments.length) return coordinates;
coordinates = x;
return greatCircle;
};
greatCircle.target = function(x) {
if (!arguments.length) return target;
target = x;
return greatCircle;
};
greatCircle.n = function(x) {
if (!arguments.length) return n;
n = +x;
greatCircle.precision = function(x) {
if (!arguments.length) return precision;
precision = +x;
return greatCircle;
};
@ -701,9 +667,62 @@ d3.geo.greatCircle = function() {
return radius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
greatCircle.polyline = function(d, i) {
return d3_geo_greatCirclePath(coordinates.call(this, d, i), precision);
};
return greatCircle;
};
function d3_geo_greatCirclePath(coordinates, precision) {
var m = coordinates.length;
if (m < 2) return coordinates;
var i = 0,
p = precision * d3_radians,
from = coordinates[0],
to,
x0 = from[0] * d3_radians,
y0 = from[1] * d3_radians,
cx0 = Math.cos(x0), sx0 = Math.sin(x0),
cy0 = Math.cos(y0), sy0 = Math.sin(y0),
path = [from];
while (++i < m) {
to = coordinates[i];
var x1 = to[0] * d3_radians,
y1 = to[1] * d3_radians,
cx1 = Math.cos(x1), sx1 = Math.sin(x1),
cy1 = Math.cos(y1), sy1 = Math.sin(y1),
d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)))),
sd = Math.sin(d),
n = Math.ceil(d / p),
f = d / n,
e = 0,
j = 0;
while (++j < n) {
e += f;
var A = Math.sin(d - e) / sd,
B = Math.sin(e) / sd,
x = A * cy0 * cx0 + B * cy1 * cx1,
y = A * cy0 * sx0 + B * cy1 * sx1,
z = A * sy0 + B * sy1;
path.push([
Math.atan2(y, x) / d3_radians,
Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians
]);
}
path.push(to);
x0 = x1;
y0 = y1;
cx0 = cx1; sx0 = sx1;
cy0 = cy1; sy0 = sy1;
}
return path;
}
function d3_geo_greatCircleSource(d) {
return d.source;
}
@ -714,10 +733,12 @@ function d3_geo_greatCircleTarget(d) {
d3.geo.clip = function() {
var origin = [0, 0],
angle = 90,
r = d3_geo_earthRadius * angle / 180 * Math.PI;
r = d3_geo_earthRadius * angle / 180 * Math.PI,
coordinates = Object;
function clip(d) {
var o = {source: origin, target: null},
function clip(d, i) {
var d = coordinates.call(this, d, i),
o = {source: origin, target: null},
n = d.length,
i = -1,
j,
@ -732,10 +753,7 @@ d3.geo.clip = function() {
if (q) {
path = d3_geo_clipGreatCircle({source: q, target: o.target});
j = d3_geo_clipClosest(path, o, r);
if (p) {
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
}
clipped.push.apply(clipped, path.slice(j));
if (path.length) clipped.push(path[j]);
p = q = null;
} else {
clipped.push(o.target);
@ -745,7 +763,7 @@ d3.geo.clip = function() {
if (!p && clipped.length) {
path = d3_geo_clipGreatCircle({source: clipped[clipped.length - 1], target: o.target});
j = d3_geo_clipClosest(path, o, r);
clipped.push.apply(clipped, path.slice(0, j));
if (path.length) clipped.push(path[j]);
p = o.target;
}
}
@ -754,14 +772,17 @@ d3.geo.clip = function() {
o.target = clipped[0];
path = d3_geo_clipGreatCircle({source: q, target: o.target});
j = d3_geo_clipClosest(path, o, r);
if (p) {
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
}
clipped.push.apply(clipped, path.slice(j));
if (path.length) clipped.push(path[j]);
}
return clipped;
}
clip.coordinates = function(x) {
if (!arguments.length) return coordinates;
coordinates = x;
return clip;
};
clip.origin = function(x) {
if (!arguments.length) return origin;
origin = x;
@ -778,7 +799,7 @@ d3.geo.clip = function() {
return clip;
}
var d3_geo_clipGreatCircle = d3.geo.greatCircle().n(100);
var d3_geo_clipGreatCircle = d3.geo.greatCircle();
function d3_geo_clipClosest(path, o, r) {
var i = -1,

2
d3.geo.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -89,7 +89,9 @@ $("#translate-y").slider({
});
$("#mode").change(function() {
xy.mode($(this).val());
var mode = $(this).val();
xy.mode(mode);
path.clip(mode === "gnomonic" ? clip : circle.polyline);
refresh(500);
});

Просмотреть файл

@ -1,6 +1,7 @@
var xy = d3.geo.azimuthal().scale(240).mode("stereographic"),
clip = d3.geo.clip().angle(89),
path = d3.geo.path().projection(xy).clip(clip),
circle = d3.geo.greatCircle().precision(10).coordinates(clip),
path = d3.geo.path().projection(xy).clip(circle.polyline),
svg = d3.select("body").append("svg:svg");
d3.json("../data/world-countries.json", function(collection) {

Просмотреть файл

@ -1,10 +1,12 @@
d3.geo.clip = function() {
var origin = [0, 0],
angle = 90,
r = d3_geo_earthRadius * angle / 180 * Math.PI;
r = d3_geo_earthRadius * angle / 180 * Math.PI,
coordinates = Object;
function clip(d) {
var o = {source: origin, target: null},
function clip(d, i) {
var d = coordinates.call(this, d, i),
o = {source: origin, target: null},
n = d.length,
i = -1,
j,
@ -19,10 +21,7 @@ d3.geo.clip = function() {
if (q) {
path = d3_geo_clipGreatCircle({source: q, target: o.target});
j = d3_geo_clipClosest(path, o, r);
if (p) {
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
}
clipped.push.apply(clipped, path.slice(j));
if (path.length) clipped.push(path[j]);
p = q = null;
} else {
clipped.push(o.target);
@ -32,7 +31,7 @@ d3.geo.clip = function() {
if (!p && clipped.length) {
path = d3_geo_clipGreatCircle({source: clipped[clipped.length - 1], target: o.target});
j = d3_geo_clipClosest(path, o, r);
clipped.push.apply(clipped, path.slice(0, j));
if (path.length) clipped.push(path[j]);
p = o.target;
}
}
@ -41,14 +40,17 @@ d3.geo.clip = function() {
o.target = clipped[0];
path = d3_geo_clipGreatCircle({source: q, target: o.target});
j = d3_geo_clipClosest(path, o, r);
if (p) {
clipped.push.apply(clipped, d3_geo_clipGreatCircle({source: p, target: o.target}));
}
clipped.push.apply(clipped, path.slice(j));
if (path.length) clipped.push(path[j]);
}
return clipped;
}
clip.coordinates = function(x) {
if (!arguments.length) return coordinates;
coordinates = x;
return clip;
};
clip.origin = function(x) {
if (!arguments.length) return origin;
origin = x;
@ -65,7 +67,7 @@ d3.geo.clip = function() {
return clip;
}
var d3_geo_clipGreatCircle = d3.geo.greatCircle().n(100);
var d3_geo_clipGreatCircle = d3.geo.greatCircle();
function d3_geo_clipClosest(path, o, r) {
var i = -1,

Просмотреть файл

@ -2,59 +2,25 @@
d3.geo.greatCircle = function() {
var source = d3_geo_greatCircleSource,
target = d3_geo_greatCircleTarget,
n = 100,
coordinates = Object,
precision = 1,
radius = d3_geo_earthRadius;
// TODO: breakAtDateLine?
function greatCircle(d, i) {
var from = source.call(this, d, i),
to = target.call(this, d, i),
x0 = from[0] * d3_radians,
y0 = from[1] * d3_radians,
x1 = to[0] * d3_radians,
y1 = to[1] * d3_radians,
cx0 = Math.cos(x0), sx0 = Math.sin(x0),
cy0 = Math.cos(y0), sy0 = Math.sin(y0),
cx1 = Math.cos(x1), sx1 = Math.sin(x1),
cy1 = Math.cos(y1), sy1 = Math.sin(y1),
d = Math.acos(sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)),
sd = Math.sin(d),
f = d / (n - 1),
e = -f,
path = [],
i = -1;
while (++i < n) {
e += f;
var A = Math.sin(d - e) / sd,
B = Math.sin(e) / sd,
x = A * cy0 * cx0 + B * cy1 * cx1,
y = A * cy0 * sx0 + B * cy1 * sx1,
z = A * sy0 + B * sy1;
path[i] = [
Math.atan2(y, x) / d3_radians,
Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians
];
}
return path;
return d3_geo_greatCirclePath([
source.call(this, d, i), target.call(this, d, i)], precision);
}
greatCircle.source = function(x) {
if (!arguments.length) return source;
source = x;
greatCircle.coordinates = function(x) {
if (!arguments.length) return coordinates;
coordinates = x;
return greatCircle;
};
greatCircle.target = function(x) {
if (!arguments.length) return target;
target = x;
return greatCircle;
};
greatCircle.n = function(x) {
if (!arguments.length) return n;
n = +x;
greatCircle.precision = function(x) {
if (!arguments.length) return precision;
precision = +x;
return greatCircle;
};
@ -79,9 +45,62 @@ d3.geo.greatCircle = function() {
return radius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
greatCircle.polyline = function(d, i) {
return d3_geo_greatCirclePath(coordinates.call(this, d, i), precision);
};
return greatCircle;
};
function d3_geo_greatCirclePath(coordinates, precision) {
var m = coordinates.length;
if (m < 2) return coordinates;
var i = 0,
p = precision * d3_radians,
from = coordinates[0],
to,
x0 = from[0] * d3_radians,
y0 = from[1] * d3_radians,
cx0 = Math.cos(x0), sx0 = Math.sin(x0),
cy0 = Math.cos(y0), sy0 = Math.sin(y0),
path = [from];
while (++i < m) {
to = coordinates[i];
var x1 = to[0] * d3_radians,
y1 = to[1] * d3_radians,
cx1 = Math.cos(x1), sx1 = Math.sin(x1),
cy1 = Math.cos(y1), sy1 = Math.sin(y1),
d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)))),
sd = Math.sin(d),
n = Math.ceil(d / p),
f = d / n,
e = 0,
j = 0;
while (++j < n) {
e += f;
var A = Math.sin(d - e) / sd,
B = Math.sin(e) / sd,
x = A * cy0 * cx0 + B * cy1 * cx1,
y = A * cy0 * sx0 + B * cy1 * sx1,
z = A * sy0 + B * sy1;
path.push([
Math.atan2(y, x) / d3_radians,
Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians
]);
}
path.push(to);
x0 = x1;
y0 = y1;
cx0 = cx1; sx0 = sx1;
cy0 = cy1; sy0 = sy1;
}
return path;
}
function d3_geo_greatCircleSource(d) {
return d.source;
}

Просмотреть файл

@ -10,8 +10,7 @@ var suite = vows.describe("d3.geo.greatCircle");
suite.addBatch({
"greatCircle": {
topic: function() {
return d3.geo.greatCircle()
.n(12);
return d3.geo.greatCircle().precision(7.5);
},
"distance": function(circle) {
assert.equal(circle.distance({source: [0, 0], target: [0, 0]}), 0);