mapbox / supercluster

A very fast geospatial point clustering library for browsers and Node.
ISC License
2.11k stars 299 forks source link

Clusters disappear under a certain zoom level #204

Closed gwendall closed 1 year ago

gwendall commented 2 years ago

ezgif com-gif-maker

Any idea why?

mourner commented 2 years ago

Can you provide a minimal reproducible live test case? It's impossible to tell otherwise.

Spaeda commented 2 years ago

Same issue for me. When zoom level is under 4.5 all clusters and markers disappear.

You can fin code used to define cluster:

interface IClusterProps extends React.PropsWithChildren<any> {
  minZoom?: number;
  maxZoom?: number;
  radius?: number;
  extent?: number;
  nodeSize?: number;
}

interface IClusterComponentProps extends IClusterProps {
  map: MB.MapRef;
}

interface IClusterComponentState {
  clusters: (supercluster.ClusterFeature<supercluster.AnyProps> | supercluster.PointFeature<supercluster.AnyProps>)[];
}

const childrenKeys = (children: any) => React.Children.toArray(children).map((child: any) => child.key);

const shallowCompareChildren = (prevChildren: any, newChildren: any) => {
  if (React.Children.count(prevChildren) !== React.Children.count(newChildren)) {
    return false;
  }

  const prevKeys = childrenKeys(prevChildren);
  const newKeys = new Set(childrenKeys(newChildren));
  return prevKeys.length === newKeys.size && prevKeys.every((key) => newKeys.has(key));
};

class ClusterComponent extends React.Component<IClusterComponentProps, IClusterComponentState, {}> {
  private cluster?: supercluster;
  private readonly defaultOptions: IClusterProps = {
    minZoom: 0,
    maxZoom: 16,
    minPoints: 2,
    radius: 40,
    extent: 512,
    nodeSize: 64,
  };

  constructor(props: any) {
    super(props);
    this.state = { clusters: [] };
  }

  componentDidMount() {
    this.createCluster(this.props);
    this.recalculate();

    this.props.map.on('moveend', this.recalculate);
  }

  componentDidUpdate(prevProps: Readonly<IClusterComponentProps>, prevState: Readonly<IClusterComponentState>, snapshot?: {} | undefined): void {
    const shouldUpdate =
      prevProps.minZoom !== this.props.minZoom ||
      prevProps.maxZoom !== this.props.maxZoom ||
      prevProps.radius !== this.props.radius ||
      prevProps.extent !== this.props.extent ||
      prevProps.nodeSize !== this.props.nodeSize ||
      !shallowCompareChildren(prevProps.children, this.props.children);

    if (shouldUpdate) {
      this.createCluster(this.props);
      this.recalculate();
    }
  }

  private createCluster = (props: IClusterProps) => {
    const options = { ...this.defaultOptions, ...props };
    const cluster = new supercluster(options);

    const points = React.Children.map(props.children, (child) => {
      if (child) {
        return point([child.props.longitude, child.props.latitude], child);
      }
      return null;
    });

    cluster.load(points ?? []);
    this.cluster = cluster;
    if (props.innerRef) {
      props.innerRef(this.cluster);
    }
  };

  private recalculate = () => {
    if (!this.cluster) {
      return;
    }
    const zoom = this.props.map.getZoom();
    const bounds = this.props.map.getBounds().toArray();
    const bbox: GeoJSON.BBox = [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]];

    const clusters = this.cluster.getClusters(bbox, Math.floor(zoom));
    this.setState({ clusters: clusters });
  };

  render() {
    const clusters = this.state.clusters.map((cluster) => {
      if (cluster.properties.cluster) {
        const [longitude, latitude] = cluster.geometry.coordinates;
        return (
          <Marker key={`cluster-${cluster.properties.cluster_id}`} latitude={latitude} longitude={longitude} style={{ zIndex: 1 }}>
            <div
              className="marker-cluster"
              onClick={(e) => {
                if (this.cluster && cluster.id) {
                  const expansionZoom = Math.min(this.cluster.getClusterExpansionZoom(cluster.id as number), 20);
                  this.props.map.setZoom(expansionZoom);
                  this.props.map.panTo({ lat: latitude, lng: longitude });
                }
              }}
            >
              {cluster.properties.point_count <= 99 ? cluster.properties.point_count : '+99'}
            </div>
          </Marker>
        );
      }
      const { type, key, props } = cluster.properties;
      return React.createElement(type, { key, ...props });
    });

    return clusters;
  }
}

export default function Cluster(props: IClusterProps) {
  const map = useMap();
  return map.current ? <ClusterComponent map={map.current} {...props}></ClusterComponent> : null;
}
export default class Map extends React.Component<IMapProps, IMapState, any> {
render() {
    return (
      <MB.Map
        ref={(o) => (this.mapRef = o)}
        initialViewState={{
          longitude: this.state.currentPosition.longitude,
          latitude: this.state.currentPosition.latitude,
          zoom: this.state.currentZoom,
        }}
        mapStyle={this.props.settings.style}
        mapboxAccessToken={this.props.settings.accessToken}
        projection={this.props.settings.projection}
        onMoveEnd={(e: MB.ViewStateChangeEvent) => this.setCurrentMapState({ latitude: e.viewState.latitude, longitude: e.viewState.longitude }, e.viewState.zoom)}
      >
        <Cluster maxZoom={this.props.settings.maxZoom - 1}>
          {this.state.pushPins.features.map((f) => (
            <MB.Marker key={f.id} latitude={f.properties.latitude} longitude={f.properties.longitude} onClick={() => this.onMarkerClick(f.properties)}>
              <div className="wander-icon">
                <div className="marker-pin">
                  <img src="/assets/AnchorIcon.svg" alt='anchor icon' />
                </div>
              </div>
            </MB.Marker>
          ))}
        </Cluster>
      </MB.Map>
    );
  }
}
Spaeda commented 2 years ago

I created a project hosted in codesandbox. So you can see the bug and all code at https://codesandbox.io/s/aged-waterfall-fqe64k

After some tests, this issue is only present with Globe projection. If I use Mercator all is fine. I hope it will help you to identify this issue.

AramRafeq commented 2 years ago

Hi, I am facing a similar issue this may help, we have placed the map in a resizable container when we drag and resize the container this happens although not always

SnailBones commented 2 years ago

Thanks for the reproduction @Spaeda.

I'm unable to reproduce this in vanilla GL JS, so I can't tell if this is coming from GL JS, your code, or possibly from react-map-gl. Are you able to reproduce this without React?

Spaeda commented 2 years ago

Thanks for reply @SnailBones

I test with vanilla JS and I have same issue (https://jsfiddle.net/Spaeda/snrkLz5d/4/). Like I said previously, it happens only with globe projection.

In this example, you can find this warning:

Globe projection does not support getBounds API, this API may behave unexpectedly

Could this be source of our issue? According to Mapbox GL JS documentation, it exists some limitations with Globe projection. So issue seem to come from the impossibility to get clusters with a BBox in Globe projection with a lower zoom level. How can we get clusters for lower zoom?

SnailBones commented 2 years ago

Thanks @Spaeda!

Globe projection does not support getBounds API, this API may behave unexpectedly

Could this be source of our issue?

Yes, this is likely the cause of this issue. We're working on a fix for getBounds with globe and it will be included in the next GL JS release.

In the meantime, here's a workaround: at low zooms, set the bound to the entire world, like so:

    const bbox = [-180, -90, 180, 80];
    const zoom = map.getZoom();
    const clusters = index.getClusters(bbox, Math.floor(zoom));
Spaeda commented 2 years ago

Thx for workaround. I will wait new release of Mapbox GL JS

oviedo97fer commented 1 year ago

Same problem here! Im using google-maps-react-markers with use-supercluster hook. Any approach?

SnailBones commented 1 year ago

@oviedo97fer What version of mapbox-gl-js are you using? This should be fixed as of v2.11.0.

mourner commented 1 year ago

Likely fixed on the GL JS side so closing.