使用 D3.js、Leaflet 或 Mapbox 用墨卡托地图包裹大圆圈
Wrapping great circles with Mercator maps with D3.js, Leaflet, or Mapbox
简而言之,问题是:如何使用 其他 而不是 Google 地图 在墨卡托中准确投影环绕大圆半径 API?
题长:
所以我有一个难题。我 运行 一个地图应用程序,它使用 Google 地图 API 将巨大的圆圈投影到墨卡托地图上——它试图显示非常大、准确的半径,比如说,大约 13,000公里。但我不想再使用 Google 地图 API,因为 Google 的新定价方案太疯狂了。所以我试图将代码转换为 Leaflet、Mapbox 或 任何东西 non-Google,但没有任何东西可以正确处理这些圆圈。
以下是 Google 地图 API 如何处理以非洲北部为中心、半径为 13,000 公里的测地线圆:
这在直觉上看起来很奇怪但是是正确的。波浪图案是由环绕地球的圆圈造成的。
D3.js 可以在正交投影中正确渲染。所以这是在 D3.js 中用 d3.geo.circle() 在地球上渲染的同一个圆圈,两次旋转:
这使得 2D-"wavy" 模式更有意义,对吧?正确的。我喜欢它。完全符合我的科学传播目的。
但是当我将我的代码转换为 Leaflet 时,它根本不起作用。为什么?因为Leaflet的圈class根本就不是大圈。相反,它似乎只是一个椭圆,随着纬度稍微扭曲,但不是以真正的测地线方式。相同的圆,相同的半径,相同的原点,我们得到:
大错特错!除了看起来完全不切实际之外,它还是不正确的——澳大利亚不会在这样一个圆半径内。这对我的申请很重要!这不行。
好吧,我想,也许诀窍就是尝试实现我自己的大圆 class。我采用的方法是迭代圆点作为距原点的距离,但使用 this very helpful website 的 "Destination point given distance and bearing from start point" 计算来计算距离,然后将它们投影为 Leaflet 中的多边形。这就是我得到的结果:
这看起来很糟糕,但实际上更接近准确!我们得到了波浪效应,这是正确的。像我一样,你可能会问,"what's really going on here?" 所以我做了一个版本,允许我突出显示每个迭代点:
你可以很清楚地看到它正确地呈现了圆,但是多边形错误地加入了它。 应该 做的(有人可能会天真地认为)是将波浪图包裹在墨卡托地图投影的多个实例周围,而不是天真地在顶部连接它们,而是将它们连接成球形。就像这个粗糙的 Photoshop 渲染:
然后多边形会 "close" 表明多边形上方的所有内容也都包含在其中。
不过,我不知道如何在 Leaflet 中实现类似的功能。或者其他任何与此有关的事情。考虑到缩放状态,也许我必须自己以某种方式处理原始 SVG?或者其他的东西?在我进入那些危险的杂草之前,我想我会要求任何 suggestions/ideas/etc。也许有一些更明显的方法。
哦,我尝试了另一件事:使用相同的 d3.geo.circle 构造函数,该构造函数在 Mercator/Leaflet 投影的正交投影中效果很好。它产生的结果与我的 "naive" Leaflet 大圆实施大致相同:
我想这很有希望。但是,如果你移动原点的经度,D3.js 版本会以更奇怪的方式换行(D3.js 为红色,我的 Leaflet class 为绿松石色):
如果 D3.js 中有某种方法可以改变它的工作方式,我不会感到惊讶,但我还没有完全进入 D3.js 兔子洞。我希望 D3.js 能成为 "easier"(因为它比 Leaflet 更 full-formed 制图工具),所以我会继续研究这个。
我还没有在 Mapbox-gl 中尝试这样做(我想那是 "attempt" 列表中的下一个)。
无论如何。谢谢阅读。重申一下这个问题:如何使用 其他 而不是 Google 地图 API 来准确投影墨卡托中的环绕大圆半径?
antimeridian cutting that's GeoJSON需要在leaflet或mapbox中正确绘制
对于 d3,它很简单 d3.geoCircle()
,对于其他地图服务,不处理逆子午线切割,您可以使用 d3 正确计算输入 json。
主要思想是将 d3 计算的坐标取消投影回 lat-lng,在其计算上使用相同的投影,未受保护的特征将被 d3 的反子午线分割。
我开发了示例,运行 代码片段并在 d3 图上拖动圆圈。
结果截图如下:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css"/>
</head>
<body style="margin: 0">
<svg style="display: inline-block"></svg>
<div id="map" style="display: inline-block; width: 350px; height: 260px"></div>
<script>
var tileLayer = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png');
var map = L.map('map').setView([0,0], 0).addLayer(tileLayer);
var bounds = [260, 260];
var colorGenerator = d3.scaleOrdinal(d3.schemeCategory10);
var projection = d3.geoMercator().translate([bounds[0] / 2, bounds[1] / 2]).scale(40);
var geoPath = d3.geoPath().projection(projection);
var geoCircle = d3.geoCircle();
var svg = d3.select('svg')
.attr("width", bounds[0])
.attr("height", bounds[1])
.attr("viewbox", "0 0 " + bounds[0] + " " + bounds[1])
.append('g');
svg.append("g")
.append("path")
.datum(d3.geoGraticule())
.attr("stroke", "gray")
.attr('d', geoPath);
function addCircle(center, radius, color) {
var g = svg.append("g");
var drag = d3.drag().on("drag", dragged);
var xy = projection(center);
var path = g.append("path")
.datum({
type: "Polygon",
coordinates: [[]],
x: xy[0],
y: xy[1]
})
.classed("zone", "true")
.attr("fill", color)
.attr("stroke", color)
.attr("fill-opacity", 0.3)
.call(drag);
update(path.datum());
function dragged(d) {
g.raise();
d.x = d3.event.x;
d.y = d3.event.y;
update(d)
}
function update(d) {
center = projection.invert([d.x, d.y]);
var poly = geoCircle.center(center).radius(radius)();
d.coordinates[0] = poly.coordinates[0];
path.attr('d', geoPath);
d.geojson && d.geojson.remove();
d.geojson = L.geoJSON(unproject(path.attr('d')), {
color: color,
}).addTo(map);
}
function unproject(d) {
var features = d.toLowerCase().split('z').join('').split('m');
features.shift();
var coords = features.map(function (feature) {
return feature.split('l').map(function (pt) {
var xy = pt.split(',');
return projection.invert([+xy[0], +xy[1]]);
});
});
return {
type: 'MultiPolygon',
coordinates: [coords]
}
}
}
d3.range(0, 4).forEach(function (i) {
addCircle([-120 + i * 60, 0], i * 10 + 10, colorGenerator(i));
});
</script>
</body>
</html>
以下函数输出 geojson,特征按 +-180 经线分割,
参数是 svg 路径的 'd' 属性,由 d3:
计算
function unproject(d, projection) {
var features = d.toLowerCase().split('z').join('').split('m');
features.shift();
var coords = features.map(function (feature) {
return feature.split('l').map(function (pt) {
var xy = pt.split(',');
return projection.invert([+xy[0], +xy[1]]);
});
});
return {
type: 'MultiPolygon',
coordinates: [coords]
}
}
此效果也可以通过 d3-geo-projection 扩展实现,代码如下:
function unproject(geojson) {
var projected = d3.geoProject(geojson, projection);
if (projected.type === "MultiPolygon") {
projected.coordinates = projected.coordinates.map(function(arr) {
return [invert(arr[0])];
});
} else {
projected.coordinates[0] = invert(projected.coordinates[0]);
}
return projected;
}
function invert(coords) {
return coords.map(function(c) {
return projection.invert(c);
});
}
这两种方法都不是处理带孔的多边形,但点变换在其他情况下是相同的
感谢阅读!
所以它最终不是一个简单的解决方案。为了实现所需的 Google 类地图行为,我最终不得不从头开始编写扩展 L.Polygon 对象的 Leaflet 插件。这是因为所需的行为包括 "wrapping" 多边形,而在 Leaflet 中没有 "magic" 方法可以做到这一点。
我最后做的是创建一个插件,它可以检测它是否应该(基于缩放级别)创建许多包裹 "copies," 然后使用一些逻辑来确定它是否应该拼接多边形或不在一起。它不是特别优雅(它比数学更符合逻辑)但简而言之,这就是我的编程。
无论如何,here is the final plugin. It can be dropped in like a regular L.Circle object (just change it to L.greatCircle) without too many other changes. You can see it in action on my MISSILEMAP(它还具有我必须编写的测地折线 class,这更容易)。
感谢提意见和建议的人
简而言之,问题是:如何使用 其他 而不是 Google 地图 在墨卡托中准确投影环绕大圆半径 API?
题长:
所以我有一个难题。我 运行 一个地图应用程序,它使用 Google 地图 API 将巨大的圆圈投影到墨卡托地图上——它试图显示非常大、准确的半径,比如说,大约 13,000公里。但我不想再使用 Google 地图 API,因为 Google 的新定价方案太疯狂了。所以我试图将代码转换为 Leaflet、Mapbox 或 任何东西 non-Google,但没有任何东西可以正确处理这些圆圈。
以下是 Google 地图 API 如何处理以非洲北部为中心、半径为 13,000 公里的测地线圆:
这在直觉上看起来很奇怪但是是正确的。波浪图案是由环绕地球的圆圈造成的。
D3.js 可以在正交投影中正确渲染。所以这是在 D3.js 中用 d3.geo.circle() 在地球上渲染的同一个圆圈,两次旋转:
这使得 2D-"wavy" 模式更有意义,对吧?正确的。我喜欢它。完全符合我的科学传播目的。
但是当我将我的代码转换为 Leaflet 时,它根本不起作用。为什么?因为Leaflet的圈class根本就不是大圈。相反,它似乎只是一个椭圆,随着纬度稍微扭曲,但不是以真正的测地线方式。相同的圆,相同的半径,相同的原点,我们得到:
大错特错!除了看起来完全不切实际之外,它还是不正确的——澳大利亚不会在这样一个圆半径内。这对我的申请很重要!这不行。
好吧,我想,也许诀窍就是尝试实现我自己的大圆 class。我采用的方法是迭代圆点作为距原点的距离,但使用 this very helpful website 的 "Destination point given distance and bearing from start point" 计算来计算距离,然后将它们投影为 Leaflet 中的多边形。这就是我得到的结果:
这看起来很糟糕,但实际上更接近准确!我们得到了波浪效应,这是正确的。像我一样,你可能会问,"what's really going on here?" 所以我做了一个版本,允许我突出显示每个迭代点:
你可以很清楚地看到它正确地呈现了圆,但是多边形错误地加入了它。 应该 做的(有人可能会天真地认为)是将波浪图包裹在墨卡托地图投影的多个实例周围,而不是天真地在顶部连接它们,而是将它们连接成球形。就像这个粗糙的 Photoshop 渲染:
然后多边形会 "close" 表明多边形上方的所有内容也都包含在其中。
不过,我不知道如何在 Leaflet 中实现类似的功能。或者其他任何与此有关的事情。考虑到缩放状态,也许我必须自己以某种方式处理原始 SVG?或者其他的东西?在我进入那些危险的杂草之前,我想我会要求任何 suggestions/ideas/etc。也许有一些更明显的方法。
哦,我尝试了另一件事:使用相同的 d3.geo.circle 构造函数,该构造函数在 Mercator/Leaflet 投影的正交投影中效果很好。它产生的结果与我的 "naive" Leaflet 大圆实施大致相同:
我想这很有希望。但是,如果你移动原点的经度,D3.js 版本会以更奇怪的方式换行(D3.js 为红色,我的 Leaflet class 为绿松石色):
如果 D3.js 中有某种方法可以改变它的工作方式,我不会感到惊讶,但我还没有完全进入 D3.js 兔子洞。我希望 D3.js 能成为 "easier"(因为它比 Leaflet 更 full-formed 制图工具),所以我会继续研究这个。
我还没有在 Mapbox-gl 中尝试这样做(我想那是 "attempt" 列表中的下一个)。
无论如何。谢谢阅读。重申一下这个问题:如何使用 其他 而不是 Google 地图 API 来准确投影墨卡托中的环绕大圆半径?
antimeridian cutting that's GeoJSON需要在leaflet或mapbox中正确绘制
对于 d3,它很简单 d3.geoCircle()
,对于其他地图服务,不处理逆子午线切割,您可以使用 d3 正确计算输入 json。
主要思想是将 d3 计算的坐标取消投影回 lat-lng,在其计算上使用相同的投影,未受保护的特征将被 d3 的反子午线分割。
我开发了示例,运行 代码片段并在 d3 图上拖动圆圈。
结果截图如下:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/leaflet@1.3.4/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.4/dist/leaflet.css"/>
</head>
<body style="margin: 0">
<svg style="display: inline-block"></svg>
<div id="map" style="display: inline-block; width: 350px; height: 260px"></div>
<script>
var tileLayer = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png');
var map = L.map('map').setView([0,0], 0).addLayer(tileLayer);
var bounds = [260, 260];
var colorGenerator = d3.scaleOrdinal(d3.schemeCategory10);
var projection = d3.geoMercator().translate([bounds[0] / 2, bounds[1] / 2]).scale(40);
var geoPath = d3.geoPath().projection(projection);
var geoCircle = d3.geoCircle();
var svg = d3.select('svg')
.attr("width", bounds[0])
.attr("height", bounds[1])
.attr("viewbox", "0 0 " + bounds[0] + " " + bounds[1])
.append('g');
svg.append("g")
.append("path")
.datum(d3.geoGraticule())
.attr("stroke", "gray")
.attr('d', geoPath);
function addCircle(center, radius, color) {
var g = svg.append("g");
var drag = d3.drag().on("drag", dragged);
var xy = projection(center);
var path = g.append("path")
.datum({
type: "Polygon",
coordinates: [[]],
x: xy[0],
y: xy[1]
})
.classed("zone", "true")
.attr("fill", color)
.attr("stroke", color)
.attr("fill-opacity", 0.3)
.call(drag);
update(path.datum());
function dragged(d) {
g.raise();
d.x = d3.event.x;
d.y = d3.event.y;
update(d)
}
function update(d) {
center = projection.invert([d.x, d.y]);
var poly = geoCircle.center(center).radius(radius)();
d.coordinates[0] = poly.coordinates[0];
path.attr('d', geoPath);
d.geojson && d.geojson.remove();
d.geojson = L.geoJSON(unproject(path.attr('d')), {
color: color,
}).addTo(map);
}
function unproject(d) {
var features = d.toLowerCase().split('z').join('').split('m');
features.shift();
var coords = features.map(function (feature) {
return feature.split('l').map(function (pt) {
var xy = pt.split(',');
return projection.invert([+xy[0], +xy[1]]);
});
});
return {
type: 'MultiPolygon',
coordinates: [coords]
}
}
}
d3.range(0, 4).forEach(function (i) {
addCircle([-120 + i * 60, 0], i * 10 + 10, colorGenerator(i));
});
</script>
</body>
</html>
以下函数输出 geojson,特征按 +-180 经线分割, 参数是 svg 路径的 'd' 属性,由 d3:
计算function unproject(d, projection) {
var features = d.toLowerCase().split('z').join('').split('m');
features.shift();
var coords = features.map(function (feature) {
return feature.split('l').map(function (pt) {
var xy = pt.split(',');
return projection.invert([+xy[0], +xy[1]]);
});
});
return {
type: 'MultiPolygon',
coordinates: [coords]
}
}
此效果也可以通过 d3-geo-projection 扩展实现,代码如下:
function unproject(geojson) {
var projected = d3.geoProject(geojson, projection);
if (projected.type === "MultiPolygon") {
projected.coordinates = projected.coordinates.map(function(arr) {
return [invert(arr[0])];
});
} else {
projected.coordinates[0] = invert(projected.coordinates[0]);
}
return projected;
}
function invert(coords) {
return coords.map(function(c) {
return projection.invert(c);
});
}
这两种方法都不是处理带孔的多边形,但点变换在其他情况下是相同的
感谢阅读!
所以它最终不是一个简单的解决方案。为了实现所需的 Google 类地图行为,我最终不得不从头开始编写扩展 L.Polygon 对象的 Leaflet 插件。这是因为所需的行为包括 "wrapping" 多边形,而在 Leaflet 中没有 "magic" 方法可以做到这一点。
我最后做的是创建一个插件,它可以检测它是否应该(基于缩放级别)创建许多包裹 "copies," 然后使用一些逻辑来确定它是否应该拼接多边形或不在一起。它不是特别优雅(它比数学更符合逻辑)但简而言之,这就是我的编程。
无论如何,here is the final plugin. It can be dropped in like a regular L.Circle object (just change it to L.greatCircle) without too many other changes. You can see it in action on my MISSILEMAP(它还具有我必须编写的测地折线 class,这更容易)。
感谢提意见和建议的人