将Web Mercator磁贴重新投影到D3的任意投影? [英] Re-project Web Mercator tiles to arbitrary projection with D3?

查看:77
本文介绍了将Web Mercator磁贴重新投影到D3的任意投影?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Jason Davies用

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

最后,网络墨卡托瓷砖在相对于瓷砖而言可预测且规则的投影空间中渲染.如果我们重新投影图块,则可能会放大或缩小每个图块的投影空间,我们应该注意这一点.在上图中,北极周围的瓷砖被重新投影,比南端的瓷砖要小得多.投影后不一定会调整瓷砖的大小.

平板投影再投影和重采样

重新投影Web服务磁贴的最大挑战是时间-而不仅仅是花在理解投影和阅读答案上的时间.

投影功能是耗时的复杂操作,必须在每个渲染的像素上完成.我见过的所有d3示例都使用该过程(或近似变体),如

的相对时间

Tɪᴍᴇᴘᴏᴘᴏɴᴛᴡᴡᴡᴡ3 D3(ᴄᴏɴᴠᴇʀᴛᴘɪxᴛʜsʟᴀᴛʟᴏɴɢ/ʟᴏɴɢ).ᴛD3ᴠ4ғᴀsᴛᴇʀ.

为了完整起见,在d3-geo投影中可以找到更广泛的投影范围:

Cᴏᴍᴘᴀʀɪɴɢᴛɪᴍᴇғᴏʀɪɪs.ɪɴᴠᴇʀᴛᴀᴅᴅᴀᴅᴅɪᴛɪᴏɴᴀʟᴘʀᴏᴊᴇᴄᴛɪᴏɴs

注释

  • 此方法可能会遇到此处所述的问题,可以通过此处的答案解决这些问题-但不同的解决方案将花费额外的处理时间.

  • 此方法使用最近邻居方法-这可能会导致质量问题.更高级的采样(例如双线性或三次采样)会增加处理时间,但可能会产生更理想的图像.

  • 如果基本图像包含文本,则可能会旋转文本或以其他方式操纵文本以使其变得更少或不可读.

  • Mike的示例是针对单个图像的,对于图块,此过程已进行了一定程度的更改,我们现在正在创建多个图像,这需要了解每个原始图块的边界以及每个重新投影的图块的边界,以及前者以瓷砖为单位,后者则以像素为单位-较小的细节.

重新投影和重采样Web墨卡托

当我开始研究此问题时,我看了艾伦·麦康琪(Alan McConchie)的解决方案/示例作为参考.花了一段时间才注意到,但是此示例中的第3步(我也相信Jason Davies的工作)在重新采样中并未考虑Web Mercator磁贴-仅用于确定磁贴边界.但是,y轴上像素之间的关系不再像Plate Carree中那样线性.

这意味着将图块放置在正确的位置,但是采样将每个图块中的y轴视为线性.当以低图块缩放级别(在图块的中/下)显示整个世界时,这种失真最明显,并且可能是艾伦在提到奇数压缩时所说的.

解决方案是在上述步骤3中为每个纬度/经度对正确投影纬度.这会增加时间,总会增加时间-该函数涉及Math.atan和Math.exp,但差别不应该太糟.在艾伦(Alan)和杰森(Jason)的作品中,这都是通过一个简单的公式完成的(但仅用于图块范围,而不是每个像素):

  Math.atan(Math.exp(-y * Math.PI/180))* 360/Math.PI-90; 

在下面的示例中,我只是使用 d3.geoMercator()来使比例因子更清晰,使用投影包括一个额外的操作来转换x坐标.

否则,四步过程保持不变.

找到合适的瓷砖

Jason Davies的d3.quadTile,我只看过一种干净的方法来查找要显示的图块,见过此github 存储库,该版本非常相似.

对于McConchie/Davies,d3.quadTile将给定具有裁剪范围(不是裁剪角度)和贴图深度的投影,并拉出与视图范围相交的所有贴图.

在艾伦·麦康琪(Alan McConchie)的解决方案/示例中,缩放级别基于投影比例-但这并不一定是最明智的:每个投影都有不同的缩放因子,一个缩放比例上的100比例将显示与另一个缩放比例上的100比例不同的程度.此外,圆柱投影中比例尺值与地图尺寸之间的关系可能是线性的,而非圆柱投影可能会在地图尺寸与比例尺之间具有非线性的关系.

我对此方法进行了一些修改-我使用比例因子来确定初始图块深度,如果d3.quadTile返回的图块计数超过一定数量,则减小该图块深度:

  geoTile.tileDepth = function(z){//粗略的起始值,需要改进:var a = [w/2-1,h/2];//点数(以像素为单位)var b = [w/2 + 1,h/2];var dx = d3.geoDistance(p.invert(a),p.invert(b));//弧度之间的距离var scale = 2/dx * tk;var z = Math.max(Math.log(scale)/Math.LN2-8,2);z = Math.min(z,15)|0;//优化:var maxTiles = w * h/256/128;var e = p.clipExtent();p.clipExtent([[0,0],[w,h]])while(d3.quadTiles(p,z).length&max; maxTiles){z--;}p.clipExtent(e);返回z;} 

然后,使用d3.quadTile拉出相关的图块:

  geoTile.tiles = function(){//使用Jason Davies的四叉树方法找出哪些图块会拦截视口:var z = geoTile.tileDepth();var e = p.clipExtent();//存储并放回去.p.clipExtent([[-1,-1],[w + 1,h + 1]])//屏幕+外部1像素边距.var set = d3.quadTiles(p,Math.max(z0,Math.min(z,z1)));//获取阵列细节图块p.clipExtent(e);返回集} 

起初,我认为从多个缩放深度拖动图块(以解决重新投影的图块大小上的差异)将是理想的:但这会遇到光栅线宽以及不连续等问题注释.

采用杰森和艾伦的作品

我将上面使用 geoTile.tiles()生成的图块集使用图块坐标(在图块坐标,行,列,缩放深度中)通过输入/更新/退出循环运行作为键,将 image 元素附加到父 g svg 上.加载图像时,一旦加载图像,我们就调用onload函数进行实际的重新投影.这与Jason和Alan基本上没有什么不同,我已经解决了我在这段代码中看到的以下挑战:

  • 重新采样未解决网络墨卡托问题(如上所述)
  • 瓷砖深度选择不正确(如上所述)
  • 将瓦片重新投影为放置在div中而不是SVG中的画布-创建两个父容器,每个容器用于一种要素类型:图块或矢量.

我相信我的示例经过很小的调整就解决了这些问题.我还添加了一些更广泛的评论以供查看:

  function onload(d,that){//d是基准面,即图像元素.//创建并填充要使用的画布.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);//将源图块移动到画布上.//var k = d.key;//磁贴地址.var tileAcross = 1<<k [2];//在给定图块的缩放深度下,地图上横跨多少个图块?//参考投影:var webMercator = d3.geoMercator().scale(tilesAcross/Math.PI/2)//参考投影填充正方形tileAcross单位宽/高..translate([0,0]).center([0,0])//重新投影的图块边界(以像素为单位).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;//获取图块边界://纬度/经度范围内的图块范围:varλ0= k [0]/tileAcross * 360-180,//左λ1=(k [0] +1)/tileAcross * 360-180,//右φ1= webMercator.invert([0,(k [1]-tileAcross/2)])[1],//顶部φ0= webMercator.invert([0,(k [1] +1-tileAcross/2)])[1];//底部.//创建一个新画布,以容纳将要重新投影的图块.var newCanvas = d3.create("canvas").node();newCanvas.width = x1-x0,//重投影图块的像素宽度.newCanvas.height = y1-y0;//重新投影图块的像素高度.var newContext = newCanvas.getContext("2d");如果(newCanvas.width&& newCanvas.height){var sourceData = mercatorContext.getImageData(0,0,tileWidth,tileHeight).data,target = newContext.createImageData(newCanvas.width,newCanvas.height),targetData = target.data;//对于重新投影图块的边界框中的每个像素:对于(var y = y0,i = -1; y< y1; ++ y){对于(var x = x0; x< x1; ++ x){//反转新图块中的像素以找出其纬度var pt = p.invert([x,y]),λ= pt [0],φ= pt [1];//确保它落入边界:if(λ>λ1||λ<λ0||φ>φ1||φ<φ0){i + = 4;如果(λ>λ1||λ<λ0||φ>φ1||φ<φ0)targetData [i] = 0;继续;}//找出源图块中与目标图块匹配的像素:var top =((((tilesAcross + webMercator([0,φ])[1])* tileHeight | 0)%256 | 0)* tileWidth;var q =((((λ-λ0)/(λ1-λ0)* tileWidth | 0)+(top))* 4;//从源图块中的像素获取数据,并将其分配给新图块中的像素.targetData [++ i] = sourceData [q];targetData [++ i] = sourceData [++ q];targetData [++ i] = sourceData [++ q];targetData [++ i] = 255;}}//绘制图像.if(target)newContext.putImageData(target,0,0);}//将数据添加到SVG中的图像:d3.select(that).attr("xlink:href",newCanvas.toDataURL())//转换为dataURL,以便我们可以将其嵌入SVG中..attr("x",x0).attr("width",newCanvas.width).attr("height",newCanvas.height).attr("y",y0);} 

将其放置在较大的结构中.

具有叠加特征的常规图块地图具有一些坐标系:

  • 瓦片单位(3D),用于标记每个图块的列,行和缩放级别(分别为x,y,z)
  • 地理坐标(3D),用于标记三维球体上某个点的纬度和经度.
  • 缩放单位(3D),用于跟踪缩放平移(x,y)和缩放比例(k).
  • 投影单位(2D),即投影到纬度和经度的像素单位.

任何草率贴图的目标是在可用系统中统一这些坐标.

重新投影图块时,我们需要添加一个坐标空间:

  • (/a)图块设置投影.

我觉得这些示例在如何将所有坐标系捆绑在一起方面还不是很清楚.因此,正如您可能已经看到的,我将上述方法放置在来自个人项目的geoTile对象中,该对象来自 d3-tile 来整理示例.

挑战不断前进

变焦速度和响应能力是我看到的最大挑战.为了解决这个问题,我将缩放功能设置为在缩放结束时触发-这在平移事件中最明显,因为通常平移会通过平移连续触发缩放功能,这可以通过转换现有图像来解决.但是,使用此方法最可靠的方法是在静态地图上.为平移事件而不是当前重新采样,对已经绘制的图像实施转换将是理想的选择.

动画化这样的地图可能是不可能的.

可能存在优化将像素转换为纬度的计算的空间,但这可能很困难.

示例

不幸的是,对于一段代码来说,代码太多了,所以我做了一些bl.ocks来演示.

这些仅进行了最少的测试,如果我设法完成基础的图块库,我将为此目的对其进行分叉,与此同时,它应该足以作为示例.该代码的内容可以在d3-reprojectSlippy.js文件的 geoTile.tile()中找到,该文件包含上述的enter/update/exit周期(很基本)和onload函数.当我在边上进行平铺工作时,我将不断更新此答案.

替代方案

重新投影图块既麻烦又费时.如果可能的话,一种替代方法是将瓷砖设置为所需的投影.这是通过 OSM磁贴完成的,但是它又麻烦又耗时-仅对于地图制作者,而不是浏览器.

TL; DR

重新投影的Mercator磁贴需要时间,您应该阅读以上内容.

It's been a few years since Jason Davies blew us away with Reprojected Raster Tiles—that map stopped working because Mapbox is blocking his site, but the Mollweide Watercolour and Interrupted Goode Raster remain great demos.

Now on Observable HQ I see docs for the most recent d3-geo-projection and d3-tile, but no modern examples on how to do what Jason did: reprojecting standard Mercator tile sets.

How can I get d3-tile to warp to a new projection?

解决方案

This answer builds upon:

These three resources also build on one another. Understanding these three examples will help in terms of understanding what occurs in my example below.

The answer also uses, as a base, my slow moving, ongoing attempt to build a tile library.

The goal of this answer is not to present a finalized resource, but a rough demonstration of how one might be put together along with associated information. The answer will also evolve as I further my thought on the problem.

Web Mercator Tiles

A Mercator map that stretches over 360 degrees of longitude and ~170 degrees of latitude (+/- 85 degrees) will fill a square (Going beyond 85 degrees in latitude causes distortion to get out of hand, and inclusion of the poles isn't advisable as the poles are at +/-infinity on the projected plane).

This square of most of the world is, for a web mapping service (with mercator tiles), zoom level 0. The map is 2^0 squares across and 2^0 squares high.

If we divide that square into a grid of two squares by two squares we have zoom level 1. The map is 2^1 by 2^1 squares.

Consequently, the zoom level dictates how many squares across and high the map is: 2^zoomLevel. If each square is the same size in pixels, then each increase of the zoom level by one increases the pixel width of the world by 2x.

Luckily for us, there is no land north of ~85 degrees and it isn't often we want to show Antarctica, so this square is suitable for most web mapping applications. However, it means if we are reprojecting web mercator tiles to anything that shows above these latitudes, we'll have a bald spot:

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

Lastly, web mercator tiles are rendered in a projected space that is predictable and regular relative to the tiles. If we re-project the tiles, we may be enlarging or shrinking the projected space for each tile, we should be mindful of this. In the image above, the tiles around the North Pole are reprojected as much smaller than those further south. Tiles will not necessarily be uniformly sized after projection.

Reprojection and Resampling Plate Carree

The biggest challenge to reprojecting web service tiles is time - and not just time spent understanding projections and reading answers like this.

Projection functions are complex time consuming operations that must be done on each and every pixel that is rendered. All d3 examples I have seen use the process (or a close variant) as seen here for the actual reprojection and resampling. This example will only work if the original image is projected with Plate Carree. The process is as follows:

  1. Create a blank new image.
  2. For each pixel in the new image, take its location in pixels and invert it (using the desired projection) to get a latitude and longitude.
  3. Determine which pixel in the original image overlaps that longitude and latitude.
  4. Take the information from that pixel in the original image and assign it to the appropriate pixel in the new image (the pixel in step 2)

When the original image uses a Plate Carree projection we don't need a d3-geoProjection, the relationship is linear between projected and unprojected coordinates. For example: if the image is 180 pixels high, each pixel represents 1 degree of latitude. This means step 3 doesn't take very long as compared with step 2 and projection.invert(). Here's Mike's function for step 3:

var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;

The time required for step 2 is related to the projection used for the reprojected image. All examples I've seen use d3.geoProjection.invert() for step two in the above list - taking a pixel location in a new image and finding out its latitude and longitude. Not all projections are born equal. Cylindrical projections will generally outperform conical projections, and conical projections will generally out perform azimuthal projections. I've also seen some odd speed discrepencies between d3v4 and d3v5 in terms of projection.invert() time:

Tɪᴍᴇ ᴛᴏ ᴜɴᴘʀᴏᴊᴇᴄᴛ ᴀ ᴘᴏɪɴᴛ ᴏɴ ᴀ ᴍᴀᴘ ᴡɪᴛʜ D3 (ᴄᴏɴᴠᴇʀᴛ ғʀᴏᴍ ᴘɪxᴇʟs ᴛᴏ ʟᴀᴛ/ʟᴏɴɢ). Iᴛ ɪs ᴜɴᴄʟᴇᴀʀ ᴡʜʏ D3ᴠ4 ᴡᴏᴜʟᴅ ʙᴇ ғᴀsᴛᴇʀ.

And for completeness, here's a wider range of projections found in d3-geo-projections:

Cᴏᴍᴘᴀʀɪɴɢ ᴛɪᴍᴇ ғᴏʀ ᴘʀᴏᴊᴇᴄᴛɪᴏɴ.ɪɴᴠᴇʀᴛ ғᴏʀ ᴀᴅᴅɪᴛɪᴏɴᴀʟ ᴘʀᴏᴊᴇᴄᴛɪᴏɴs

Notes

  • This approach can run into problems described here, these problems can be addressed through the answers there - but different solutions will cost additional time to process.

  • This approach uses a nearest neighbour approach - which might lead to quality issues. More advanced sampling such as bilinear or cubic will add time to the process but may result in more desirable image.

  • If the base image has text, text may be rotated or otherwise manipulated to make it less or unreadable.

  • Mike's example is for a single image, for tiles, the process is altered to some degree, we are now creating multiple images, and this requires knowing each original tile's bounds and each reprojected tile's bounds in degrees as well as in tile units for the former and pixels for the latter - minor details.

Reprojection and Resampling Web Mercator

When I started looking at this question I looked at Alan McConchie's solution/example as a reference. It took a while to notice, but step 3 in this example (and I believe Jason Davies' work too) doesn't account for a web Mercator tile in the resampling - only in determining the tile bounds. But, the relationship between pixels on the y axis is no longer linear as it was in the Plate Carree.

This means tiles are placed in the right places, but the sampling treats the y axis as linear within each tile. This distortion was most visible when showing the whole world with a low tile zoom level (midway down/up the tile), and may be what Alan speaks of when he mentions odd compression.

The solution is to properly project the latitude for each latitude/longitude pair in step 3 above. This adds time, always more time - the function involves Math.atan and Math.exp, but the difference shouldn't be too bad. In both Alan and Jason's work this is done with a simple formula (but only used for tile bounds, not each pixel):

Math.atan(Math.exp(-y * Math.PI / 180)) * 360 / Math.PI - 90;

In my example below, I've just used d3.geoMercator() to make for a more clear scaling factor, using the projection includes one extra operation to transform the x coordinate.

Otherwise, the 4 step process remains the same.

Finding the right tiles

I've only seen one clean approach to finding what tiles to show, Jason Davies' d3.quadTile, seen here. I believe that Alan McConchie uses an unminified version that may be otherwise altered. There is also this github repository for another version of d3.quadTiles, which is very similar.

For McConchie/Davies, d3.quadTile will, given a projection with a clip extent (not clip angle) and a tile depth, pull out all the tiles that intersect the view extent.

In Alan McConchie's solution/example, the zoom level is based off of the projection scale - but this is not necessarily the wisest: each projection has different scaling factors, a scale of 100 on one scale will show a different extent than a scale of a 100 on another. Also, the relationship between scale values and map size in a cylindrical projection may be linear, while non-cylindrical projections may have non linear relationships between map size and scale.

I've modified this approach a bit - I use a scale factor to determine an initial tile depth, and then reduce that tile depth if the tile count returned by d3.quadTile exceeds a certain number:

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;
}

Then, using d3.quadTile I pull out the relevant tiles:

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;
}

At first I thought that pulling tiles from multiple zoom depths (to account for discrepencies in size of reprojected tiles) would be ideal: but this will run into problems with things like line thickness in the raster as well as discontinuous annotations.

Adopting Jason and Alan's Work

I take the tile set generated above with geoTile.tiles() and run it through an enter/update/exit cycle using the tile coordinate (in tile coordinates, row, column, zoom depth) as a key, appending image elements to a parent g or svg. When loading images, once an image loads, we call a onload function to do the actual reprojection. This is largely unchanged from Jason and Alan, I've addressed the following challenges I saw in this code:

  • Resampling did not account for web mercator (mentioned above)
  • Tile depth was not selected well (mentioned above)
  • Tiles were reprojected as canvas-es placed in a div rather than an SVG - creating two parent containers, one for each type of feature: tile or vector.

I believe my example, with very minor tweaks, has addressed these. I've also added some more extensive comments for viewing:

    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);
    }

Placing it within a larger structure.

A regular tile map with overlain features has a few coordinate systems:

  • Tile units (3D), which mark column, row, and zoom level of each tile (x,y,z respectively)
  • Geographic coordinates (3D), which mark latitude and longitude of a point on a three dimensional sphere.
  • Zoom units (3D), which keep track of zoom translate (x,y) and zoom scale (k).
  • projected units (2D), pixels units which latitude and longitude are projected to.

The goal of any slippy map is to unify these coordinates in a usable system.

When we reproject the tiles, we need to add a coordinate space:

  • The(/a) tile set projection.

I felt the examples were not particularily clear in how they tied together all the coordinate systems. So, I've placed the above methods, as you might have seen, in a geoTile object that is taken from a personal project for a tile library. The goal of this is for a bit smoother coordination of different units. I am not trying to plug it, it's still in development in any event (just too busy to really finish it); however, I will see if time affords me the opportunity to rig an example up with d3-tile.

Challenges moving forward

Zoom speed and responsiveness is the greatest challenge I see. To address this I've set the zoom function to trigger on zoom end - this is most noticable on pan events, as normally a pan triggers the zoom function continuously through the pan, this could be addressed by translating existing images. The most reliable way to use this, however, is on a static map. Implementing a translate for already drawn images would be ideal for pan events, rather than resampling as currently.

Animating such a map is probably not possible.

There is probably room for optimizing the calculations that turn pixel to latitude, but this might be difficult.

Examples

Unfortunately, the code is too much for a snippet, so I've made a few bl.ocks to demonstrate.

These have had only minimal testing, if I manage to finish the foundational tile library, I'll fork it for this purpose, in the meantime it should suffice as an example. The guts of the code is found in geoTile.tile() in the d3-reprojectSlippy.js file, which contains the enter/update/exit cycle (fairly basic) and the onload function described above. As I do work on the tiles a fair bit on the side I'll keep this answer updated.

The alternative

Reprojecting tiles is cumbersome and time consuming. An alternative, if possible, would be to produce a tile set in the desired projection. This has been done with OSM tiles, but is also cumbersome and time consuming - just for the map maker, not the browser.

TL;DR

Reprojected Mercator tiles take time, you should read the above.

这篇关于将Web Mercator磁贴重新投影到D3的任意投影?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆