mirror of
https://github.com/symfony/ux-map.git
synced 2026-03-23 23:42:07 +01:00
[Map] Add Clustering Algorithms
This commit is contained in:
committed by
Hugo Alliaume
parent
e824d0f34a
commit
211ed3c911
@@ -3,11 +3,11 @@
|
||||
## 2.29.0
|
||||
|
||||
- Add Symfony 8 support
|
||||
- Add `Cluster` class and `ClusteringAlgorithmInterface` with two implementations `GridClusteringAlgorithm` and `MortonClusteringAlgorithm`
|
||||
|
||||
## 2.28
|
||||
|
||||
- Add `minZoom` and `maxZoom` options to `Map` to set the minimum and maximum zoom levels
|
||||
- The package is not experimental anymore
|
||||
|
||||
## 2.27
|
||||
|
||||
|
||||
@@ -727,7 +727,7 @@ property available in ``Map``, ``Marker``, ``InfoWindow``, ``Polygon``, ``Polyli
|
||||
));
|
||||
|
||||
On the JavaScript side, you can access these extra data by listening to ``ux:map:pre-connect``,
|
||||
``ux:map:connect``, ``ux:map:*:before-create``, ``ux:map:*:after-create`` events::
|
||||
``ux:map:connect``, ``ux:map:*:before-create``, ``ux:map:*:after-create`` events:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
@@ -842,6 +842,45 @@ You can retrieve the map instance using the ``getMap()`` method, and change the
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Advanced: Clusters
|
||||
------------------
|
||||
|
||||
.. versionadded:: 2.29
|
||||
|
||||
Clusters were added in UX Map 2.29.
|
||||
|
||||
A cluster is a group of points that are close to each other on a map.
|
||||
|
||||
Clustering reduces clutter and improves performance when displaying many points.
|
||||
This makes maps easier to read and faster to render.
|
||||
|
||||
UX Map supports two algorithms:
|
||||
|
||||
- **Grid**: Fast, divides map into cells.
|
||||
- **Morton**: Uses Z-order curves for spatial locality.
|
||||
|
||||
Create a clustering algorithm, cluster your points, and add cluster markers::
|
||||
|
||||
use Symfony\UX\Map\Cluster\GridClusteringAlgorithm;
|
||||
use Symfony\UX\Map\Cluster\MortonClusteringAlgorithm;
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
// Initialize clustering algorithm
|
||||
$clusteringAlgorithm = new GridClusteringAlgorithm();
|
||||
// or
|
||||
// $clusteringAlgorithm = new MortonClusteringAlgorithm();
|
||||
|
||||
// Create clusters of points
|
||||
$points = [new Point(48.8566, 2.3522), new Point(45.7640, 4.8357), /* ... */];
|
||||
$clusters = $clusteringAlgorithm->cluster($points, zoom: 5.0);
|
||||
|
||||
// Iterate over each cluster
|
||||
foreach ($clusters as $cluster) {
|
||||
$cluster->getCenter(); // A Point, representing the cluster center
|
||||
$cluster->getPoints(); // A list of Point
|
||||
$cluster->count(); // The number of points in the cluster
|
||||
}
|
||||
|
||||
Backward Compatibility promise
|
||||
------------------------------
|
||||
|
||||
|
||||
81
src/Cluster/Cluster.php
Normal file
81
src/Cluster/Cluster.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Cluster;
|
||||
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
/**
|
||||
* Cluster representation.
|
||||
*
|
||||
* @implements \IteratorAggregate<int, Point>
|
||||
*
|
||||
* @author Simon André <smn.andre@gmail.com>
|
||||
*/
|
||||
final class Cluster implements \Countable, \IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var Point[]
|
||||
*/
|
||||
private array $points = [];
|
||||
|
||||
private float $sumLat = 0.0;
|
||||
private float $sumLng = 0.0;
|
||||
private int $count = 0;
|
||||
|
||||
/**
|
||||
* Initializes the cluster with an initial point.
|
||||
*/
|
||||
public function __construct(Point $initialPoint)
|
||||
{
|
||||
$this->addPoint($initialPoint);
|
||||
}
|
||||
|
||||
public function addPoint(Point $point): void
|
||||
{
|
||||
$this->points[] = $point;
|
||||
$this->sumLat += $point->getLatitude();
|
||||
$this->sumLng += $point->getLongitude();
|
||||
++$this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the center of the cluster as a Point.
|
||||
*/
|
||||
public function getCenter(): Point
|
||||
{
|
||||
return new Point($this->sumLat / $this->count, $this->sumLng / $this->count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<Point>
|
||||
*/
|
||||
public function getPoints(): array
|
||||
{
|
||||
return $this->points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of points in the cluster.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Traversable<int, Point>
|
||||
*/
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
return new \ArrayIterator($this->points);
|
||||
}
|
||||
}
|
||||
30
src/Cluster/ClusteringAlgorithmInterface.php
Normal file
30
src/Cluster/ClusteringAlgorithmInterface.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Cluster;
|
||||
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
/**
|
||||
* Interface for various Clustering implementations.
|
||||
*/
|
||||
interface ClusteringAlgorithmInterface
|
||||
{
|
||||
/**
|
||||
* Clusters a set of points.
|
||||
*
|
||||
* @param Point[] $points List of points to be clustered
|
||||
* @param float $zoom The zoom level, determining grid resolution
|
||||
*
|
||||
* @return Cluster[] An array of clusters, each containing grouped points
|
||||
*/
|
||||
public function cluster(array $points, float $zoom): array;
|
||||
}
|
||||
67
src/Cluster/GridClusteringAlgorithm.php
Normal file
67
src/Cluster/GridClusteringAlgorithm.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Cluster;
|
||||
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
/**
|
||||
* Grid-based clustering algorithm for spatial data.
|
||||
*
|
||||
* This algorithm groups points into fixed-size grid cells based on the given zoom level.
|
||||
*
|
||||
* Best for:
|
||||
* - Fast, scalable clustering on large geographical datasets
|
||||
* - Real-time clustering where performance is critical
|
||||
* - Use cases where a simple, predictable grid structure is sufficient
|
||||
*
|
||||
* Slower for:
|
||||
* - Highly dynamic data that requires adaptive cluster sizes
|
||||
* - Scenarios where varying density should influence cluster sizes (e.g., DBSCAN-like approaches)
|
||||
* - Irregularly shaped clusters that do not fit a strict grid pattern
|
||||
*
|
||||
* @author Simon André <smn.andre@gmail.com>
|
||||
*/
|
||||
final class GridClusteringAlgorithm implements ClusteringAlgorithmInterface
|
||||
{
|
||||
/**
|
||||
* Clusters a set of points using a fixed grid resolution based on the zoom level.
|
||||
*
|
||||
* @param Point[] $points List of points to be clustered
|
||||
* @param float $zoom The zoom level, determining grid resolution
|
||||
*
|
||||
* @return Cluster[] An array of clusters, each containing grouped points
|
||||
*/
|
||||
public function cluster(iterable $points, float $zoom): array
|
||||
{
|
||||
$gridResolution = 1 << (int) $zoom;
|
||||
$gridSize = 360 / $gridResolution;
|
||||
$invGridSize = 1 / $gridSize;
|
||||
|
||||
$cells = [];
|
||||
|
||||
foreach ($points as $point) {
|
||||
$lng = $point->getLongitude();
|
||||
$lat = $point->getLatitude();
|
||||
$gridX = (int) (($lng + 180) * $invGridSize);
|
||||
$gridY = (int) (($lat + 90) * $invGridSize);
|
||||
$key = ($gridX << 16) | $gridY;
|
||||
|
||||
if (!isset($cells[$key])) {
|
||||
$cells[$key] = new Cluster($point);
|
||||
} else {
|
||||
$cells[$key]->addPoint($point);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($cells);
|
||||
}
|
||||
}
|
||||
76
src/Cluster/MortonClusteringAlgorithm.php
Normal file
76
src/Cluster/MortonClusteringAlgorithm.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Cluster;
|
||||
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
/**
|
||||
* Clustering algorithm based on Morton codes (Z-order curves).
|
||||
*
|
||||
* This approach is optimized for spatial data and preserves locality efficiently.
|
||||
*
|
||||
* Best for:
|
||||
* - Large-scale spatial clustering
|
||||
* - Hierarchical clustering with fast locality-based grouping
|
||||
* - Datasets where preserving spatial proximity is crucial
|
||||
*
|
||||
* Slower for:
|
||||
* - High-dimensional data (beyond 2D/3D) due to Morton code limitations
|
||||
* - Non-spatial or categorical data
|
||||
* - Scenarios requiring dynamic cluster adjustments (e.g., streaming data)
|
||||
*
|
||||
* @author Simon André <smn.andre@gmail.com>
|
||||
*/
|
||||
final class MortonClusteringAlgorithm implements ClusteringAlgorithmInterface
|
||||
{
|
||||
/**
|
||||
* @param Point[] $points
|
||||
*
|
||||
* @return Cluster[]
|
||||
*/
|
||||
public function cluster(iterable $points, float $zoom): array
|
||||
{
|
||||
$resolution = 1 << (int) $zoom;
|
||||
$clustersMap = [];
|
||||
|
||||
foreach ($points as $point) {
|
||||
$xNorm = ($point->getLatitude() + 180) / 360;
|
||||
$yNorm = ($point->getLongitude() + 90) / 180;
|
||||
|
||||
$x = (int) floor($xNorm * $resolution);
|
||||
$y = (int) floor($yNorm * $resolution);
|
||||
|
||||
$x &= 0xFFFF;
|
||||
$y &= 0xFFFF;
|
||||
|
||||
$x = ($x | ($x << 8)) & 0x00FF00FF;
|
||||
$x = ($x | ($x << 4)) & 0x0F0F0F0F;
|
||||
$x = ($x | ($x << 2)) & 0x33333333;
|
||||
$x = ($x | ($x << 1)) & 0x55555555;
|
||||
|
||||
$y = ($y | ($y << 8)) & 0x00FF00FF;
|
||||
$y = ($y | ($y << 4)) & 0x0F0F0F0F;
|
||||
$y = ($y | ($y << 2)) & 0x33333333;
|
||||
$y = ($y | ($y << 1)) & 0x55555555;
|
||||
|
||||
$code = ($y << 1) | $x;
|
||||
|
||||
if (!isset($clustersMap[$code])) {
|
||||
$clustersMap[$code] = new Cluster($point);
|
||||
} else {
|
||||
$clustersMap[$code]->addPoint($point);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($clustersMap);
|
||||
}
|
||||
}
|
||||
79
tests/Cluster/ClusterTest.php
Normal file
79
tests/Cluster/ClusterTest.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Tests\Cluster;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\UX\Map\Cluster\Cluster;
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
class ClusterTest extends TestCase
|
||||
{
|
||||
public function testAddPointAndGetCenter(): void
|
||||
{
|
||||
$point1 = new Point(10.0, 20.0);
|
||||
$cluster = new Cluster($point1);
|
||||
|
||||
$this->assertEquals(10.0, $cluster->getCenter()->getLatitude());
|
||||
$this->assertEquals(20.0, $cluster->getCenter()->getLongitude());
|
||||
|
||||
$point2 = new Point(12.0, 22.0);
|
||||
$cluster->addPoint($point2);
|
||||
|
||||
$this->assertEquals(11.0, $cluster->getCenter()->getLatitude());
|
||||
$this->assertEquals(21.0, $cluster->getCenter()->getLongitude());
|
||||
}
|
||||
|
||||
public function testGetPoints(): void
|
||||
{
|
||||
$point1 = new Point(10.0, 20.0);
|
||||
$point2 = new Point(12.0, 22.0);
|
||||
$cluster = new Cluster($point1);
|
||||
$cluster->addPoint($point2);
|
||||
|
||||
$points = $cluster->getPoints();
|
||||
$this->assertCount(2, $points);
|
||||
$this->assertSame($point1, $points[0]);
|
||||
$this->assertSame($point2, $points[1]);
|
||||
}
|
||||
|
||||
public function testCount(): void
|
||||
{
|
||||
$cluster = new Cluster(new Point(10.0, 20.0));
|
||||
$this->assertCount(1, $cluster);
|
||||
|
||||
$cluster->addPoint(new Point(10.0, 20.0));
|
||||
$this->assertCount(2, $cluster);
|
||||
}
|
||||
|
||||
public function testIterator(): void
|
||||
{
|
||||
$point1 = new Point(10.0, 20.0);
|
||||
$point2 = new Point(12.0, 22.0);
|
||||
$cluster = new Cluster($point1);
|
||||
$cluster->addPoint($point2);
|
||||
|
||||
$points = iterator_to_array($cluster);
|
||||
$this->assertCount(2, $points);
|
||||
$this->assertSame($point1, $points[0]);
|
||||
$this->assertSame($point2, $points[1]);
|
||||
}
|
||||
|
||||
public function testCreateCluster(): void
|
||||
{
|
||||
$point1 = new Point(10.0, 20.0);
|
||||
$cluster = new Cluster($point1);
|
||||
|
||||
$this->assertCount(1, $cluster->getPoints());
|
||||
$this->assertEquals(10.0, $cluster->getCenter()->getLatitude());
|
||||
$this->assertEquals(20.0, $cluster->getCenter()->getLongitude());
|
||||
}
|
||||
}
|
||||
110
tests/Cluster/ClusteringPerformanceTest.php
Normal file
110
tests/Cluster/ClusteringPerformanceTest.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Tests\Cluster;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\UX\Map\Cluster\ClusteringAlgorithmInterface;
|
||||
use Symfony\UX\Map\Cluster\GridClusteringAlgorithm;
|
||||
use Symfony\UX\Map\Cluster\MortonClusteringAlgorithm;
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
class ClusteringPerformanceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @const array<float>
|
||||
*/
|
||||
private const ZOOMS = [
|
||||
2.0,
|
||||
5.0,
|
||||
8.0,
|
||||
];
|
||||
|
||||
/**
|
||||
* @const array<string>
|
||||
*/
|
||||
private const ALGORITHMS = [
|
||||
GridClusteringAlgorithm::class,
|
||||
MortonClusteringAlgorithm::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @return iterable<array{0: ClusteringAlgorithmInterface, 1: float}>
|
||||
*/
|
||||
public static function algorithmProvider(): iterable
|
||||
{
|
||||
foreach (self::ZOOMS as $zoom) {
|
||||
foreach (self::ALGORITHMS as $algorithm) {
|
||||
yield $algorithm.' '.$zoom => [new $algorithm(), $zoom];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario 1: Large number of points (50,000), concentrated area (Paris region).
|
||||
*
|
||||
* @dataProvider algorithmProvider
|
||||
*/
|
||||
public function testScenarioRegion50000(ClusteringAlgorithmInterface $algorithm, float $zoom): void
|
||||
{
|
||||
$points = $this->generatePoints(50000, 48.8, 49, 2.2, 2.5);
|
||||
|
||||
$this->runPerformanceTest($algorithm, $points, $zoom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario 2: Moderate number of points (5,000), broad area (France and surroundings).
|
||||
*
|
||||
* @dataProvider algorithmProvider
|
||||
*/
|
||||
public function testScenarioCountry5000(ClusteringAlgorithmInterface $algorithm, float $zoom): void
|
||||
{
|
||||
$points = $this->generatePoints(5000, 30, 60, -10, 35);
|
||||
|
||||
$this->runPerformanceTest($algorithm, $points, $zoom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario 3: Very large number of points (100,000), global distribution.
|
||||
*
|
||||
* @dataProvider algorithmProvider
|
||||
*/
|
||||
public function testScenarioWorld100000(ClusteringAlgorithmInterface $algorithm, float $zoom): void
|
||||
{
|
||||
$points = $this->generatePoints(100000, -90, 90, -180, 180);
|
||||
|
||||
$this->runPerformanceTest($algorithm, $points, $zoom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Point> $points
|
||||
*/
|
||||
private function runPerformanceTest(ClusteringAlgorithmInterface $algorithm, array $points, float $zoom): void
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$algorithm->cluster($points, $zoom);
|
||||
$elapsed = microtime(true) - $startTime;
|
||||
|
||||
$this->assertLessThan(2.0, $elapsed, $algorithm::class." took too long: {$elapsed} seconds (zoom {$zoom}, ".\count($points).' points)');
|
||||
}
|
||||
|
||||
private function generatePoints(int $count, float $latMin, float $latMax, float $lngMin, float $lngMax): array
|
||||
{
|
||||
$points = [];
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$lat = random_int((int) ($latMin * 100), (int) ($latMax * 100)) / 100.0;
|
||||
$lng = random_int((int) ($lngMin * 100), (int) ($lngMax * 100)) / 100.0;
|
||||
$points[] = new Point($lat, $lng);
|
||||
}
|
||||
|
||||
return $points;
|
||||
}
|
||||
}
|
||||
98
tests/Cluster/GridClusteringAlgorithmTest.php
Normal file
98
tests/Cluster/GridClusteringAlgorithmTest.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Tests\Cluster;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\UX\Map\Cluster\Cluster;
|
||||
use Symfony\UX\Map\Cluster\GridClusteringAlgorithm;
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
class GridClusteringAlgorithmTest extends TestCase
|
||||
{
|
||||
public function testSinglePointCreatesSingleCluster(): void
|
||||
{
|
||||
$point = new Point(10.0, 20.0);
|
||||
$algorithm = new GridClusteringAlgorithm();
|
||||
$clusters = $algorithm->cluster([$point], 1.0);
|
||||
|
||||
$this->assertCount(1, $clusters);
|
||||
|
||||
/** @var Cluster $cluster */
|
||||
$cluster = $clusters[0];
|
||||
|
||||
$this->assertEquals(10.0, $cluster->getCenter()->getLatitude());
|
||||
$this->assertEquals(20.0, $cluster->getCenter()->getLongitude());
|
||||
$this->assertCount(1, $cluster->getPoints());
|
||||
}
|
||||
|
||||
public function testPointsInSameGridAreClusteredTogether(): void
|
||||
{
|
||||
$point1 = new Point(10.0, 20.0);
|
||||
$point2 = new Point(10.1, 20.1);
|
||||
$algorithm = new GridClusteringAlgorithm();
|
||||
|
||||
$clusters = $algorithm->cluster([$point1, $point2], 1.0);
|
||||
|
||||
$this->assertCount(1, $clusters, 'One cluster should have been created due to the low zoom value.');
|
||||
|
||||
$cluster = $clusters[0];
|
||||
|
||||
$this->assertCount(2, $cluster->getPoints());
|
||||
$this->assertEqualsWithDelta(10.05, $cluster->getCenter()->getLatitude(), 0.0001);
|
||||
$this->assertEqualsWithDelta(20.05, $cluster->getCenter()->getLongitude(), 0.0001);
|
||||
}
|
||||
|
||||
public function testPointsInDifferentGridsAreNotClustered(): void
|
||||
{
|
||||
$point1 = new Point(10.0, 20.0);
|
||||
$point2 = new Point(-10.0, -20.0); // Far away
|
||||
$algorithm = new GridClusteringAlgorithm();
|
||||
|
||||
$clusters = $algorithm->cluster([$point1, $point2], 5.0);
|
||||
|
||||
$this->assertCount(2, $clusters, 'Two clusters should have created due to the high zoom value.');
|
||||
}
|
||||
|
||||
public function testEmptyPointsArray(): void
|
||||
{
|
||||
$algorithm = new GridClusteringAlgorithm();
|
||||
|
||||
// Empty points array
|
||||
$clusters = $algorithm->cluster([], 2.0);
|
||||
|
||||
$this->assertCount(0, $clusters);
|
||||
}
|
||||
|
||||
public function testLargeCoordinates(): void
|
||||
{
|
||||
$point1 = new Point(89.9, 179.9);
|
||||
$point2 = new Point(-89.9, -179.9);
|
||||
$algorithm = new GridClusteringAlgorithm();
|
||||
|
||||
$clusters = $algorithm->cluster([$point1, $point2], 3.0);
|
||||
|
||||
$this->assertGreaterThanOrEqual(1, \count($clusters));
|
||||
}
|
||||
|
||||
public function testZeroZoomLevel(): void
|
||||
{
|
||||
$point1 = new Point(10, 20);
|
||||
$point2 = new Point(30, 40);
|
||||
$algorithm = new GridClusteringAlgorithm();
|
||||
|
||||
// With zoom 0, everything should be in one big cluster.
|
||||
$clusters = $algorithm->cluster([$point1, $point2], 0.0);
|
||||
|
||||
$this->assertCount(1, $clusters);
|
||||
$this->assertCount(2, $clusters[0]->getPoints());
|
||||
}
|
||||
}
|
||||
83
tests/Cluster/MortonClusteringAlgorithmTest.php
Normal file
83
tests/Cluster/MortonClusteringAlgorithmTest.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\UX\Map\Tests\Cluster;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\UX\Map\Cluster\Cluster;
|
||||
use Symfony\UX\Map\Cluster\MortonClusteringAlgorithm;
|
||||
use Symfony\UX\Map\Point;
|
||||
|
||||
class MortonClusteringAlgorithmTest extends TestCase
|
||||
{
|
||||
public function testSinglePointCreatesSingleCluster(): void
|
||||
{
|
||||
$point = new Point(10.0, 20.0);
|
||||
$algorithm = new MortonClusteringAlgorithm();
|
||||
$clusters = $algorithm->cluster([$point], 1.0);
|
||||
|
||||
$this->assertCount(1, $clusters);
|
||||
|
||||
/** @var Cluster $cluster */
|
||||
$cluster = $clusters[0];
|
||||
|
||||
$this->assertEquals(10.0, $cluster->getCenter()->getLatitude());
|
||||
$this->assertEquals(20.0, $cluster->getCenter()->getLongitude());
|
||||
$this->assertCount(1, $cluster->getPoints());
|
||||
}
|
||||
|
||||
public function testPointsWithSameMortonCodeAreClustered(): void
|
||||
{
|
||||
// These points should have the same Morton code at zoom level 1
|
||||
$point1 = new Point(45.0, 90.0);
|
||||
$point2 = new Point(45.1, 90.1);
|
||||
$algorithm = new MortonClusteringAlgorithm();
|
||||
|
||||
$clusters = $algorithm->cluster([$point1, $point2], 1.0);
|
||||
|
||||
$this->assertCount(1, $clusters);
|
||||
$this->assertCount(2, $clusters[0]->getPoints());
|
||||
}
|
||||
|
||||
public function testPointsWithDifferentMortonCodeAreNotClustered(): void
|
||||
{
|
||||
// These points will have different Morton codes at zoom level 5
|
||||
$point1 = new Point(45.0, 90.0);
|
||||
$point2 = new Point(-45.0, -90.0);
|
||||
$algorithm = new MortonClusteringAlgorithm();
|
||||
|
||||
$clusters = $algorithm->cluster([$point1, $point2], 5.0);
|
||||
|
||||
$this->assertCount(2, $clusters);
|
||||
}
|
||||
|
||||
public function testEmptyPointsArray(): void
|
||||
{
|
||||
$algorithm = new MortonClusteringAlgorithm();
|
||||
|
||||
$clusters = $algorithm->cluster([], 2.0);
|
||||
|
||||
$this->assertCount(0, $clusters);
|
||||
}
|
||||
|
||||
public function testZeroZoomLevel(): void
|
||||
{
|
||||
$point1 = new Point(10, 20);
|
||||
$point2 = new Point(30, 40);
|
||||
$algorithm = new MortonClusteringAlgorithm();
|
||||
|
||||
$clusters = $algorithm->cluster([$point1, $point2], 0.0);
|
||||
|
||||
// With zoom 0, everything should be in one big cluster
|
||||
$this->assertCount(1, $clusters);
|
||||
$this->assertCount(2, $clusters[0]->getPoints());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user