计算墨卡托 d3 缩放以匹配 Leaflet 缩放级别
Calculating Mercator d3 scaling to match Leaflet zoom level
给定 Leaflet 缩放级别 (0..22),我将如何计算 geoMercator 投影的 D3 缩放值?在 zoom=0 时,整个世界都在一个图块 (256x256) 中。在图块中,世界大小为 2^zoom x 2^zoom 图块。
与墨卡托地图的 d3 比例进行比较:
墨卡托地图使用此公式:
var point = [x, Math.log(Math.tan(Math.PI / 4 + y / 2))];
d3 地图中的比例因子基本上应用以下变换:
point[0] = point[0] * k;
point[1] = -point[1] * k;
d3 墨卡托投影的默认地图比例为 961/2π,即 961 像素 x 360 度。
墨卡托公式的一个怪癖是 "whole" 世界的正方形视图实际上将在 ±85.05113 度 North/South 处达到上限。 Web Mercators 可以推动这一点,因为它们不是等角投影。 Leaflet 将 "whole" 世界的范围推至 ±89.15 度左右 North/South。
因此,d3 使用适当的墨卡托,Leaflet 使用网络墨卡托,这意味着比例值可能无法很好地啮合。 GIS.stackexchange 上的这个答案将提供有关两者之间差异的更多信息。
但是,您仍然可以将两者对齐(大多数情况下)。
Yurik 在下面的 中提到的一种方法是使用缩放级别和图块大小来获得比例因子:
Web 地图服务使用分块网格,其中增加缩放级别会使显示的分块数量增加四倍。在缩放级别 1 中,世界适合一个图块,在缩放级别 2 中,世界适合四个方形图块。那么瓦片数量的通用公式为:
图块数 = 4^zoomLevel
最重要的是,每次我们放大时,穿过地图(一行)所需的图块数量都会翻倍。
以此为出发点,我们可以想出两者的搭配方法。
d3 geoMercator 使用 961/(2*Math.PI)
作为默认比例 - 这将赤道的 360 度(或 2 弧度)扩展到 961 个像素。要将其设置为适用于基于图块的图层,我们只需要知道图块大小和缩放级别。
我们需要知道赤道分布了多少个像素,为此我们使用:
tileSize * Math.pow(2,zoomLevel)
这为我们提供了环绕赤道的所有方块的宽度。然后我们可以将其除以 2Pi 并得到我们的 d3 比例:
projection.scale(tileSize * Math.pow(2,zoomLevel) / (2 * Math.PI))
由于 d3 墨卡托和 web 墨卡托之间的差异,可能会出现失真问题,具体取决于您所处的位置和缩放的距离,但这在大多数情况下应该可以提供良好的对齐效果。
或者,我们可以使用传单地图的实际角来确定合适的比例:
D3 具有一种允许投影将地理范围适合 svg 范围的方法:projection.fitExtent()
。此方法采用 geojson 或几何图形。在这个答案中,我使用的是 geojson。
.getBounds()
将 return 传单地图范围,因此您可以相当轻松地创建一个 geojson 边界框:
var bbox = {
"type": "Polygon",
"coordinates": [
[
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat]
]
]
}
请注意,绕线顺序在 d3 中实际上很重要 - 外圈是逆时针方向
然后您所要做的就是将投影设置为适合该范围:
var projection = d3.geoMercator()
.fitSize([600, 400], bbox);
使用稍作修改的 leaflet example(更改居中点和缩放),整个内容如下所示:
var features = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-0.17565250396728513,
51.510118018740904
],
[
-0.17586708068847653,
51.509744084227556
],
[
-0.17584562301635742,
51.50871574849058
],
[
-0.17359256744384766,
51.50613812964363
],
[
-0.17204761505126953,
51.50552375337273
],
[
-0.16966581344604492,
51.50481587478995
],
[
-0.16599655151367188,
51.50454874793857
],
[
-0.1624774932861328,
51.504001132997686
],
[
-0.16058921813964844,
51.5039744199054
],
[
-0.16033172607421875,
51.50426826305929
],
[
-0.16013860702514648,
51.5043884710761
],
[
-0.16016006469726562,
51.50465559886706
],
[
-0.15996694564819336,
51.50510971251776
],
[
-0.16282081604003906,
51.505737450406535
],
[
-0.16466617584228516,
51.5058710105437
],
[
-0.16835689544677734,
51.50588436653591
],
[
-0.1705455780029297,
51.506098061878475
],
[
-0.17273426055908203,
51.506672363145654
],
[
-0.17282009124755857,
51.50681927626061
],
[
-0.17468690872192383,
51.508729103648925
],
[
-0.17511606216430664,
51.50999782583918
],
[
-0.17526626586914062,
51.510144728231545
],
[
-0.17565250396728513,
51.510118018740904
]
]
]
}
}
]
};
var mymap = L.map('mapid').setView([51.5, -0.171], 14);
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
maxZoom: 18,
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
id: 'mapbox.streets'
}).addTo(mymap);
var svg = d3.select('#mapid')
.append('svg')
.attr('width',600)
.attr('height',400);
// Create a geojson bounding box:
var bbox = {
"type": "Polygon",
"coordinates": [
[
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat]
]
]
}
var projection = d3.geoMercator()
.fitSize([600, 400], bbox);
var path = d3.geoPath().projection(projection);
svg.append("path")
.datum(features)
.attr('d',path);
svg {
z-index: 10000;
position: relative;
}
<div id="mapid" style="width: 600px; height: 400px;"></div>
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" integrity="sha512-07I2e+7D8p6he1SIM+1twR5TIrhUQn9+I6yjqD53JQjFiMf8EtC93ty0/5vJTZGF8aAocvHYNEDJajGdNx1IsQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js" integrity="sha512-A7vV8IFfih/D732iSSKi20u/ooOfj/AGehOKq0f4vLT1Zr2Y+RX7C+w8A1gaSasGtRUZpF/NZgzSAu4/Gc41Lg==" crossorigin=""></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.js"></script>
我没有添加地图变化时更新投影的代码,但这只是重新计算边界框并重新应用fitSize方法。
给定 Leaflet 缩放级别 (0..22),我将如何计算 geoMercator 投影的 D3 缩放值?在 zoom=0 时,整个世界都在一个图块 (256x256) 中。在图块中,世界大小为 2^zoom x 2^zoom 图块。
与墨卡托地图的 d3 比例进行比较:
墨卡托地图使用此公式:
var point = [x, Math.log(Math.tan(Math.PI / 4 + y / 2))];
d3 地图中的比例因子基本上应用以下变换:
point[0] = point[0] * k;
point[1] = -point[1] * k;
d3 墨卡托投影的默认地图比例为 961/2π,即 961 像素 x 360 度。
墨卡托公式的一个怪癖是 "whole" 世界的正方形视图实际上将在 ±85.05113 度 North/South 处达到上限。 Web Mercators 可以推动这一点,因为它们不是等角投影。 Leaflet 将 "whole" 世界的范围推至 ±89.15 度左右 North/South。
因此,d3 使用适当的墨卡托,Leaflet 使用网络墨卡托,这意味着比例值可能无法很好地啮合。 GIS.stackexchange 上的这个答案将提供有关两者之间差异的更多信息。
但是,您仍然可以将两者对齐(大多数情况下)。
Yurik 在下面的
Web 地图服务使用分块网格,其中增加缩放级别会使显示的分块数量增加四倍。在缩放级别 1 中,世界适合一个图块,在缩放级别 2 中,世界适合四个方形图块。那么瓦片数量的通用公式为:
图块数 = 4^zoomLevel
最重要的是,每次我们放大时,穿过地图(一行)所需的图块数量都会翻倍。
以此为出发点,我们可以想出两者的搭配方法。
d3 geoMercator 使用 961/(2*Math.PI)
作为默认比例 - 这将赤道的 360 度(或 2 弧度)扩展到 961 个像素。要将其设置为适用于基于图块的图层,我们只需要知道图块大小和缩放级别。
我们需要知道赤道分布了多少个像素,为此我们使用:
tileSize * Math.pow(2,zoomLevel)
这为我们提供了环绕赤道的所有方块的宽度。然后我们可以将其除以 2Pi 并得到我们的 d3 比例:
projection.scale(tileSize * Math.pow(2,zoomLevel) / (2 * Math.PI))
由于 d3 墨卡托和 web 墨卡托之间的差异,可能会出现失真问题,具体取决于您所处的位置和缩放的距离,但这在大多数情况下应该可以提供良好的对齐效果。
或者,我们可以使用传单地图的实际角来确定合适的比例:
D3 具有一种允许投影将地理范围适合 svg 范围的方法:projection.fitExtent()
。此方法采用 geojson 或几何图形。在这个答案中,我使用的是 geojson。
.getBounds()
将 return 传单地图范围,因此您可以相当轻松地创建一个 geojson 边界框:
var bbox = {
"type": "Polygon",
"coordinates": [
[
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat]
]
]
}
请注意,绕线顺序在 d3 中实际上很重要 - 外圈是逆时针方向
然后您所要做的就是将投影设置为适合该范围:
var projection = d3.geoMercator()
.fitSize([600, 400], bbox);
使用稍作修改的 leaflet example(更改居中点和缩放),整个内容如下所示:
var features = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-0.17565250396728513,
51.510118018740904
],
[
-0.17586708068847653,
51.509744084227556
],
[
-0.17584562301635742,
51.50871574849058
],
[
-0.17359256744384766,
51.50613812964363
],
[
-0.17204761505126953,
51.50552375337273
],
[
-0.16966581344604492,
51.50481587478995
],
[
-0.16599655151367188,
51.50454874793857
],
[
-0.1624774932861328,
51.504001132997686
],
[
-0.16058921813964844,
51.5039744199054
],
[
-0.16033172607421875,
51.50426826305929
],
[
-0.16013860702514648,
51.5043884710761
],
[
-0.16016006469726562,
51.50465559886706
],
[
-0.15996694564819336,
51.50510971251776
],
[
-0.16282081604003906,
51.505737450406535
],
[
-0.16466617584228516,
51.5058710105437
],
[
-0.16835689544677734,
51.50588436653591
],
[
-0.1705455780029297,
51.506098061878475
],
[
-0.17273426055908203,
51.506672363145654
],
[
-0.17282009124755857,
51.50681927626061
],
[
-0.17468690872192383,
51.508729103648925
],
[
-0.17511606216430664,
51.50999782583918
],
[
-0.17526626586914062,
51.510144728231545
],
[
-0.17565250396728513,
51.510118018740904
]
]
]
}
}
]
};
var mymap = L.map('mapid').setView([51.5, -0.171], 14);
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
maxZoom: 18,
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
id: 'mapbox.streets'
}).addTo(mymap);
var svg = d3.select('#mapid')
.append('svg')
.attr('width',600)
.attr('height',400);
// Create a geojson bounding box:
var bbox = {
"type": "Polygon",
"coordinates": [
[
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._southWest.lat],
[mymap.getBounds()._southWest.lng, mymap.getBounds()._northEast.lat],
[mymap.getBounds()._northEast.lng, mymap.getBounds()._northEast.lat]
]
]
}
var projection = d3.geoMercator()
.fitSize([600, 400], bbox);
var path = d3.geoPath().projection(projection);
svg.append("path")
.datum(features)
.attr('d',path);
svg {
z-index: 10000;
position: relative;
}
<div id="mapid" style="width: 600px; height: 400px;"></div>
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" integrity="sha512-07I2e+7D8p6he1SIM+1twR5TIrhUQn9+I6yjqD53JQjFiMf8EtC93ty0/5vJTZGF8aAocvHYNEDJajGdNx1IsQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js" integrity="sha512-A7vV8IFfih/D732iSSKi20u/ooOfj/AGehOKq0f4vLT1Zr2Y+RX7C+w8A1gaSasGtRUZpF/NZgzSAu4/Gc41Lg==" crossorigin=""></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.js"></script>
我没有添加地图变化时更新投影的代码,但这只是重新计算边界框并重新应用fitSize方法。