rorystephenson / supercluster_dart Goto Github PK
View Code? Open in Web Editor NEWA port of MapBox's javascript supercluster library for fast marker clustering.
License: ISC License
A port of MapBox's javascript supercluster library for fast marker clustering.
License: ISC License
Hi, it's me again :)
Previously I was using SuperclusterImmutable.load()
(which was working well for me), but recently I wanted to switch to SuperclusterMutable
since the underlying data structure changed. I noticed that it produces different results when I use the insert
method. To me the one produced by load
looks "more correct" to me. To verify the differences I wrote a little test:
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
import 'package:supercluster/supercluster.dart';
void main() async {
final points = <LatLng>[
LatLng(50.813443, 12.931275),
LatLng(50.813362, 12.931059),
LatLng(50.814048, 12.931202),
LatLng(50.814171, 12.931484),
LatLng(50.814273, 12.931415),
LatLng(50.813756, 12.93058),
LatLng(50.813559, 12.930912),
LatLng(50.813859, 12.930643),
LatLng(50.813898, 12.930761),
LatLng(50.813623, 12.931012),
];
final mutableSuperCluster = SuperclusterMutable<LatLng>(
getX: (p) => p.longitude,
getY: (p) => p.latitude,
minZoom: 0,
maxZoom: 20,
radius: 120,
extent: 512,
nodeSize: 64,
);
void testCluster(int zoom) {
for (final p in points) {
mutableSuperCluster.insert(p);
}
final mutableClustersWithInsert = mutableSuperCluster.search(-180, -85, 180, 85, zoom);
mutableSuperCluster.load(points);
final mutableClustersWithLoad = mutableSuperCluster.search(-180, -85, 180, 85, zoom);
expect(mutableClustersWithInsert.length, equals(mutableClustersWithLoad.length));
for (var i = 0; i < mutableClustersWithInsert.length; i++) {
expect(mutableClustersWithInsert[i].numPoints, equals(mutableClustersWithLoad[i].numPoints));
expect(mutableClustersWithInsert[i].x, equals(mutableClustersWithLoad[i].x));
expect(mutableClustersWithInsert[i].y, equals(mutableClustersWithLoad[i].y));
}
}
test('test clusters', () {
testCluster(17); // throws: Expected: <2> Actual: <3>
});
test('test clusters', () {
testCluster(0); //throws: Expected: <10> Actual: <20>
});
}
I also noticed that SuperclusterImmutable.load()
and SuperclusterMutable.load()
produce different results:
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
import 'package:supercluster/supercluster.dart';
void main() async {
final points = <LatLng>[
LatLng(50.813443, 12.931275),
LatLng(50.813362, 12.931059),
LatLng(50.814048, 12.931202),
LatLng(50.814171, 12.931484),
LatLng(50.814273, 12.931415),
LatLng(50.813756, 12.93058),
LatLng(50.813559, 12.930912),
LatLng(50.813859, 12.930643),
LatLng(50.813898, 12.930761),
LatLng(50.813623, 12.931012),
];
final mutableSuperCluster = SuperclusterMutable<LatLng>(
getX: (p) => p.longitude,
getY: (p) => p.latitude,
minZoom: 0,
maxZoom: 20,
radius: 120,
extent: 512,
nodeSize: 64,
);
final immutableSuperCluster = SuperclusterImmutable<LatLng>(
getX: (p) => p.longitude,
getY: (p) => p.latitude,
minZoom: 0,
maxZoom: 20,
radius: 120,
extent: 512,
nodeSize: 64,
);
void testCluster(int zoom) {
mutableSuperCluster.load(points);
immutableSuperCluster.load(points);
final mutableClusters = mutableSuperCluster.search(-180, -85, 180, 85, zoom);
final immutableClusters = immutableSuperCluster.search(-180, -85, 180, 85, zoom);
expect(mutableClusters.length, equals(immutableClusters.length));
for (var i = 0; i < mutableClusters.length; i++) {
expect(mutableClusters[i].numPoints, equals(immutableClusters[i].numPoints));
expect(mutableClusters[i].x, equals(immutableClusters[i].x));
expect(mutableClusters[i].y, equals(immutableClusters[i].y));
}
}
test('test clusters', () {
testCluster(17); // throws: Expected: <0.5359196236111111> Actual: <0.5359202083333333>
});
test('test clusters', () {
testCluster(0); //throws: Expected: <0.5359195397222223> Actual: <0.5359202083333333>
});
}
As always thanks for your work!
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.
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.
Is it possible to make the computation part as async (isolate) functions ?
When I`m using supercluster for map markers clustering, the huge number of points clusters computation blocks the main thread and Ui starts to freeze.
I think there is a mistake in the docs.
It is stated that SuperclusterMutable.load
will replace existing points.
However since this uses the rbush load method under the hood it is actually just a method for inserting multiple points at once.
// Bulk load elements (empty in this case, so here it's a no-op).
tree.load([]);
from: https://pub.dev/packages/rbush
I've also tested this.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.