rorystephenson / supercluster_dart

A port of MapBox's javascript supercluster library for fast marker clustering.
ISC License
2 stars 3 forks source link

Clustering difficulties / calibrate radius and extent #6

Open kai-trone opened 6 months ago

kai-trone commented 6 months ago

The problem: Trying to adjust the cluster radius

The goal: The clustering logic should not remove any markers

Hey! I've ben working with flutter maplibre_gl and supercluster to create a map with markers and clusters. The map is working fine, so are the clusters. The challenge is to fill the map with more markers, because in a highly populated area I have to zoom in really far to see all markers.

Here is a comparison, this is the map and cluster with almost no parameters

https://github.com/rorystephenson/supercluster_dart/assets/142009975/1e18ef6a-49b9-44ed-9dbe-dbc60093c9cc

And here I try to dynamically calculate a radius and extend according to the zoom level (more zoom = lower radius and thus less clustering). As you can see some markers will appear or disappear out of nowhere without any changes in the clusters. I've tried changing only radius or only extent as well. Also tried playing with the other parameters.

https://github.com/rorystephenson/supercluster_dart/assets/142009975/2d696aa8-4107-4cce-9837-47655ad9782d

Replicating might be difficult, since the class has gotten quite big. Here is at least some code for reference Here Is the cluster logic:

// Clusters must be global functions
// dynamic radius calculation, zoom is saved in poi point
SuperclusterImmutable<POIPoint> _calculateClusters(List<POIPoint> data) {
  if (data.isEmpty) {
    return SuperclusterImmutable<POIPoint>(radius: 50, getX: (m) => m.longitude!, getY: (m) => m.latitude!);
  }

  const int minZoom = 2, maxZoom = 20;
  const int minRadius = 7, maxRadius = 75;
  const int minExtent = 51, maxExtent = 512;
  double currentZoom = data.first.zoomLevel ?? 10;

  int radius = calculateRadius(currentZoom, minZoom, maxZoom, minRadius, maxRadius);
  int extent = calculateExtent(currentZoom, minZoom, maxZoom, minExtent, maxExtent);

  debugPrint('Radius/Zoom/Extent: $radius $currentZoom $extent');

  final cluster = SuperclusterImmutable<POIPoint>(
    radius: radius,
    extent: extent,
    getX: (m) => m.longitude!,
    getY: (m) => m.latitude!,
    minZoom: minZoom,
    maxZoom: maxZoom,
  )..load(data);

  return cluster;
}

int calculateRadius(double currentZoom, int minZoom, int maxZoom, int minRadius, int maxRadius) {
  int radius;
  if (currentZoom <= minZoom) {
    radius = maxRadius;
  } else if (currentZoom >= maxZoom) {
    radius = minRadius;
  } else if (currentZoom < 13.5) {
    radius = ((maxRadius - minRadius) * (maxZoom - currentZoom) / (maxZoom - minZoom) + minRadius).round();
  } else {
    radius = ((maxRadius - minRadius) * (maxZoom - currentZoom) / (maxZoom - minZoom) + minRadius - 15).round();
  }
  return radius < minRadius ? minRadius : radius;
}

int calculateExtent(double currentZoom, int minZoom, int maxZoom, int minExtent, int maxExtent) {
  int extent;
  if (currentZoom <= minZoom) {
    extent = maxExtent;
  } else if (currentZoom >= maxZoom) {
    extent = minExtent;
  } else if (currentZoom < 13.5) {
    extent = ((maxExtent - minExtent) * (currentZoom - minZoom) / (maxZoom - minZoom) + minExtent).round();
  } else {
    extent = ((maxExtent - minExtent) * (currentZoom - minZoom) / (maxZoom - minZoom) + minExtent - 100).round();
  }
  return extent < minExtent ? minExtent : extent;
}

And I call this by using

Future ComputePoints() async {
  await updatePoints();
  CombinedProvider.cluster = await compute(_calculateClusters, CombinedProvider.poiPoints.values.toList());
}

After this computing is done the clusters are also transformed to markers and then added to the map.

calculateClusters() async {
  int start = DateTime.now().millisecond;
  List<Map<String, dynamic>> clusterDatas = [];
  //Search all map
  var clusterSearch = CombinedProvider.cluster!
      .search(180, -90, 180, 90, _mlMapController.cameraPosition!.zoom.toInt())
      .map((e) => e.map(cluster: (c) {
            clusterDatas.add({
              'marker': true,
              'lat': c.latitude,
              'lng': c.longitude,
              'zoom': CalculationUtil.zoomMore(c.highestZoom + 0),
            });
            return getClusterSymbol(c);
          }, point: (p) {
            clusterDatas.add({
              'poi': p.originalPoint.toJson(),
              'lat': p.originalPoint.latitude,
              'lng': p.originalPoint.longitude,
            });
            return getPOISymbol(p);
          }));
  int end = DateTime.now().millisecond;
  CombinedProvider().updateAllSymbols(clusterSearch.toList(), clusterDatas);
  debugPrint('clusterSearch took ${end - start}ms: ${clusterSearch.length}/${CombinedProvider.cluster!.length}');
  _lastClusterCalculateCameraPos = _mlMapController.cameraPosition;
  redrawPOISymbols();
}

If you need more information please let me know.

rorystephenson commented 2 months ago

Hi @kai-trone . Thanks for opening an issue. Unfortunately I don't currently have a lot of time to dedicate to this project as I'm busy with work and life commitments.

As you probably know you can use supercluster's radius and minPoints to tweak when markers are formed in to clusters but I'm conscious that this is not a full solution. If you have big variations in point density then it might not be possible to find a sweet spot with these values.

I can't see myself having time to work on this for the foreseeable future but I would be more than happy to consider a PR!