Skip to content

Duplicate coordinates throwing No cluster with the specified ID #103

Open

Description

@ollieh-m

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.

Activity

  1. mourner commented on Oct 22, 2018

    @mourner
    Member

    @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?

  2. mourner commented on Nov 1, 2018

    @mourner
    Member

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

  3. djibarian commented on May 6, 2021

    @djibarian

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

  4. reopened this on May 6, 2021
  5. mourner commented on May 6, 2021

    @mourner
    Member

    @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?

  6. ozaik commented on May 6, 2021

    @ozaik

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

  7. djibarian commented on May 6, 2021

    @djibarian

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

  8. mourner commented on May 6, 2021

    @mourner
    Member
  9. ozaik commented on May 6, 2021

    @ozaik

    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
    
    
  10. mourner commented on May 10, 2021

    @mourner
    Member

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

  11. fjorgemota commented on Sep 22, 2021

    @fjorgemota

    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.

  12. Oryss commented on Oct 21, 2021

    @Oryss

    release 7.1.4 fixed it for me

  13. mourner commented on Oct 21, 2021

    @mourner
    Member

    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.

  14. fjorgemota commented on Oct 21, 2021

    @fjorgemota

    @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.

  15. Oryss commented on Oct 21, 2021

    @Oryss

    (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)

  16. jeremy-ellis-tech commented on Jan 6, 2023

    @jeremy-ellis-tech

    Also having this issue with 7.1.5 with maxZoom > 30

  17. semoal commented on Jul 23, 2023

    @semoal

    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]
      );
  18. jgabriel98 commented on Aug 7, 2024

    @jgabriel98

    Issue still happening on version 8.0.1. Can't go beyond zoom 30.

  19. aliunco commented on Dec 10, 2024

    @aliunco

    I'm also facing the same issue using the react-native-map-clustering (simple wrapper of this library)

    you can reproduce it using the code below:
    just try to zoom in (by tapping 3 items) and then zoom out, then boom :)

    import React from "react";
    import MapView from "react-native-map-clustering";
    import { Marker } from "react-native-maps";
    
    const INITIAL_REGION = {
      latitude: 52.5,
      longitude: 19.2,
      latitudeDelta: 8.5,
      longitudeDelta: 8.5,
    };
    
    const App = () => (
      <MapView initialRegion={INITIAL_REGION} style={{ flex: 1 }}>
        <Marker coordinate={{ latitude: 52.4, longitude: 18.7 }} />
        <Marker coordinate={{ latitude: 52.4, longitude: 18.7 }} />
        <Marker coordinate={{ latitude: 52.4, longitude: 18.7 }} />
      </MapView>
    );
    
    export default App;

    my solution (tricky one) was to add some small noise to the markers by doing something like

    let markers = listItems.map((item, index) => {
        const randomNoiseLat = Math.random() * 0.0001
        return <Marker 
            key={`marker-id-${index}`}
            coordinate={{ 
               latitude: parseFloat(`${item.lat}`) + randomNoiseLat, 
               longitude: parseFloat(`${item.lng}`)
             }} />
    }

    but still it's not a real fix, just bypassing the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions