• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / geo / 13418525576

19 Feb 2025 05:15PM UTC coverage: 47.546% (+0.2%) from 47.345%
13418525576

push

github

BenMorel
Add withers to geometries

11 of 11 new or added lines in 7 files covered. (100.0%)

60 existing lines in 4 files now uncovered.

1608 of 3382 relevant lines covered (47.55%)

971.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.36
/src/Engine/DatabaseEngine.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Geo\Engine;
6

7
use Brick\Geo\Curve;
8
use Brick\Geo\Exception\GeometryEngineException;
9
use Brick\Geo\Geometry;
10
use Brick\Geo\MultiCurve;
11
use Brick\Geo\MultiSurface;
12
use Brick\Geo\MultiPolygon;
13
use Brick\Geo\Point;
14
use Brick\Geo\Polygon;
15
use Brick\Geo\Proxy;
16
use Brick\Geo\Surface;
17

18
/**
19
 * Database implementation of the GeometryEngine.
20
 *
21
 * The target database must have support for GIS functions.
22
 */
23
abstract class DatabaseEngine implements GeometryEngine
24
{
25
    private readonly bool $useProxy;
26

27
    public function __construct(bool $useProxy)
28
    {
29
        $this->useProxy = $useProxy;
×
30
    }
31

32
    /**
33
     * Executes a SQL query.
34
     *
35
     * @psalm-param list<GeometryParameter|string|float|int> $parameters
36
     * @psalm-return list<mixed>
37
     *
38
     * @param string $query      The SQL query to execute.
39
     * @param array  $parameters The geometry data or scalar values to pass as parameters.
40
     *
41
     * @return array A numeric result array.
42
     *
43
     * @throws GeometryEngineException
44
     */
45
    abstract protected function executeQuery(string $query, array $parameters) : array;
46

47
    /**
48
     * Returns the syntax required to perform a ST_GeomFromText(), together with placeholders.
49
     *
50
     * This method may be overridden if necessary.
51
     */
52
    protected function getGeomFromTextSyntax(): string
53
    {
54
        return 'ST_GeomFromText(?, ?)';
120✔
55
    }
56

57
    /**
58
     * Returns the syntax required to perform a ST_GeomFromWKB(), together with placeholders.
59
     *
60
     * This method may be overridden if necessary.
61
     */
62
    protected function getGeomFromWKBSyntax(): string
63
    {
64
        return 'ST_GeomFromWKB(?, ?)';
409✔
65
    }
66

67
    /**
68
     * Returns the placeholder syntax for the given parameter.
69
     *
70
     * This method may be overridden to perform explicit type casts if necessary.
71
     */
72
    protected function getParameterPlaceholder(string|float|int $parameter): string
73
    {
74
        return '?';
102✔
75
    }
76

77
    /**
78
     * Builds and executes a SQL query for a GIS function.
79
     *
80
     * @psalm-param array<Geometry|string|float|int> $parameters
81
     * @psalm-return list<mixed>
82
     *
83
     * @param string $function        The SQL GIS function to execute.
84
     * @param array  $parameters      The Geometry objects or scalar values to pass as parameters.
85
     * @param bool   $returnsGeometry Whether the GIS function returns a Geometry.
86
     *
87
     * @return array A numeric result array.
88
     *
89
     * @throws GeometryEngineException
90
     */
91
    private function query(string $function, array $parameters, bool $returnsGeometry) : array
92
    {
93
        $queryParameters = [];
1,263✔
94
        $queryValues = [];
1,263✔
95

96
        foreach ($parameters as $parameter) {
1,263✔
97
            if ($parameter instanceof Geometry) {
1,263✔
98
                if ($parameter instanceof Proxy\ProxyInterface) {
1,263✔
99
                    $sendAsBinary = $parameter->isProxyBinary();
93✔
100
                } else {
101
                    $sendAsBinary = ! $parameter->isEmpty();
1,263✔
102
                }
103

104
                $queryParameters[] = $sendAsBinary
1,263✔
105
                    ? $this->getGeomFromWKBSyntax()
1,153✔
106
                    : $this->getGeomFromTextSyntax();
120✔
107

108
                $queryValues[] = new GeometryParameter($parameter, $sendAsBinary);
1,263✔
109
            } else {
110
                $queryParameters[] = $this->getParameterPlaceholder($parameter);
106✔
111
                $queryValues[] = $parameter;
106✔
112
            }
113
        }
114

115
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
1,263✔
116

117
        if ($returnsGeometry) {
1,263✔
118
            $query = sprintf('
313✔
119
                SELECT
120
                    CASE WHEN ST_IsEmpty(g) THEN ST_AsText(g) ELSE NULL END,
121
                    CASE WHEN ST_IsEmpty(g) THEN NULL ELSE ST_AsBinary(g) END,
122
                    ST_GeometryType(g),
123
                    ST_SRID(g)
124
                FROM (%s AS g) AS q
125
            ', $query);
313✔
126
        }
127

128
        return $this->executeQuery($query, $queryValues);
1,263✔
129
    }
130

131
    /**
132
     * Queries a GIS function returning a boolean value.
133
     *
134
     * @param string                       $function   The SQL GIS function to execute.
135
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
136
     *
137
     * @throws GeometryEngineException
138
     */
139
    private function queryBoolean(string $function, Geometry|string|float|int ...$parameters) : bool
140
    {
141
        [$result] = $this->query($function, $parameters, false);
848✔
142

143
        // SQLite3 returns -1 when calling a boolean GIS function on a NULL result,
144
        // MariaDB returns -1 when an unsupported operation is performed on a Z/M geometry.
145
        if ($result === null || $result === -1 || $result === '-1') {
732✔
146
            throw GeometryEngineException::operationYieldedNoResult();
96✔
147
        }
148

149
        return (bool) $result;
636✔
150
    }
151

152
    /**
153
     * Queries a GIS function returning a floating point value.
154
     *
155
     * @param string                       $function   The SQL GIS function to execute.
156
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
157
     *
158
     * @throws GeometryEngineException
159
     */
160
    private function queryFloat(string $function, Geometry|string|float|int ...$parameters) : float
161
    {
162
        [$result] = $this->query($function, $parameters, false);
221✔
163

164
        if ($result === null) {
191✔
165
            throw GeometryEngineException::operationYieldedNoResult();
27✔
166
        }
167

168
        return (float) $result;
164✔
169
    }
170

171
    /**
172
     * Queries a GIS function returning a Geometry object.
173
     *
174
     * @param string                       $function   The SQL GIS function to execute.
175
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
176
     *
177
     * @throws GeometryEngineException
178
     */
179
    private function queryGeometry(string $function, Geometry|string|float|int ...$parameters) : Geometry
180
    {
181
        /** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
182
        $result = $this->query($function, $parameters, true);
313✔
183

184
        [$wkt, $wkb, $geometryType, $srid] = $result;
247✔
185

186
        $srid = (int) $srid;
247✔
187

188
        if ($wkt !== null) {
247✔
189
            if ($this->useProxy) {
5✔
190
                $proxyClassName = $this->getProxyClassName($geometryType);
5✔
191

192
                return new $proxyClassName($wkt, false, $srid);
5✔
193
            }
194

195
            return Geometry::fromText($wkt, $srid);
×
196
        }
197

198
        if ($wkb !== null) {
242✔
199
            if (is_resource($wkb)) {
228✔
200
                $wkb = stream_get_contents($wkb);
59✔
201
            }
202

203
            if ($this->useProxy) {
228✔
204
                $proxyClassName = $this->getProxyClassName($geometryType);
228✔
205

206
                return new $proxyClassName($wkb, true, $srid);
228✔
207
            }
208

209
            return Geometry::fromBinary($wkb, $srid);
×
210
        }
211

212
        throw GeometryEngineException::operationYieldedNoResult();
14✔
213
    }
214

215
    /**
216
     * @psalm-return class-string<Proxy\ProxyInterface&Geometry>
217
     *
218
     * @throws GeometryEngineException
219
     */
220
    private function getProxyClassName(string $geometryType) : string
221
    {
222
        $proxyClasses = [
233✔
223
            'CIRCULARSTRING'     => Proxy\CircularStringProxy::class,
233✔
224
            'COMPOUNDCURVE'      => Proxy\CompoundCurveProxy::class,
233✔
225
            'CURVE'              => Proxy\CurveProxy::class,
233✔
226
            'CURVEPOLYGON'       => Proxy\CurvePolygonProxy::class,
233✔
227
            'GEOMCOLLECTION'     => Proxy\GeometryCollectionProxy::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
233✔
228
            'GEOMETRY'           => Proxy\GeometryProxy::class,
233✔
229
            'GEOMETRYCOLLECTION' => Proxy\GeometryCollectionProxy::class,
233✔
230
            'LINESTRING'         => Proxy\LineStringProxy::class,
233✔
231
            'MULTICURVE'         => Proxy\MultiCurveProxy::class,
233✔
232
            'MULTILINESTRING'    => Proxy\MultiLineStringProxy::class,
233✔
233
            'MULTIPOINT'         => Proxy\MultiPointProxy::class,
233✔
234
            'MULTIPOLYGON'       => Proxy\MultiPolygonProxy::class,
233✔
235
            'MULTISURFACE'       => Proxy\MultiSurfaceProxy::class,
233✔
236
            'POINT'              => Proxy\PointProxy::class,
233✔
237
            'POLYGON'            => Proxy\PolygonProxy::class,
233✔
238
            'POLYHEDRALSURFACE'  => Proxy\PolyhedralSurfaceProxy::class,
233✔
239
            'SURFACE'            => Proxy\SurfaceProxy::class,
233✔
240
            'TIN'                => Proxy\TINProxy::class,
233✔
241
            'TRIANGLE'           => Proxy\TriangleProxy::class
233✔
242
        ];
233✔
243

244
        $geometryType = strtoupper($geometryType);
233✔
245
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
233✔
246
        $geometryType = preg_replace('/ .*/', '', $geometryType);
233✔
247

248
        if (! isset($proxyClasses[$geometryType])) {
233✔
249
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
250
        }
251

252
        return $proxyClasses[$geometryType];
233✔
253
    }
254

255
    public function contains(Geometry $a, Geometry $b) : bool
256
    {
257
        return $this->queryBoolean('ST_Contains', $a, $b);
76✔
258
    }
259

260
    public function intersects(Geometry $a, Geometry $b) : bool
261
    {
262
        return $this->queryBoolean('ST_Intersects', $a, $b);
36✔
263
    }
264

265
    public function union(Geometry $a, Geometry $b) : Geometry
266
    {
267
        return $this->queryGeometry('ST_Union', $a, $b);
22✔
268
    }
269

270
    public function intersection(Geometry $a, Geometry $b) : Geometry
271
    {
272
        return $this->queryGeometry('ST_Intersection', $a, $b);
12✔
273
    }
274

275
    public function difference(Geometry $a, Geometry $b) : Geometry
276
    {
277
        return $this->queryGeometry('ST_Difference', $a, $b);
12✔
278
    }
279

280
    public function envelope(Geometry $g) : Geometry
281
    {
282
        return $this->queryGeometry('ST_Envelope', $g);
18✔
283
    }
284

285
    public function centroid(Geometry $g) : Point
286
    {
287
        /** @var Point */
288
        return $this->queryGeometry('ST_Centroid', $g);
45✔
289
    }
290

291
    public function pointOnSurface(Surface|MultiSurface $g) : Point
292
    {
293
        /** @var Point */
294
        return $this->queryGeometry('ST_PointOnSurface', $g);
42✔
295
    }
296

297
    public function length(Curve|MultiCurve $g) : float
298
    {
299
        return $this->queryFloat('ST_Length', $g);
95✔
300
    }
301

302
    public function area(Surface|MultiSurface $g) : float
303
    {
304
        return $this->queryFloat('ST_Area', $g);
66✔
305
    }
306

307
    public function azimuth(Point $observer, Point $subject) : float
308
    {
309
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
12✔
310
    }
311

312
    public function boundary(Geometry $g) : Geometry
313
    {
314
        return $this->queryGeometry('ST_Boundary', $g);
36✔
315
    }
316

317
    public function isValid(Geometry $g) : bool
318
    {
319
        return $this->queryBoolean('ST_IsValid', $g);
46✔
320
    }
321

322
    public function isClosed(Geometry $g) : bool
323
    {
324
        return $this->queryBoolean('ST_IsClosed', $g);
276✔
325
    }
326

327
    public function isSimple(Geometry $g) : bool
328
    {
329
        return $this->queryBoolean('ST_IsSimple', $g);
102✔
330
    }
331

332
    public function isRing(Curve $curve) : bool
333
    {
334
        try {
335
            return $this->queryBoolean('ST_IsRing', $curve);
58✔
UNCOV
336
        } catch (GeometryEngineException) {
10✔
337
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
UNCOV
338
            return $this->isClosed($curve) && $this->isSimple($curve);
10✔
339
        }
340
    }
341

342
    public function makeValid(Geometry $g) : Geometry
343
    {
344
        return $this->queryGeometry('ST_MakeValid', $g);
12✔
345
    }
346

347
    public function equals(Geometry $a, Geometry $b) : bool
348
    {
349
        return $this->queryBoolean('ST_Equals', $a, $b);
122✔
350
    }
351

352
    public function disjoint(Geometry $a, Geometry $b) : bool
353
    {
354
        return $this->queryBoolean('ST_Disjoint', $a, $b);
36✔
355
    }
356

357
    public function touches(Geometry $a, Geometry $b) : bool
358
    {
359
        return $this->queryBoolean('ST_Touches', $a, $b);
48✔
360
    }
361

362
    public function crosses(Geometry $a, Geometry $b) : bool
363
    {
364
        return $this->queryBoolean('ST_Crosses', $a, $b);
48✔
365
    }
366

367
    public function within(Geometry $a, Geometry $b) : bool
368
    {
369
        return $this->queryBoolean('ST_Within', $a, $b);
30✔
370
    }
371

372
    public function overlaps(Geometry $a, Geometry $b) : bool
373
    {
374
        return $this->queryBoolean('ST_Overlaps', $a, $b);
12✔
375
    }
376

377
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
378
    {
379
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
24✔
380
    }
381

382
    public function locateAlong(Geometry $g, float $mValue) : Geometry
383
    {
384
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
12✔
385
    }
386

387
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
388
    {
389
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
12✔
390
    }
391

392
    public function distance(Geometry $a, Geometry $b) : float
393
    {
394
        return $this->queryFloat('ST_Distance', $a, $b);
30✔
395
    }
396

397
    public function buffer(Geometry $g, float $distance) : Geometry
398
    {
399
        return $this->queryGeometry('ST_Buffer', $g, $distance);
18✔
400
    }
401

402
    public function convexHull(Geometry $g) : Geometry
403
    {
404
        return $this->queryGeometry('ST_ConvexHull', $g);
18✔
405
    }
406

407
    public function symDifference(Geometry $a, Geometry $b) : Geometry
408
    {
409
        return $this->queryGeometry('ST_SymDifference', $a, $b);
6✔
410
    }
411

412
    public function snapToGrid(Geometry $g, float $size) : Geometry
413
    {
414
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
24✔
415
    }
416

417
    public function simplify(Geometry $g, float $tolerance) : Geometry
418
    {
419
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
12✔
420
    }
421

422
    public function maxDistance(Geometry $a, Geometry $b) : float
423
    {
424
        return $this->queryFloat('ST_MaxDistance', $a, $b);
18✔
425
    }
426

427
    public function transform(Geometry $g, int $srid) : Geometry
428
    {
429
        return $this->queryGeometry('ST_Transform', $g, $srid);
4✔
430
    }
431

432
    public function split(Geometry $g, Geometry $blade) : Geometry
433
    {
434
        return $this->queryGeometry('ST_Split', $g, $blade);
8✔
435
    }
436
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc