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

brick / geo / 13861125046

14 Mar 2025 04:33PM UTC coverage: 48.122% (-2.9%) from 51.017%
13861125046

push

github

BenMorel
Add more tests for spatial equality

This engine method is the foundation for other tests, so we need to ensure that it works fine in more complex cases.

1755 of 3647 relevant lines covered (48.12%)

863.52 hits per line

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

90.55
/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\Engine\Internal\TypeChecker;
9
use Brick\Geo\Exception\GeometryEngineException;
10
use Brick\Geo\Geometry;
11
use Brick\Geo\LineString;
12
use Brick\Geo\MultiCurve;
13
use Brick\Geo\MultiPoint;
14
use Brick\Geo\MultiSurface;
15
use Brick\Geo\MultiPolygon;
16
use Brick\Geo\Point;
17
use Brick\Geo\Polygon;
18
use Brick\Geo\Proxy;
19
use Brick\Geo\Proxy\ProxyInterface;
20
use Brick\Geo\Surface;
21
use Override;
22

23
/**
24
 * Database implementation of the GeometryEngine.
25
 *
26
 * The target database must have support for GIS functions.
27
 */
28
abstract class DatabaseEngine implements GeometryEngine
29
{
30
    private readonly bool $useProxy;
31

32
    public function __construct(bool $useProxy)
33
    {
34
        $this->useProxy = $useProxy;
×
35
    }
36

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

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

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

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

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

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

103
                $queryParameters[] = $sendAsBinary
1,466✔
104
                    ? $this->getGeomFromWKBSyntax()
1,374✔
105
                    : $this->getGeomFromTextSyntax();
100✔
106

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

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

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

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

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

143
        $value = $result[0];
639✔
144

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

151
        return (bool) $value;
543✔
152
    }
153

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

167
        $value = $result[0];
150✔
168

169
        if ($value === null) {
150✔
170
            throw GeometryEngineException::operationYieldedNoResult();
26✔
171
        }
172

173
        return (float) $value;
124✔
174
    }
175

176
    /**
177
     * Queries a GIS function returning a Geometry object.
178
     *
179
     * @param string             $function   The SQL GIS function to execute.
180
     * @param Geometry|scalar ...$parameters The Geometry objects or scalar values to pass as parameters.
181
     *
182
     * @throws GeometryEngineException
183
     */
184
    final protected function queryGeometry(string $function, Geometry|string|float|int|bool ...$parameters) : Geometry
185
    {
186
        /** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
187
        $result = $this->query($function, $parameters, true);
603✔
188

189
        [$wkt, $wkb, $geometryType, $srid] = $result;
330✔
190

191
        $srid = (int) $srid;
330✔
192

193
        if ($wkt !== null) {
330✔
194
            if ($this->useProxy) {
×
195
                $proxyClassName = $this->getProxyClassName($geometryType);
×
196

197
                return new $proxyClassName($wkt, false, $srid);
×
198
            }
199

200
            return Geometry::fromText($wkt, $srid);
×
201
        }
202

203
        if ($wkb !== null) {
330✔
204
            if (is_resource($wkb)) {
311✔
205
                $wkb = stream_get_contents($wkb);
×
206
            }
207

208
            if ($this->useProxy) {
311✔
209
                $proxyClassName = $this->getProxyClassName($geometryType);
311✔
210

211
                return new $proxyClassName($wkb, true, $srid);
311✔
212
            }
213

214
            return Geometry::fromBinary($wkb, $srid);
×
215
        }
216

217
        throw GeometryEngineException::operationYieldedNoResult();
19✔
218
    }
219

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

249
        $geometryType = strtoupper($geometryType);
311✔
250
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
311✔
251
        assert($geometryType !== null);
252
        $geometryType = preg_replace('/ .*/', '', $geometryType);
311✔
253
        assert($geometryType !== null);
254

255
        if (! isset($proxyClasses[$geometryType])) {
311✔
256
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
257
        }
258

259
        return $proxyClasses[$geometryType];
311✔
260
    }
261

262
    #[Override]
263
    public function contains(Geometry $a, Geometry $b) : bool
264
    {
265
        return $this->queryBoolean('ST_Contains', $a, $b);
61✔
266
    }
267

268
    #[Override]
269
    public function intersects(Geometry $a, Geometry $b) : bool
270
    {
271
        return $this->queryBoolean('ST_Intersects', $a, $b);
30✔
272
    }
273

274
    #[Override]
275
    public function union(Geometry $a, Geometry $b) : Geometry
276
    {
277
        return $this->queryGeometry('ST_Union', $a, $b);
18✔
278
    }
279

280
    #[Override]
281
    public function intersection(Geometry $a, Geometry $b) : Geometry
282
    {
283
        return $this->queryGeometry('ST_Intersection', $a, $b);
10✔
284
    }
285

286
    #[Override]
287
    public function difference(Geometry $a, Geometry $b) : Geometry
288
    {
289
        return $this->queryGeometry('ST_Difference', $a, $b);
10✔
290
    }
291

292
    #[Override]
293
    public function envelope(Geometry $g) : Geometry
294
    {
295
        return $this->queryGeometry('ST_Envelope', $g);
15✔
296
    }
297

298
    #[Override]
299
    public function centroid(Geometry $g) : Point
300
    {
301
        $centroid = $this->queryGeometry('ST_Centroid', $g);
36✔
302
        TypeChecker::check($centroid, Point::class);
36✔
303

304
        return $centroid;
36✔
305
    }
306

307
    #[Override]
308
    public function pointOnSurface(Surface|MultiSurface $g) : Point
309
    {
310
        $pointOnSurface = $this->queryGeometry('ST_PointOnSurface', $g);
35✔
311
        TypeChecker::check($pointOnSurface, Point::class);
21✔
312

313
        return $pointOnSurface;
21✔
314
    }
315

316
    #[Override]
317
    public function length(Curve|MultiCurve $g) : float
318
    {
319
        return $this->queryFloat('ST_Length', $g);
79✔
320
    }
321

322
    #[Override]
323
    public function area(Surface|MultiSurface $g) : float
324
    {
325
        return $this->queryFloat('ST_Area', $g);
55✔
326
    }
327

328
    #[Override]
329
    public function azimuth(Point $observer, Point $subject) : float
330
    {
331
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
30✔
332
    }
333

334
    #[Override]
335
    public function boundary(Geometry $g) : Geometry
336
    {
337
        return $this->queryGeometry('ST_Boundary', $g);
30✔
338
    }
339

340
    #[Override]
341
    public function isValid(Geometry $g) : bool
342
    {
343
        return $this->queryBoolean('ST_IsValid', $g);
34✔
344
    }
345

346
    #[Override]
347
    public function isClosed(Geometry $g) : bool
348
    {
349
        return $this->queryBoolean('ST_IsClosed', $g);
227✔
350
    }
351

352
    #[Override]
353
    public function isSimple(Geometry $g) : bool
354
    {
355
        return $this->queryBoolean('ST_IsSimple', $g);
86✔
356
    }
357

358
    #[Override]
359
    public function isRing(Curve $curve) : bool
360
    {
361
        try {
362
            return $this->queryBoolean('ST_IsRing', $curve);
33✔
363
        } catch (GeometryEngineException) {
10✔
364
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
365
            return $this->isClosed($curve) && $this->isSimple($curve);
10✔
366
        }
367
    }
368

369
    #[Override]
370
    public function makeValid(Geometry $g) : Geometry
371
    {
372
        return $this->queryGeometry('ST_MakeValid', $g);
30✔
373
    }
374

375
    #[Override]
376
    public function equals(Geometry $a, Geometry $b) : bool
377
    {
378
        return $this->queryBoolean('ST_Equals', $a, $b);
160✔
379
    }
380

381
    #[Override]
382
    public function disjoint(Geometry $a, Geometry $b) : bool
383
    {
384
        return $this->queryBoolean('ST_Disjoint', $a, $b);
30✔
385
    }
386

387
    #[Override]
388
    public function touches(Geometry $a, Geometry $b) : bool
389
    {
390
        return $this->queryBoolean('ST_Touches', $a, $b);
40✔
391
    }
392

393
    #[Override]
394
    public function crosses(Geometry $a, Geometry $b) : bool
395
    {
396
        return $this->queryBoolean('ST_Crosses', $a, $b);
40✔
397
    }
398

399
    #[Override]
400
    public function within(Geometry $a, Geometry $b) : bool
401
    {
402
        return $this->queryBoolean('ST_Within', $a, $b);
25✔
403
    }
404

405
    #[Override]
406
    public function overlaps(Geometry $a, Geometry $b) : bool
407
    {
408
        return $this->queryBoolean('ST_Overlaps', $a, $b);
10✔
409
    }
410

411
    #[Override]
412
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
413
    {
414
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
20✔
415
    }
416

417
    #[Override]
418
    public function locateAlong(Geometry $g, float $mValue) : Geometry
419
    {
420
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
10✔
421
    }
422

423
    #[Override]
424
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
425
    {
426
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
10✔
427
    }
428

429
    #[Override]
430
    public function distance(Geometry $a, Geometry $b) : float
431
    {
432
        return $this->queryFloat('ST_Distance', $a, $b);
25✔
433
    }
434

435
    #[Override]
436
    public function buffer(Geometry $g, float $distance) : Geometry
437
    {
438
        return $this->queryGeometry('ST_Buffer', $g, $distance);
15✔
439
    }
440

441
    #[Override]
442
    public function convexHull(Geometry $g) : Geometry
443
    {
444
        return $this->queryGeometry('ST_ConvexHull', $g);
15✔
445
    }
446

447
    #[Override]
448
    public function concaveHull(Geometry $g, float $convexity, bool $allowHoles) : Geometry
449
    {
450
        return $this->queryGeometry('ST_ConcaveHull', $g, $convexity, $allowHoles);
25✔
451
    }
452

453
    #[Override]
454
    public function symDifference(Geometry $a, Geometry $b) : Geometry
455
    {
456
        return $this->queryGeometry('ST_SymDifference', $a, $b);
5✔
457
    }
458

459
    #[Override]
460
    public function snapToGrid(Geometry $g, float $size) : Geometry
461
    {
462
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
20✔
463
    }
464

465
    #[Override]
466
    public function simplify(Geometry $g, float $tolerance) : Geometry
467
    {
468
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
10✔
469
    }
470

471
    #[Override]
472
    public function maxDistance(Geometry $a, Geometry $b) : float
473
    {
474
        return $this->queryFloat('ST_MaxDistance', $a, $b);
15✔
475
    }
476

477
    #[Override]
478
    public function transform(Geometry $g, int $srid) : Geometry
479
    {
480
        return $this->queryGeometry('ST_Transform', $g, $srid);
35✔
481
    }
482

483
    #[Override]
484
    public function split(Geometry $g, Geometry $blade) : Geometry
485
    {
486
        return $this->queryGeometry('ST_Split', $g, $blade);
20✔
487
    }
488

489
    #[Override]
490
    public function lineInterpolatePoint(LineString $lineString, float $fraction) : Point
491
    {
492
        $result = $this->queryGeometry('ST_LineInterpolatePoint', $lineString, $fraction);
84✔
493
        TypeChecker::check($result, Point::class);
42✔
494

495
        return $result;
42✔
496
    }
497

498
    #[Override]
499
    public function lineInterpolatePoints(LineString $lineString, float $fraction) : MultiPoint
500
    {
501
        $result = $this->queryGeometry('ST_LineInterpolatePoints', $lineString, $fraction);
149✔
502

503
        if ($result instanceof MultiPoint) {
58✔
504
            return $result;
58✔
505
        }
506

507
        TypeChecker::check($result, Point::class);
×
508

509
        // POINT EMPTY
510
        if ($result->isEmpty()) {
×
511
            return new MultiPoint($result->coordinateSystem());
×
512
        }
513

514
        // POINT
515
        return MultiPoint::of($result);
×
516
    }
517
}
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