简而言之,是一个问题:如何使用除Google Maps API之外的其他 精确投影在Mercator中的环绕大圆半径?
问题很长:
所以我有一个难题。我运行了一个映射应用程序,该应用程序使用Google Maps API在Mercator地图上投影了大圆圈-试图显示非常大且准确的半径,例如大约13,000 km。但是我不想再使用Google Maps API,因为Google的新定价方案非常疯狂。因此,我正在尝试将代码转换为Leaflet,Mapbox或任何非Google 的东西,并且没有任何东西可以正确处理这些圈子。
以下是Google Maps API处理以非洲北部为中心,半径为13,000 km的测地线的方式:
从直觉上看这很奇怪,但是正确。。波浪形是由环绕地球的圆引起的。
D3.js可以正确地以正投影方式呈现。因此,这是在D3.js中使用地球仪上的d3.geo.circle()进行两次旋转的同一个圆:
哪个使2D-“波浪状”图案更有意义,对吗?对。我喜欢它。完全出于我的科学交流目的而工作。
但是当我将代码转换为Leaflet时,它根本不起作用。为什么?因为Leaflet的圈子类别根本不是一个好圈子。取而代之的是,它看起来像是一个椭圆,虽然它在纬度上有些失真,但并不是以真正的测地线方式。相同的圆,相同的半径,相同的原点,我们得到了:
错了,错了!除了看起来完全不现实之外,这是不正确的-澳大利亚会不在这样的圆半径内。这对我的应用很重要!这不能。
好的,我想,也许诀窍是尝试实现自己的很棒的圈子类。我采用的方法是将圆点迭代为距原点的距离,但是使用this very helpful website计算得出的“目标点给定距离和距起点的距离”,然后将其投影为多边形在传单中。结果就是这样:
这看起来很糟糕,但实际上更接近于准确!我们正在得到波浪效应,这是正确的。像我一样,您可能会问:“这里到底发生了什么?”所以我做了一个版本,让我可以突出显示每个迭代点:
您可以很清楚地看到它正确渲染了圆,但是多边形错误地连接了它。 应该要做的事情(可能是天真的想法)是将波形图包裹在墨卡托地图投影的多个实例周围,而不是天真的将它们连接在顶部,而是将它们球形地连接。像这样的Photoshop原始渲染一样:
然后,该多边形将以表明该多边形上方的所有内容也都包含在其中的方式“关闭”。
不过,我不知道如何在Leaflet中实现类似的功能。或其他任何事情。考虑到缩放状态,也许我必须自己以某种方式处理原始SVG?或者其他的东西?在进入那些危险的杂草之前,我想我会要求任何建议/想法/等。也许有一些更明显的方法。
哦,我尝试了另一件事:使用相同的d3.geo.circle构造函数,在Mercator / Leaflet投影的正投影中效果很好。它产生的效果与我的“天真” Leaflet大圆环实现大致相同:
我想这是有前途的。但是,如果您移动原点的经度,则D3.js版本会以一种非常奇怪的方式包装(红色的D3.js,绿松石的Leaflet类):
如果D3.js中有某种方法可以改变它的工作方式,我不会感到惊讶,但是我还没有完全了解D3.js的兔子漏洞。我希望D3.js可以使此操作更“容易”(因为它是比Leaflet更为完整的制图工具),所以我将继续研究这一点。
我还没有在Mapbox-gl中尝试这样做(我想这是“尝试”列表中的下一个)。
无论如何。谢谢阅读。要重申一下这个问题:如何使用除Google Maps API之外的其他 精确投影在Mercator中的环绕大圆半径?
答案 0 :(得分:4)
antimeridian cutting是GeoJSON,需要在传单或地图框中正确绘制。
对于d3而言,其简单的d3.geoCircle()
对于其他地图服务而言,由于无法处理时间轴切割,因此您可以使用d3来正确计算输入json。
主要思想是,将d3计算出的坐标解投影回lat-lng,方法是对它计算出的相同投影进行投影,不受保护的要素将被antimeridian分割为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>
以下函数输出带有由+ -180子午线分割的特征的geojson, 参数是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);
});
}
这两种方法都不是处理带孔的多边形,但是在其他情况下,点变换将相同
感谢您的阅读!
答案 1 :(得分:0)
因此,最终没有一个简单的解决方案。为了实现所需的类似Google Maps的行为,我最终不得不从头开始编写一个Leaflet插件,以扩展L.Polygon对象。这是因为所需的行为包括“包装”多边形,并且在Leaflet中没有“魔术”方法可以做到这一点。
我最终要做的是创建一个插件,该插件可以检测它是否应该(基于缩放级别)创建许多包装的“副本”,然后使用一点逻辑来确定是否应将多边形拼接在一起或不。并不是特别优雅(它比数学更具逻辑性),但这简直就是我的编程。
无论如何,here is the final plugin。可以像常规L.Circle对象一样将其放入(只需将其更改为L.greatCircle),而无需进行太多其他更改。您可以在我的MISSILEMAP上看到它的运行情况(它还具有我必须编写的测地线折线类,这要容易得多)。
感谢那些提供建议的人。