Рисование круга с радиусом в милях/метрах с помощью Mapbox GL JS

Я собираюсь преобразовать карту с помощью mapbox.js в mapbox-gl.js, и у меня возникают проблемы с созданием круга, который использует мили или метров радиуса, а не пикселей. Этот конкретный круг используется для отображения области для расстояния в любом направлении от центральной точки.

Раньше я мог использовать следующее, которое затем было добавлено в группу слоев:

// 500 miles = 804672 meters
L.circle(L.latLng(41.0804, -85.1392), 804672, {
    stroke: false,
    fill: true,
    fillOpacity: 0.6,
    fillColor: "#5b94c6",
    className: "circle_500"
});

Единственная документация, которую я нашел для этого в Mapbox GL, выглядит следующим образом:

map.addSource("source_circle_500", {
    "type": "geojson",
    "data": {
        "type": "FeatureCollection",
        "features": [{
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [-85.1392, 41.0804]
            }
        }]
    }
});

map.addLayer({
    "id": "circle500",
    "type": "circle",
    "source": "source_circle_500",
    "layout": {
        "visibility": "none"
    },
    "paint": {
        "circle-radius": 804672,
        "circle-color": "#5b94c6",
        "circle-opacity": 0.6
    }
});

Но это отображает круг в пикселях, который не масштабируется с увеличением. Есть ли способ Mapbox GL для отображения слоя с кругом (или несколькими), который основан на расстоянии и масштабах с увеличением?

В настоящее время я использую v0.19.0 из Mapbox GL.

Ответ 1

Разрабатывая ответ Лукаса, я придумал способ оценки параметров, чтобы нарисовать круг, основанный на определенном размере метрики.

Карта поддерживает уровни масштабирования между 0 и 20. Пусть мы определяем радиус следующим образом:

"circle-radius": {
  stops: [
    [0, 0],
    [20, RADIUS]
  ],
  base: 2
}

Карта будет отображать круг на всех уровнях масштабирования, так как мы определили значение для наименьшего уровня масштабирования (0) и самого большого (20). Для всех уровней масштабирования между ними получается радиус (приблизительно) RADIUS/2^(20-zoom). Таким образом, если мы установим RADIUS в правильный размер пикселя, который соответствует нашему метрическому значению, мы получим правильный радиус для всех уровней масштабирования.

Таким образом, мы в основном после коэффициента преобразования, который преобразует метры в пиксель на уровне масштабирования 20. Конечно, этот фактор зависит от широты. Если мы измеряем длину горизонтальной линии на экваторе при максимальном уровне масштабирования 20 и делим на количество пикселей, которое занимает эта линия, мы получаем коэффициент ~ 0,075 м/пикселей (метры на пиксель). Применяя масштабный коэффициент масштабирования меркатора 1 / cos(phi), мы получаем правильное отношение метра к пикселю для любой широты:

const metersToPixelsAtMaxZoom = (meters, latitude) =>
  meters / 0.075 / Math.cos(latitude * Math.PI / 180)

Таким образом, установка RADIUS в metersToPixelsAtMaxZoom(radiusInMeters, latitude) дает нам круг с правильным размером:

"circle-radius": {
  stops: [
    [0, 0],
    [20, metersToPixelsAtMaxZoom(radiusInMeters, latitude)]
  ],
  base: 2
}

Ответ 2

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

Дополнительным преимуществом этого метода является то, что он автоматически изменит свой шаг, размер, подшипник и т.д. с помощью карты автоматически.

Вот функция генерации многоугольника GeoJSON

var createGeoJSONCircle = function(center, radiusInKm, points) {
    if(!points) points = 64;

    var coords = {
        latitude: center[1],
        longitude: center[0]
    };

    var km = radiusInKm;

    var ret = [];
    var distanceX = km/(111.320*Math.cos(coords.latitude*Math.PI/180));
    var distanceY = km/110.574;

    var theta, x, y;
    for(var i=0; i<points; i++) {
        theta = (i/points)*(2*Math.PI);
        x = distanceX*Math.cos(theta);
        y = distanceY*Math.sin(theta);

        ret.push([coords.longitude+x, coords.latitude+y]);
    }
    ret.push(ret[0]);

    return {
        "type": "geojson",
        "data": {
            "type": "FeatureCollection",
            "features": [{
                "type": "Feature",
                "geometry": {
                    "type": "Polygon",
                    "coordinates": [ret]
                }
            }]
        }
    };
};

Вы можете использовать его следующим образом:

map.addSource("polygon", createGeoJSONCircle([-93.6248586, 41.58527859], 0.5));

map.addLayer({
    "id": "polygon",
    "type": "fill",
    "source": "polygon",
    "layout": {},
    "paint": {
        "fill-color": "blue",
        "fill-opacity": 0.6
    }
});

Если вам нужно обновить созданный вами круг, вы можете сделать это так (обратите внимание на необходимость захватить свойство data для передачи в setData):

map.getSource('polygon').setData(createGeoJSONCircle([-93.6248586, 41.58527859], 1).data);

И результат выглядит следующим образом:

Пример изображения

Ответ 3

Эта функция не встроена в GL JS, но вы можете эмулировать ее с помощью функций.

<!DOCTYPE html>
<html>

<head>
  <meta charset='utf-8' />
  <title></title>
  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
  <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.19.0/mapbox-gl.js'></script>
  <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.19.0/mapbox-gl.css' rel='stylesheet' />
  <style>
    body {
      margin: 0;
      padding: 0;
    }
    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }
  </style>
</head>

<body>

  <div id='map'></div>
  <script>
    mapboxgl.accessToken = 'pk.eyJ1IjoibHVjYXN3b2oiLCJhIjoiNWtUX3JhdyJ9.WtCTtw6n20XV2DwwJHkGqQ';
    var map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/streets-v8',
      center: [-74.50, 40],
      zoom: 9,
      minZoom: 5,
      maxZoom: 15
    });

    map.on('load', function() {
      map.addSource("source_circle_500", {
        "type": "geojson",
        "data": {
          "type": "FeatureCollection",
          "features": [{
            "type": "Feature",
            "geometry": {
              "type": "Point",
              "coordinates": [-74.50, 40]
            }
          }]
        }
      });

      map.addLayer({
        "id": "circle500",
        "type": "circle",
        "source": "source_circle_500",
        "paint": {
          "circle-radius": {
            stops: [
              [5, 1],
              [15, 1024]
            ],
            base: 2
          },
          "circle-color": "red",
          "circle-opacity": 0.6
        }
      });
    });
  </script>

</body>

</html>