Skip to content

Commit f06a57a

Browse files
authored
Merge pull request #692 from utopia-php/spatial-distance-fix
SpatialValidator update, distance in meter update
2 parents 57358ac + 297b29a commit f06a57a

File tree

9 files changed

+275
-9
lines changed

9 files changed

+275
-9
lines changed

src/Database/Adapter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,13 @@ abstract public function getSupportForSpatialIndexOrder(): bool;
10771077
*/
10781078
abstract public function getSupportForBoundaryInclusiveContains(): bool;
10791079

1080+
/**
1081+
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
1082+
*
1083+
* @return bool
1084+
*/
1085+
abstract public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool;
1086+
10801087
/**
10811088
* Get current attribute count from collection document
10821089
*

src/Database/Adapter/MariaDB.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,14 +1358,16 @@ public function deleteDocument(string $collection, string $id): bool
13581358
* @param Query $query
13591359
* @param array<string, mixed> $binds
13601360
* @param string $attribute
1361+
* @param string $type
13611362
* @param string $alias
13621363
* @param string $placeholder
13631364
* @return string
13641365
*/
1365-
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
1366+
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $type, string $alias, string $placeholder): string
13661367
{
13671368
$distanceParams = $query->getValues()[0];
1368-
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
1369+
$wkt = $this->convertArrayToWKT($distanceParams[0]);
1370+
$binds[":{$placeholder}_0"] = $wkt;
13691371
$binds[":{$placeholder}_1"] = $distanceParams[1];
13701372

13711373
$useMeters = isset($distanceParams[2]) && $distanceParams[2] === true;
@@ -1388,6 +1390,11 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str
13881390
}
13891391

13901392
if ($useMeters) {
1393+
$wktType = $this->getSpatialTypeFromWKT($wkt);
1394+
$attrType = strtolower($type);
1395+
if ($wktType != Database::VAR_POINT || $attrType != Database::VAR_POINT) {
1396+
throw new DatabaseException('Distance in meters is not supported between '.$attrType . ' and '. $wktType);
1397+
}
13911398
return "ST_DISTANCE_SPHERE({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0), 6371000) {$operator} :{$placeholder}_1";
13921399
}
13931400
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) {$operator} :{$placeholder}_1";
@@ -1399,11 +1406,12 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str
13991406
* @param Query $query
14001407
* @param array<string, mixed> $binds
14011408
* @param string $attribute
1409+
* @param string $type
14021410
* @param string $alias
14031411
* @param string $placeholder
14041412
* @return string
14051413
*/
1406-
protected function handleSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
1414+
protected function handleSpatialQueries(Query $query, array &$binds, string $attribute, string $type, string $alias, string $placeholder): string
14071415
{
14081416
switch ($query->getMethod()) {
14091417
case Query::TYPE_CROSSES:
@@ -1418,7 +1426,7 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
14181426
case Query::TYPE_DISTANCE_NOT_EQUAL:
14191427
case Query::TYPE_DISTANCE_GREATER_THAN:
14201428
case Query::TYPE_DISTANCE_LESS_THAN:
1421-
return $this->handleDistanceSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
1429+
return $this->handleDistanceSpatialQueries($query, $binds, $attribute, $type, $alias, $placeholder);
14221430

14231431
case Query::TYPE_INTERSECTS:
14241432
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($query->getValues()[0]);
@@ -1487,7 +1495,7 @@ protected function getSQLCondition(Query $query, array &$binds, array $attribute
14871495
$attributeType = $this->getAttributeType($query->getAttribute(), $attributes);
14881496

14891497
if (in_array($attributeType, Database::SPATIAL_TYPES)) {
1490-
return $this->handleSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
1498+
return $this->handleSpatialQueries($query, $binds, $attribute, $attributeType, $alias, $placeholder);
14911499
}
14921500

14931501
switch ($query->getMethod()) {
@@ -1868,4 +1876,14 @@ public function getSupportForSpatialIndexOrder(): bool
18681876
{
18691877
return true;
18701878
}
1879+
1880+
/**
1881+
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
1882+
*
1883+
* @return bool
1884+
*/
1885+
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
1886+
{
1887+
return false;
1888+
}
18711889
}

src/Database/Adapter/MySQL.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ public function getSizeOfCollectionOnDisk(string $collection): int
8585
* @param Query $query
8686
* @param array<string, mixed> $binds
8787
* @param string $attribute
88+
* @param string $type
8889
* @param string $alias
8990
* @param string $placeholder
9091
* @return string
9192
*/
92-
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
93+
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $type, string $alias, string $placeholder): string
9394
{
9495
$distanceParams = $query->getValues()[0];
9596
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
@@ -173,4 +174,14 @@ public function getSupportForSpatialIndexOrder(): bool
173174
{
174175
return false;
175176
}
177+
178+
/**
179+
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
180+
*
181+
* @return bool
182+
*/
183+
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
184+
{
185+
return true;
186+
}
176187
}

src/Database/Adapter/Pool.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,4 +530,13 @@ public function getSupportForSpatialIndexOrder(): bool
530530
{
531531
return $this->delegate(__FUNCTION__, \func_get_args());
532532
}
533+
/**
534+
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
535+
*
536+
* @return bool
537+
*/
538+
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
539+
{
540+
return $this->delegate(__FUNCTION__, \func_get_args());
541+
}
533542
}

src/Database/Adapter/Postgres.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,9 +1488,8 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str
14881488
}
14891489

14901490
if ($meters) {
1491-
// Transform both attribute and input geometry to 3857 (meters) for distance calculation
1492-
$attr = "ST_Transform({$alias}.{$attribute}, 3857)";
1493-
$geom = "ST_Transform(ST_GeomFromText(:{$placeholder}_0, " . Database::SRID . "), 3857)";
1491+
$attr = "({$alias}.{$attribute}::geography)";
1492+
$geom = "ST_SetSRID(ST_GeomFromText(:{$placeholder}_0), " . Database::SRID . ")::geography";
14941493
return "ST_Distance({$attr}, {$geom}) {$operator} :{$placeholder}_1";
14951494
}
14961495

@@ -1982,4 +1981,14 @@ public function getSupportForSpatialIndexOrder(): bool
19821981
{
19831982
return false;
19841983
}
1984+
1985+
/**
1986+
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
1987+
*
1988+
* @return bool
1989+
*/
1990+
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
1991+
{
1992+
return true;
1993+
}
19851994
}

src/Database/Adapter/SQL.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2657,4 +2657,14 @@ public function sum(Document $collection, string $attribute, array $queries = []
26572657

26582658
return $result['sum'] ?? 0;
26592659
}
2660+
2661+
public function getSpatialTypeFromWKT(string $wkt): string
2662+
{
2663+
$wkt = trim($wkt);
2664+
$pos = strpos($wkt, '(');
2665+
if ($pos === false) {
2666+
throw new DatabaseException("Invalid spatial type");
2667+
}
2668+
return strtolower(trim(substr($wkt, 0, $pos)));
2669+
}
26602670
}

src/Database/Adapter/SQLite.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,4 +1263,14 @@ public function getSupportForBoundaryInclusiveContains(): bool
12631263
{
12641264
return false;
12651265
}
1266+
1267+
/**
1268+
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
1269+
*
1270+
* @return bool
1271+
*/
1272+
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
1273+
{
1274+
return false;
1275+
}
12661276
}

src/Database/Validator/Spatial.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ public function getType(): string
144144
return self::TYPE_ARRAY;
145145
}
146146

147+
public function getSpatialType(): string
148+
{
149+
return $this->spatialType;
150+
}
151+
147152
/**
148153
* Main validation entrypoint
149154
*/

tests/e2e/Adapter/Scopes/SpatialTests.php

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2158,4 +2158,191 @@ public function testSpatialDistanceInMeter(): void
21582158
$database->deleteCollection($collectionName);
21592159
}
21602160
}
2161+
2162+
public function testSpatialDistanceInMeterForMultiDimensionGeometry(): void
2163+
{
2164+
/** @var Database $database */
2165+
$database = static::getDatabase();
2166+
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
2167+
$this->markTestSkipped('Adapter does not support spatial attributes');
2168+
}
2169+
2170+
if (!$database->getAdapter()->getSupportForDistanceBetweenMultiDimensionGeometryInMeters()) {
2171+
$this->markTestSkipped('Adapter does not support spatial distance(in meter) for multidimension');
2172+
}
2173+
2174+
$multiCollection = 'spatial_distance_meters_multi_';
2175+
try {
2176+
$database->createCollection($multiCollection);
2177+
2178+
// Create spatial attributes
2179+
$this->assertEquals(true, $database->createAttribute($multiCollection, 'loc', Database::VAR_POINT, 0, true));
2180+
$this->assertEquals(true, $database->createAttribute($multiCollection, 'line', Database::VAR_LINESTRING, 0, true));
2181+
$this->assertEquals(true, $database->createAttribute($multiCollection, 'poly', Database::VAR_POLYGON, 0, true));
2182+
2183+
// Create indexes
2184+
$this->assertEquals(true, $database->createIndex($multiCollection, 'idx_loc', Database::INDEX_SPATIAL, ['loc']));
2185+
$this->assertEquals(true, $database->createIndex($multiCollection, 'idx_line', Database::INDEX_SPATIAL, ['line']));
2186+
$this->assertEquals(true, $database->createIndex($multiCollection, 'idx_poly', Database::INDEX_SPATIAL, ['poly']));
2187+
2188+
// Geometry sets: near origin and far east
2189+
$docNear = $database->createDocument($multiCollection, new Document([
2190+
'$id' => 'near',
2191+
'loc' => [0.0000, 0.0000],
2192+
'line' => [[0.0000, 0.0000], [0.0010, 0.0000]], // ~111m
2193+
'poly' => [[
2194+
[-0.0010, -0.0010],
2195+
[-0.0010, 0.0010],
2196+
[ 0.0010, 0.0010],
2197+
[ 0.0010, -0.0010],
2198+
[-0.0010, -0.0010] // closed
2199+
]],
2200+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())]
2201+
]));
2202+
2203+
$docFar = $database->createDocument($multiCollection, new Document([
2204+
'$id' => 'far',
2205+
'loc' => [0.2000, 0.0000], // ~22 km east
2206+
'line' => [[0.2000, 0.0000], [0.2020, 0.0000]],
2207+
'poly' => [[
2208+
[0.1980, -0.0020],
2209+
[0.1980, 0.0020],
2210+
[0.2020, 0.0020],
2211+
[0.2020, -0.0020],
2212+
[0.1980, -0.0020] // closed
2213+
]],
2214+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())]
2215+
]));
2216+
2217+
$this->assertInstanceOf(Document::class, $docNear);
2218+
$this->assertInstanceOf(Document::class, $docFar);
2219+
2220+
// polygon vs polygon (~1 km from near, ~22 km from far)
2221+
$polyPolyWithin3km = $database->find($multiCollection, [
2222+
Query::distanceLessThan('poly', [[
2223+
[0.0080, -0.0010],
2224+
[0.0080, 0.0010],
2225+
[0.0110, 0.0010],
2226+
[0.0110, -0.0010],
2227+
[0.0080, -0.0010] // closed
2228+
]], 3000, true)
2229+
], Database::PERMISSION_READ);
2230+
$this->assertCount(1, $polyPolyWithin3km);
2231+
$this->assertEquals('near', $polyPolyWithin3km[0]->getId());
2232+
2233+
$polyPolyGreater3km = $database->find($multiCollection, [
2234+
Query::distanceGreaterThan('poly', [[
2235+
[0.0080, -0.0010],
2236+
[0.0080, 0.0010],
2237+
[0.0110, 0.0010],
2238+
[0.0110, -0.0010],
2239+
[0.0080, -0.0010] // closed
2240+
]], 3000, true)
2241+
], Database::PERMISSION_READ);
2242+
$this->assertCount(1, $polyPolyGreater3km);
2243+
$this->assertEquals('far', $polyPolyGreater3km[0]->getId());
2244+
2245+
// point vs polygon (~0 km near, ~22 km far)
2246+
$ptPolyWithin500 = $database->find($multiCollection, [
2247+
Query::distanceLessThan('loc', [[
2248+
[-0.0010, -0.0010],
2249+
[-0.0010, 0.0020],
2250+
[ 0.0020, 0.0020],
2251+
[-0.0010, -0.0010]
2252+
]], 500, true)
2253+
], Database::PERMISSION_READ);
2254+
$this->assertCount(1, $ptPolyWithin500);
2255+
$this->assertEquals('near', $ptPolyWithin500[0]->getId());
2256+
2257+
$ptPolyGreater500 = $database->find($multiCollection, [
2258+
Query::distanceGreaterThan('loc', [[
2259+
[-0.0010, -0.0010],
2260+
[-0.0010, 0.0020],
2261+
[ 0.0020, 0.0020],
2262+
[-0.0010, -0.0010]
2263+
]], 500, true)
2264+
], Database::PERMISSION_READ);
2265+
$this->assertCount(1, $ptPolyGreater500);
2266+
$this->assertEquals('far', $ptPolyGreater500[0]->getId());
2267+
2268+
// Zero-distance checks
2269+
$lineEqualZero = $database->find($multiCollection, [
2270+
Query::distanceEqual('line', [[0.0000, 0.0000], [0.0010, 0.0000]], 0, true)
2271+
], Database::PERMISSION_READ);
2272+
$this->assertNotEmpty($lineEqualZero);
2273+
$this->assertEquals('near', $lineEqualZero[0]->getId());
2274+
2275+
$polyEqualZero = $database->find($multiCollection, [
2276+
Query::distanceEqual('poly', [[
2277+
[-0.0010, -0.0010],
2278+
[-0.0010, 0.0010],
2279+
[ 0.0010, 0.0010],
2280+
[ 0.0010, -0.0010],
2281+
[-0.0010, -0.0010]
2282+
]], 0, true)
2283+
], Database::PERMISSION_READ);
2284+
$this->assertNotEmpty($polyEqualZero);
2285+
$this->assertEquals('near', $polyEqualZero[0]->getId());
2286+
2287+
} finally {
2288+
$database->deleteCollection($multiCollection);
2289+
}
2290+
}
2291+
2292+
public function testSpatialDistanceInMeterError(): void
2293+
{
2294+
/** @var Database $database */
2295+
$database = static::getDatabase();
2296+
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
2297+
$this->markTestSkipped('Adapter does not support spatial attributes');
2298+
}
2299+
2300+
if ($database->getAdapter()->getSupportForDistanceBetweenMultiDimensionGeometryInMeters()) {
2301+
$this->markTestSkipped('Adapter supports spatial distance (in meter) for multidimension geometries');
2302+
}
2303+
2304+
$collection = 'spatial_distance_error_test';
2305+
$database->createCollection($collection);
2306+
$this->assertEquals(true, $database->createAttribute($collection, 'loc', Database::VAR_POINT, 0, true));
2307+
$this->assertEquals(true, $database->createAttribute($collection, 'line', Database::VAR_LINESTRING, 0, true));
2308+
$this->assertEquals(true, $database->createAttribute($collection, 'poly', Database::VAR_POLYGON, 0, true));
2309+
2310+
$doc = $database->createDocument($collection, new Document([
2311+
'$id' => 'doc1',
2312+
'loc' => [0.0, 0.0],
2313+
'line' => [[0.0, 0.0], [0.001, 0.0]],
2314+
'poly' => [[[ -0.001, -0.001 ], [ -0.001, 0.001 ], [ 0.001, 0.001 ], [ -0.001, -0.001 ]]],
2315+
'$permissions' => []
2316+
]));
2317+
$this->assertInstanceOf(Document::class, $doc);
2318+
2319+
// Invalid geometry pairs
2320+
$cases = [
2321+
['attr' => 'line', 'geom' => [0.002, 0.0], 'expected' => ['linestring', 'point']],
2322+
['attr' => 'poly', 'geom' => [0.002, 0.0], 'expected' => ['polygon', 'point']],
2323+
['attr' => 'loc', 'geom' => [[0.0, 0.0], [0.001, 0.001]], 'expected' => ['point', 'linestring']],
2324+
['attr' => 'poly', 'geom' => [[0.0, 0.0], [0.001, 0.001]], 'expected' => ['polygon', 'linestring']],
2325+
['attr' => 'loc', 'geom' => [[[0.0, 0.0], [0.001, 0.0], [0.001, 0.001], [0.0, 0.0]]], 'expected' => ['point', 'polygon']],
2326+
['attr' => 'line', 'geom' => [[[0.0, 0.0], [0.001, 0.0], [0.001, 0.001], [0.0, 0.0]]], 'expected' => ['linestring', 'polygon']],
2327+
['attr' => 'poly', 'geom' => [[[0.002, -0.001], [0.002, 0.001], [0.004, 0.001], [0.002, -0.001]]], 'expected' => ['polygon', 'polygon']],
2328+
['attr' => 'line', 'geom' => [[0.002, 0.0], [0.003, 0.0]], 'expected' => ['linestring', 'linestring']],
2329+
];
2330+
2331+
foreach ($cases as $case) {
2332+
try {
2333+
$database->find($collection, [
2334+
Query::distanceLessThan($case['attr'], $case['geom'], 1000, true)
2335+
]);
2336+
$this->fail('Expected Exception not thrown for ' . implode(' vs ', $case['expected']));
2337+
} catch (\Exception $e) {
2338+
$this->assertInstanceOf(\Exception::class, $e);
2339+
2340+
// Validate exception message contains correct type names
2341+
$msg = strtolower($e->getMessage());
2342+
var_dump($msg);
2343+
$this->assertStringContainsString($case['expected'][0], $msg, 'Attr type missing in exception');
2344+
$this->assertStringContainsString($case['expected'][1], $msg, 'Geom type missing in exception');
2345+
}
2346+
}
2347+
}
21612348
}

0 commit comments

Comments
 (0)