supercluster icon indicating copy to clipboard operation
supercluster copied to clipboard

Duplicate coordinates throwing `No cluster with the specified ID`

Open ollieh-m opened this issue 5 years ago • 16 comments

We are using Supercluster wrapped in https://github.com/novalabio/react-native-maps-super-cluster

On loading our map, we've experienced the No cluster with the specified ID error when clicking on a cluster. This was happening within the following call to getLeaves(), where clusteringEngine is Supercluster.

clusteringEngine
    .getLeaves(clusterId, 100)
    .map(c => c.properties.item);

It turns out the error happened when a cluster had two points that shared exactly the same coordinates, leading to the below stack trace, where I log out from the getChildren() function. As you can see, it thinks the child of the cluster is also a cluster at the same coordinates, and the child of that cluster is also apparently a cluster at the same coordinates, but that 'cluster' doesn't have any children IDs so the error is thrown.

I'm wondering:

  1. whether Supercluster is already meant to handle multiple points at the same coordinates, in which case is this a bug?; and
  2. whether in the event that getChildren cannot find the children for a cluster, an empty array should be returned rather than an error thrown. Or perhaps _appendLeaves should set children to an empty array if the error gets thrown, like this:
_appendLeaves: function (result, clusterId, limit, offset, skipped) {
        try {
          var children = this.getChildren(clusterId);
        } catch(error){
          if (error.message.includes('No cluster with the specified id')) {
            children = []
          } else {
            throw error
         }
        }
     ...

Stack trace:

2018-10-19 21:27:40.887 [info][tid:com.facebook.react.JavaScript] ------------------------ get children
2018-10-19 21:27:40.887170+0100 Refill[68457:2682417] ------------------------ get children
2018-10-19 21:27:40.887 [info][tid:com.facebook.react.JavaScript] 'clusterId', 368017
2018-10-19 21:27:40.887494+0100 Refill[68457:2682417] 'clusterId', 368017
2018-10-19 21:27:40.888 [info][tid:com.facebook.react.JavaScript] '------------------------ ids', [ 11500 ]
2018-10-19 21:27:40.887822+0100 Refill[68457:2682417] '------------------------ ids', [ 11500 ]
2018-10-19 21:27:40.888 [info][tid:com.facebook.react.JavaScript] 'c.parentId', 368017
2018-10-19 21:27:40.888069+0100 Refill[68457:2682417] 'c.parentId', 368017
2018-10-19 21:27:40.888 [info][tid:com.facebook.react.JavaScript] push into children array
2018-10-19 21:27:40.888282+0100 Refill[68457:2682417] push into children array
2018-10-19 21:27:40.889 [info][tid:com.facebook.react.JavaScript] 'children', [ { type: 'Feature',
    properties: 
     { cluster: true,
       cluster_id: 385970,
       point_count: 2,
       point_count_abbreviated: 2 },
    geometry: 
     { type: 'Point',
       coordinates: [ -5.538370000000008, 50.11913999999999 ] } } ]
2018-10-19 21:27:40.888730+0100 Refill[68457:2682417] 'children', [ { type: 'Feature',
    properties: 
     { cluster: true,
       cluster_id: 385970,
       point_count: 2,
       point_count_abbreviated: 2 },
    geometry: 
     { type: 'Point',
       coordinates: [ -5.538370000000008, 50.11913999999999 ] } } ]
2018-10-19 21:27:40.889 [info][tid:com.facebook.react.JavaScript] ------------------------ get children
2018-10-19 21:27:40.888954+0100 Refill[68457:2682417] ------------------------ get children
2018-10-19 21:27:40.889 [info][tid:com.facebook.react.JavaScript] 'clusterId', 385970
2018-10-19 21:27:40.889217+0100 Refill[68457:2682417] 'clusterId', 385970
2018-10-19 21:27:40.889 [info][tid:com.facebook.react.JavaScript] '------------------------ ids', [ 12061 ]
2018-10-19 21:27:40.889467+0100 Refill[68457:2682417] '------------------------ ids', [ 12061 ]
2018-10-19 21:27:40.890 [info][tid:com.facebook.react.JavaScript] 'c.parentId', 385970
2018-10-19 21:27:40.889677+0100 Refill[68457:2682417] 'c.parentId', 385970
2018-10-19 21:27:40.890 [info][tid:com.facebook.react.JavaScript] push into children array
2018-10-19 21:27:40.889931+0100 Refill[68457:2682417] push into children array
2018-10-19 21:27:40.890 [info][tid:com.facebook.react.JavaScript] 'children', [ { type: 'Feature',
    properties: 
     { cluster: true,
       cluster_id: 388691,
       point_count: 2,
       point_count_abbreviated: 2 },
    geometry: 
     { type: 'Point',
       coordinates: [ -5.538370000000008, 50.11913999999999 ] } } ]
2018-10-19 21:27:40.890418+0100 Refill[68457:2682417] 'children', [ { type: 'Feature',
    properties: 
     { cluster: true,
       cluster_id: 388691,
       point_count: 2,
       point_count_abbreviated: 2 },
    geometry: 
     { type: 'Point',
       coordinates: [ -5.538370000000008, 50.11913999999999 ] } } ]
2018-10-19 21:27:40.891 [info][tid:com.facebook.react.JavaScript] ------------------------ get children
2018-10-19 21:27:40.890627+0100 Refill[68457:2682417] ------------------------ get children
2018-10-19 21:27:40.891 [info][tid:com.facebook.react.JavaScript] 'clusterId', 388691
2018-10-19 21:27:40.890848+0100 Refill[68457:2682417] 'clusterId', 388691
2018-10-19 21:27:40.891 [info][tid:com.facebook.react.JavaScript] '------------------------ ids', []
2018-10-19 21:27:40.891095+0100 Refill[68457:2682417] '------------------------ ids', []
2018-10-19 21:27:40.891 [info][tid:com.facebook.react.JavaScript] 'children', []
2018-10-19 21:27:40.891329+0100 Refill[68457:2682417] 'children', []
2018-10-19 21:27:42.898 [warn][tid:com.facebook.react.JavaScript] Possible Unhandled Promise Rejection (id: 0):
Error: No cluster with the specified id.

ollieh-m avatar Oct 22 '18 12:10 ollieh-m

@ollieh-m thanks for the report! In theory, duplicate points shouldn't be a problem for the library. Would it be possible to set up a minimal reproducible test case so that I could investigate what's going on under the hood?

mourner avatar Oct 22 '18 13:10 mourner

Closing because of not being able to reproduce this. Will reopen if there's a minimal reproducible use case.

mourner avatar Nov 01 '18 08:11 mourner

I’m having the same problem and is clearly because of points with exactly the same coordinates inside the cluster. Initially I set the clusterMaxZoom value to 99 because I wanted those clusters to return a zoom value of 100 in getClusterExpansionZoom(), but that doesn’t seem to work.

Playing with clusterMaxZoom values it looks that there is a hard limit for the biggest zoom level returned to be 31 (probably as a safety check for infinite recursion, or maybe because a zoom level bigger than 31 can’t be specified nowhere in Mapbox?), so clusterMaxZoom has to have a maximum value of 30 to avoid the error. This way single-coordinate clusters can be recognised because they return a zoom value of 31.

For the record, but it would be good to fix the getClusterExpansionZoom() function to return a more descriptive error or else document this behavior.

(By the way, amazed that nobody else complained in more than two years!)

djibarian avatar May 06 '21 08:05 djibarian

@djibarian thanks for bringing this up! Would you be able to set up a minimal test case for this so that I could promptly provide a fix?

mourner avatar May 06 '21 09:05 mourner

We had the same problem (with a lot of duplicate coordinate), we set the cluster maxZoom to 30 and everything works.

ozaik avatar May 06 '21 09:05 ozaik

@mourner How do I do that? Do you have a sandbox or playground I can set up?

djibarian avatar May 06 '21 09:05 djibarian

@djibarian you could just use https://jsfiddle.net/ or https://jsbin.com/

mourner avatar May 06 '21 10:05 mourner

To reproduce the error :

  • Just click on the cluster with 2 points on the same place, this throws an error.

To fix it adjust the maxZoom to a value under 31

The code I used


import React, { useRef, useState } from 'react'
import GoogleMapReact from 'google-map-react'
import useSupercluster from 'use-supercluster'

function MainMap (props) {
    const mapRef = useRef(null)
    const [bounds, setBounds] = useState(null)
    const [zoom, setZoom] = useState(10)
    const Marker = ({ children }) => children

    const points = [
        {
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [
                    6.59874,
                    46.55043
                ]
            },
            properties: {
                nickname: 'Doris'
            }
        },
        {
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [
                    6.59874,
                    46.55043
                ]
            },
            properties: {
                nickname: 'Doris'
            }
        }
    ]

    const { clusters, supercluster } = useSupercluster({
        points,
        bounds,
        zoom,
        // Switch maxZoom to 30 to fix the error
        options: { radius: 100, maxZoom: 31 }
    })

    const defaultProps = {
        center: {
            lat: 46.657505,
            lng: 7.099246
        },
        zoom: 9
    }

    return (
        <div style={{ height: '100vh', width: '100%' }}>
            <GoogleMapReact
                bootstrapURLKeys={{ /* key:  YourKey */ }}
                defaultCenter={defaultProps.center}
                defaultZoom={defaultProps.zoom}
                onGoogleApiLoaded={({ map }) => {
                    console.log(map)
                    mapRef.current = map
                }}
                onChange={({ zoom, bounds }) => {
                    setZoom(zoom)
                    setBounds([
                        bounds.nw.lng,
                        bounds.se.lat,
                        bounds.se.lng,
                        bounds.nw.lat
                    ])
                }}
            >
                {clusters.map(cluster => {
                    const [longitude, latitude] = cluster.geometry.coordinates
                    const {
                        cluster: isCluster,
                        point_count: pointCount
                    } = cluster.properties

                    if (isCluster) {
                        return (
                            <Marker
                                key={`cluster-${cluster.id}`}
                                lat={latitude}
                                lng={longitude}
                            >
                                <div
                                    style={{
                                        width: `${10 + (pointCount / points.length) * 20}px`,
                                        height: `${10 + (pointCount / points.length) * 20}px`,
                                        color: '#fff',
                                        background: '#1978c8',
                                        borderRadius: '50%',
                                        padding: '10px',
                                        display: 'flex',
                                        justifyContent: 'center',
                                        alignItems: 'center'
                                    }}
                                    onClick={() => {
                                        console.log(cluster)
                                        console.log(cluster.id)
                                        console.log(supercluster.getLeaves(cluster.id, Infinity))
                                    }}
                                >
                                    {pointCount}
                                </div>
                            </Marker>
                        )
                    } else {
                        return (
                            <Marker
                                key={cluster.id}
                                lat={latitude}
                                lng={longitude}
                            >
                                <button
                                    style={{
                                        color: '#fff',
                                        background: '#1978c8',
                                        borderRadius: '50%',
                                        padding: '5px',
                                        display: 'flex',
                                        justifyContent: 'center',
                                        alignItems: 'center'
                                    }}
                                    onClick={() => { console.log(cluster) }}>
                                </button>
                            </Marker>
                        )
                    }
                })}
            </GoogleMapReact>
        </div>
    )
}
export default MainMap

ozaik avatar May 06 '21 13:05 ozaik

@ozaik would you mind turning this into a minimal (purely Supercluster without other libraries), live (a link to JSFiddle/JSBin) test case?

mourner avatar May 10 '21 08:05 mourner

Hello!

I copied the example made by @ozaik into a codesandbox and adapted it so we can see the issue without any external library. Only supercluster here. Here's the link to the codesandbox: https://codesandbox.io/s/supercluster-no-cluster-with-the-specified-id-rtiqk?file=/src/index.js - I know it's not JSFiddle/JSbin, but I hope it's sufficient as a test case (as you don't need an account to mess with it either)

By the way. I'm having the same issue while using supercluster with the ol-supercluster: I get an apparently valid cluster_id (9) by scanning the output of getClusters, but for some reason when I call getLeaves(cluster_id, Infinity) I get that weird error. However, the maxZoom computed by OpenLayers is..28, so I don't think the issue is only the thing that affects if the error will happen or not here.

fjorgemota avatar Sep 22 '21 02:09 fjorgemota

release 7.1.4 fixed it for me

Oryss avatar Oct 21 '21 13:10 Oryss

Fantastic! So this might have been the same issue as #189. 🎉 Let's close this one for now then, unless anyone is still experiencing problems on the latest version.

mourner avatar Oct 21 '21 13:10 mourner

@mourner Before closing the issue, could you please have checked the example I posted in the comment above?

For some weird scenarios, this issue still happens, even on version 7.1.4. Here's the updated codesandbox with the code still failing on 7.1.4: https://codesandbox.io/s/supercluster-no-cluster-with-the-specified-id-forked-r8jc6?file=/src/index.js

Thanks.

fjorgemota avatar Oct 21 '21 13:10 fjorgemota

(My issue wasn't related to zoom level, but it did fix the exception thrown when iterating over clusters and trying to use getLeaves on a cluster)

Oryss avatar Oct 21 '21 14:10 Oryss

Also having this issue with 7.1.5 with maxZoom > 30

jeremy-ellis-tech avatar Jan 06 '23 00:01 jeremy-ellis-tech

On my case, if this helps someone was:

 const [points, supercluster] = useClusterer(
    debouncedDataToBeMerged ? debouncedDataToBeMerged : [],
    SCREEN,
    mapCoordsDebounced,
    SUPERCLUSTER_OPTS
  );

I have this hook that creates a supercluster instance, but forgot to add the dep into onClusterPress:

  const onClusterPress = useCallback(
    async (clusterId: number) => {
      {
        const regionToAnimate = supercluster.expandCluster(clusterId);
        const zoom = supercluster.getClusterExpansionZoom(clusterId);
        mapRef.current?.animateCamera({
          center: {
            ...regionToAnimate,
          },
          zoom: zoom + 2,
        });
      }
    },
-  [],
+ [supercluster]
  );

semoal avatar Jul 23 '23 21:07 semoal