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

brick / geo / 13592026859

28 Feb 2025 04:11PM UTC coverage: 47.753% (+0.2%) from 47.546%
13592026859

Pull #55

github

web-flow
Merge 5d43f9c85 into 5c954d80e
Pull Request #55: Add LineInterpolatePoint for Postgis

17 of 23 new or added lines in 3 files covered. (73.91%)

6 existing lines in 1 file now uncovered.

1626 of 3405 relevant lines covered (47.75%)

691.32 hits per line

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

94.26
/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\LineString;
11
use Brick\Geo\MultiCurve;
12
use Brick\Geo\MultiPoint;
13
use Brick\Geo\MultiSurface;
14
use Brick\Geo\MultiPolygon;
15
use Brick\Geo\Point;
16
use Brick\Geo\Polygon;
17
use Brick\Geo\Proxy;
18
use Brick\Geo\Proxy\ProxyInterface;
19
use Brick\Geo\Surface;
20

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

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

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

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

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

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

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

99
        foreach ($parameters as $parameter) {
858✔
100
            if ($parameter instanceof Geometry) {
858✔
101
                if ($parameter instanceof Proxy\ProxyInterface) {
858✔
102
                    $sendAsBinary = $parameter->isProxyBinary();
63✔
103
                } else {
104
                    $sendAsBinary = ! $parameter->isEmpty();
858✔
105
                }
106

107
                $queryParameters[] = $sendAsBinary
858✔
108
                    ? $this->getGeomFromWKBSyntax()
791✔
109
                    : $this->getGeomFromTextSyntax();
73✔
110

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

118
        $query = sprintf('SELECT %s(%s)', $function, implode(', ', $queryParameters));
858✔
119

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

131
        return $this->executeQuery($query, $queryValues);
858✔
132
    }
133

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

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

152
        return (bool) $result;
454✔
153
    }
154

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

167
        if ($result === null) {
145✔
168
            throw GeometryEngineException::operationYieldedNoResult();
27✔
169
        }
170

171
        return (float) $result;
118✔
172
    }
173

174
    /**
175
     * Queries a GIS function returning a MultiPoint value.
176
     *
177
     * @param string                       $function   The SQL GIS function to execute.
178
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
179
     *
180
     * @throws GeometryEngineException
181
     */
182
    private function queryMultiPoint(string $function, Geometry|string|float|int ...$parameters) : MultiPoint
183
    {
184
        $result = $this->queryGeometry($function, ...$parameters);
5✔
185

186
        if ($result instanceof MultiPoint) {
5✔
187
            return $result;
2✔
188
        }
189

190
        $geometry = $result instanceof ProxyInterface ? $result->getGeometry() : $result;
3✔
191

192
        if ($geometry->isEmpty()) {
3✔
193
            return MultiPoint::fromText('MULTIPOINT EMPTY');
1✔
194
        }
195

196
        return MultiPoint::of($result);
2✔
197
    }
198

199
    /**
200
     * Queries a GIS function returning a Geometry object.
201
     *
202
     * @param string                       $function   The SQL GIS function to execute.
203
     * @param Geometry|string|float|int ...$parameters The Geometry objects or scalar values to pass as parameters.
204
     *
205
     * @throws GeometryEngineException
206
     */
207
    protected function queryGeometry(string $function, Geometry|string|float|int ...$parameters) : Geometry
208
    {
209
        /** @var array{string|null, string|resource|null, string, int|numeric-string} $result */
210
        $result = $this->query($function, $parameters, true);
228✔
211

212
        [$wkt, $wkb, $geometryType, $srid] = $result;
208✔
213

214
        $srid = (int) $srid;
208✔
215

216
        if ($wkt !== null) {
208✔
217
            if ($this->useProxy) {
6✔
218
                $proxyClassName = $this->getProxyClassName($geometryType);
6✔
219

220
                return new $proxyClassName($wkt, false, $srid);
6✔
221
            }
222

UNCOV
223
            return Geometry::fromText($wkt, $srid);
×
224
        }
225

226
        if ($wkb !== null) {
202✔
227
            if (is_resource($wkb)) {
188✔
228
                $wkb = stream_get_contents($wkb);
67✔
229
            }
230

231
            if ($this->useProxy) {
188✔
232
                $proxyClassName = $this->getProxyClassName($geometryType);
188✔
233

234
                return new $proxyClassName($wkb, true, $srid);
188✔
235
            }
236

UNCOV
237
            return Geometry::fromBinary($wkb, $srid);
×
238
        }
239

240
        throw GeometryEngineException::operationYieldedNoResult();
14✔
241
    }
242

243
    /**
244
     * @psalm-return class-string<Proxy\ProxyInterface&Geometry>
245
     *
246
     * @throws GeometryEngineException
247
     */
248
    private function getProxyClassName(string $geometryType) : string
249
    {
250
        $proxyClasses = [
194✔
251
            'CIRCULARSTRING'     => Proxy\CircularStringProxy::class,
194✔
252
            'COMPOUNDCURVE'      => Proxy\CompoundCurveProxy::class,
194✔
253
            'CURVE'              => Proxy\CurveProxy::class,
194✔
254
            'CURVEPOLYGON'       => Proxy\CurvePolygonProxy::class,
194✔
255
            'GEOMCOLLECTION'     => Proxy\GeometryCollectionProxy::class, /* MySQL 8 - https://github.com/brick/geo/pull/33 */
194✔
256
            'GEOMETRY'           => Proxy\GeometryProxy::class,
194✔
257
            'GEOMETRYCOLLECTION' => Proxy\GeometryCollectionProxy::class,
194✔
258
            'LINESTRING'         => Proxy\LineStringProxy::class,
194✔
259
            'MULTICURVE'         => Proxy\MultiCurveProxy::class,
194✔
260
            'MULTILINESTRING'    => Proxy\MultiLineStringProxy::class,
194✔
261
            'MULTIPOINT'         => Proxy\MultiPointProxy::class,
194✔
262
            'MULTIPOLYGON'       => Proxy\MultiPolygonProxy::class,
194✔
263
            'MULTISURFACE'       => Proxy\MultiSurfaceProxy::class,
194✔
264
            'POINT'              => Proxy\PointProxy::class,
194✔
265
            'POLYGON'            => Proxy\PolygonProxy::class,
194✔
266
            'POLYHEDRALSURFACE'  => Proxy\PolyhedralSurfaceProxy::class,
194✔
267
            'SURFACE'            => Proxy\SurfaceProxy::class,
194✔
268
            'TIN'                => Proxy\TINProxy::class,
194✔
269
            'TRIANGLE'           => Proxy\TriangleProxy::class
194✔
270
        ];
194✔
271

272
        $geometryType = strtoupper($geometryType);
194✔
273
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
194✔
274
        $geometryType = preg_replace('/ .*/', '', $geometryType);
194✔
275

276
        if (! isset($proxyClasses[$geometryType])) {
194✔
UNCOV
277
            throw new GeometryEngineException('Unknown geometry type: ' . $geometryType);
×
278
        }
279

280
        return $proxyClasses[$geometryType];
194✔
281
    }
282

283
    public function contains(Geometry $a, Geometry $b) : bool
284
    {
285
        return $this->queryBoolean('ST_Contains', $a, $b);
60✔
286
    }
287

288
    public function intersects(Geometry $a, Geometry $b) : bool
289
    {
290
        return $this->queryBoolean('ST_Intersects', $a, $b);
24✔
291
    }
292

293
    public function union(Geometry $a, Geometry $b) : Geometry
294
    {
295
        return $this->queryGeometry('ST_Union', $a, $b);
14✔
296
    }
297

298
    public function intersection(Geometry $a, Geometry $b) : Geometry
299
    {
300
        return $this->queryGeometry('ST_Intersection', $a, $b);
8✔
301
    }
302

303
    public function difference(Geometry $a, Geometry $b) : Geometry
304
    {
305
        return $this->queryGeometry('ST_Difference', $a, $b);
8✔
306
    }
307

308
    public function envelope(Geometry $g) : Geometry
309
    {
310
        return $this->queryGeometry('ST_Envelope', $g);
12✔
311
    }
312

313
    public function centroid(Geometry $g) : Point
314
    {
315
        /** @var Point */
316
        return $this->queryGeometry('ST_Centroid', $g);
29✔
317
    }
318

319
    public function pointOnSurface(Surface|MultiSurface $g) : Point
320
    {
321
        /** @var Point */
322
        return $this->queryGeometry('ST_PointOnSurface', $g);
28✔
323
    }
324

325
    public function length(Curve|MultiCurve $g) : float
326
    {
327
        return $this->queryFloat('ST_Length', $g);
63✔
328
    }
329

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

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

340
    public function boundary(Geometry $g) : Geometry
341
    {
342
        return $this->queryGeometry('ST_Boundary', $g);
24✔
343
    }
344

345
    public function isValid(Geometry $g) : bool
346
    {
347
        return $this->queryBoolean('ST_IsValid', $g);
34✔
348
    }
349

350
    public function isClosed(Geometry $g) : bool
351
    {
352
        return $this->queryBoolean('ST_IsClosed', $g);
178✔
353
    }
354

355
    public function isSimple(Geometry $g) : bool
356
    {
357
        return $this->queryBoolean('ST_IsSimple', $g);
64✔
358
    }
359

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

370
    public function makeValid(Geometry $g) : Geometry
371
    {
372
        return $this->queryGeometry('ST_MakeValid', $g);
12✔
373
    }
374

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

380
    public function disjoint(Geometry $a, Geometry $b) : bool
381
    {
382
        return $this->queryBoolean('ST_Disjoint', $a, $b);
24✔
383
    }
384

385
    public function touches(Geometry $a, Geometry $b) : bool
386
    {
387
        return $this->queryBoolean('ST_Touches', $a, $b);
32✔
388
    }
389

390
    public function crosses(Geometry $a, Geometry $b) : bool
391
    {
392
        return $this->queryBoolean('ST_Crosses', $a, $b);
32✔
393
    }
394

395
    public function within(Geometry $a, Geometry $b) : bool
396
    {
397
        return $this->queryBoolean('ST_Within', $a, $b);
20✔
398
    }
399

400
    public function overlaps(Geometry $a, Geometry $b) : bool
401
    {
402
        return $this->queryBoolean('ST_Overlaps', $a, $b);
8✔
403
    }
404

405
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
406
    {
407
        return $this->queryBoolean('ST_Relate', $a, $b, $matrix);
16✔
408
    }
409

410
    public function locateAlong(Geometry $g, float $mValue) : Geometry
411
    {
412
        return $this->queryGeometry('ST_LocateAlong', $g, $mValue);
8✔
413
    }
414

415
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
416
    {
417
        return $this->queryGeometry('ST_LocateBetween', $g, $mStart, $mEnd);
8✔
418
    }
419

420
    public function distance(Geometry $a, Geometry $b) : float
421
    {
422
        return $this->queryFloat('ST_Distance', $a, $b);
20✔
423
    }
424

425
    public function buffer(Geometry $g, float $distance) : Geometry
426
    {
427
        return $this->queryGeometry('ST_Buffer', $g, $distance);
12✔
428
    }
429

430
    public function convexHull(Geometry $g) : Geometry
431
    {
432
        return $this->queryGeometry('ST_ConvexHull', $g);
12✔
433
    }
434

435
    public function symDifference(Geometry $a, Geometry $b) : Geometry
436
    {
437
        return $this->queryGeometry('ST_SymDifference', $a, $b);
4✔
438
    }
439

440
    public function snapToGrid(Geometry $g, float $size) : Geometry
441
    {
442
        return $this->queryGeometry('ST_SnapToGrid', $g, $size);
16✔
443
    }
444

445
    public function simplify(Geometry $g, float $tolerance) : Geometry
446
    {
447
        return $this->queryGeometry('ST_Simplify', $g, $tolerance);
8✔
448
    }
449

450
    public function maxDistance(Geometry $a, Geometry $b) : float
451
    {
452
        return $this->queryFloat('ST_MaxDistance', $a, $b);
12✔
453
    }
454

455
    public function transform(Geometry $g, int $srid) : Geometry
456
    {
457
        return $this->queryGeometry('ST_Transform', $g, $srid);
4✔
458
    }
459

460
    public function split(Geometry $g, Geometry $blade) : Geometry
461
    {
462
        return $this->queryGeometry('ST_Split', $g, $blade);
8✔
463
    }
464

465
    /**
466
     * @throws GeometryEngineException
467
     */
468
    public function lineInterpolatePoint(LineString $linestring, float $fraction) : Point
469
    {
470
        $result = $this->queryGeometry('ST_LineInterpolatePoint', $linestring, $fraction);
4✔
471
        if (! $result instanceof Point) {
4✔
NEW
UNCOV
472
            throw new GeometryEngineException('This operation yielded wrong type: ' . $result::class);
×
473
        }
474

475
        return $result;
4✔
476
    }
477

478
    public function lineInterpolatePoints(LineString $linestring, float $fraction) : Point|MultiPoint
479
    {
480
        return $this->queryMultiPoint('ST_LineInterpolatePoints', $linestring, $fraction);
5✔
481
    }
482
}
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