Skip to content

Commit 7356f88

Browse files
feat(#305): add support for PostGIS operators
1 parent c4b4609 commit 7356f88

File tree

20 files changed

+820
-4
lines changed

20 files changed

+820
-4
lines changed

fixtures/MartinGeorgiev/Doctrine/Entity/ContainsGeometries.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
class ContainsGeometries extends Entity
1212
{
1313
#[ORM\Column(type: 'geometry')]
14-
public ?WktSpatialData $geometry1 = null;
14+
public WktSpatialData $geometry1;
1515

1616
#[ORM\Column(type: 'geometry')]
17-
public ?WktSpatialData $geometry2 = null;
17+
public WktSpatialData $geometry2;
1818

1919
#[ORM\Column(type: 'geography')]
20-
public ?WktSpatialData $geography1 = null;
20+
public WktSpatialData $geography1;
2121

2222
#[ORM\Column(type: 'geography')]
23-
public ?WktSpatialData $geography2 = null;
23+
public WktSpatialData $geography2;
2424
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS 2D bounding box distance operator (using <#>).
9+
*
10+
* Returns the 2D distance between A and B bounding boxes.
11+
* This is useful for index-based distance queries.
12+
*
13+
* @see https://postgis.net/docs/reference.html#Operators_Distance
14+
* @since 3.5
15+
*
16+
* @author Martin Georgiev <martin.georgiev@gmail.com>
17+
*
18+
* @example Using it in DQL: "SELECT BOUNDING_BOX_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2"
19+
* Returns numeric distance value.
20+
*/
21+
class BoundingBoxDistance extends BaseFunction
22+
{
23+
protected function customizeFunction(): void
24+
{
25+
$this->setFunctionPrototype('(%s <#> %s)');
26+
$this->addNodeMapping('StringPrimary');
27+
$this->addNodeMapping('StringPrimary');
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS 2D distance between geometries operator (using <->).
9+
*
10+
* Returns the 2D distance between A and B geometries.
11+
* This is different from the existing Distance class which uses <@> for point distance.
12+
*
13+
* @see https://postgis.net/docs/reference.html#Operators_Distance
14+
* @since 3.5
15+
*
16+
* @author Martin Georgiev <martin.georgiev@gmail.com>
17+
*
18+
* @example Using it in DQL: "SELECT GEOMETRY_DISTANCE(g1.geometry, g2.geometry) FROM Entity g1, Entity g2"
19+
* Returns numeric distance value.
20+
*/
21+
class GeometryDistance extends BaseFunction
22+
{
23+
protected function customizeFunction(): void
24+
{
25+
$this->setFunctionPrototype('(%s <-> %s)');
26+
$this->addNodeMapping('StringPrimary');
27+
$this->addNodeMapping('StringPrimary');
28+
}
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS bounding box strictly above operator (using |>>).
9+
*
10+
* Returns TRUE if A's bounding box is strictly above B's.
11+
*
12+
* @see https://postgis.net/docs/reference.html#Operators_Geometry
13+
* @since 3.5
14+
*
15+
* @author Martin Georgiev <martin.georgiev@gmail.com>
16+
*
17+
* @example Using it in DQL with boolean comparison: "WHERE STRICTLY_ABOVE(g1.geometry, g2.geometry) = TRUE"
18+
* Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL.
19+
*/
20+
class StrictlyAbove extends BaseFunction
21+
{
22+
protected function customizeFunction(): void
23+
{
24+
$this->setFunctionPrototype('(%s |>> %s)');
25+
$this->addNodeMapping('StringPrimary');
26+
$this->addNodeMapping('StringPrimary');
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS bounding box strictly below operator (using <<|).
9+
*
10+
* Returns TRUE if A's bounding box is strictly below B's.
11+
*
12+
* @see https://postgis.net/docs/reference.html#Operators_Geometry
13+
* @since 3.5
14+
*
15+
* @author Martin Georgiev <martin.georgiev@gmail.com>
16+
*
17+
* @example Using it in DQL with boolean comparison: "WHERE STRICTLY_BELOW(g1.geometry, g2.geometry) = TRUE"
18+
* Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL.
19+
*/
20+
class StrictlyBelow extends BaseFunction
21+
{
22+
protected function customizeFunction(): void
23+
{
24+
$this->setFunctionPrototype('(%s <<| %s)');
25+
$this->addNodeMapping('StringPrimary');
26+
$this->addNodeMapping('StringPrimary');
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS bounding box strictly to the left operator (using <<).
9+
*
10+
* Returns TRUE if A's bounding box is strictly to the left of B's.
11+
*
12+
* @see https://postgis.net/docs/reference.html#Operators_Geometry
13+
* @since 3.5
14+
*
15+
* @author Martin Georgiev <martin.georgiev@gmail.com>
16+
*
17+
* @example Using it in DQL with boolean comparison: "WHERE STRICTLY_LEFT(g1.geometry, g2.geometry) = TRUE"
18+
* Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL.
19+
*/
20+
class StrictlyLeft extends BaseFunction
21+
{
22+
protected function customizeFunction(): void
23+
{
24+
$this->setFunctionPrototype('(%s << %s)');
25+
$this->addNodeMapping('StringPrimary');
26+
$this->addNodeMapping('StringPrimary');
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostGIS bounding box strictly to the right operator (using >>).
9+
*
10+
* Returns TRUE if A's bounding box is strictly to the right of B's.
11+
*
12+
* @see https://postgis.net/docs/reference.html#Operators_Geometry
13+
* @since 3.5
14+
*
15+
* @author Martin Georgiev <martin.georgiev@gmail.com>
16+
*
17+
* @example Using it in DQL with boolean comparison: "WHERE STRICTLY_RIGHT(g1.geometry, g2.geometry) = TRUE"
18+
* Returns boolean, must be used with "= TRUE" or "= FALSE" in DQL.
19+
*/
20+
class StrictlyRight extends BaseFunction
21+
{
22+
protected function customizeFunction(): void
23+
{
24+
$this->setFunctionPrototype('(%s >> %s)');
25+
$this->addNodeMapping('StringPrimary');
26+
$this->addNodeMapping('StringPrimary');
27+
}
28+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Integration\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BoundingBoxDistance;
8+
use PHPUnit\Framework\Attributes\Test;
9+
10+
class BoundingBoxDistanceTest extends SpatialOperatorTestCase
11+
{
12+
protected function getStringFunctions(): array
13+
{
14+
return [
15+
'BOUNDING_BOX_DISTANCE' => BoundingBoxDistance::class,
16+
];
17+
}
18+
19+
#[Test]
20+
public function bounding_box_distance_returns_zero_for_identical_geometries(): void
21+
{
22+
// Identical geometries have zero bounding box distance
23+
$dql = "SELECT BOUNDING_BOX_DISTANCE('POINT(1 1)', 'POINT(1 1)') as distance
24+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
25+
WHERE g.id = 1";
26+
27+
$result = $this->executeDqlQuery($dql);
28+
$this->assertEquals(0, $result[0]['distance'], 'Bounding box distance for identical geometries should be 0');
29+
}
30+
31+
#[Test]
32+
public function bounding_box_distance_returns_zero_for_overlapping_bounding_boxes(): void
33+
{
34+
// Overlapping polygons have zero bounding box distance
35+
$dql = "SELECT BOUNDING_BOX_DISTANCE('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 'POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))') as distance
36+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
37+
WHERE g.id = 1";
38+
39+
$result = $this->executeDqlQuery($dql);
40+
$this->assertEquals(0, $result[0]['distance'], 'Overlapping bounding boxes should have distance 0');
41+
}
42+
43+
#[Test]
44+
public function bounding_box_distance_returns_correct_distance_for_separated_geometries(): void
45+
{
46+
// Distance between separated points
47+
$dql = "SELECT BOUNDING_BOX_DISTANCE('POINT(0 0)', 'POINT(3 4)') as distance
48+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
49+
WHERE g.id = 1";
50+
51+
$result = $this->executeDqlQuery($dql);
52+
$this->assertEquals(5.0, $result[0]['distance'], 'Bounding box distance between (0,0) and (3,4) should be 5');
53+
}
54+
55+
#[Test]
56+
public function bounding_box_distance_with_separated_polygons(): void
57+
{
58+
// Distance between non-overlapping polygons
59+
$dql = "SELECT BOUNDING_BOX_DISTANCE('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))', 'POLYGON((3 3, 4 3, 4 4, 3 4, 3 3))') as distance
60+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
61+
WHERE g.id = 1";
62+
63+
$result = $this->executeDqlQuery($dql);
64+
$this->assertIsNumeric($result[0]['distance']);
65+
$this->assertGreaterThan(0, $result[0]['distance']);
66+
// Distance should be approximately sqrt((3-1)² + (3-1)²) = sqrt(8) ≈ 2.83
67+
$this->assertEqualsWithDelta(2.83, $result[0]['distance'], 0.1, 'Distance between separated polygons');
68+
}
69+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Integration\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\GeometryDistance;
8+
use PHPUnit\Framework\Attributes\Test;
9+
10+
class GeometryDistanceTest extends SpatialOperatorTestCase
11+
{
12+
protected function getStringFunctions(): array
13+
{
14+
return [
15+
'GEOMETRY_DISTANCE' => GeometryDistance::class,
16+
];
17+
}
18+
19+
#[Test]
20+
public function geometry_distance_returns_correct_distance_between_test_points(): void
21+
{
22+
// Distance between POINT(0 0) and POINT(1 1) from test data
23+
$dql = 'SELECT GEOMETRY_DISTANCE(g.geometry1, g.geometry2) as distance
24+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
25+
WHERE g.id = 1';
26+
27+
$result = $this->executeDqlQuery($dql);
28+
$this->assertIsNumeric($result[0]['distance']);
29+
$this->assertGreaterThan(0, $result[0]['distance']);
30+
// Distance between (0,0) and (1,1) should be sqrt(2) ≈ 1.414
31+
$this->assertEqualsWithDelta(1.414, $result[0]['distance'], 0.01, 'Distance between (0,0) and (1,1)');
32+
}
33+
34+
#[Test]
35+
public function geometry_distance_returns_zero_for_identical_geometries(): void
36+
{
37+
// Identical geometries should have zero distance
38+
$dql = 'SELECT GEOMETRY_DISTANCE(g.geometry1, g.geometry1) as distance
39+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
40+
WHERE g.id = 1';
41+
42+
$result = $this->executeDqlQuery($dql);
43+
$this->assertEquals(0, $result[0]['distance'], 'Distance between identical geometries should be 0');
44+
}
45+
46+
#[Test]
47+
public function geometry_distance_with_horizontal_points(): void
48+
{
49+
// Distance between POINT(0 0) and POINT(5 0) should be 5
50+
$dql = "SELECT GEOMETRY_DISTANCE('POINT(0 0)', 'POINT(5 0)') as distance
51+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
52+
WHERE g.id = 1";
53+
54+
$result = $this->executeDqlQuery($dql);
55+
$this->assertEquals(5.0, $result[0]['distance'], 'Horizontal distance should be 5');
56+
}
57+
58+
#[Test]
59+
public function geometry_distance_with_vertical_points(): void
60+
{
61+
// Distance between POINT(0 0) and POINT(0 3) should be 3
62+
$dql = "SELECT GEOMETRY_DISTANCE('POINT(0 0)', 'POINT(0 3)') as distance
63+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
64+
WHERE g.id = 1";
65+
66+
$result = $this->executeDqlQuery($dql);
67+
$this->assertEquals(3.0, $result[0]['distance'], 'Vertical distance should be 3');
68+
}
69+
70+
#[Test]
71+
public function geometry_distance_with_geography_types(): void
72+
{
73+
// Geography types should return distance in meters
74+
$dql = 'SELECT GEOMETRY_DISTANCE(g.geography1, g.geography2) as distance
75+
FROM Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries g
76+
WHERE g.id = 1';
77+
78+
$result = $this->executeDqlQuery($dql);
79+
$this->assertIsNumeric($result[0]['distance']);
80+
$this->assertGreaterThan(1000000, $result[0]['distance']); // Lisbon to London is > 1M meters
81+
}
82+
}

0 commit comments

Comments
 (0)