【问题标题】:How to fix map boundaries on d3 cartographic raster reprojection?如何在 d3 制图栅格重投影上修复地图边界?
【发布时间】:2017-06-09 11:30:27
【问题描述】:

我尝试使用this example 之后的地图的光栅重投影。如果我将示例 kavrayskiy7 投影更改为方位角等距投影,

var projection = d3.geo.azimuthalEquidistant()
    .scale(90)
    .translate([width / 2, height / 2])
    .clipAngle(180 - 1e-3)
    .precision(.1);

它应该将地球投影到光盘上(投影图的图像)。但是,光栅重投影超出了该圆盘并用扩展图片填充了整个画布(反投影函数不是单射的,地图上的几个 x/y 点对应于单个 lon/lat 坐标)。在原始示例中,应该使用

行来避免这种情况
if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }

但是对于这个例子不起作用。例如,由于相同的效果,我在使用 Mollweide 投影时发现了其他故障(两条线出现在两极)。

要解决这个问题,一种方法是修复逆投影,以便在 x/y 输入超出范围时返回错误或 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 路径内?
  • 好奇,d3path函数获取地图边界轮廓的算法代码在哪里?我在 github repo 上找不到它。

谢谢。

编辑:我检查了this example 中的所有 44 个投影,并在以下 25 个中发现了故障:

Albers、Bromley、Collignon、Eckert II、Eckert IV、Eckert VI、Hammer、Hill、Goode Homolosine、Lambert 圆柱等积、Larrivée、Laskowski、McBryde-Thomas Flat-Polar Parabolic、McBryde-Thomas Flat-Polar Quartic , McBryde-Thomas Flat-Polar Sinusoidal, Mollweide, Natural Earth, Nell-Hammer, Polyconic, Sinu-Mollweide, van der Grinten, van der Grinten IV, Wagner IV, Wagner VII, Winkel Tripel。

【问题讨论】:

    标签: javascript d3.js raster map-projections d3.geo


    【解决方案1】:

    我使用第二个答案只是因为这是解决同一问题的不同方法。同样,这个答案是另一种方法,它试图避免使用投影范围的 svg 轮廓的多边形解决方案中的一个点。

    这种替代方法应该(我只尝试过少数)适用于任何投影,而我的其他答案仅适用于投影到光盘上的投影。其次,这种方法不会尝试定义投影区域来确定是否应该渲染像素,而是使用 d3.projection 本身。


    由于多个点可以通过 projection.invert 返回相同的值,我们可以运行正向投影来验证是否应该绘制像素。

    如果 projection(projection.invert(point)) == point 则该点在我们的投影范围内。

    当然,这可能存在一些精度/舍入误差,因此可以指定某种程度的公差。

    此检查适合 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 方法的合适替代方法。

    【讨论】:

    • 这个解决方案有效,尽管检查函数是否完成了它应该做的事情确实不是一个很好的性能。我认为这个检查应该在projection.invert 函数内完成,只针对当前有故障的预测(对于其他预测来说,这是不必要的并且浪费计算)。总之,我认为它更多的是反函数的错误,而不是要处理的功能。目前这对我来说很好,谢谢。
    • 顺便说一句,你仍然需要if ( λ &gt; 180 || λ &lt; -180 || φ &gt; 90 || φ &lt; -90 ) { i += 4; continue; }这一行,因为前向投影也接受[-180,180]x[-90,90]范围之外的点(这可能不应该)。检查例如 Laskowski 投影。
    • 啊,我想知道 if 语句是否会起作用。在我的几次测试中,它似乎没有出现,所以我想我会尝试优化。我会把它放回代码块中,感谢那里的捕获。再次,同意 projection.invert 应该在没有解决方法的情况下进行此检查。
    【解决方案2】:

    虽然我很确定您正确使用了 projection.inverse 函数, 依靠:

    if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
    

    剪辑投影总是会失败,因为 projection.inverse 似乎总是返回东/西 180 度以内的角度。虽然可能有一种方法可以修改投影本身以返回大于 180 度的值,但它可能比其他方法更困难(老实说,这远远超出了我能给出的任何答案)。

    同样,使用 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 (graticule.outline) 函数以获取有关 d3 如何获取投影边界轮廓的一些信息。

    【讨论】:

    • 您的解决方案当然是对该投影的适当检查,但我正在寻找适用于 d3.geo 上所有投影的东​​西。事实上,点在圆上的这个条件应该在逆投影的代码上,否则它不是真正的数学逆(映射应该是双射的)。
    • 是的,同意,它似乎应该在projection.invert中使用这样的检查。我可能有一种替代且更简单(尽管可能不是计算方式)的方法,该方法适用于任何给定的投影,将其添加为单独的答案,因为它是一种完全不同的方法。
    猜你喜欢
    • 1970-01-01
    • 2014-03-01
    • 1970-01-01
    • 2012-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-03
    • 1970-01-01
    相关资源
    最近更新 更多