7

简而言之,问题是:如何使用Google Maps API 以外的其他工具在墨卡托中准确地投影一个环绕的大圆半径?

问题很长:

所以我有一个难题。我运行了一个地图应用程序,它使用 Google Maps API 将巨大的圆圈投影到墨卡托地图上——它试图显示非常大、精确的半径,例如,大约 13,000 公里。但我不想再使用 Google Maps API,因为 Google 的新定价方案太疯狂了。因此,我试图将代码转换为 Leaflet、Mapbox 或任何非 Google 的东西,没有任何东西可以正确处理这些圆圈。

以下是 Google Maps API 如何处理以非洲北部为中心、半径为 13,000 公里的测地圆: Google Maps API 大圈

这在直觉上看起来很奇怪,但却是正确的。波浪形图案是由环绕地球的圆圈引起的。

D3.js 可以在正交投影中正确渲染它。所以这是在 D3.js 中使用 d3.geo.circle() 在地球上渲染的同一个圆圈,旋转两次:

正交 v1 上的 D3.js 大圆正交 v2 上的 D3.js 大圆

这使得 2D-“波浪”模式更有意义,对吧?对。我喜欢它。完全适用于我的科学传播等目的。

但是当我将代码转换为 Leaflet 时,它根本不起作用。为什么?因为 Leaflet 的 circle 类根本就不是一个大圆。相反,它似乎只是一个随着纬度有点扭曲的椭圆,但不是以真正的测地线方式。相同的圆,相同的半径,相同的原点,我们得到:

Leaflet 尝试绕 13000 公里

大错特错,大错特错!除了看起来完全不切实际之外,这是不正确的——澳大利亚不会在这样的圆形半径内。这对我的申请很重要!这是不行的。

好吧,我想,也许诀窍就是尝试实现我自己的大圆圈类。我采用的方法是将圆点作为距原点的距离进行迭代,但是使用这个非常有用的网站上的“目标点给定距离和距起点的方位”计算来计算距离,然后将它们投影为多边形传单。这是我得到的结果:

尝试在 Leaflet 中实现 Great Circle

这看起来很糟糕,但实际上更接近准确!我们得到了波浪效应,这是正确的。像我一样,你可能会问,“这里到底发生了什么?” 所以我做了一个版本,让我可以突出每个迭代点:

尝试在带有点的传单中实现大圆

你可以很清楚地看到它正确地渲染了圆,但是多边形错误地加入了它。它应该做的(人们可能会天真地认为)是将波浪形包裹在墨卡托地图投影的多个实例周围,而不是天真地将它们连接在顶部,而是将它们连接成球形。就像这个粗糙的 Photoshop 渲染一样:

传单的 Photoshop 版本

然后多边形将以某种方式“关闭”,表明多边形上方的所有内容也都包含在其中。

不过,我不知道如何在 Leaflet 中实现这样的东西。或其他任何事情。考虑到缩放状态,也许我必须自己处理原始 SVG?或者其他的东西?在我进入那些危险的杂草之前,我想我会征求任何建议/想法/等。也许有一些更明显的方法。

哦,我尝试了另一件事:使用相同的 d3.geo.circle 构造函数,该构造函数在墨卡托/传单投影的正交投影中效果很好。它产生的结果或多或少与我的“天真”传单大圆实现相同:

墨卡托的 D3.js 大圆

我猜这是有希望的。但是,如果您移动原点的经度,D3.js 版本会以一种更奇怪的方式包装(D3.js 为红色,我的 Leaflet 类为青绿色):

不同经度的 D3.js vs Leaflet

如果 D3.js 中有某种方法可以改变它的工作方式,我不会感到惊讶,但我还没有完全进入 D3.js 的兔子洞。我希望 D3.js 能让这个“更容易”(因为它是比 Leaflet 更完整的制图工具),所以我会继续研究这个。

我还没有尝试在 Mapbox-gl 中执行此操作(我想这是“尝试”列表中的下一个)。

反正。谢谢阅读。重申这个问题:如何使用Google Maps API 以外的其他工具在墨卡托中准确地投影一个环绕的大圆半径?

4

2 回答 2

5

需要在传单或地图框中正确绘制GeoJSON逆子午线切割

对于 d3 它很简单d3.geoCircle(),对于其他不处理反子午线切割的地图服务,您可以使用 d3 正确计算输入 json。

主要思想是将 d3 计算的坐标解投影回 lat-lng,在其计算的未受保护的特征上使用相同的投影,将由 d3 沿反子午线分割。

projection.invert()

我开发了示例,运行代码片段并在 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);
    });
}

两种方法都不是处理带孔的多边形,但在其他情况下点变换将是相同的

感谢您的阅读!

于 2018-10-28T12:20:02.200 回答
2

所以它最终不是一个简单的解决方案。为了实现所需的类似谷歌地图的行为,我最终不得不从头开始编写一个扩展 L.Polygon 对象的传单插件。这是因为所需的行为包括“包装”多边形,并且在 Leaflet 中没有“神奇”的方式来做到这一点。

我最终做的是创建一个插件,它可以检测它是否应该(基于缩放级别)创建大量包装的“副本”,然后使用一些逻辑来确定它是否应该将多边形拼接在一起。它不是特别优雅(它比数学更符合逻辑),但简而言之,这就是我的编程。

无论如何,这是最终的插件。它可以像常规的 L.Circle 对象一样放入(只需将其更改为 L.greatCircle),而无需进行太多其他更改。您可以在我的MISSILEMAP上看到它的实际效果(它还具有我必须编写的测地线折线类,这要容易得多)。

感谢那些给予意见和建议的人。

于 2018-11-08T01:06:21.003 回答