使用墨西哥地图与D3.js,Leaflet或Mapbox包装大圆圈

nuc*_*eon 7 great-circle map-projections mercator d3.js leaflet

问题,简而言之:如何使用Google Maps API之外的其他东西在Mercator中准确地投影包裹大圆半径?

问题,长期:

所以我有一个难题.我运行了一个地图应用程序,它使用谷歌地图API将巨大的圆圈投射到墨卡托地图上 - 它试图显示非常大的,准确的半径,比如大约13,000公里.但我不想再使用谷歌地图API,因为谷歌的新定价方案是疯了.所以我试图将代码转换为Leaflet,Mapbox或任何非谷歌,并且没有任何东西可以正确处理这些圈子.

以下是Google Maps API处理非洲北部中心13,000公里半径的测地圆的方法: Google Maps API很棒

这看起来很直观,但是是正确的.波浪形图案是由环绕地球的圆圈引起的.

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

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

这使2D-"波浪"图案更有意义,对吧?对.我喜欢它.完全符合我的科学传播目的和所有这些.

但是当我将我的代码转换为Leaflet时,它根本不起作用.为什么?因为Leaflet的圆圈类不是一个很棒的圆圈.相反,它似乎只是一个椭圆,它与纬度有点扭曲,但不是真正的测地线.相同的圆,相同的半径,相同的原点,我们得到这个:

Leaflet试图在13000公里的圈子

太错了,错了!除了看起来完全不现实之外,它只是不正确 - 澳大利亚不会在这样的圆形范围内.这对我的申请很重要!这不可能.

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

在Leaflet中尝试Great Circle实现

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

使用积分在Leaflet中尝试Great Circle实现

你可以非常清楚地看到它正确地渲染了圆圈,但是多边形错误地连接了它.它该做什么(人们可能天真地想到)是围绕墨卡托地图投影的多个实例包裹该波形图,而不是天真地将它们连接在顶部,而是将它们球形地连接起来.像这样粗糙的Photoshop渲染:

Leaflet的Photoshop版本

然后多边形将以一种方式"关闭",表明多边形上方的所有东西都包含在其中.

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

哦,我尝试了另外一件事:使用相同的d3.geo.circle构造函数,该构造函数在墨卡托/ Leaflet投影的正交投影中运行良好.它产生或多或少与我的"天真"Leaflet大圆实现相同的结果:

墨卡托的D3.js大圆

我想这是有希望的.但是,如果移动原点的经度,则D3.js版本以更奇怪的方式包装(D3.js为红色,我的Leaflet类为绿松石):

D3.js vs Leaflet在不同的经度

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

我还没有尝试在Mapbox-gl中这样做(我想这是"尝试"列表中的下一个).

无论如何.谢谢阅读.重申一个问题:如何使用Google Maps API之外的其他内容准确地在Mercator中投影包裹大圆半径?

Str*_*e Q 5

这是子午线切割这是GeoJSON的需要在单张或mapbox绘制正确。

For d3 its simple d3.geoCircle(), for other mapping services, that does not handle antimeridian cutting you can use d3 to properly calculate input json.

The main idea is to unproject coordinates calculated by d3 back to lat-lng using same projection on its calculated, unprotected features will be splitted by antimeridian by d3.

See projection.invert()

I've developed example, run code snippet and drag the circles on d3 plot.

Here is result screenshot:

在此处输入图片说明

<!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>
Run Code Online (Sandbox Code Playgroud)

following function outputs geojson with features splitted by +-180 meridian, argument is svg path's 'd' attribute, calculated by 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]
    }
}
Run Code Online (Sandbox Code Playgroud)

Also this effect can be achieved with d3-geo-projection extesion with following code:

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);
    });
}
Run Code Online (Sandbox Code Playgroud)

Both approaches is not handle polygons with holes, but point transformations will be same in other cases

感谢您的阅读!


nuc*_*eon 3

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

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

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

感谢那些提出意见和建议的人。