Skip to content

Commit f499a46

Browse files
authored
Merge pull request #698 from utopia-php/spatial-encode-decode
Spatial decoding
2 parents b55d806 + 09be1c0 commit f499a46

6 files changed

Lines changed: 506 additions & 122 deletions

File tree

src/Database/Adapter.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,9 +1153,9 @@ abstract public function getKeywords(): array;
11531153
*
11541154
* @param array<string> $selections
11551155
* @param string $prefix
1156-
* @return mixed
1156+
* @return string
11571157
*/
1158-
abstract protected function getAttributeProjection(array $selections, string $prefix): mixed;
1158+
abstract protected function getAttributeProjection(array $selections, string $prefix): string;
11591159

11601160
/**
11611161
* Get all selected attributes from queries
@@ -1285,4 +1285,28 @@ abstract public function getTenantQuery(string $collection, string $alias = ''):
12851285
* @return bool
12861286
*/
12871287
abstract protected function execute(mixed $stmt): bool;
1288+
1289+
/**
1290+
* Decode a WKB or textual POINT into [x, y]
1291+
*
1292+
* @param string $wkb
1293+
* @return float[] Array with two elements: [x, y]
1294+
*/
1295+
abstract public function decodePoint(string $wkb): array;
1296+
1297+
/**
1298+
* Decode a WKB or textual LINESTRING into [[x1, y1], [x2, y2], ...]
1299+
*
1300+
* @param string $wkb
1301+
* @return float[][] Array of points, each as [x, y]
1302+
*/
1303+
abstract public function decodeLinestring(string $wkb): array;
1304+
1305+
/**
1306+
* Decode a WKB or textual POLYGON into [[[x1, y1], [x2, y2], ...], ...]
1307+
*
1308+
* @param string $wkb
1309+
* @return float[][][] Array of rings, each ring is an array of points [x, y]
1310+
*/
1311+
abstract public function decodePolygon(string $wkb): array;
12881312
}

src/Database/Adapter/Pool.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -470,13 +470,7 @@ public function getKeywords(): array
470470
return $this->delegate(__FUNCTION__, \func_get_args());
471471
}
472472

473-
/**
474-
* @param array<string,mixed> $selections
475-
* @param string $prefix
476-
* @param array<string,mixed> $spatialAttributes
477-
* @return mixed
478-
*/
479-
protected function getAttributeProjection(array $selections, string $prefix, array $spatialAttributes = []): mixed
473+
protected function getAttributeProjection(array $selections, string $prefix): string
480474
{
481475
return $this->delegate(__FUNCTION__, \func_get_args());
482476
}
@@ -549,4 +543,19 @@ public function getSupportForSpatialAxisOrder(): bool
549543
{
550544
return $this->delegate(__FUNCTION__, \func_get_args());
551545
}
546+
547+
public function decodePoint(string $wkb): array
548+
{
549+
return $this->delegate(__FUNCTION__, \func_get_args());
550+
}
551+
552+
public function decodeLinestring(string $wkb): array
553+
{
554+
return $this->delegate(__FUNCTION__, \func_get_args());
555+
}
556+
557+
public function decodePolygon(string $wkb): array
558+
{
559+
return $this->delegate(__FUNCTION__, \func_get_args());
560+
}
552561
}

src/Database/Adapter/Postgres.php

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,4 +2000,243 @@ public function getSupportForSpatialAxisOrder(): bool
20002000
{
20012001
return false;
20022002
}
2003+
2004+
public function decodePoint(string $wkb): array
2005+
{
2006+
if (str_starts_with(strtoupper($wkb), 'POINT(')) {
2007+
$start = strpos($wkb, '(') + 1;
2008+
$end = strrpos($wkb, ')');
2009+
$inside = substr($wkb, $start, $end - $start);
2010+
2011+
$coords = explode(' ', trim($inside));
2012+
return [(float)$coords[0], (float)$coords[1]];
2013+
}
2014+
2015+
$bin = hex2bin($wkb);
2016+
if ($bin === false) {
2017+
throw new DatabaseException('Invalid hex WKB string');
2018+
}
2019+
2020+
if (strlen($bin) < 13) { // 1 byte endian + 4 bytes type + 8 bytes for X
2021+
throw new DatabaseException('WKB too short');
2022+
}
2023+
2024+
$isLE = ord($bin[0]) === 1;
2025+
2026+
// Type (4 bytes)
2027+
$typeBytes = substr($bin, 1, 4);
2028+
if (strlen($typeBytes) !== 4) {
2029+
throw new DatabaseException('Failed to extract type bytes from WKB');
2030+
}
2031+
2032+
$typeArr = unpack($isLE ? 'V' : 'N', $typeBytes);
2033+
if ($typeArr === false || !isset($typeArr[1])) {
2034+
throw new DatabaseException('Failed to unpack type from WKB');
2035+
}
2036+
$type = $typeArr[1];
2037+
2038+
// Offset to coordinates (skip SRID if present)
2039+
$offset = 5 + (($type & 0x20000000) ? 4 : 0);
2040+
2041+
if (strlen($bin) < $offset + 16) { // 16 bytes for X,Y
2042+
throw new DatabaseException('WKB too short for coordinates');
2043+
}
2044+
2045+
$fmt = $isLE ? 'e' : 'E'; // little vs big endian double
2046+
2047+
// X coordinate
2048+
$xArr = unpack($fmt, substr($bin, $offset, 8));
2049+
if ($xArr === false || !isset($xArr[1])) {
2050+
throw new DatabaseException('Failed to unpack X coordinate');
2051+
}
2052+
$x = (float)$xArr[1];
2053+
2054+
// Y coordinate
2055+
$yArr = unpack($fmt, substr($bin, $offset + 8, 8));
2056+
if ($yArr === false || !isset($yArr[1])) {
2057+
throw new DatabaseException('Failed to unpack Y coordinate');
2058+
}
2059+
$y = (float)$yArr[1];
2060+
2061+
return [$x, $y];
2062+
}
2063+
2064+
public function decodeLinestring(mixed $wkb): array
2065+
{
2066+
if (str_starts_with(strtoupper($wkb), 'LINESTRING(')) {
2067+
$start = strpos($wkb, '(') + 1;
2068+
$end = strrpos($wkb, ')');
2069+
$inside = substr($wkb, $start, $end - $start);
2070+
2071+
$points = explode(',', $inside);
2072+
return array_map(function ($point) {
2073+
$coords = explode(' ', trim($point));
2074+
return [(float)$coords[0], (float)$coords[1]];
2075+
}, $points);
2076+
}
2077+
2078+
if (ctype_xdigit($wkb)) {
2079+
$wkb = hex2bin($wkb);
2080+
if ($wkb === false) {
2081+
throw new DatabaseException("Failed to convert hex WKB to binary.");
2082+
}
2083+
}
2084+
2085+
if (strlen($wkb) < 9) {
2086+
throw new DatabaseException("WKB too short to be a valid geometry");
2087+
}
2088+
2089+
$byteOrder = ord($wkb[0]);
2090+
if ($byteOrder === 0) {
2091+
throw new DatabaseException("Big-endian WKB not supported");
2092+
} elseif ($byteOrder !== 1) {
2093+
throw new DatabaseException("Invalid byte order in WKB");
2094+
}
2095+
2096+
// Type + SRID flag
2097+
$typeField = unpack('V', substr($wkb, 1, 4));
2098+
if ($typeField === false) {
2099+
throw new DatabaseException('Failed to unpack the type field from WKB.');
2100+
}
2101+
2102+
$typeField = $typeField[1];
2103+
$geomType = $typeField & 0xFF;
2104+
$hasSRID = ($typeField & 0x20000000) !== 0;
2105+
2106+
if ($geomType !== 2) { // 2 = LINESTRING
2107+
throw new DatabaseException("Not a LINESTRING geometry type, got {$geomType}");
2108+
}
2109+
2110+
$offset = 5;
2111+
if ($hasSRID) {
2112+
$offset += 4;
2113+
}
2114+
2115+
$numPoints = unpack('V', substr($wkb, $offset, 4));
2116+
if ($numPoints === false) {
2117+
throw new DatabaseException("Failed to unpack number of points at offset {$offset}.");
2118+
}
2119+
2120+
$numPoints = $numPoints[1];
2121+
$offset += 4;
2122+
2123+
$points = [];
2124+
for ($i = 0; $i < $numPoints; $i++) {
2125+
$x = unpack('e', substr($wkb, $offset, 8));
2126+
if ($x === false) {
2127+
throw new DatabaseException("Failed to unpack X coordinate at offset {$offset}.");
2128+
}
2129+
2130+
$x = (float) $x[1];
2131+
2132+
$offset += 8;
2133+
2134+
$y = unpack('e', substr($wkb, $offset, 8));
2135+
if ($y === false) {
2136+
throw new DatabaseException("Failed to unpack Y coordinate at offset {$offset}.");
2137+
}
2138+
2139+
$y = (float) $y[1];
2140+
2141+
$offset += 8;
2142+
$points[] = [$x, $y];
2143+
}
2144+
2145+
return $points;
2146+
}
2147+
2148+
public function decodePolygon(string $wkb): array
2149+
{
2150+
// POLYGON((x1,y1),(x2,y2))
2151+
if (str_starts_with($wkb, 'POLYGON((')) {
2152+
$start = strpos($wkb, '((') + 2;
2153+
$end = strrpos($wkb, '))');
2154+
$inside = substr($wkb, $start, $end - $start);
2155+
2156+
$rings = explode('),(', $inside);
2157+
return array_map(function ($ring) {
2158+
$points = explode(',', $ring);
2159+
return array_map(function ($point) {
2160+
$coords = explode(' ', trim($point));
2161+
return [(float)$coords[0], (float)$coords[1]];
2162+
}, $points);
2163+
}, $rings);
2164+
}
2165+
2166+
// Convert hex string to binary if needed
2167+
if (preg_match('/^[0-9a-fA-F]+$/', $wkb)) {
2168+
$wkb = hex2bin($wkb);
2169+
if ($wkb === false) {
2170+
throw new DatabaseException("Invalid hex WKB");
2171+
}
2172+
}
2173+
2174+
if (strlen($wkb) < 9) {
2175+
throw new DatabaseException("WKB too short");
2176+
}
2177+
2178+
$uInt32 = 'V'; // little-endian 32-bit unsigned
2179+
$uDouble = 'd'; // little-endian double
2180+
2181+
$typeInt = unpack($uInt32, substr($wkb, 1, 4));
2182+
if ($typeInt === false) {
2183+
throw new DatabaseException('Failed to unpack type field from WKB.');
2184+
}
2185+
2186+
$typeInt = (int) $typeInt[1];
2187+
$hasSrid = ($typeInt & 0x20000000) !== 0;
2188+
$geomType = $typeInt & 0xFF;
2189+
2190+
if ($geomType !== 3) { // 3 = POLYGON
2191+
throw new DatabaseException("Not a POLYGON geometry type, got {$geomType}");
2192+
}
2193+
2194+
$offset = 5;
2195+
if ($hasSrid) {
2196+
$offset += 4;
2197+
}
2198+
2199+
// Number of rings
2200+
$numRings = unpack($uInt32, substr($wkb, $offset, 4));
2201+
if ($numRings === false) {
2202+
throw new DatabaseException('Failed to unpack number of rings from WKB.');
2203+
}
2204+
2205+
$numRings = (int) $numRings[1];
2206+
$offset += 4;
2207+
2208+
$rings = [];
2209+
for ($r = 0; $r < $numRings; $r++) {
2210+
$numPoints = unpack($uInt32, substr($wkb, $offset, 4));
2211+
if ($numPoints === false) {
2212+
throw new DatabaseException('Failed to unpack number of points from WKB.');
2213+
}
2214+
2215+
$numPoints = (int) $numPoints[1];
2216+
$offset += 4;
2217+
$points = [];
2218+
for ($i = 0; $i < $numPoints; $i++) {
2219+
$x = unpack($uDouble, substr($wkb, $offset, 8));
2220+
if ($x === false) {
2221+
throw new DatabaseException('Failed to unpack X coordinate from WKB.');
2222+
}
2223+
2224+
$x = (float) $x[1];
2225+
2226+
$y = unpack($uDouble, substr($wkb, $offset + 8, 8));
2227+
if ($y === false) {
2228+
throw new DatabaseException('Failed to unpack Y coordinate from WKB.');
2229+
}
2230+
2231+
$y = (float) $y[1];
2232+
2233+
$points[] = [$x, $y];
2234+
$offset += 16;
2235+
}
2236+
$rings[] = $points;
2237+
}
2238+
2239+
return $rings; // array of rings, each ring is array of [x,y]
2240+
}
2241+
20032242
}

0 commit comments

Comments
 (0)