Javascript 使用D3将Web Mercator瓷砖重新投影到任意投影?

Javascript 使用D3将Web Mercator瓷砖重新投影到任意投影?,javascript,d3.js,gis,map-projections,Javascript,D3.js,Gis,Map Projections,杰森·戴维斯(Jason Davies)让我们大吃一惊已经有几年了——地图停止工作是因为Mapbox屏蔽了他的网站,但它仍然是伟大的演示 现在在Observable HQ上,我看到了关于Jason所做的事情的最新和最新的文档,但没有现代的例子:重新投影标准墨卡托瓷砖集 如何使d3平铺扭曲到新投影?此答案基于: ,及 这三种资源也相互促进。理解这三个例子将有助于理解我下面的例子中发生的事情 答案还以我缓慢的、持续的尝试建立一个 这个答案的目的不是提供一个最终确定的资源,而是粗略地演示如何将

杰森·戴维斯(Jason Davies)让我们大吃一惊已经有几年了——地图停止工作是因为Mapbox屏蔽了他的网站,但它仍然是伟大的演示

现在在Observable HQ上,我看到了关于Jason所做的事情的最新和最新的文档,但没有现代的例子:重新投影标准墨卡托瓷砖集


如何使d3平铺扭曲到新投影?

此答案基于:

  • ,及
这三种资源也相互促进。理解这三个例子将有助于理解我下面的例子中发生的事情

答案还以我缓慢的、持续的尝试建立一个

这个答案的目的不是提供一个最终确定的资源,而是粗略地演示如何将一个资源与相关信息组合在一起。随着我进一步思考这个问题,答案也会不断演变

网墨卡托瓷砖 延伸超过360度经度和~170度纬度(+/-85度)的墨卡托地图将填充一个正方形(超过85度纬度会导致失真失控,不建议包含极点,因为极点在投影平面上处于+/-无穷远处)

对于web地图服务(使用mercator tiles),世界大部分地区的这个广场的缩放级别为0。地图宽2^0平方米,高2^0平方米

如果我们把那个正方形分成两个正方形乘两个正方形的网格,我们就有了缩放级别1。地图是2^1乘2^1的正方形

因此,缩放级别决定地图的宽度和高度:2^缩放级别。如果每个正方形的像素大小相同,则缩放级别每增加1,世界的像素宽度将增加2倍

幸运的是,北纬约85度的地方没有陆地,我们也不想经常看到南极洲,所以这个广场适合大多数网络地图应用。然而,这意味着,如果我们将web mercator瓷砖重新投影到这些纬度以上的任何地方,我们将有一个秃顶点:

Wᴇʙ-Mᴇʀᴄᴀᴛᴏʀ ᴛɪʟᴇʀᴇᴘʀᴏᴊᴇᴄᴛᴇᴅ ғᴏʀ ᴀ Mᴇʀᴄᴀᴛᴏʀ ᴘʀᴏᴊᴇᴄᴛɪᴏɴ ᴛʜᴀᴛ ʜᴀsʙᴇᴇɴ ʀᴏᴛᴀᴛᴇᴅ ᴛᴏ sʜᴏᴡ ᴛʜᴇ NᴏʀᴛʜPᴏʟᴇ.

最后,web墨卡托图块在相对于图块可预测且规则的投影空间中渲染。如果我们重新投影瓷砖,我们可能会扩大或缩小每个瓷砖的投影空间,我们应该注意这一点。在上面的图片中,北极周围的瓷砖被重新投影,比更南的瓷砖要小得多。瓷砖在投影后不一定大小一致

重投影和重采样板Carree 重新投射web服务贴片的最大挑战是时间,而不仅仅是花在理解预测和阅读这样的答案上的时间

投影函数是复杂的耗时操作,必须在渲染的每个像素上执行。我看到的所有d3示例都使用了实际重投影和重采样的过程(或近似变体)。此示例仅适用于使用投影原始图像的情况。程序如下:

  • 创建一个空白的新图像
  • 对于新图像中的每个像素,以像素为单位获取其位置并反转(使用所需的投影)以获得纬度和经度
  • 确定原始图像中的哪个像素与该经度和纬度重叠
  • 从原始图像中的该像素获取信息,并将其分配给新图像中的相应像素(步骤2中的像素)
  • 当原始图像使用Plate Carree投影时,我们不需要d3地质投影,投影坐标和未投影坐标之间的关系是线性的。例如:如果图像高度为180像素,则每个像素表示1个纬度。这意味着与第2步和projection.invert()相比,第3步所用的时间不会太长。下面是Mike在步骤3中的函数:

    var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
    
    在下面的示例中,我刚刚使用了
    d3.geoMercator()
    来获得更清晰的比例因子,使用投影包括一个额外的操作来变换x坐标

    否则,4步流程保持不变

    寻找合适的瓷砖 我只见过一种干净的方法来找到要显示的瓷砖,Jason Davies的d3.quadTile,Seed。我相信Alan McConchie使用的是未统一的版本,可能会被修改。还有另一个版本的d3.quadTiles的存储库,它非常类似

    对于McConchie/Davies,d3.quadTile将在给定具有剪辑范围(而非剪辑角度)和平铺深度的投影后,拉出与视图范围相交的所有平铺

    在艾伦·麦康奇(Alan McConchie)的作品中,缩放级别基于投影比例——但这并不一定是最明智的:每个投影都有不同的比例因子,一个比例上100的比例将显示不同于另一个比例上100的比例。此外,柱面投影中比例值和地图大小之间的关系可能是线性的,而非柱面投影可能在地图大小和比例之间具有非线性关系

    我对这种方法做了一些修改-我使用比例因子来确定初始分幅深度,然后如果d3.quadTile返回的分幅计数超过某个数字,则减少该分幅深度:

    geoTile.tileDepth = function(z) {
        // rough starting value, needs improvement:
        var a = [w/2-1,h/2]; // points in pixels
        var b = [w/2+1,h/2];
        var dx = d3.geoDistance(p.invert(a), p.invert(b)) ; // distance between in radians      
        var scale = 2/dx*tk;
        var z = Math.max(Math.log(scale) / Math.LN2 - 8, 2);
        z = Math.min(z,15) | 0;
    
        // Refine:
        var maxTiles = w*h/256/128;
        var e = p.clipExtent();
        p.clipExtent([[0,0],[w,h]])
        while(d3.quadTiles(p, z).length > maxTiles) {
            z--;
        }
        p.clipExtent(e);
    
        return z;
    }
    
    然后,使用d3.quadTile我拉出相关的瓷砖:

    geoTile.tiles = function() {
        // Use Jason Davies' quad tree method to find out what tiles intercept the viewport:
        var z = geoTile.tileDepth();
        var e = p.clipExtent(); // store and put back after.
    
        p.clipExtent([[-1,-1],[w+1,h+1]]) // screen + 1 pixel margin on outside.
        var set = d3.quadTiles(p, Math.max(z0,Math.min(z,z1))); // Get array detailing tiles
        p.clipExtent(e);
    
        return set;
    }
    
    起初,我认为从多个缩放深度(考虑重新投影的瓷砖大小的差异)拖动瓷砖是理想的:但这会遇到光栅中的线条厚度以及不连续注释等问题

    收养杰森和艾伦的作品 我使用
    geoTile.tiles()
    获取上面生成的tile集,并通过
    geoTile.tiles = function() {
        // Use Jason Davies' quad tree method to find out what tiles intercept the viewport:
        var z = geoTile.tileDepth();
        var e = p.clipExtent(); // store and put back after.
    
        p.clipExtent([[-1,-1],[w+1,h+1]]) // screen + 1 pixel margin on outside.
        var set = d3.quadTiles(p, Math.max(z0,Math.min(z,z1))); // Get array detailing tiles
        p.clipExtent(e);
    
        return set;
    }
    
        function onload(d, that) { // d is datum, that is image element.
    
            // Create and fill a canvas to work with.
            var mercatorCanvas = d3.create("canvas")
              .attr("width",tileWidth)
              .attr("height",tileHeight);
            var mercatorContext = mercatorCanvas.node().getContext("2d");           
            mercatorContext.drawImage(d.image, 0, 0, tileWidth, tileHeight); // move the source tile to a canvas.
    
            //
            var k = d.key; // the tile address.
            var tilesAcross = 1 << k[2]; // how many tiles is the map across at a given tile's zoom depth?
    
            // Reference projection:
            var webMercator = d3.geoMercator()
              .scale(tilesAcross/Math.PI/2) // reference projection fill square tilesAcross units wide/high.
              .translate([0,0])
              .center([0,0])
    
            // Reprojected tile boundaries in pixels.           
            var reprojectedTileBounds = path.bounds(d),
            x0 = reprojectedTileBounds[0][0] | 0,
            y0 = reprojectedTileBounds[0][1] | 0,
            x1 = (reprojectedTileBounds[1][0] + 1) | 0,
            y1 = (reprojectedTileBounds[1][1] + 1) | 0;
    
            // Get the tile bounds:
            // Tile bounds in latitude/longitude:
            var λ0 = k[0] / tilesAcross * 360 - 180,                     // left        
            λ1 = (k[0] + 1) / tilesAcross * 360 - 180,                   // right
            φ1 = webMercator.invert([0,(k[1] - tilesAcross/2) ])[1],     // top
            φ0 = webMercator.invert([0,(k[1] + 1 - tilesAcross/2) ])[1]; // bottom.             
    
            // Create a new canvas to hold the what will become the reprojected tile.
            var newCanvas = d3.create("canvas").node();
    
            newCanvas.width = x1 - x0,      // pixel width of reprojected tile.
            newCanvas.height = y1 - y0;     // pixel height of reprojected tile.
            var newContext = newCanvas.getContext("2d");    
    
            if (newCanvas.width && newCanvas.height) {
                var sourceData = mercatorContext.getImageData(0, 0, tileWidth, tileHeight).data,
                    target = newContext.createImageData(newCanvas.width, newCanvas.height),
                    targetData = target.data;
    
                // For every pixel in the reprojected tile's bounding box:
                for (var y = y0, i = -1; y < y1; ++y) {
                  for (var x = x0; x < x1; ++x) {
                    // Invert a pixel in the new tile to find out it's lat long
                    var pt = p.invert([x, y]), λ = pt[0], φ = pt[1];
    
                    // Make sure it falls in the bounds:
                    if (λ > λ1 || λ < λ0 || φ > φ1 || φ < φ0) { i += 4; targetData[i] = 0; continue; }  
                        // Find out what pixel in the source tile matches the destination tile:
                        var top = (((tilesAcross + webMercator([0,φ])[1]) * tileHeight | 0) % 256  | 0) * tileWidth;
                        var q = (((λ - λ0) / (λ1 - λ0) * tileWidth | 0) + (top)) * 4;
    
                        // Take the data from a pixel in the source tile and assign it to a pixel in the new tile.
                        targetData[++i] = sourceData[q];
                        targetData[++i] = sourceData[++q];
                        targetData[++i] = sourceData[++q];
                        targetData[++i] = 255;
                  }
                }
                // Draw the image.
                if(target) newContext.putImageData(target, 0, 0);
            }
    
            // Add the data to the image in the SVG:
            d3.select(that)
              .attr("xlink:href", newCanvas.toDataURL()) // convert to a dataURL so that we can embed within the SVG.
              .attr("x", x0)
              .attr("width", newCanvas.width)
              .attr("height",newCanvas.height)
              .attr("y", y0);
        }