Skip to content

Commit ed402c4

Browse files
committed
feature #2554 [Map] Add Clustering Algorithms (smnandre)
This PR was merged into the 2.x branch. Discussion ---------- [Map] Add Clustering Algorithms | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Issues | Fix #... | License | MIT Provide an interface and the first two PHP implementations of Clustering algorithms - GridClustering - Morton Performances: < 1ms per 1000 Points * * tested on a non production server (... my iMac 😅 ) **Next Step** will require JS code. But this is already usefull with PHP classes only, for whomever has large amount of markers Commits ------- db2a416 [Map] Add Clustering Algorithms
2 parents e0f4d84 + db2a416 commit ed402c4

10 files changed

+665
-2
lines changed

src/Map/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
## 2.29.0
44

55
- Add Symfony 8 support
6+
- Add `Cluster` class and `ClusteringAlgorithmInterface` with two implementations `GridClusteringAlgorithm` and `MortonClusteringAlgorithm`
67

78
## 2.28
89

910
- Add `minZoom` and `maxZoom` options to `Map` to set the minimum and maximum zoom levels
10-
- The package is not experimental anymore
1111

1212
## 2.27
1313

src/Map/doc/index.rst

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ property available in ``Map``, ``Marker``, ``InfoWindow``, ``Polygon``, ``Polyli
727727
));
728728

729729
On the JavaScript side, you can access these extra data by listening to ``ux:map:pre-connect``,
730-
``ux:map:connect``, ``ux:map:*:before-create``, ``ux:map:*:after-create`` events::
730+
``ux:map:connect``, ``ux:map:*:before-create``, ``ux:map:*:after-create`` events:
731731

732732
.. code-block:: javascript
733733
@@ -842,6 +842,45 @@ You can retrieve the map instance using the ``getMap()`` method, and change the
842842
</button>
843843
</div>
844844

845+
Advanced: Clusters
846+
------------------
847+
848+
.. versionadded:: 2.29
849+
850+
Clusters were added in UX Map 2.29.
851+
852+
A cluster is a group of points that are close to each other on a map.
853+
854+
Clustering reduces clutter and improves performance when displaying many points.
855+
This makes maps easier to read and faster to render.
856+
857+
UX Map supports two algorithms:
858+
859+
- **Grid**: Fast, divides map into cells.
860+
- **Morton**: Uses Z-order curves for spatial locality.
861+
862+
Create a clustering algorithm, cluster your points, and add cluster markers::
863+
864+
use Symfony\UX\Map\Cluster\GridClusteringAlgorithm;
865+
use Symfony\UX\Map\Cluster\MortonClusteringAlgorithm;
866+
use Symfony\UX\Map\Point;
867+
868+
// Initialize clustering algorithm
869+
$clusteringAlgorithm = new GridClusteringAlgorithm();
870+
// or
871+
// $clusteringAlgorithm = new MortonClusteringAlgorithm();
872+
873+
// Create clusters of points
874+
$points = [new Point(48.8566, 2.3522), new Point(45.7640, 4.8357), /* ... */];
875+
$clusters = $clusteringAlgorithm->cluster($points, zoom: 5.0);
876+
877+
// Iterate over each cluster
878+
foreach ($clusters as $cluster) {
879+
$cluster->getCenter(); // A Point, representing the cluster center
880+
$cluster->getPoints(); // A list of Point
881+
$cluster->count(); // The number of points in the cluster
882+
}
883+
845884
Backward Compatibility promise
846885
------------------------------
847886

src/Map/src/Cluster/Cluster.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Cluster;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Cluster representation.
18+
*
19+
* @implements \IteratorAggregate<int, Point>
20+
*
21+
* @author Simon André <[email protected]>
22+
*/
23+
final class Cluster implements \Countable, \IteratorAggregate
24+
{
25+
/**
26+
* @var Point[]
27+
*/
28+
private array $points = [];
29+
30+
private float $sumLat = 0.0;
31+
private float $sumLng = 0.0;
32+
private int $count = 0;
33+
34+
/**
35+
* Initializes the cluster with an initial point.
36+
*/
37+
public function __construct(Point $initialPoint)
38+
{
39+
$this->addPoint($initialPoint);
40+
}
41+
42+
public function addPoint(Point $point): void
43+
{
44+
$this->points[] = $point;
45+
$this->sumLat += $point->getLatitude();
46+
$this->sumLng += $point->getLongitude();
47+
++$this->count;
48+
}
49+
50+
/**
51+
* Returns the center of the cluster as a Point.
52+
*/
53+
public function getCenter(): Point
54+
{
55+
return new Point($this->sumLat / $this->count, $this->sumLng / $this->count);
56+
}
57+
58+
/**
59+
* @return non-empty-list<Point>
60+
*/
61+
public function getPoints(): array
62+
{
63+
return $this->points;
64+
}
65+
66+
/**
67+
* Returns the number of points in the cluster.
68+
*/
69+
public function count(): int
70+
{
71+
return $this->count;
72+
}
73+
74+
/**
75+
* @return \Traversable<int, Point>
76+
*/
77+
public function getIterator(): \Traversable
78+
{
79+
return new \ArrayIterator($this->points);
80+
}
81+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Cluster;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Interface for various Clustering implementations.
18+
*/
19+
interface ClusteringAlgorithmInterface
20+
{
21+
/**
22+
* Clusters a set of points.
23+
*
24+
* @param Point[] $points List of points to be clustered
25+
* @param float $zoom The zoom level, determining grid resolution
26+
*
27+
* @return Cluster[] An array of clusters, each containing grouped points
28+
*/
29+
public function cluster(array $points, float $zoom): array;
30+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Cluster;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Grid-based clustering algorithm for spatial data.
18+
*
19+
* This algorithm groups points into fixed-size grid cells based on the given zoom level.
20+
*
21+
* Best for:
22+
* - Fast, scalable clustering on large geographical datasets
23+
* - Real-time clustering where performance is critical
24+
* - Use cases where a simple, predictable grid structure is sufficient
25+
*
26+
* Slower for:
27+
* - Highly dynamic data that requires adaptive cluster sizes
28+
* - Scenarios where varying density should influence cluster sizes (e.g., DBSCAN-like approaches)
29+
* - Irregularly shaped clusters that do not fit a strict grid pattern
30+
*
31+
* @author Simon André <[email protected]>
32+
*/
33+
final class GridClusteringAlgorithm implements ClusteringAlgorithmInterface
34+
{
35+
/**
36+
* Clusters a set of points using a fixed grid resolution based on the zoom level.
37+
*
38+
* @param Point[] $points List of points to be clustered
39+
* @param float $zoom The zoom level, determining grid resolution
40+
*
41+
* @return Cluster[] An array of clusters, each containing grouped points
42+
*/
43+
public function cluster(iterable $points, float $zoom): array
44+
{
45+
$gridResolution = 1 << (int) $zoom;
46+
$gridSize = 360 / $gridResolution;
47+
$invGridSize = 1 / $gridSize;
48+
49+
$cells = [];
50+
51+
foreach ($points as $point) {
52+
$lng = $point->getLongitude();
53+
$lat = $point->getLatitude();
54+
$gridX = (int) (($lng + 180) * $invGridSize);
55+
$gridY = (int) (($lat + 90) * $invGridSize);
56+
$key = ($gridX << 16) | $gridY;
57+
58+
if (!isset($cells[$key])) {
59+
$cells[$key] = new Cluster($point);
60+
} else {
61+
$cells[$key]->addPoint($point);
62+
}
63+
}
64+
65+
return array_values($cells);
66+
}
67+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Map\Cluster;
13+
14+
use Symfony\UX\Map\Point;
15+
16+
/**
17+
* Clustering algorithm based on Morton codes (Z-order curves).
18+
*
19+
* This approach is optimized for spatial data and preserves locality efficiently.
20+
*
21+
* Best for:
22+
* - Large-scale spatial clustering
23+
* - Hierarchical clustering with fast locality-based grouping
24+
* - Datasets where preserving spatial proximity is crucial
25+
*
26+
* Slower for:
27+
* - High-dimensional data (beyond 2D/3D) due to Morton code limitations
28+
* - Non-spatial or categorical data
29+
* - Scenarios requiring dynamic cluster adjustments (e.g., streaming data)
30+
*
31+
* @author Simon André <[email protected]>
32+
*/
33+
final class MortonClusteringAlgorithm implements ClusteringAlgorithmInterface
34+
{
35+
/**
36+
* @param Point[] $points
37+
*
38+
* @return Cluster[]
39+
*/
40+
public function cluster(iterable $points, float $zoom): array
41+
{
42+
$resolution = 1 << (int) $zoom;
43+
$clustersMap = [];
44+
45+
foreach ($points as $point) {
46+
$xNorm = ($point->getLatitude() + 180) / 360;
47+
$yNorm = ($point->getLongitude() + 90) / 180;
48+
49+
$x = (int) floor($xNorm * $resolution);
50+
$y = (int) floor($yNorm * $resolution);
51+
52+
$x &= 0xFFFF;
53+
$y &= 0xFFFF;
54+
55+
$x = ($x | ($x << 8)) & 0x00FF00FF;
56+
$x = ($x | ($x << 4)) & 0x0F0F0F0F;
57+
$x = ($x | ($x << 2)) & 0x33333333;
58+
$x = ($x | ($x << 1)) & 0x55555555;
59+
60+
$y = ($y | ($y << 8)) & 0x00FF00FF;
61+
$y = ($y | ($y << 4)) & 0x0F0F0F0F;
62+
$y = ($y | ($y << 2)) & 0x33333333;
63+
$y = ($y | ($y << 1)) & 0x55555555;
64+
65+
$code = ($y << 1) | $x;
66+
67+
if (!isset($clustersMap[$code])) {
68+
$clustersMap[$code] = new Cluster($point);
69+
} else {
70+
$clustersMap[$code]->addPoint($point);
71+
}
72+
}
73+
74+
return array_values($clustersMap);
75+
}
76+
}

0 commit comments

Comments
 (0)