Skip to content

Commit

Permalink
Perf: use more efficient data representation and significantly reduce…
Browse files Browse the repository at this point in the history
… virtual function overhead (plus some minor cleanups, e.g. spiderify property to layer).

In my tests, this reduces the execution time (i.e. ClusterManager.addLayer) for the "Clustering Many Markers" example experience from ~1s to 700ms, i.e. roughly a 30% reduction.
  • Loading branch information
ignatz committed Nov 7, 2023
1 parent 429601a commit 02a1f55
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 115 deletions.
50 changes: 24 additions & 26 deletions lib/src/cluster_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ class ClusterManager {
final Alignment? alignment;
final Size predefinedSize;
final Size Function(List<Marker>)? computeSize;
final int minZoom;

late final Map<int, DistanceGrid<MarkerClusterNode>> _gridClusters;
late final Map<int, DistanceGrid<MarkerNode>> _gridUnclustered;
late MarkerClusterNode _topClusterLevel;
final List<DistanceGrid<MarkerClusterNode>> _gridClusters;
final List<DistanceGrid<MarkerNode>> _gridUnclustered;
final MarkerClusterNode _topClusterLevel;

MarkerClusterNode? spiderfyCluster;

ClusterManager._({
const ClusterManager._({
required this.mapCalculator,
required this.alignment,
required this.predefinedSize,
required this.computeSize,
required Map<int, DistanceGrid<MarkerClusterNode>> gridClusters,
required Map<int, DistanceGrid<MarkerNode>> gridUnclustered,
required this.minZoom,
required List<DistanceGrid<MarkerClusterNode>> gridClusters,
required List<DistanceGrid<MarkerNode>> gridUnclustered,
required MarkerClusterNode topClusterLevel,
}) : _gridClusters = gridClusters,
_gridUnclustered = gridUnclustered,
Expand All @@ -39,13 +39,13 @@ class ClusterManager {
required int maxZoom,
required int maxClusterRadius,
}) {
final gridClusters = <int, DistanceGrid<MarkerClusterNode>>{};
final gridUnclustered = <int, DistanceGrid<MarkerNode>>{};

for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
gridClusters[zoom] = DistanceGrid(maxClusterRadius);
gridUnclustered[zoom] = DistanceGrid(maxClusterRadius);
}
final len = maxZoom - minZoom + 1;
final gridClusters = List<DistanceGrid<MarkerClusterNode>>.generate(
len, (_) => DistanceGrid(maxClusterRadius),
growable: false);
final gridUnclustered = List<DistanceGrid<MarkerNode>>.generate(
len, (_) => DistanceGrid(maxClusterRadius),
growable: false);

final topClusterLevel = MarkerClusterNode(
alignment: alignment,
Expand All @@ -59,31 +59,29 @@ class ClusterManager {
mapCalculator: mapCalculator,
predefinedSize: predefinedSize,
computeSize: computeSize,
minZoom: minZoom,
gridClusters: gridClusters,
gridUnclustered: gridUnclustered,
topClusterLevel: topClusterLevel,
);
}

bool isSpiderfyCluster(MarkerClusterNode cluster) {
return spiderfyCluster != null &&
spiderfyCluster!.bounds.center == cluster.bounds.center;
}

void addLayer(MarkerNode marker, int disableClusteringAtZoom, int maxZoom,
int minZoom) {
for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
final markerPoint =
mapCalculator.project(marker.point, zoom: zoom.toDouble());
if (zoom <= disableClusteringAtZoom) {
// try find a cluster close by
final cluster = _gridClusters[zoom]!.getNearObject(markerPoint);
final cluster =
_gridClusters[zoom - minZoom].getNearObject(markerPoint);
if (cluster != null) {
cluster.addChild(marker, marker.point);
return;
}

final closest = _gridUnclustered[zoom]!.getNearObject(markerPoint);
final closest =
_gridUnclustered[zoom - minZoom].getNearObject(markerPoint);
if (closest != null) {
final parent = closest.parent!;
parent.removeChild(closest);
Expand All @@ -97,7 +95,7 @@ class ClusterManager {
..addChild(closest, closest.point)
..addChild(marker, closest.point);

_gridClusters[zoom]!.addObject(
_gridClusters[zoom - minZoom].addObject(
newCluster,
mapCalculator.project(
newCluster.bounds.center,
Expand All @@ -119,7 +117,7 @@ class ClusterManager {
lastParent.bounds.center,
);
lastParent = newParent;
_gridClusters[z]!.addObject(
_gridClusters[z - minZoom].addObject(
lastParent,
mapCalculator.project(
closest.point,
Expand All @@ -134,7 +132,7 @@ class ClusterManager {
}
}

_gridUnclustered[zoom]!.addObject(marker, markerPoint);
_gridUnclustered[zoom - minZoom].addObject(marker, markerPoint);
}

//Didn't get in anything, add us to the top
Expand All @@ -144,7 +142,7 @@ class ClusterManager {
void _removeFromNewPosToMyPosGridUnclustered(
MarkerNode marker, int zoom, int minZoom) {
for (; zoom >= minZoom; zoom--) {
if (!_gridUnclustered[zoom]!.removeObject(marker)) {
if (!_gridUnclustered[zoom - minZoom].removeObject(marker)) {
break;
}
}
Expand Down
146 changes: 73 additions & 73 deletions lib/src/core/distance_grid.dart
Original file line number Diff line number Diff line change
@@ -1,108 +1,108 @@
import 'dart:collection';
import 'dart:math';

class CellEntry<T> {
final T obj;
final double x;
final double y;

const CellEntry(this.x, this.y, this.obj);
}

class GridKey {
final int row;
final int col;

const GridKey(this.row, this.col);

@override
bool operator ==(Object other) =>
other is GridKey && other.row == row && other.col == col;

@override
int get hashCode => (col << 26) ^ row;
}

class DistanceGrid<T> {
final num cellSize;
final int cellSize;
final double _sqCellSize;

final num _sqCellSize;
final Map<num, Map<num, List<T>>> _grid = {};
final Map<T, Point> _objectPoint = {};
final _grid = HashMap<GridKey, List<CellEntry<T>>>();
final _objectPoint = HashMap<T, GridKey>();

DistanceGrid(this.cellSize) : _sqCellSize = cellSize * cellSize;
DistanceGrid(int cellSize)
: cellSize = cellSize > 0 ? cellSize : 1,
_sqCellSize = (cellSize * cellSize).toDouble();

void addObject(T obj, Point point) {
final x = _getCoord(point.x), y = _getCoord(point.y);
final row = _grid[y] ??= {};
final cell = row[x] ??= [];
void clear() {
_grid.clear();
_objectPoint.clear();
}

_objectPoint[obj] = point;
void addObject(T obj, Point<double> point) {
final key = GridKey(_getCoord(point.y), _getCoord(point.x));
final cell = _grid[key] ??= [];

cell.add(obj);
_objectPoint[obj] = key;
cell.add(CellEntry<T>(point.x, point.y, obj));
}

void updateObject(T obj, Point point) {
void updateObject(T obj, Point<double> point) {
removeObject(obj);
addObject(obj, point);
}

//Returns true if the object was found
bool removeObject(T obj) {
final point = _objectPoint[obj];
if (point == null) return false;

final x = _getCoord(point.x), y = _getCoord(point.y);
final row = _grid[y] ??= {};
final cell = row[x] ??= [];

_objectPoint.remove(obj);

final len = cell.length;
for (var i = 0; i < len; i++) {
if (cell[i] == obj) {
cell.removeAt(i);

if (len == 1) {
row.remove(x);

if (_grid[y]!.isEmpty) {
_grid.remove(y);
}
}

return true;
}
final key = _objectPoint.remove(obj);
if (key == null) return false;

// Object existed in the _objectPoint map, thus must exist in the grid.
final cell = _grid[key]!;
cell.removeWhere((e) => e.obj == obj);
if (cell.isEmpty) {
_grid.remove(key);
}
return false;
return true;
}

void eachObject(Function(T) fn) {
for (final i in _grid.keys) {
final row = _grid[i]!;

for (final j in row.keys) {
final cell = row[j]!;

for (var k = 0; k < cell.length; k++) {
fn(cell[k]);
}
for (final cell in _grid.values) {
for (final entry in cell) {
fn(entry.obj);
}
}
}

T? getNearObject(Point point) {
final x = _getCoord(point.x), y = _getCoord(point.y);
var closestDistSq = _sqCellSize;
T? getNearObject(Point<double> point) {
final px = point.x;
final py = point.y;

final x = _getCoord(px), y = _getCoord(py);
double closestDistSq = _sqCellSize;
T? closest;

for (var i = y - 1; i <= y + 1; i++) {
final row = _grid[i];
if (row != null) {
for (var j = x - 1; j <= x + 1; j++) {
final cell = row[j];
if (cell != null) {
for (var k = 0; k < cell.length; k++) {
final obj = cell[k];
final dist = _sqDist(_objectPoint[obj]!, point);

if (dist < closestDistSq ||
dist <= closestDistSq && closest == null) {
closestDistSq = dist;
closest = obj;
}
// Checks rows and columns with index +/- 1.
for (int i = y - 1; i <= y + 1; i++) {
for (int j = x - 1; j <= x + 1; j++) {
final cell = _grid[GridKey(i, j)];
if (cell != null) {
for (final entry in cell) {
final double dx = px - entry.x;
final double dy = py - entry.y;
final double distSq = dx * dx + dy * dy;

if (distSq <= closestDistSq) {
closestDistSq = distSq;
closest = entry.obj;
}
}
}
}
}
return closest;
}

num _getCoord(num x) {
final coord = x / cellSize;
return coord.isFinite ? coord.floor() : x;
return closest;
}

num _sqDist(Point p1, Point p2) {
final dx = p2.x - p1.x, dy = p2.y - p1.y;
return dx * dx + dy * dy;
}
int _getCoord(double x) => x ~/ cellSize;
}
6 changes: 3 additions & 3 deletions lib/src/map_calculator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ class MapCalculator {

MapCalculator(this.mapState);

Point<num> getPixelFromPoint(LatLng point) {
return mapState.project(point).subtract(mapState.pixelOrigin);
Point<double> getPixelFromPoint(LatLng point) {
return mapState.project(point).subtract(mapState.pixelOrigin).toDoublePoint();
}

Point project(LatLng latLng, {double? zoom}) =>
Point<double> project(LatLng latLng, {double? zoom}) =>
mapState.project(latLng, zoom);

LatLng unproject(Point point, {double? zoom}) =>
Expand Down
Loading

0 comments on commit 02a1f55

Please sign in to comment.