Обтекание больших кругов картами Меркатора с помощью D3.js, Leaflet или Mapbox - PullRequest
0 голосов
/ 27 октября 2018

Вопрос, вкратце: Как точно спроецировать большой радиус окружности в Mercator, используя что-то другое , чем Google Maps API?

Вопрос, в длинне:

Итак, у меня есть загадка. Я запускаю картографическое приложение, которое использует API Карт Google для проецирования огромных кругов на карту Меркатора - это попытка показать очень большие, точные радиусы, скажем, порядка 13 000 км. Но я больше не хочу использовать API Карт Google, потому что новая схема ценообразования Google безумна. Поэтому я пытаюсь преобразовать код в Leaflet, или Mapbox, или что-нибудь, не принадлежащее Google , и ничто не может правильно обработать эти круги.

Вот как Google Maps API обрабатывает геодезический круг с радиусом 13 000 км, центрированный к северу от Африки: Google Maps API great circle

Это выглядит интуитивно странно , но правильно . Волнообразный рисунок вызван окружением Земли.

D3.js может правильно отобразить это в ортографической проекции . Итак, вот тот же круг, созданный в D3.js с d3.geo.circle () на глобусе, в двух поворотах:

D3.js great circle on orthographic v1D3.js great circle on orthographic v2

Что делает 2D- "волнистый" рисунок более понятным, верно? Правильно. Я люблю это. Полностью работает для моих целей научного общения и все такое.

Но когда я конвертирую свой код в Leaflet, он совсем не работает. Зачем? Потому что класс кружка Листовки совсем не велик. Вместо этого кажется, что это просто эллипс, который немного искажен широтой, но не по-геодезически. Тот же круг, тот же радиус, та же точка начала, и мы получаем это:

Leaflet's attempt at a 13000 km circle

Так неправильно, так неправильно! Помимо того, что выглядеть совершенно нереально, это просто неправильно - Австралия будет не находиться внутри такого радиуса круга. Это важно для моего заявления! Это не может сделать.

Ладно, подумал я, может быть, хитрость заключается в том, чтобы просто попытаться реализовать свой собственный класс большого круга. Подход, который я выбрал, состоял в том, чтобы выполнить итерации по точкам окружности как расстояниям от исходной точки, но рассчитать расстояния, используя расчеты «Заданное расстояние с учетом расстояния и начальная точка» из этого очень полезного веб-сайта , а затем спроектировать их как многоугольник в Leaflet. Вот что я получаю в результате:

Attempted Great Circle implementation in Leaflet

Это выглядит плохо, но на самом деле гораздо ближе к точности! Мы получаем волновой эффект, и это правильно. Как и я, вы можете спросить: «Что здесь на самом деле происходит?» Поэтому я сделал версию, которая позволила мне выделить каждую итерацию:

Attempted Great Circle implementation in Leaflet with points

И вы можете ясно видеть, что круг правильно отрисован, но полигон неправильно соединяется с ним. То, что должно делать (можно наивно подумать), оборачивает эту волновую фигуру вокруг множества экземпляров проекции карты Меркатора, не наивно соединяя их сверху, а соединяя их сферически. Как этот грубый перевод Photoshop:

Photoshopped version of Leaflet

И тогда многоугольник «закроется» таким образом, чтобы это указывало на то, что все, что находится над многоугольником, также было заключено в нем.

Я понятия не имею, как реализовать что-то подобное в Leaflet. Или что-нибудь еще по этому вопросу. Может быть, мне нужно как-то обработать необработанный SVG самостоятельно, учитывая состояние масштабирования? Или что-то? Прежде чем я уйду в эти коварные сорняки, я подумал, что буду просить любые предложения / идеи / и т.д. Может быть, есть более очевидный подход.

О, и я попробовал еще одну вещь: использовать тот же конструктор d3.geo.circle, который так хорошо работал в ортографической проекции для проекции Меркатора / Листовки. Он дает более или менее те же результаты, что и моя «наивная» реализация Leaflet Great Circle:

D3.js Great Circle in Mercator

Что перспективно, я думаю.Однако, если вы переместите долготу исходной точки, версия D3.js будет выглядеть более странно (D3.js - красный, мой класс Leaflet - бирюзовый):

D3.js vs Leaflet at different Longitude

Я бы не удивился, если бы в D3.js был какой-то способ изменить, как это работает, но я не дошел до кроличьей норы D3.js.Я надеялся, что D3.js сделает это «проще» (поскольку он является более полноформатным картографическим инструментом, чем Leaflet), поэтому я буду продолжать в этом разбираться.

Я еще не пытался сделать это в Mapbox-gl (я думаю, это будет следующим в списке "попыток").

В любом случае.Спасибо за прочтение.Повторим вопрос: Как точно спроецировать большой радиус окружности в Меркаторе, используя что-то другое , чем Google Maps API?

Ответы [ 2 ]

0 голосов
/ 08 ноября 2018

Так что это оказалось не просто решением. Чтобы добиться желаемого поведения, подобного Google Maps, мне пришлось с нуля закодировать плагин Leaflet, расширяющий объект L.Polygon. Это потому, что желаемое поведение включает в себя «оборачивающий» многоугольник, и в Leaflet нет «волшебного» способа сделать это.

Я закончил тем, что создал плагин, который мог бы определять, следует ли (на основе уровня масштабирования) создавать многочисленные обернутые «копии», а затем использовать небольшую логику, чтобы определить, следует ли объединять многоугольники или не. Это не особенно элегантно (это скорее логика, чем математика), но это мое программирование в двух словах.

В любом случае, вот последний плагин . Его можно вставить как обычный объект L.Circle (просто измените его на L.greatCircle) без слишком большого количества других изменений. Вы можете увидеть это в действии на моем MISSILEMAP (который также содержит класс геодезической полилинии, который мне пришлось написать, что было намного проще).

Спасибо тем, кто давал советы и предложения.

0 голосов
/ 28 октября 2018

Это антимеридианская резка , это GeoJSON должен быть правильно нарисован в листовке или картографическом окне.

Для d3 это просто d3.geoCircle(), для других картографических сервисов,который не обрабатывает антимеридианскую резку, вы можете использовать d3 для правильного вычисления входного json.

Основная идея состоит в том, чтобы отменить проецирование координат, рассчитанных d3, обратно на широту, используя ту же проекцию для вычисленных незащищенных объектов, которые будут разделены наантимеридиан по d3.

См. projection.invert ()

Я разработал пример, запустите фрагмент кода и перетащите круги на график d3.

Вот результатснимок экрана:

enter image description here

<!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, аргументом является атрибут 'd' пути svg, рассчитанный по 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);
    });
}

Оба подхода не обрабатывают многоугольники с отверстиями, но преобразования точек будут такими же в других случаях

Спасибо за чтение!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...