如何修复 d3 制图栅格重投影上的地图边界?
How to fix map boundaries on d3 cartographic raster reprojection?
我尝试使用 this example 之后的地图的光栅重投影。如果我用方位角等距投影更改示例 kavrayskiy7
投影,
var projection = d3.geo.azimuthalEquidistant()
.scale(90)
.translate([width / 2, height / 2])
.clipAngle(180 - 1e-3)
.precision(.1);
它应该将地球投影到一个圆盘上(投影图的图像)。然而,光栅重投影超出了那个圆盘,并用扩展图片填充了整个 canvas(逆投影函数不是单射的,地图上的几个 x/y 点对应一个 lon/lat坐标)。在原始示例中,应使用行
来避免这种情况
if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
但是对于这个例子不起作用。由于相同的效果,我在使用 Mollweide 投影(两条线出现在两极)时发现了其他故障。
要解决此问题,一种方法是修复逆投影,以便在 x/y 输入超出范围时出现 return 错误或 None。我的尝试是使用整个球体的前向投影来检查一个点是否在范围内,以获得具有地图边界的 SVG 路径,如以下代码所示:
var path = d3.geo.path()
.projection(projection);
var bdry = svg.append("defs").append("path")
.datum({type: "Sphere"})
.attr("id", "sphere")
.attr("d", path);
(参见 this example)。但是,我发现没有简单的方法来检查点 [x,y]
是否在 SVG 封闭路径内。
所以我的问题是:
- 逆投影有问题,还是我没有正确使用它们?
- 我如何找到
[x,y]
点是否在 svg 路径内,假设这是最好的方法?
- 好奇,d3
path
函数获取地图边界剖面的算法代码在哪里?我无法在 github 存储库中找到它。
谢谢。
编辑:我检查了 this example 中的所有 44 个投影,发现以下 25 个有问题:
Albers、Bromley、Collignon、Eckert II、Eckert IV、Eckert VI、Hammer、Hill、Goode Homolosine、Lambert 圆柱等积、Larrivée、Laskowski、McBryde–Thomas 平极抛物线、McBryde–Thomas 平-极地四次方程、McBryde–Thomas 平极正弦曲线、Mollweide、Natural Earth、Nell–Hammer、Polyconic、Sinu-Mollweide、van der Grinten、van der Grinten IV、Wagner IV、Wagner VII、Winkel Tripel。
虽然我很确定您正确使用了 projection.inverse 函数,
依赖:
if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
剪辑投影总是会失败,因为 projection.inverse 似乎总是 return 角度在 180 度内 east/west。虽然可能有一种方法可以将投影本身修改为大于 180 度的 return 值,但它可能比其他方法更难(老实说,这远远超出了我能给出的任何答案)。
同样,使用 SVG 路径来表示世界轮廓,然后以此为基础来确定是否应该绘制一个点可能会使事情复杂化。
相反,假设是一个圆盘,您可以轻松地计算出圆盘的半径,并从那里确定是否应该绘制一个像素:
var edge = {};
var center = {};
edge.x = projection([180 - 1e-6, 0])[0];
edge.y = projection([180 - 1e-6, 0])[1];
center.x = width/2;
center.y = height/2;
var radius = Math.pow( Math.pow(center.x - edge.x,2) + Math.pow(center.y - edge.y,2) , 0.5 )
使用圆盘的半径,我们可以计算在 for 循环中像素是落在圆盘上还是落在圆盘之外:
for (var y = 0, i = -1; y < height; ++y) {
for (var x = 0; x < width; ++x) {
var p = projection.invert([x, y]), λ = p[0], φ = p[1];
if (Math.pow( Math.pow(center.x-x,2) + Math.pow(center.y-y,2), 0.5) < radius) {
if ( λ > 180 || λ < -180 || φ > 90 || φ < -90 ) { i += 4; continue; }
var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
else {
targetData[++i] = 0;
targetData[++i] = 0;
targetData[++i] = 0;
targetData[++i] = 0;
}
}
}
一起,这些给了我:
为了美观效果,将半径缩小一定百分比可能是值得的。当然,对于不同的投影,这种方法可能很难或不可能。
我已将代码放入 bl.ock here(在此过程中我将其移至 d3 v4,部分原因是为了查看 projection.inverse 的行为是否相同) .
对于问题的第三部分,您可以尝试使用 d3 的格线 (graticule.outline) 函数来获取有关 d3 如何获取投影边界轮廓的信息。
我使用第二个答案只是因为这是解决同一问题的不同方法。同样,这个答案是一种替代方法,它试图避免使用投影范围的 svg 轮廓的多边形解决方案中的点。
这个替代方案应该(我只试过几次)适用于任何投影,而我的其他答案仅适用于投影到光盘的投影。其次,这种方法并不试图定义投影区域来确定是否应该渲染像素,而是使用 d3.projection 本身。
由于多个点可以 return 与 projection.invert 相同的值,我们可以 运行 一个前向投影来验证是否应该绘制一个像素。
如果projection(projection.invert(point)) == point
则该点在我们的投影范围内。
当然,其中可能存在一些 precision/rounding 错误,因此可以指定一定程度的容差。
此检查适合 for 循环:
for (var y = 0, i = -1; y < height; ++y) {
for (var x = 0; x < width; ++x) {
var p = projection.invert([x, y]), λ = p[0], φ = p[1];
var pxy = projection(p);
var tolerance = 0.5;
if ( λ > 180 || λ < -180 || φ > 90 || φ < -90 ) { i += 4; continue; }
if ( (Math.abs(pxy[0] - x) < tolerance ) && (Math.abs(pxy[1] - y) < tolerance ) ) {
var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
else {
i += 4;
}
}
}
与其他答案一样,我用它构建了一个块here。
我没有检查这个答案的性能,需要这种检查似乎很奇怪,但它可能是您问题中提出的 svg 方法的合适替代方法。
我尝试使用 this example 之后的地图的光栅重投影。如果我用方位角等距投影更改示例 kavrayskiy7
投影,
var projection = d3.geo.azimuthalEquidistant()
.scale(90)
.translate([width / 2, height / 2])
.clipAngle(180 - 1e-3)
.precision(.1);
它应该将地球投影到一个圆盘上(投影图的图像)。然而,光栅重投影超出了那个圆盘,并用扩展图片填充了整个 canvas(逆投影函数不是单射的,地图上的几个 x/y 点对应一个 lon/lat坐标)。在原始示例中,应使用行
来避免这种情况if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
但是对于这个例子不起作用。由于相同的效果,我在使用 Mollweide 投影(两条线出现在两极)时发现了其他故障。
要解决此问题,一种方法是修复逆投影,以便在 x/y 输入超出范围时出现 return 错误或 None。我的尝试是使用整个球体的前向投影来检查一个点是否在范围内,以获得具有地图边界的 SVG 路径,如以下代码所示:
var path = d3.geo.path()
.projection(projection);
var bdry = svg.append("defs").append("path")
.datum({type: "Sphere"})
.attr("id", "sphere")
.attr("d", path);
(参见 this example)。但是,我发现没有简单的方法来检查点 [x,y]
是否在 SVG 封闭路径内。
所以我的问题是:
- 逆投影有问题,还是我没有正确使用它们?
- 我如何找到
[x,y]
点是否在 svg 路径内,假设这是最好的方法? - 好奇,d3
path
函数获取地图边界剖面的算法代码在哪里?我无法在 github 存储库中找到它。
谢谢。
编辑:我检查了 this example 中的所有 44 个投影,发现以下 25 个有问题:
Albers、Bromley、Collignon、Eckert II、Eckert IV、Eckert VI、Hammer、Hill、Goode Homolosine、Lambert 圆柱等积、Larrivée、Laskowski、McBryde–Thomas 平极抛物线、McBryde–Thomas 平-极地四次方程、McBryde–Thomas 平极正弦曲线、Mollweide、Natural Earth、Nell–Hammer、Polyconic、Sinu-Mollweide、van der Grinten、van der Grinten IV、Wagner IV、Wagner VII、Winkel Tripel。
虽然我很确定您正确使用了 projection.inverse 函数, 依赖:
if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
剪辑投影总是会失败,因为 projection.inverse 似乎总是 return 角度在 180 度内 east/west。虽然可能有一种方法可以将投影本身修改为大于 180 度的 return 值,但它可能比其他方法更难(老实说,这远远超出了我能给出的任何答案)。
同样,使用 SVG 路径来表示世界轮廓,然后以此为基础来确定是否应该绘制一个点可能会使事情复杂化。
相反,假设是一个圆盘,您可以轻松地计算出圆盘的半径,并从那里确定是否应该绘制一个像素:
var edge = {};
var center = {};
edge.x = projection([180 - 1e-6, 0])[0];
edge.y = projection([180 - 1e-6, 0])[1];
center.x = width/2;
center.y = height/2;
var radius = Math.pow( Math.pow(center.x - edge.x,2) + Math.pow(center.y - edge.y,2) , 0.5 )
使用圆盘的半径,我们可以计算在 for 循环中像素是落在圆盘上还是落在圆盘之外:
for (var y = 0, i = -1; y < height; ++y) {
for (var x = 0; x < width; ++x) {
var p = projection.invert([x, y]), λ = p[0], φ = p[1];
if (Math.pow( Math.pow(center.x-x,2) + Math.pow(center.y-y,2), 0.5) < radius) {
if ( λ > 180 || λ < -180 || φ > 90 || φ < -90 ) { i += 4; continue; }
var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
else {
targetData[++i] = 0;
targetData[++i] = 0;
targetData[++i] = 0;
targetData[++i] = 0;
}
}
}
一起,这些给了我:
为了美观效果,将半径缩小一定百分比可能是值得的。当然,对于不同的投影,这种方法可能很难或不可能。
我已将代码放入 bl.ock here(在此过程中我将其移至 d3 v4,部分原因是为了查看 projection.inverse 的行为是否相同) .
对于问题的第三部分,您可以尝试使用 d3 的格线 (graticule.outline) 函数来获取有关 d3 如何获取投影边界轮廓的信息。
我使用第二个答案只是因为这是解决同一问题的不同方法。同样,这个答案是一种替代方法,它试图避免使用投影范围的 svg 轮廓的多边形解决方案中的点。
这个替代方案应该(我只试过几次)适用于任何投影,而我的其他答案仅适用于投影到光盘的投影。其次,这种方法并不试图定义投影区域来确定是否应该渲染像素,而是使用 d3.projection 本身。
由于多个点可以 return 与 projection.invert 相同的值,我们可以 运行 一个前向投影来验证是否应该绘制一个像素。
如果projection(projection.invert(point)) == point
则该点在我们的投影范围内。
当然,其中可能存在一些 precision/rounding 错误,因此可以指定一定程度的容差。
此检查适合 for 循环:
for (var y = 0, i = -1; y < height; ++y) {
for (var x = 0; x < width; ++x) {
var p = projection.invert([x, y]), λ = p[0], φ = p[1];
var pxy = projection(p);
var tolerance = 0.5;
if ( λ > 180 || λ < -180 || φ > 90 || φ < -90 ) { i += 4; continue; }
if ( (Math.abs(pxy[0] - x) < tolerance ) && (Math.abs(pxy[1] - y) < tolerance ) ) {
var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
else {
i += 4;
}
}
}
与其他答案一样,我用它构建了一个块here。
我没有检查这个答案的性能,需要这种检查似乎很奇怪,但它可能是您问题中提出的 svg 方法的合适替代方法。