Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,13 @@ abstract public function getSupportForSpatialIndexOrder(): bool;
*/
abstract public function getSupportForBoundaryInclusiveContains(): bool;

/**
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
*
* @return bool
*/
abstract public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool;

/**
* Get current attribute count from collection document
*
Expand Down
28 changes: 23 additions & 5 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1358,14 +1358,16 @@ public function deleteDocument(string $collection, string $id): bool
* @param Query $query
* @param array<string, mixed> $binds
* @param string $attribute
* @param string $type
* @param string $alias
* @param string $placeholder
* @return string
*/
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $type, string $alias, string $placeholder): string
{
$distanceParams = $query->getValues()[0];
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
$wkt = $this->convertArrayToWKT($distanceParams[0]);
$binds[":{$placeholder}_0"] = $wkt;
$binds[":{$placeholder}_1"] = $distanceParams[1];

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

if ($useMeters) {
$wktType = $this->getSpatialTypeFromWKT($wkt);
$attrType = strtolower($type);
if ($wktType != Database::VAR_POINT || $attrType != Database::VAR_POINT) {
throw new DatabaseException('Distance in meters is not supported between '.$attrType . ' and '. $wktType);
}
return "ST_DISTANCE_SPHERE({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0), 6371000) {$operator} :{$placeholder}_1";
}
return "ST_Distance({$alias}.{$attribute}, ST_GeomFromText(:{$placeholder}_0)) {$operator} :{$placeholder}_1";
Expand All @@ -1399,11 +1406,12 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str
* @param Query $query
* @param array<string, mixed> $binds
* @param string $attribute
* @param string $type
* @param string $alias
* @param string $placeholder
* @return string
*/
protected function handleSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
protected function handleSpatialQueries(Query $query, array &$binds, string $attribute, string $type, string $alias, string $placeholder): string
{
switch ($query->getMethod()) {
case Query::TYPE_CROSSES:
Expand All @@ -1418,7 +1426,7 @@ protected function handleSpatialQueries(Query $query, array &$binds, string $att
case Query::TYPE_DISTANCE_NOT_EQUAL:
case Query::TYPE_DISTANCE_GREATER_THAN:
case Query::TYPE_DISTANCE_LESS_THAN:
return $this->handleDistanceSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
return $this->handleDistanceSpatialQueries($query, $binds, $attribute, $type, $alias, $placeholder);

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

if (in_array($attributeType, Database::SPATIAL_TYPES)) {
return $this->handleSpatialQueries($query, $binds, $attribute, $alias, $placeholder);
return $this->handleSpatialQueries($query, $binds, $attribute, $attributeType, $alias, $placeholder);
}

switch ($query->getMethod()) {
Expand Down Expand Up @@ -1868,4 +1876,14 @@ public function getSupportForSpatialIndexOrder(): bool
{
return true;
}

/**
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
*
* @return bool
*/
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
{
return false;
}
}
13 changes: 12 additions & 1 deletion src/Database/Adapter/MySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ public function getSizeOfCollectionOnDisk(string $collection): int
* @param Query $query
* @param array<string, mixed> $binds
* @param string $attribute
* @param string $type
* @param string $alias
* @param string $placeholder
* @return string
*/
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $alias, string $placeholder): string
protected function handleDistanceSpatialQueries(Query $query, array &$binds, string $attribute, string $type, string $alias, string $placeholder): string
{
$distanceParams = $query->getValues()[0];
$binds[":{$placeholder}_0"] = $this->convertArrayToWKT($distanceParams[0]);
Expand Down Expand Up @@ -173,4 +174,14 @@ public function getSupportForSpatialIndexOrder(): bool
{
return false;
}

/**
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
*
* @return bool
*/
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
{
return true;
}
}
9 changes: 9 additions & 0 deletions src/Database/Adapter/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -530,4 +530,13 @@ public function getSupportForSpatialIndexOrder(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}
/**
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
*
* @return bool
*/
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}
}
15 changes: 12 additions & 3 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -1488,9 +1488,8 @@ protected function handleDistanceSpatialQueries(Query $query, array &$binds, str
}

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

Expand Down Expand Up @@ -1982,4 +1981,14 @@ public function getSupportForSpatialIndexOrder(): bool
{
return false;
}

/**
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
*
* @return bool
*/
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
{
return true;
}
}
10 changes: 10 additions & 0 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -2657,4 +2657,14 @@ public function sum(Document $collection, string $attribute, array $queries = []

return $result['sum'] ?? 0;
}

public function getSpatialTypeFromWKT(string $wkt): string
{
$wkt = trim($wkt);
$pos = strpos($wkt, '(');
if ($pos === false) {
throw new DatabaseException("Invalid spatial type");
}
return strtolower(trim(substr($wkt, 0, $pos)));
}
}
10 changes: 10 additions & 0 deletions src/Database/Adapter/SQLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -1263,4 +1263,14 @@ public function getSupportForBoundaryInclusiveContains(): bool
{
return false;
}

/**
* Does the adapter support calculating distance(in meters) between multidimension geometry(line, polygon,etc)?
*
* @return bool
*/
public function getSupportForDistanceBetweenMultiDimensionGeometryInMeters(): bool
{
return false;
}
}
5 changes: 5 additions & 0 deletions src/Database/Validator/Spatial.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ public function getType(): string
return self::TYPE_ARRAY;
}

public function getSpatialType(): string
{
return $this->spatialType;
}

/**
* Main validation entrypoint
*/
Expand Down
187 changes: 187 additions & 0 deletions tests/e2e/Adapter/Scopes/SpatialTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -2158,4 +2158,191 @@ public function testSpatialDistanceInMeter(): void
$database->deleteCollection($collectionName);
}
}

public function testSpatialDistanceInMeterForMultiDimensionGeometry(): void
{
/** @var Database $database */
$database = static::getDatabase();
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
$this->markTestSkipped('Adapter does not support spatial attributes');
}

if (!$database->getAdapter()->getSupportForDistanceBetweenMultiDimensionGeometryInMeters()) {
$this->markTestSkipped('Adapter does not support spatial distance(in meter) for multidimension');
}

$multiCollection = 'spatial_distance_meters_multi_';
try {
$database->createCollection($multiCollection);

// Create spatial attributes
$this->assertEquals(true, $database->createAttribute($multiCollection, 'loc', Database::VAR_POINT, 0, true));
$this->assertEquals(true, $database->createAttribute($multiCollection, 'line', Database::VAR_LINESTRING, 0, true));
$this->assertEquals(true, $database->createAttribute($multiCollection, 'poly', Database::VAR_POLYGON, 0, true));

// Create indexes
$this->assertEquals(true, $database->createIndex($multiCollection, 'idx_loc', Database::INDEX_SPATIAL, ['loc']));
$this->assertEquals(true, $database->createIndex($multiCollection, 'idx_line', Database::INDEX_SPATIAL, ['line']));
$this->assertEquals(true, $database->createIndex($multiCollection, 'idx_poly', Database::INDEX_SPATIAL, ['poly']));

// Geometry sets: near origin and far east
$docNear = $database->createDocument($multiCollection, new Document([
'$id' => 'near',
'loc' => [0.0000, 0.0000],
'line' => [[0.0000, 0.0000], [0.0010, 0.0000]], // ~111m
'poly' => [[
[-0.0010, -0.0010],
[-0.0010, 0.0010],
[ 0.0010, 0.0010],
[ 0.0010, -0.0010],
[-0.0010, -0.0010] // closed
]],
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())]
]));

$docFar = $database->createDocument($multiCollection, new Document([
'$id' => 'far',
'loc' => [0.2000, 0.0000], // ~22 km east
'line' => [[0.2000, 0.0000], [0.2020, 0.0000]],
'poly' => [[
[0.1980, -0.0020],
[0.1980, 0.0020],
[0.2020, 0.0020],
[0.2020, -0.0020],
[0.1980, -0.0020] // closed
]],
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())]
]));

$this->assertInstanceOf(Document::class, $docNear);
$this->assertInstanceOf(Document::class, $docFar);

// polygon vs polygon (~1 km from near, ~22 km from far)
$polyPolyWithin3km = $database->find($multiCollection, [
Query::distanceLessThan('poly', [[
[0.0080, -0.0010],
[0.0080, 0.0010],
[0.0110, 0.0010],
[0.0110, -0.0010],
[0.0080, -0.0010] // closed
]], 3000, true)
], Database::PERMISSION_READ);
$this->assertCount(1, $polyPolyWithin3km);
$this->assertEquals('near', $polyPolyWithin3km[0]->getId());

$polyPolyGreater3km = $database->find($multiCollection, [
Query::distanceGreaterThan('poly', [[
[0.0080, -0.0010],
[0.0080, 0.0010],
[0.0110, 0.0010],
[0.0110, -0.0010],
[0.0080, -0.0010] // closed
]], 3000, true)
], Database::PERMISSION_READ);
$this->assertCount(1, $polyPolyGreater3km);
$this->assertEquals('far', $polyPolyGreater3km[0]->getId());

// point vs polygon (~0 km near, ~22 km far)
$ptPolyWithin500 = $database->find($multiCollection, [
Query::distanceLessThan('loc', [[
[-0.0010, -0.0010],
[-0.0010, 0.0020],
[ 0.0020, 0.0020],
[-0.0010, -0.0010]
]], 500, true)
], Database::PERMISSION_READ);
$this->assertCount(1, $ptPolyWithin500);
$this->assertEquals('near', $ptPolyWithin500[0]->getId());

$ptPolyGreater500 = $database->find($multiCollection, [
Query::distanceGreaterThan('loc', [[
[-0.0010, -0.0010],
[-0.0010, 0.0020],
[ 0.0020, 0.0020],
[-0.0010, -0.0010]
]], 500, true)
], Database::PERMISSION_READ);
$this->assertCount(1, $ptPolyGreater500);
$this->assertEquals('far', $ptPolyGreater500[0]->getId());

// Zero-distance checks
$lineEqualZero = $database->find($multiCollection, [
Query::distanceEqual('line', [[0.0000, 0.0000], [0.0010, 0.0000]], 0, true)
], Database::PERMISSION_READ);
$this->assertNotEmpty($lineEqualZero);
$this->assertEquals('near', $lineEqualZero[0]->getId());

$polyEqualZero = $database->find($multiCollection, [
Query::distanceEqual('poly', [[
[-0.0010, -0.0010],
[-0.0010, 0.0010],
[ 0.0010, 0.0010],
[ 0.0010, -0.0010],
[-0.0010, -0.0010]
]], 0, true)
], Database::PERMISSION_READ);
$this->assertNotEmpty($polyEqualZero);
$this->assertEquals('near', $polyEqualZero[0]->getId());

} finally {
$database->deleteCollection($multiCollection);
}
}

public function testSpatialDistanceInMeterError(): void
{
/** @var Database $database */
$database = static::getDatabase();
if (!$database->getAdapter()->getSupportForSpatialAttributes()) {
$this->markTestSkipped('Adapter does not support spatial attributes');
}

if ($database->getAdapter()->getSupportForDistanceBetweenMultiDimensionGeometryInMeters()) {
$this->markTestSkipped('Adapter supports spatial distance (in meter) for multidimension geometries');
}

$collection = 'spatial_distance_error_test';
$database->createCollection($collection);
$this->assertEquals(true, $database->createAttribute($collection, 'loc', Database::VAR_POINT, 0, true));
$this->assertEquals(true, $database->createAttribute($collection, 'line', Database::VAR_LINESTRING, 0, true));
$this->assertEquals(true, $database->createAttribute($collection, 'poly', Database::VAR_POLYGON, 0, true));

$doc = $database->createDocument($collection, new Document([
'$id' => 'doc1',
'loc' => [0.0, 0.0],
'line' => [[0.0, 0.0], [0.001, 0.0]],
'poly' => [[[ -0.001, -0.001 ], [ -0.001, 0.001 ], [ 0.001, 0.001 ], [ -0.001, -0.001 ]]],
'$permissions' => []
]));
$this->assertInstanceOf(Document::class, $doc);

// Invalid geometry pairs
$cases = [
['attr' => 'line', 'geom' => [0.002, 0.0], 'expected' => ['linestring', 'point']],
['attr' => 'poly', 'geom' => [0.002, 0.0], 'expected' => ['polygon', 'point']],
['attr' => 'loc', 'geom' => [[0.0, 0.0], [0.001, 0.001]], 'expected' => ['point', 'linestring']],
['attr' => 'poly', 'geom' => [[0.0, 0.0], [0.001, 0.001]], 'expected' => ['polygon', 'linestring']],
['attr' => 'loc', 'geom' => [[[0.0, 0.0], [0.001, 0.0], [0.001, 0.001], [0.0, 0.0]]], 'expected' => ['point', 'polygon']],
['attr' => 'line', 'geom' => [[[0.0, 0.0], [0.001, 0.0], [0.001, 0.001], [0.0, 0.0]]], 'expected' => ['linestring', 'polygon']],
['attr' => 'poly', 'geom' => [[[0.002, -0.001], [0.002, 0.001], [0.004, 0.001], [0.002, -0.001]]], 'expected' => ['polygon', 'polygon']],
['attr' => 'line', 'geom' => [[0.002, 0.0], [0.003, 0.0]], 'expected' => ['linestring', 'linestring']],
];

foreach ($cases as $case) {
try {
$database->find($collection, [
Query::distanceLessThan($case['attr'], $case['geom'], 1000, true)
]);
$this->fail('Expected Exception not thrown for ' . implode(' vs ', $case['expected']));
} catch (\Exception $e) {
$this->assertInstanceOf(\Exception::class, $e);

// Validate exception message contains correct type names
$msg = strtolower($e->getMessage());
var_dump($msg);
$this->assertStringContainsString($case['expected'][0], $msg, 'Attr type missing in exception');
$this->assertStringContainsString($case['expected'][1], $msg, 'Geom type missing in exception');
}
}
}
}