web-dev-qa-db-fra.com

Dessiner un cercle avec le rayon en miles / mètres avec Mapbox GL JS

Je suis en train de convertir une carte en utilisant mapbox.js en mapbox-gl.js , et j'ai du mal à dessiner un cercle qui utilise des milles ou des mètres pour son rayon au lieu de pixels. Ce cercle particulier est utilisé pour montrer la zone de distance dans n'importe quelle direction à partir d'un point central.

Auparavant, je pouvais utiliser les éléments suivants, qui ont ensuite été ajoutés à un groupe de calques:

// 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"
});

Le seul documentation que j'ai trouvé pour le faire dans Mapbox GL est le suivant:

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
    }
});

Mais cela rend le cercle en pixels, ce qui n'est pas mis à l'échelle avec le zoom. Existe-t-il actuellement un moyen avec Mapbox GL pour rendre une couche avec un cercle (ou plusieurs) qui est basé sur la distance et les échelles avec zoom?

J'utilise actuellement la v0.19.0 de Mapbox GL.

25
jrrdnx

En élaborant sur réponse de Lucas , j'ai trouvé un moyen d'estimer les paramètres afin de dessiner un cercle basé sur une certaine taille métrique.

La carte prend en charge des niveaux de zoom compris entre 0 et 20. Disons que nous définissons le rayon comme suit:

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

La carte va rendre le cercle à tous les niveaux de zoom puisque nous avons défini une valeur pour le plus petit niveau de zoom (0) et le plus grand (20). Pour tous les niveaux de zoom intermédiaires, il en résulte un rayon de (approximativement) RADIUS/2^(20-zoom). Ainsi, si nous définissons RADIUS à la taille de pixel correcte qui correspond à notre valeur métrique, nous obtenons le rayon correct pour tous les niveaux de zoom.

Nous recherchons donc essentiellement un facteur de conversion qui transforme les mètres en pixels au niveau de zoom 20. Bien sûr, ce facteur dépend de la latitude. Si nous mesurons la longueur d'une ligne horizontale à l'équateur au niveau de zoom maximal 20 et divisons par le nombre de pixels que cette ligne couvre, nous obtenons un facteur ~ 0,075 m/px (mètres par pixel). En appliquant le facteur d'échelle de latitude mercator de 1 / cos(phi), nous obtenons le bon rapport mètre/pixel pour n'importe quelle latitude:

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

Ainsi, en définissant RADIUS sur metersToPixelsAtMaxZoom(radiusInMeters, latitude), nous obtenons un cercle avec la taille correcte:

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

J'ai résolu ce problème pour mes cas d'utilisation en utilisant un polygone GeoJSON. Ce n'est pas strictement un cercle, mais en augmentant le nombre de côtés du polygone, vous pouvez vous rapprocher assez.

L'avantage supplémentaire de cette méthode est qu'elle changera automatiquement son pas, sa taille, son relèvement, etc. avec la carte automatiquement.

Voici la fonction pour générer le polygone 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]
                }
            }]
        }
    };
};

Vous pouvez l'utiliser comme ceci:

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
    }
});

Si vous devez mettre à jour le cercle que vous avez créé plus tard, vous pouvez le faire comme ceci (notez la nécessité de saisir la propriété data pour passer à setData):

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

Et la sortie ressemble à ceci:

Example Image

60
Brad Dwyer

Cette fonctionnalité n'est pas intégrée à GL JS mais vous pouvez l'imiter en utilisant fonctions .

<!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>

Mises en garde importantes:

  • La détermination des paramètres de fonction pour une mesure réelle particulière n'est pas simple. Ils changent avec la longitude/latitude de l'entité.
  • Les cercles de plus de 1024 pixels ne seront pas rendus correctement en raison de la nature des données en mosaïque et de la façon dont nous emballons les données pour WebGL
3