cesium icon indicating copy to clipboard operation
cesium copied to clipboard

EntityCluster BUG

Open Duan971231 opened this issue 3 weeks ago • 8 comments

When I use the EntityCluster of dataSource, using the following code and only replacing the version number will yield different results. The aggregation of the new version will directly return a single entity in the part outside the window. When dragging the camera, you will see a part of the single entity, which disappears after the aggregation starts. The effect is more pronounced in 2D view

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/dhs.jpeg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="stylesheet" href="/Cesium-1.135/Build/Cesium/Widgets/widgets.css" />
    <script type="text/javascript" src="/Cesium-1.135/Build/Cesium/Cesium.js"></script>
  </head>
  <body style="margin: 0; overflow: hidden; background: #fff; width: 100%; height: 100%; position: absolute; top: 0">
    <div id="map" style="margin: 0 auto; width: 100%; height: 100%"></div>

    <!-- ./video.mp4 -->
    <script type="text/javascript">
      const viewer = new Cesium.Viewer('map', {});
      viewer.scene.debugShowFramesPerSecond = true;

      class pointCluster {
        constructor(opt) {
          this.options = {
            dataOrUrl: '',
            pixelRange: 15,
            enabled: true,
            colorArr: [
              {
                num: 1,
                size: 48,
                color: '#e6a23cbb',
              },
            ],
            img: '',
          };
          this.options = Object.assign(this.options, opt);
          this.viewer = opt.viewer;
          this.dataSources = null;
          this.clustericon = {};
          if (this.options.dataOrUrl && this.options.dataOrUrl != '') {
            this.loadjson();
          }
        }
        loadjson() {
          var this_ = this;
          new Cesium.GeoJsonDataSource().load(this_.options.dataOrUrl).then((geoJsonDataSource) => {
            this_.showcluster(geoJsonDataSource);
          });
        }
        showcluster(geoJsonDataSource) {
          var this_ = this;
          this.dataSources = geoJsonDataSource;
          this.viewer.dataSources.add(this.dataSources);

          var pixelRange = this.options.pixelRange;
          var minimumClusterSize = 1;
          var enabled = this.options.enabled;
          //开启聚合
          this.dataSources.clustering.enabled = enabled;
          this.dataSources.clustering.pixelRange = pixelRange;
          this.dataSources.clustering.minimumClusterSize = minimumClusterSize;

          var removeListener;
          //聚合
          function customStyle() {
            if (Cesium.defined(removeListener)) {
              removeListener();
              removeListener = undefined;
            } else {
              removeListener = this_.dataSources.clustering.clusterEvent.addEventListener(
                function (clusteredEntities, cluster) {
                  cluster.label.show = false;
                  cluster.billboard.show = true;
                  cluster.billboard.id = cluster.label.id;
                  cluster.billboard.verticalOrigin = Cesium.VerticalOrigin.CENTER;

                  var xx = -1;
                  for (var i = 0; i < this_.options.colorArr.length; i++) {
                    if (clusteredEntities.length > this_.options.colorArr[i].num) {
                      xx = i;
                    }
                  }
                  if (xx == -1) {
                    cluster.billboard.image = this_.options.img;
                  } else {
                    cluster.billboard.image = this_.drawImage(
                      clusteredEntities.length,
                      this_.options.colorArr[xx].size,
                      this_.options.colorArr[xx].color
                    );
                  }
                }
              );
            }

            // force a re-cluster with the new styling
            var pixelRange = this_.dataSources.clustering.pixelRange;
            this_.dataSources.clustering.pixelRange = 0;
            this_.dataSources.clustering.pixelRange = pixelRange;
          }
          customStyle();
        }

        drawImage(text, size, color) {
          if (this.clustericon[text + '_' + size + '_' + color]) {
            return this.clustericon[text + '_' + size + '_' + color];
          }
          var canvas = document.createElement('canvas');
          canvas.height = size;
          canvas.width = size;
          var ctx = canvas.getContext('2d');
          //画圈
          ctx.beginPath();
          ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2, true);
          ctx.fillStyle = color;
          ctx.closePath();
          ctx.fill();

          ctx.font = '16px bold 楷体';
          ctx.fillStyle = '#ffffff';
          var tx = (size - (text + '').length * 9) / 2;
          ctx.fillText(text, tx, size / 2 + 6);
          var canvasdataurl = canvas.toDataURL();
          this.clustericon[text + '_' + size + '_' + color] = canvasdataurl;
          return canvasdataurl;
        }
        remove() {
          this.viewer.dataSources.remove(this.dataSources);
          this.dataSources = null;
          this.clustericon = {};
        }
      }

      function randomPointsWithinBbox(xmin, xmax, ymin, ymax, num, type) {
        var points = {
          type: 'FeatureCollection',
          features: [],
        };
        for (var i = 0; i < num; i++) {
          var point = {
            type: 'Feature',
            properties: {
              value: parseInt(Math.random() * 10000000),
            },
            geometry: {
              type: 'Point',
              coordinates: [Math.random() * (xmax - xmin) + xmin, Math.random() * (ymax - ymin) + ymin],
            },
          };
          points.features.push(point);
        }
        return points;
      }
      var results = randomPointsWithinBbox(-120, -90, 25, 40, 1000, 'geojson');
      var clu = new pointCluster({
        viewer: viewer,
        dataOrUrl: results,
        pixelRange: 20,
        colorArr: [
          {
            num: 1,
            size: 30,
            color: '#1c86d1cc',
          },
          {
            num: 50,
            size: 32,
            color: '#67c23acc',
          },
          {
            num: 100,
            size: 34,
            color: '#f56c6ccc',
          },
          {
            num: 200,
            size: 36,
            color: '#e6a23ccc',
          },
        ],
        img: '/marker6.png',
      });
    </script>
  </body>
</html>

Version 98 effect:is Ok

Image

Version 135 effect:

Image

Duan971231 avatar Nov 29 '25 14:11 Duan971231

[](https://sandcastle.cesium.com/#c=jVTbjtowEP2VES8kJRhYdR+6XFSVqlWrrlp1pfah7INJBnDr2JEvLGnFv9d2AoRAV/UDvp0zc5gzMcsLqQy8AKphjprZHFZK5rDopGG36IwXYiFSKbSBLcMnVDAFgU81mnwLZ9EBP5fCUCZQLTqxY4InQxgrK1LDpABFRSbzL5IJo78zs2HizVLuol3ORAK7nO4SKMO6DGth8wRMWWAMfw6xALZUQRFCODmNcwjQO+i+Q2qswrnkHEPebtJEraprfQc/HhsX+/FpvZIKIp+HuRTDsZsmXoxb9Hrxec6jmraYtpxzEQCFkgUqw7yQFtFH5dZxC6o0fhAmuqdmQ6rqRbGzbDSsRtwKum/t1yhzNKq8kqJWF8xoawNIpVQZE9SEOrXTR94r6IM3LoYeVAZeoMoKVdYoPz8+K7jpAdQmk4NfpLB6E4XDuIHbn5YKHVDUvCPkCPBWuTiWh875Ry/2RzfDBPqv3M/NbQIv3eyLnUDX1fKnds0Uj0+d3fga3qP86K7fUkMfpFUpRjHhkmZRnTImZoMiitZtXAzTWaunwodFsiNEE5plV5gNJefjAkpSbrVBxcSaoKBLjpmrgVEWz0r+LK9gO+RfqVijo45u/5/ofGe5eyCqkwf2OwQ44+/jcSfpTLQpOc78xWtWPU9W8YiQgcG84L4bB0ub/kJDUq39358MDpRJxrbAsumV5whSTrV2NyvLuU+/6MwmA4c/o3mznNrPW1Sclh6yGc0+VYeEkMnAbS9ZRkq+pKoR8S8)

The result is as follows: Image

Duan971231 avatar Nov 29 '25 14:11 Duan971231

First, move the camera to the lowest point above the ground and only display the ElementCluster in the bottom right corner. Then slowly raise the camera, and at this point, slowly move to view the entity on the left. You will find many 1s appearing.

Duan971231 avatar Nov 29 '25 14:11 Duan971231

Thanks for this report @Duan971231 . I am unable to reproduce the issue so far. This seems to depend on camera movement, right? Could you provide a screen capture video to show more precisely how you are moving the camera to produce the buggy clustering?

lukemckinstry avatar Dec 04 '25 20:12 lukemckinstry

I have never looked closer at clustering, and am not sure how well-defined its behavior is. But I think that the effect can be observed quite easily when RIGHT-mouse dragging up (to zoom out). Only when that bunch of '1's is then moved into the center of the view, the clustering kicks in.

Image

javagl avatar Dec 04 '25 21:12 javagl

@lukemckinstry https://github.com/CesiumGS/cesium/pull/13064 You can take a look at this pull request. It's similar to the screen recording provided by the friend above. When the map camera is raised, you can clearly see that there are issues with the aggregation on the left and top sides.

I have already found the problem, which is due to the new version of kdbush. When using the old method, negative numbers would be converted to 2^23 during comparison. Therefore, entities on the left and top would never enter the EntityCluster. Only in the next calculation, when their position on the screen becomes positive, do they get included. ...

Duan971231 avatar Dec 05 '25 02:12 Duan971231

thanks for the follow up @Duan971231 and for identifying the root cause 👍 Feel free to open a PR with the fix if you are willing to.

lukemckinstry avatar Dec 05 '25 15:12 lukemckinstry

@lukemckinstry The linked PR at https://github.com/CesiumGS/cesium/pull/13064 was opened by Duan971231, and (I assume) should already fix the issue. (I haven't tested it yet, though...)

javagl avatar Dec 05 '25 18:12 javagl

@lukemckinstry https://github.com/CesiumGS/cesium/pull/13064 This is my PR

Duan971231 avatar Dec 06 '25 09:12 Duan971231