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

brick / geo / 13589038087

28 Feb 2025 01:21PM UTC coverage: 47.633% (+0.09%) from 47.546%
13589038087

Pull #55

github

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

12 of 19 new or added lines in 3 files covered. (63.16%)

7 existing lines in 1 file now uncovered.

1620 of 3401 relevant lines covered (47.63%)

1379.6 hits per line

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

94.92
/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\Surface;
19

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

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

34
    /**
35
     * Executes a SQL query.
36
     *
37
     * @psalm-param list<GeometryParameter|string|float|int> $parameters
38
     * @psalm-return list<mixed>
39
     *
40
     * @param string $query      The SQL query to execute.
41
     * @param array  $parameters The geometry data or scalar values to pass as parameters.
42
     *
43
     * @return array 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(?, ?)';
168✔
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(?, ?)';
419✔
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 $parameter): string
75
    {
76
        return '?';
164✔
77
    }
78

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

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

106
                $queryParameters[] = $sendAsBinary
1,707✔
107
                    ? $this->getGeomFromWKBSyntax()
1,553✔
108
                    : $this->getGeomFromTextSyntax();
168✔
109

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

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

119
        if ($returnsGeometry) {
1,707✔
120
            $query = sprintf('
437✔
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);
437✔
128
        }
129

130
        return $this->executeQuery($query, $queryValues);
1,707✔
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
        [$result] = $this->query($function, $parameters, false);
1,133✔
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 ($result === null || $result === -1 || $result === '-1') {
959✔
148
            throw GeometryEngineException::operationYieldedNoResult();
142✔
149
        }
150

151
        return (bool) $result;
817✔
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|string|float|int ...$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 ...$parameters) : float
163
    {
164
        [$result] = $this->query($function, $parameters, false);
291✔
165

166
        if ($result === null) {
246✔
167
            throw GeometryEngineException::operationYieldedNoResult();
36✔
168
        }
169

170
        return (float) $result;
210✔
171
    }
172

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

186
        [$wkt, $wkb, $geometryType, $srid] = $result;
338✔
187

188
        $srid = (int) $srid;
338✔
189

190
        if ($wkt !== null) {
338✔
191
            if ($this->useProxy) {
5✔
192
                $proxyClassName = $this->getProxyClassName($geometryType);
5✔
193

194
                return new $proxyClassName($wkt, false, $srid);
5✔
195
            }
196

197
            return Geometry::fromText($wkt, $srid);
×
198
        }
199

200
        if ($wkb !== null) {
333✔
201
            if (is_resource($wkb)) {
314✔
202
                $wkb = stream_get_contents($wkb);
65✔
203
            }
204

205
            if ($this->useProxy) {
314✔
206
                $proxyClassName = $this->getProxyClassName($geometryType);
314✔
207

208
                return new $proxyClassName($wkb, true, $srid);
314✔
209
            }
210

211
            return Geometry::fromBinary($wkb, $srid);
×
212
        }
213

214
        throw GeometryEngineException::operationYieldedNoResult();
19✔
215
    }
216

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

246
        $geometryType = strtoupper($geometryType);
319✔
247
        $geometryType = preg_replace('/^ST_/', '', $geometryType);
319✔
248
        $geometryType = preg_replace('/ .*/', '', $geometryType);
319✔
249

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

254
        return $proxyClasses[$geometryType];
319✔
255
    }
256

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

434
    public function split(Geometry $g, Geometry $blade) : Geometry
435
    {
436
        return $this->queryGeometry('ST_Split', $g, $blade);
8✔
437
    }
438

439
    /**
440
     * @throws GeometryEngineException
441
     */
442
    public function lineInterpolatePoint(LineString $linestring, float $fraction) : Point
443
    {
444
        $result = $this->queryGeometry('ST_LineInterpolatePoint', $linestring, $fraction);
16✔
445
        if (! $result instanceof Point) {
16✔
NEW
446
            throw new GeometryEngineException('This operation yielded wrong type: ' . $result::class);
×
447
        }
448

449
        return $result;
16✔
450
    }
451

452
    /**
453
     * @throws GeometryEngineException
454
     */
455
    public function lineInterpolateEquidistantPoints(LineString $linestring, float $fraction) : Point|MultiPoint
456
    {
457
        $result = $this->queryGeometry('ST_LineInterpolatePoints', $linestring, $fraction);
8✔
458
        if (! $result instanceof Point && ! $result instanceof MultiPoint) {
8✔
NEW
459
            throw new GeometryEngineException('This operation yielded the wrong geometry type: ' . $result::class);
×
460
        }
461

462
        return $result;
8✔
463
    }
464
}
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