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

brick / geo / 13766209603

09 Mar 2025 10:35PM UTC coverage: 87.414% (+3.3%) from 84.117%
13766209603

push

github

BenMorel
Add Point::isEqualTo() (WIP: finish? keep?)

8 of 8 new or added lines in 2 files covered. (100.0%)

73 existing lines in 16 files now uncovered.

1653 of 1891 relevant lines covered (87.41%)

1946.79 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Brick\Geo\Engine;
6

7
use Brick\Geo\CircularString;
8
use Brick\Geo\CompoundCurve;
9
use Brick\Geo\Curve;
10
use Brick\Geo\CurvePolygon;
11
use Brick\Geo\Engine\Internal\TypeChecker;
12
use Brick\Geo\Exception\GeometryEngineException;
13
use Brick\Geo\Geometry;
14
use Brick\Geo\GeometryCollection;
15
use Brick\Geo\LineString;
16
use Brick\Geo\MultiCurve;
17
use Brick\Geo\MultiLineString;
18
use Brick\Geo\MultiPoint;
19
use Brick\Geo\MultiSurface;
20
use Brick\Geo\MultiPolygon;
21
use Brick\Geo\Point;
22
use Brick\Geo\Polygon;
23
use Brick\Geo\PolyhedralSurface;
24
use Brick\Geo\Proxy\ProxyFactory;
25
use Brick\Geo\Surface;
26
use Brick\Geo\TIN;
27
use Brick\Geo\Triangle;
28
use Override;
29

30
/**
31
 * Database implementation of the GeometryEngine.
32
 *
33
 * The target database must have support for GIS functions.
34
 */
35
abstract class DatabaseEngine implements GeometryEngine
36
{
37
    private readonly bool $useProxy;
38

39
    public function __construct(bool $useProxy)
40
    {
UNCOV
41
        $this->useProxy = $useProxy;
×
42
    }
43

44
    /**
45
     * Executes a SQL query.
46
     *
47
     * @param string                                   $query      The SQL query to execute.
48
     * @param list<GeometryParameter|string|float|int> $parameters The geometry data or scalar values to pass as parameters.
49
     *
50
     * @return list<mixed> A numeric result array.
51
     *
52
     * @throws GeometryEngineException
53
     */
54
    abstract protected function executeQuery(string $query, array $parameters) : array;
55

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

66
    /**
67
     * Returns the syntax required to perform an ST_GeomFromWKB(), together with placeholders.
68
     *
69
     * This method may be overridden if necessary.
70
     */
71
    protected function getGeomFromWKBSyntax(): string
72
    {
73
        return 'ST_GeomFromWKB(?, ?)';
519✔
74
    }
75

76
    /**
77
     * Returns the placeholder syntax for the given parameter.
78
     *
79
     * This method may be overridden to perform explicit type casts if necessary.
80
     */
81
    protected function getParameterPlaceholder(string|float|int $parameter): string
82
    {
83
        return '?';
442✔
84
    }
85

86
    /**
87
     * Builds and executes a SQL query for a GIS function.
88
     *
89
     * @param string                           $function        The SQL GIS function to execute.
90
     * @param array<Geometry|string|float|int> $parameters      The Geometry objects or scalar values to pass as parameters.
91
     * @param bool                             $returnsGeometry Whether the GIS function returns a Geometry.
92
     *
93
     * @return list<mixed> A numeric result array.
94
     *
95
     * @throws GeometryEngineException
96
     */
97
    private function query(string $function, array $parameters, bool $returnsGeometry) : array
98
    {
99
        $queryParameters = [];
1,670✔
100
        $queryValues = [];
1,670✔
101

102
        foreach ($parameters as $parameter) {
1,670✔
103
            if ($parameter instanceof Geometry) {
1,670✔
104
                $sendAsBinary = ! $parameter->isEmpty();
1,670✔
105

106
                $queryParameters[] = $sendAsBinary
1,670✔
107
                    ? $this->getGeomFromWKBSyntax()
1,555✔
108
                    : $this->getGeomFromTextSyntax();
125✔
109

110
                $queryValues[] = new GeometryParameter($parameter, $sendAsBinary);
1,670✔
111
            } else {
112
                $queryParameters[] = $this->getParameterPlaceholder($parameter);
449✔
113
                $queryValues[] = $parameter;
449✔
114
            }
115
        }
116

117
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
1,670✔
118

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

130
        return $this->executeQuery($query, $queryValues);
1,670✔
131
    }
132

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

146
        $value = $result[0];
732✔
147

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

154
        return (bool) $value;
636✔
155
    }
156

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

170
        $value = $result[0];
191✔
171

172
        if ($value === null) {
191✔
173
            throw GeometryEngineException::operationYieldedNoResult();
27✔
174
        }
175

176
        return (float) $value;
164✔
177
    }
178

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

192
        [$wkt, $wkb, $geometryType, $srid] = $result;
436✔
193

194
        $srid = (int) $srid;
436✔
195

196
        if ($wkt !== null) {
436✔
197
            if ($this->useProxy) {
6✔
198
                $geometryClass = $this->getGeometryClass($geometryType);
6✔
199

200
                return ProxyFactory::createWktProxy($geometryClass, $wkt, $srid);
6✔
201
            }
202

UNCOV
203
            return Geometry::fromText($wkt, $srid);
×
204
        }
205

206
        if ($wkb !== null) {
430✔
207
            if (is_resource($wkb)) {
416✔
208
                $wkb = stream_get_contents($wkb);
112✔
209

210
                if ($wkb === false) {
112✔
UNCOV
211
                    throw new GeometryEngineException('Cannot read stream contents.');
×
212
                }
213
            }
214

215
            if ($this->useProxy) {
416✔
216
                $geometryClass = $this->getGeometryClass($geometryType);
416✔
217

218
                return ProxyFactory::createWkbProxy($geometryClass, $wkb, $srid);
416✔
219
            }
220

UNCOV
221
            return Geometry::fromBinary($wkb, $srid);
×
222
        }
223

224
        throw GeometryEngineException::operationYieldedNoResult();
14✔
225
    }
226

227
    /**
228
     * @return class-string<Geometry>
229
     *
230
     * @throws GeometryEngineException
231
     */
232
    private function getGeometryClass(string $geometryType) : string
233
    {
234
        $geometryClasses = [
422✔
235
            'CIRCULARSTRING'     => CircularString::class,
422✔
236
            'COMPOUNDCURVE'      => CompoundCurve::class,
422✔
237
            'CURVE'              => Curve::class,
422✔
238
            'CURVEPOLYGON'       => CurvePolygon::class,
422✔
239
            'GEOMCOLLECTION'     => GeometryCollection::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
422✔
240
            'GEOMETRY'           => Geometry::class,
422✔
241
            'GEOMETRYCOLLECTION' => GeometryCollection::class,
422✔
242
            'LINESTRING'         => LineString::class,
422✔
243
            'MULTICURVE'         => MultiCurve::class,
422✔
244
            'MULTILINESTRING'    => MultiLineString::class,
422✔
245
            'MULTIPOINT'         => MultiPoint::class,
422✔
246
            'MULTIPOLYGON'       => MultiPolygon::class,
422✔
247
            'MULTISURFACE'       => MultiSurface::class,
422✔
248
            'POINT'              => Point::class,
422✔
249
            'POLYGON'            => Polygon::class,
422✔
250
            'POLYHEDRALSURFACE'  => PolyhedralSurface::class,
422✔
251
            'SURFACE'            => Surface::class,
422✔
252
            'TIN'                => TIN::class,
422✔
253
            'TRIANGLE'           => Triangle::class
422✔
254
        ];
422✔
255

256
        $geometryType = strtoupper($geometryType);
422✔
257
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
422✔
258
        assert($geometryType !== null);
259
        $geometryType = preg_replace('/ .*/', '', $geometryType);
422✔
260
        assert($geometryType !== null);
261

262
        if (! isset($geometryClasses[$geometryType])) {
422✔
UNCOV
263
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
264
        }
265

266
        return $geometryClasses[$geometryType];
422✔
267
    }
268

269
    #[Override]
270
    public function contains(Geometry $a, Geometry $b) : bool
271
    {
272
        return $this->queryBoolean('ST_Contains', $a, $b);
76✔
273
    }
274

275
    #[Override]
276
    public function intersects(Geometry $a, Geometry $b) : bool
277
    {
278
        return $this->queryBoolean('ST_Intersects', $a, $b);
36✔
279
    }
280

281
    #[Override]
282
    public function union(Geometry $a, Geometry $b) : Geometry
283
    {
284
        return $this->queryGeometry('ST_Union', $a, $b);
22✔
285
    }
286

287
    #[Override]
288
    public function intersection(Geometry $a, Geometry $b) : Geometry
289
    {
290
        return $this->queryGeometry('ST_Intersection', $a, $b);
12✔
291
    }
292

293
    #[Override]
294
    public function difference(Geometry $a, Geometry $b) : Geometry
295
    {
296
        return $this->queryGeometry('ST_Difference', $a, $b);
12✔
297
    }
298

299
    #[Override]
300
    public function envelope(Geometry $g) : Geometry
301
    {
302
        return $this->queryGeometry('ST_Envelope', $g);
18✔
303
    }
304

305
    #[Override]
306
    public function centroid(Geometry $g) : Point
307
    {
308
        $centroid = $this->queryGeometry('ST_Centroid', $g);
45✔
309
        TypeChecker::check($centroid, Point::class);
45✔
310

311
        return $centroid;
45✔
312
    }
313

314
    #[Override]
315
    public function pointOnSurface(Surface|MultiSurface $g) : Point
316
    {
317
        $pointOnSurface = $this->queryGeometry('ST_PointOnSurface', $g);
42✔
318
        TypeChecker::check($pointOnSurface, Point::class);
28✔
319

320
        return $pointOnSurface;
28✔
321
    }
322

323
    #[Override]
324
    public function length(Curve|MultiCurve $g) : float
325
    {
326
        return $this->queryFloat('ST_Length', $g);
95✔
327
    }
328

329
    #[Override]
330
    public function area(Surface|MultiSurface $g) : float
331
    {
332
        return $this->queryFloat('ST_Area', $g);
66✔
333
    }
334

335
    #[Override]
336
    public function azimuth(Point $observer, Point $subject) : float
337
    {
338
        return $this->queryFloat('ST_Azimuth', $observer, $subject);
36✔
339
    }
340

341
    #[Override]
342
    public function boundary(Geometry $g) : Geometry
343
    {
344
        return $this->queryGeometry('ST_Boundary', $g);
36✔
345
    }
346

347
    #[Override]
348
    public function isValid(Geometry $g) : bool
349
    {
350
        return $this->queryBoolean('ST_IsValid', $g);
46✔
351
    }
352

353
    #[Override]
354
    public function isClosed(Geometry $g) : bool
355
    {
356
        return $this->queryBoolean('ST_IsClosed', $g);
276✔
357
    }
358

359
    #[Override]
360
    public function isSimple(Geometry $g) : bool
361
    {
362
        return $this->queryBoolean('ST_IsSimple', $g);
102✔
363
    }
364

365
    #[Override]
366
    public function isRing(Curve $curve) : bool
367
    {
368
        try {
369
            return $this->queryBoolean('ST_IsRing', $curve);
58✔
370
        } catch (GeometryEngineException) {
10✔
371
            // Not all RDBMS (hello, MySQL) support ST_IsRing(), but we have an easy fallback
372
            return $this->isClosed($curve) && $this->isSimple($curve);
10✔
373
        }
374
    }
375

376
    #[Override]
377
    public function makeValid(Geometry $g) : Geometry
378
    {
379
        return $this->queryGeometry('ST_MakeValid', $g);
36✔
380
    }
381

382
    #[Override]
383
    public function equals(Geometry $a, Geometry $b) : bool
384
    {
385
        return $this->queryBoolean('ST_Equals', $a, $b);
122✔
386
    }
387

388
    #[Override]
389
    public function disjoint(Geometry $a, Geometry $b) : bool
390
    {
391
        return $this->queryBoolean('ST_Disjoint', $a, $b);
36✔
392
    }
393

394
    #[Override]
395
    public function touches(Geometry $a, Geometry $b) : bool
396
    {
397
        return $this->queryBoolean('ST_Touches', $a, $b);
48✔
398
    }
399

400
    #[Override]
401
    public function crosses(Geometry $a, Geometry $b) : bool
402
    {
403
        return $this->queryBoolean('ST_Crosses', $a, $b);
48✔
404
    }
405

406
    #[Override]
407
    public function within(Geometry $a, Geometry $b) : bool
408
    {
409
        return $this->queryBoolean('ST_Within', $a, $b);
30✔
410
    }
411

412
    #[Override]
413
    public function overlaps(Geometry $a, Geometry $b) : bool
414
    {
415
        return $this->queryBoolean('ST_Overlaps', $a, $b);
12✔
416
    }
417

418
    #[Override]
419
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
420
    {
421
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
24✔
422
    }
423

424
    #[Override]
425
    public function locateAlong(Geometry $g, float $mValue) : Geometry
426
    {
427
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
12✔
428
    }
429

430
    #[Override]
431
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
432
    {
433
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
12✔
434
    }
435

436
    #[Override]
437
    public function distance(Geometry $a, Geometry $b) : float
438
    {
439
        return $this->queryFloat('ST_Distance', $a, $b);
30✔
440
    }
441

442
    #[Override]
443
    public function buffer(Geometry $g, float $distance) : Geometry
444
    {
445
        return $this->queryGeometry('ST_Buffer', $g, $distance);
18✔
446
    }
447

448
    #[Override]
449
    public function convexHull(Geometry $g) : Geometry
450
    {
451
        return $this->queryGeometry('ST_ConvexHull', $g);
18✔
452
    }
453

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

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

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

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

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

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

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

496
        return $result;
63✔
497
    }
498

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

504
        if ($result instanceof MultiPoint) {
88✔
505
            return $result;
70✔
506
        }
507

508
        TypeChecker::check($result, Point::class);
18✔
509

510
        // POINT EMPTY
511
        if ($result->isEmpty()) {
18✔
512
            return new MultiPoint($result->coordinateSystem());
1✔
513
        }
514

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