Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
115e341
Spatial
fogelito Sep 10, 2025
745d785
Merge branch 'main' of github.com:utopia-php/database into spatial-en…
fogelito Sep 11, 2025
a313da3
Merge branch 'main' of github.com:utopia-php/database into spatial-en…
fogelito Sep 14, 2025
6f8bdf9
Postgres point
fogelito Sep 14, 2025
4ef5206
decodePoint
fogelito Sep 14, 2025
37db0b1
same decode
fogelito Sep 14, 2025
d01fdbc
decodeLinestring
fogelito Sep 14, 2025
f25ede9
polygon
fogelito Sep 14, 2025
9e9d50a
Postgres linestring
fogelito Sep 14, 2025
4e195f8
Postgres polygon
fogelito Sep 14, 2025
bb4b3e4
Mysql polygon
fogelito Sep 14, 2025
b94dada
clean var_dump
fogelito Sep 14, 2025
7f7dfbb
Remove decodeSpatialData method
fogelito Sep 15, 2025
773df87
Remove try catch
fogelito Sep 15, 2025
f6bd630
Add hints
fogelito Sep 15, 2025
cc972d2
dbg
fogelito Sep 15, 2025
1b9b095
formatting
fogelito Sep 15, 2025
9a0e285
signature
fogelito Sep 15, 2025
5d2c0c3
fix Pool adapter
fogelito Sep 15, 2025
44cc3f3
formatting
fogelito Sep 15, 2025
7849c6c
formatting
fogelito Sep 15, 2025
2aed9dc
fix getAttributeProjection
fogelito Sep 15, 2025
4ed19c6
Runn tests
fogelito Sep 15, 2025
ee7fbc1
decode polygon
fogelito Sep 15, 2025
2648ef9
remove $spatialAttributes
fogelito Sep 15, 2025
a0b963a
unpack
fogelito Sep 15, 2025
cfe9b84
stopOnFailure
fogelito Sep 15, 2025
a1a52f6
fix decode point
fogelito Sep 16, 2025
0275563
Merge branch 'main' of github.com:utopia-php/database into spatial-en…
fogelito Sep 16, 2025
c349c08
DatabaseException
fogelito Sep 16, 2025
96e8a5e
Postgres update point
fogelito Sep 16, 2025
09be1c0
formatting
fogelito Sep 16, 2025
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
28 changes: 26 additions & 2 deletions src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1153,9 +1153,9 @@ abstract public function getKeywords(): array;
*
* @param array<string> $selections
* @param string $prefix
* @return mixed
* @return string
*/
abstract protected function getAttributeProjection(array $selections, string $prefix): mixed;
abstract protected function getAttributeProjection(array $selections, string $prefix): string;

/**
* Get all selected attributes from queries
Expand Down Expand Up @@ -1285,4 +1285,28 @@ abstract public function getTenantQuery(string $collection, string $alias = ''):
* @return bool
*/
abstract protected function execute(mixed $stmt): bool;

/**
* Decode a WKB or textual POINT into [x, y]
*
* @param string $wkb
* @return float[] Array with two elements: [x, y]
*/
abstract public function decodePoint(string $wkb): array;

/**
* Decode a WKB or textual LINESTRING into [[x1, y1], [x2, y2], ...]
*
* @param string $wkb
* @return float[][] Array of points, each as [x, y]
*/
abstract public function decodeLinestring(string $wkb): array;

/**
* Decode a WKB or textual POLYGON into [[[x1, y1], [x2, y2], ...], ...]
*
* @param string $wkb
* @return float[][][] Array of rings, each ring is an array of points [x, y]
*/
abstract public function decodePolygon(string $wkb): array;
}
23 changes: 16 additions & 7 deletions src/Database/Adapter/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -470,13 +470,7 @@ public function getKeywords(): array
return $this->delegate(__FUNCTION__, \func_get_args());
}

/**
* @param array<string,mixed> $selections
* @param string $prefix
* @param array<string,mixed> $spatialAttributes
* @return mixed
*/
protected function getAttributeProjection(array $selections, string $prefix, array $spatialAttributes = []): mixed
protected function getAttributeProjection(array $selections, string $prefix): string
{
return $this->delegate(__FUNCTION__, \func_get_args());
}
Expand Down Expand Up @@ -549,4 +543,19 @@ public function getSupportForSpatialAxisOrder(): bool
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function decodePoint(string $wkb): array
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function decodeLinestring(string $wkb): array
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function decodePolygon(string $wkb): array
{
return $this->delegate(__FUNCTION__, \func_get_args());
}
}
239 changes: 239 additions & 0 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -2000,4 +2000,243 @@ public function getSupportForSpatialAxisOrder(): bool
{
return false;
}

public function decodePoint(string $wkb): array
{
if (str_starts_with(strtoupper($wkb), 'POINT(')) {
$start = strpos($wkb, '(') + 1;
$end = strrpos($wkb, ')');
$inside = substr($wkb, $start, $end - $start);

$coords = explode(' ', trim($inside));
return [(float)$coords[0], (float)$coords[1]];
}

$bin = hex2bin($wkb);
if ($bin === false) {
throw new DatabaseException('Invalid hex WKB string');
}

if (strlen($bin) < 13) { // 1 byte endian + 4 bytes type + 8 bytes for X
throw new DatabaseException('WKB too short');
}

$isLE = ord($bin[0]) === 1;

// Type (4 bytes)
$typeBytes = substr($bin, 1, 4);
if (strlen($typeBytes) !== 4) {
throw new DatabaseException('Failed to extract type bytes from WKB');
}

$typeArr = unpack($isLE ? 'V' : 'N', $typeBytes);
if ($typeArr === false || !isset($typeArr[1])) {
throw new DatabaseException('Failed to unpack type from WKB');
}
$type = $typeArr[1];

// Offset to coordinates (skip SRID if present)
$offset = 5 + (($type & 0x20000000) ? 4 : 0);

if (strlen($bin) < $offset + 16) { // 16 bytes for X,Y
throw new DatabaseException('WKB too short for coordinates');
}

$fmt = $isLE ? 'e' : 'E'; // little vs big endian double

// X coordinate
$xArr = unpack($fmt, substr($bin, $offset, 8));
if ($xArr === false || !isset($xArr[1])) {
throw new DatabaseException('Failed to unpack X coordinate');
}
$x = (float)$xArr[1];

// Y coordinate
$yArr = unpack($fmt, substr($bin, $offset + 8, 8));
if ($yArr === false || !isset($yArr[1])) {
throw new DatabaseException('Failed to unpack Y coordinate');
}
$y = (float)$yArr[1];

return [$x, $y];
}

public function decodeLinestring(mixed $wkb): array
{
if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
$start = strpos($wkb, '(') + 1;
$end = strrpos($wkb, ')');
$inside = substr($wkb, $start, $end - $start);

$points = explode(',', $inside);
return array_map(function ($point) {
$coords = explode(' ', trim($point));
return [(float)$coords[0], (float)$coords[1]];
}, $points);
}

if (ctype_xdigit($wkb)) {
$wkb = hex2bin($wkb);
if ($wkb === false) {
throw new DatabaseException("Failed to convert hex WKB to binary.");
}
}

if (strlen($wkb) < 9) {
throw new DatabaseException("WKB too short to be a valid geometry");
}

$byteOrder = ord($wkb[0]);
if ($byteOrder === 0) {
throw new DatabaseException("Big-endian WKB not supported");
} elseif ($byteOrder !== 1) {
throw new DatabaseException("Invalid byte order in WKB");
}

// Type + SRID flag
$typeField = unpack('V', substr($wkb, 1, 4));
if ($typeField === false) {
throw new DatabaseException('Failed to unpack the type field from WKB.');
}

$typeField = $typeField[1];
$geomType = $typeField & 0xFF;
$hasSRID = ($typeField & 0x20000000) !== 0;

if ($geomType !== 2) { // 2 = LINESTRING
throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}");
}

$offset = 5;
if ($hasSRID) {
$offset += 4;
}

$numPoints = unpack('V', substr($wkb, $offset, 4));
if ($numPoints === false) {
throw new DatabaseException("Failed to unpack number of points at offset {$offset}.");
}

$numPoints = $numPoints[1];
$offset += 4;

$points = [];
for ($i = 0; $i < $numPoints; $i++) {
$x = unpack('e', substr($wkb, $offset, 8));
if ($x === false) {
throw new DatabaseException("Failed to unpack X coordinate at offset {$offset}.");
}

$x = (float) $x[1];

$offset += 8;

$y = unpack('e', substr($wkb, $offset, 8));
if ($y === false) {
throw new DatabaseException("Failed to unpack Y coordinate at offset {$offset}.");
}

$y = (float) $y[1];

$offset += 8;
$points[] = [$x, $y];
}

return $points;
}

public function decodePolygon(string $wkb): array
{
// POLYGON((x1,y1),(x2,y2))
if (str_starts_with($wkb, 'POLYGON((')) {
$start = strpos($wkb, '((') + 2;
$end = strrpos($wkb, '))');
$inside = substr($wkb, $start, $end - $start);

$rings = explode('),(', $inside);
return array_map(function ($ring) {
$points = explode(',', $ring);
return array_map(function ($point) {
$coords = explode(' ', trim($point));
return [(float)$coords[0], (float)$coords[1]];
}, $points);
}, $rings);
}

// Convert hex string to binary if needed
if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) {
$wkb = hex2bin($wkb);
if ($wkb === false) {
throw new DatabaseException("Invalid hex WKB");
}
}

if (strlen($wkb) < 9) {
throw new DatabaseException("WKB too short");
}

$uInt32 = 'V'; // little-endian 32-bit unsigned
$uDouble = 'd'; // little-endian double

$typeInt = unpack($uInt32, substr($wkb, 1, 4));
if ($typeInt === false) {
throw new DatabaseException('Failed to unpack type field from WKB.');
}

$typeInt = (int) $typeInt[1];
$hasSrid = ($typeInt & 0x20000000) !== 0;
$geomType = $typeInt & 0xFF;

if ($geomType !== 3) { // 3 = POLYGON
throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}");
}

$offset = 5;
if ($hasSrid) {
$offset += 4;
}

// Number of rings
$numRings = unpack($uInt32, substr($wkb, $offset, 4));
if ($numRings === false) {
throw new DatabaseException('Failed to unpack number of rings from WKB.');
}

$numRings = (int) $numRings[1];
$offset += 4;

$rings = [];
for ($r = 0; $r < $numRings; $r++) {
$numPoints = unpack($uInt32, substr($wkb, $offset, 4));
if ($numPoints === false) {
throw new DatabaseException('Failed to unpack number of points from WKB.');
}

$numPoints = (int) $numPoints[1];
$offset += 4;
$points = [];
for ($i = 0; $i < $numPoints; $i++) {
$x = unpack($uDouble, substr($wkb, $offset, 8));
if ($x === false) {
throw new DatabaseException('Failed to unpack X coordinate from WKB.');
}

$x = (float) $x[1];

$y = unpack($uDouble, substr($wkb, $offset + 8, 8));
if ($y === false) {
throw new DatabaseException('Failed to unpack Y coordinate from WKB.');
}

$y = (float) $y[1];

$points[] = [$x, $y];
$offset += 16;
}
$rings[] = $points;
}

return $rings; // array of rings, each ring is array of [x,y]
}

}
Loading