cᴏᴍᴘᴀʀɪɴɢᴏᴍᴘᴀʀɪɴɢ。
注意事项
这种方法可能会遇到here 描述的问题,这些问题可以通过那里的答案解决 - 但不同的解决方案将花费额外的时间来处理。
此方法使用最近邻方法 - 这可能会导致质量问题。更高级的采样(例如双线性或三次采样)会增加处理时间,但可能会产生更理想的图像。
如果基础图像包含文本,则可以旋转或以其他方式处理文本以使其不那么可读或不可读。
Mike 的示例是针对单个图像,对于瓦片,该过程在一定程度上有所改变,我们现在正在创建多个图像,这需要知道每个原始瓦片的边界和每个重新投影瓦片的度数以及前者以平铺单位为单位,后者以像素为单位 - 次要细节。
重投影和重采样 Web Mercator
当我开始查看这个问题时,我查看了 Alan McConchie 的 solution/example 作为参考。花了一些时间才注意到,但本示例中的第 3 步(我相信 Jason Davies 的工作也是如此)并未考虑重采样中的 Web 墨卡托图块 - 仅在确定图块边界时。但是,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 坐标。
否则,4 步流程保持不变。
找到合适的图块
我只见过一种简洁的方法来查找要显示的瓷砖,Jason Davies 的 d3.quadTile,见 here。 我相信 Alan McConchie 使用了未缩小的版本,可能会被更改。还有另一个版本的 d3.quadTiles 的 this github 存储库,非常相似。
对于 McConchie/Davies,d3.quadTile 将在给定具有裁剪范围(不是裁剪角度)和切片深度的投影的情况下,拉出与视图范围相交的所有切片。
在 Alan McConchie 的 solution/example 中,缩放级别基于投影比例 - 但这不一定是最明智的:每个投影都有不同的比例因子,一个比例的 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() 生成的图块集,并使用图块坐标(在图块坐标、行、列、缩放深度中)作为键,通过输入/更新/退出循环运行它,附加image 元素给父母g 或svg。加载图像时,一旦加载图像,我们就会调用 onload 函数来进行实际的重投影。这与 Jason 和 Alan 基本没有变化,我已经解决了我在这段代码中看到的以下挑战:
- 重采样不考虑网络墨卡托(如上所述)
- 瓷砖深度没有选择好(如上所述)
- 瓷砖被重新投影为放置在 div 而不是 SVG 中的 canvas-es - 创建两个父容器,一个用于每种类型的功能:瓷砖或矢量。
我相信我的示例经过非常细微的调整,已经解决了这些问题。我还添加了一些更广泛的 cmets 供查看:
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);
}
将其放置在更大的结构中。
具有重叠特征的常规瓦片地图有几个坐标系:
- 平铺单元 (3D),用于标记每个平铺的列、行和缩放级别(分别为 x、y、z)
- 地理坐标 (3D),用于标记三维球体上某个点的纬度和经度。
- 缩放单位 (3D),用于跟踪缩放平移 (x,y) 和缩放比例 (k)。
- 投影单位 (2D),经纬度投影到的像素单位。
任何滑动地图的目标都是在一个可用的系统中统一这些坐标。
当我们重新投影瓦片时,我们需要添加一个坐标空间:
我觉得这些示例在如何将所有坐标系联系在一起方面并不是特别清楚。因此,正如您可能已经看到的,我已将上述方法放在一个 geoTile 对象中,该对象取自 tile library 的个人项目。这样做的目的是为了更顺畅地协调不同的单位。我不是想插上它,无论如何它仍在开发中(只是太忙而无法真正完成它);不过,我会看看是否有时间让我有机会用d3-tile 构建一个示例。
前进的挑战
缩放速度和响应能力是我看到的最大挑战。为了解决这个问题,我将缩放功能设置为在缩放结束时触发 - 这在平移事件中最为明显,因为通常平移会通过平移连续触发缩放功能,这可以通过翻译现有图像来解决。然而,最可靠的使用方法是在静态地图上。对已绘制的图像实施翻译将是平移事件的理想选择,而不是像当前那样重新采样。
动画这样的地图可能是不可能的。
可能有优化将像素转换为纬度的计算的空间,但这可能很困难。
示例
不幸的是,sn-p 的代码太多了,所以我做了几个块来演示。
这些只进行了最少的测试,如果我设法完成了基础磁贴库,我将为此目的分叉它,同时它应该足以作为示例。代码的核心位于 d3-reprojectSlippy.js 文件中的geoTile.tile() 中,其中包含进入/更新/退出循环(相当基本)和上面描述的 onload 函数。当我在瓷砖上工作时,我会保持更新这个答案。
另一种选择
重新投影图块既麻烦又耗时。如果可能的话,另一种选择是在所需的投影中生成一个瓦片集。这已通过 OSM tiles 完成,但也很麻烦且耗时 - 仅适用于地图制作者,而不是浏览器。
TL;DR
重新投影墨卡托图块需要时间,您应该阅读以上内容。