MapBox - 聚类缩放

MapBox - Cluster Zooming

是否可以像 Craigslist Mapview 对 MapBox 那样重新创建单击以放大聚类?单击时,它会缩放到包含这些位置点的区域。

这是我的聚类代码:

map.on('load', function() {
map.addSource("location", {
    type: "geojson",
    data: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/73873/test.geojson",
    cluster: true,
    clusterMaxZoom: 14,
    clusterRadius: 100
});

    map.addLayer({
    id: "clusters",
    type: "circle",
    source: "location",
    filter: ["has", "point_count"],
    paint: {
        "circle-color": {
            property: "point_count",
            type: "interval",
            stops: [
                [0, "#71AAC6"],
                [100, "#71AAC6"],
                [750, "#71AAC6"],
            ]
        },
        "circle-radius": {
            property: "point_count",
            type: "interval",
            stops: [
                [0, 20],
                [100, 30],
                [750, 40]
            ]
        }
    }
});

这是我的演示

Codepen Demo

我不认为你可以在纯 Mapbox 中做到这一点,至少不容易。

但是对于那些特定于集群的用法,Supercluster(一个官方的 Mapbox 地理空间点集群库)正是您想要的:

getClusterExpansionZoom(clusterId)

Returns the zoom on which the cluster expands into several children (useful for "click to zoom" feature) given the cluster's cluster_id.

编辑:实际上你可以在没有 Supercluster 的情况下做到这一点:

  1. 使用此 JsFiddle example 您可以检索您单击的群集的点。

    map.on('click', function(e) {
        const cluster = map.queryRenderedFeatures(e.point, { layers: ["cluster"] });
    
        if (cluster[0]) {
        // features: from the added source that are clustered
        const pointsInCluster = features.filter(f => {
            const pointPixels = map.project(f.geometry.coordinates)
          const pixelDistance = Math.sqrt(
            Math.pow(e.point.x - pointPixels.x, 2) + 
            Math.pow(e.point.y - pointPixels.y, 2) 
          );
          return Math.abs(pixelDistance) <= clusterRadius;
        });
        console.log(cluster, pointsInCluster);
      }
    });
    
  2. 然后你可以用所有这些点创建一个 mapboxgl.LngLatBoundsextend 它。

  3. 您将获得一个包含所有点的 LngLatBounds,因此您只需调用 fitBounds 就可以了。

正如@MeltedPenguin 所说。不用 SuperCluster 也可以做到。我搜索了几个答案,最后使用 coffeescript 完成了我自己的解决方案(您可以使用 http://js2.coffee/ 等工具将其转换回 JS):

    @clusterRadius = 30
    @map.on 'click', (e) =>
          features = @map.queryRenderedFeatures(e.point, { layers: ['markers_layer'] });
          if features && features.length > 0
            if features[0].properties.cluster
              cluster = features[0].properties

              allMarkers = @map.queryRenderedFeatures(layers:['markers_layer_dot']);
              self = @ #just a way to use 'this' un a function, its more verbose then =>    

              #Get all Points of a Specific Cluster
              pointsInCluster = allMarkers.filter((mk) ->
                pointPixels = self.map.project(mk.geometry.coordinates) #get the point pixel
                #Get the distance between the Click Point and the Point we are evaluating from the Matrix of All point
                pixelDistance = Math.sqrt((e.point.x - (pointPixels.x)) ** 2 + (e.point.y - (pointPixels.y)) ** 2)

                #If the distant is greater then the disance that define a cluster,  then the point si in the cluster
                # add it to the boundaries
                Math.abs(pixelDistance) <= self.clusterRadius
              )

              #build the bounding box with the selected points coordinates
              bounds = new (mapboxgl.LngLatBounds)
              pointsInCluster.forEach (feature) ->
                bounds.extend feature.geometry.coordinates
                return

              #Move the map to fit the Bounding Box (BBox)
              @map.fitBounds bounds, {padding:45, maxZoom: 16}

            else
              window.open('/en/ad/' + features[0].properties.propertyId)

在我的页面上,我有 2 个图层基于相同的数据源但具有不同的属性。一个定义所有点(没有簇),另一个定义点和簇。 对于我的显示,我使用带簇的 "markers_layer",为了计算距离和其他东西,我使用另一个作为点的 DB。

来源:

  @map.addSource "markers_source_wo_cluster",
    type: "geojson"
    data:
      type: "FeatureCollection"
      features: []
    cluster: false
    clusterMaxZoom: 10
    clusterRadius: @clusterRadius

  @map.addSource "markers_source",
    type: "geojson"
    data:
      type: "FeatureCollection"
      features: []
    cluster: true
    clusterMaxZoom: 60
    clusterRadius: @clusterRadius

图层:

##============================================================##
## Add marker layer (Layer for QueryRender all dot without cluster)
##============================================================##
@map.addLayer
  id: 'markers_layer_dot'
  source: 'markers_source_wo_cluster'
  type: "circle"
  paint:
    "circle-radius": 0 #This are 1 pixel dot for ref only

##============================================================##
## Add marker layer
##============================================================##
@map.addLayer
  id: 'markers_layer'
  source: 'markers_source'
  type: 'symbol'
  layout:
    'icon-allow-overlap': true
    'icon-image':'pin_map'
    'icon-size':
      stops: [[0,0.4], [40,0.4]]

我是这样解决的https://github.com/mapbox/mapbox-gl-js/issues/9707

const handleClusterClick = React.useCallback(
    (event: LayerMouseEvent, sourceId: string) => {
      if (event && mapRef.current) {
        const properties = getFeaturePropertiesFromMapEvent(event);
        const coordinates = event.lngLat.toArray() as [number, number];

        // skip cluster logic, clicked no point
        if (properties?.type && coordinates) {
          handlePointClick(event);
          return;
        }

        const feature = event?.features?.[0];
        const source = mapRef.current.getSource(sourceId);

        if (
          feature &&
          feature.properties !== null &&
          // @ts-expect-error typings
          typeof source.getClusterExpansionZoom === "function"
        ) {
          source
            // @ts-expect-error typings
            .getClusterExpansionZoom(
              feature.properties.cluster_id,
              (err: Error, expansionZoom: number) => {
                if (!err && mapRef.current) {
                  mapRef.current.flyTo({
                    // @ts-expect-error typings
                    center: feature.geometry.coordinates,
                    zoom: expansionZoom,
                    maxDuration: MAX_FLY_TO_DURATION_MS,
                  });
                }
              },
            );
        } else {
          mapRef.current.flyTo({
            maxDuration: MAX_FLY_TO_DURATION_MS,
            center: event.lngLat,
            zoom: mapRef.current.getZoom() + 3, // This is bad UX, requires multiple clicks
          });
        }
      }
    },
    [handlePointClick],
  );