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

brick / geo / 13730559764

07 Mar 2025 10:35PM UTC coverage: 84.117%. Remained the same
13730559764

push

github

BenMorel
Add tests for CompoundCurve explicit form

1557 of 1851 relevant lines covered (84.12%)

1351.65 hits per line

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

95.54
/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\Exception\GeometryEngineException;
12
use Brick\Geo\Geometry;
13
use Brick\Geo\GeometryCollection;
14
use Brick\Geo\LineString;
15
use Brick\Geo\MultiCurve;
16
use Brick\Geo\MultiLineString;
17
use Brick\Geo\MultiPoint;
18
use Brick\Geo\MultiSurface;
19
use Brick\Geo\MultiPolygon;
20
use Brick\Geo\Point;
21
use Brick\Geo\Polygon;
22
use Brick\Geo\PolyhedralSurface;
23
use Brick\Geo\Proxy\ProxyFactory;
24
use Brick\Geo\Surface;
25
use Brick\Geo\TIN;
26
use Brick\Geo\Triangle;
27
use Override;
28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

191
        [$wkt, $wkb, $geometryType, $srid] = $result;
247✔
192

193
        $srid = (int) $srid;
247✔
194

195
        if ($wkt !== null) {
247✔
196
            if ($this->useProxy) {
5✔
197
                $geometryClass = $this->getGeometryClass($geometryType);
5✔
198

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

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

205
        if ($wkb !== null) {
242✔
206
            if (is_resource($wkb)) {
228✔
207
                $wkb = stream_get_contents($wkb);
59✔
208

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

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

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

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

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

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

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

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

265
        return $geometryClasses[$geometryType];
233✔
266
    }
267

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

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

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

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

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

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

304
    #[Override]
305
    public function centroid(Geometry $g) : Point
306
    {
307
        /** @var Point */
308
        return $this->queryGeometry('ST_Centroid', $g);
45✔
309
    }
310

311
    #[Override]
312
    public function pointOnSurface(Surface|MultiSurface $g) : Point
313
    {
314
        /** @var Point */
315
        return $this->queryGeometry('ST_PointOnSurface', $g);
42✔
316
    }
317

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

449
    #[Override]
450
    public function symDifference(Geometry $a, Geometry $b) : Geometry
451
    {
452
        return $this->queryGeometry('ST_SymDifference', $a, $b);
6✔
453
    }
454

455
    #[Override]
456
    public function snapToGrid(Geometry $g, float $size) : Geometry
457
    {
458
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
24✔
459
    }
460

461
    #[Override]
462
    public function simplify(Geometry $g, float $tolerance) : Geometry
463
    {
464
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
12✔
465
    }
466

467
    #[Override]
468
    public function maxDistance(Geometry $a, Geometry $b) : float
469
    {
470
        return $this->queryFloat('ST_MaxDistance', $a, $b);
18✔
471
    }
472

473
    #[Override]
474
    public function transform(Geometry $g, int $srid) : Geometry
475
    {
476
        return $this->queryGeometry('ST_Transform', $g, $srid);
4✔
477
    }
478

479
    #[Override]
480
    public function split(Geometry $g, Geometry $blade) : Geometry
481
    {
482
        return $this->queryGeometry('ST_Split', $g, $blade);
8✔
483
    }
484
}
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