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

brick / geo / 14087467812

26 Mar 2025 03:39PM UTC coverage: 63.783%. Remained the same
14087467812

push

github

BenMorel
Test with all 3 PDO errmodes

1939 of 3040 relevant lines covered (63.78%)

2122.25 hits per line

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

84.21
/src/Engine/GeosOpEngine.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\Exception\GeometryException;
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\Point;
16
use Brick\Geo\Surface;
17
use Override;
18

19
/**
20
 * GeometryEngine implementation based on the geosop binary.
21
 *
22
 * https://libgeos.org/usage/tools/#geosop
23
 */
24
final readonly class GeosOpEngine implements GeometryEngine
25
{
26
    public function __construct(
27
        /** Path to the geosop binary. */
28
        private string $geosopPath,
29
    ) {
30
    }
×
31

32
    #[Override]
33
    public function union(Geometry $a, Geometry $b) : Geometry
34
    {
35
        return $this->queryGeometry('union', [$a, $b], Geometry::class);
3✔
36
    }
37

38
    #[Override]
39
    public function difference(Geometry $a, Geometry $b) : Geometry
40
    {
41
        return $this->queryGeometry('difference', [$a, $b], Geometry::class);
1✔
42
    }
43

44
    #[Override]
45
    public function envelope(Geometry $g) : Geometry
46
    {
47
        return $this->queryGeometry('envelope', [$g], Geometry::class);
×
48
    }
49

50
    #[Override]
51
    public function length(Curve|MultiCurve $g) : float
52
    {
53
        return $this->queryFloat('length', [$g]);
16✔
54
    }
55

56
    #[Override]
57
    public function area(Surface|MultiSurface $g) : float
58
    {
59
        // geosop does have an area operation, but it is broken (return a geometry).
60
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
11✔
61
    }
62

63
    #[Override]
64
    public function azimuth(Point $observer, Point $subject) : float
65
    {
66
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
6✔
67
    }
68

69
    #[Override]
70
    public function centroid(Geometry $g) : Point
71
    {
72
        return $this->queryGeometry('centroid', [$g], Point::class);
6✔
73
    }
74

75
    #[Override]
76
    public function pointOnSurface(Surface|MultiSurface $g) : Point
77
    {
78
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
7✔
79
    }
80

81
    #[Override]
82
    public function boundary(Geometry $g) : Geometry
83
    {
84
        return $this->queryGeometry('boundary', [$g], Geometry::class);
6✔
85
    }
86

87
    #[Override]
88
    public function isValid(Geometry $g) : bool
89
    {
90
        return $this->queryBoolean('isValid', [$g]);
10✔
91
    }
92

93
    #[Override]
94
    public function isClosed(Geometry $g) : bool
95
    {
96
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
49✔
97
    }
98

99
    #[Override]
100
    public function isSimple(Geometry $g) : bool
101
    {
102
        return $this->queryBoolean('isSimple', [$g]);
16✔
103
    }
104

105
    #[Override]
106
    public function isRing(Curve $curve) : bool
107
    {
108
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
109
    }
110

111
    #[Override]
112
    public function makeValid(Geometry $g): Geometry
113
    {
114
        return $this->queryGeometry('makeValid', [$g], Geometry::class);
4✔
115
    }
116

117
    #[Override]
118
    public function equals(Geometry $a, Geometry $b) : bool
119
    {
120
        return $this->queryBoolean('equals', [$a, $b]);
24✔
121
    }
122

123
    #[Override]
124
    public function disjoint(Geometry $a, Geometry $b) : bool
125
    {
126
        return $this->queryBoolean('disjoint', [$a, $b]);
6✔
127
    }
128

129
    #[Override]
130
    public function intersects(Geometry $a, Geometry $b) : bool
131
    {
132
        return $this->queryBoolean('intersects', [$a, $b]);
6✔
133
    }
134

135
    #[Override]
136
    public function touches(Geometry $a, Geometry $b) : bool
137
    {
138
        return $this->queryBoolean('touches', [$a, $b]);
8✔
139
    }
140

141
    #[Override]
142
    public function crosses(Geometry $a, Geometry $b) : bool
143
    {
144
        return $this->queryBoolean('crosses', [$a, $b]);
8✔
145
    }
146

147
    #[Override]
148
    public function within(Geometry $a, Geometry $b) : bool
149
    {
150
        return $this->queryBoolean('within', [$a, $b]);
5✔
151
    }
152

153
    #[Override]
154
    public function contains(Geometry $a, Geometry $b) : bool
155
    {
156
        return $this->queryBoolean('contains', [$a, $b]);
8✔
157
    }
158

159
    #[Override]
160
    public function overlaps(Geometry $a, Geometry $b) : bool
161
    {
162
        return $this->queryBoolean('overlaps', [$a, $b]);
2✔
163
    }
164

165
    #[Override]
166
    public function relate(Geometry $a, Geometry $b, string $matrix) : bool
167
    {
168
        // geosop has a relate operation, but no support for matrix.
169
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
4✔
170
    }
171

172
    #[Override]
173
    public function locateAlong(Geometry $g, float $mValue) : Geometry
174
    {
175
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
2✔
176
    }
177

178
    #[Override]
179
    public function locateBetween(Geometry $g, float $mStart, float $mEnd) : Geometry
180
    {
181
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
2✔
182
    }
183

184
    #[Override]
185
    public function distance(Geometry $a, Geometry $b) : float
186
    {
187
        return $this->queryFloat('distance', [$a, $b]);
5✔
188
    }
189

190
    #[Override]
191
    public function buffer(Geometry $g, float $distance) : Geometry
192
    {
193
        return $this->queryGeometry('buffer', [$g, $distance], Geometry::class);
3✔
194
    }
195

196
    #[Override]
197
    public function convexHull(Geometry $g) : Geometry
198
    {
199
        return $this->queryGeometry('convexHull', [$g], Geometry::class);
3✔
200
    }
201

202
    #[Override]
203
    public function concaveHull(Geometry $g, float $convexity, bool $allowHoles) : Geometry
204
    {
205
        if ($allowHoles) {
5✔
206
            throw new GeometryEngineException('geosop does not support concaveHull with holes.');
3✔
207
        }
208

209
        return $this->queryGeometry('concaveHull', [$g, $convexity], Geometry::class);
2✔
210
    }
211

212
    #[Override]
213
    public function intersection(Geometry $a, Geometry $b) : Geometry
214
    {
215
        return $this->queryGeometry('intersection', [$a, $b], Geometry::class);
1✔
216
    }
217

218
    #[Override]
219
    public function symDifference(Geometry $a, Geometry $b) : Geometry
220
    {
221
        return $this->queryGeometry('symDifference', [$a, $b], Geometry::class);
×
222
    }
223

224
    #[Override]
225
    public function snapToGrid(Geometry $g, float $size) : Geometry
226
    {
227
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
4✔
228
    }
229

230
    #[Override]
231
    public function simplify(Geometry $g, float $tolerance) : Geometry
232
    {
233
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
2✔
234
    }
235

236
    #[Override]
237
    public function maxDistance(Geometry $a, Geometry $b) : float
238
    {
239
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
3✔
240
    }
241

242
    #[Override]
243
    public function transform(Geometry $g, int $srid) : Geometry
244
    {
245
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
7✔
246
    }
247

248
    #[Override]
249
    public function split(Geometry $g, Geometry $blade) : Geometry
250
    {
251
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
4✔
252
    }
253

254
    #[Override]
255
    public function lineInterpolatePoint(LineString $lineString, float $fraction) : Point
256
    {
257
        // Unlike the GEOS PHP extension, interpolate has no support normalized=true, which we need here.
258
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
21✔
259
    }
260

261
    #[Override]
262
    public function lineInterpolatePoints(LineString $lineString, float $fraction) : MultiPoint
263
    {
264
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
30✔
265
    }
266

267
    /**
268
     * @throws GeometryEngineException
269
     */
270
    public function getGeosOpVersion(): string
271
    {
272
        // No --version yet, we have to parse the first line of the output!
273
        $output = $this->execute([]);
81✔
274
        $lines = explode("\n", $output);
81✔
275

276
        $firstLine = $lines[0];
81✔
277

278
        if (preg_match('/^geosop - GEOS (\S+)$/', $firstLine, $matches) !== 1) {
81✔
279
            throw new GeometryEngineException(sprintf('Failed to parse geosop version from output: "%s"', $firstLine));
×
280
        }
281

282
        return $matches[1];
81✔
283
    }
284

285
    /**
286
     * @param list<string> $arguments The CLI arguments for geosop.
287
     *
288
     * @return string The stdout output.
289
     *
290
     * @throws GeometryEngineException
291
     */
292
    private function execute(array $arguments) : string
293
    {
294
        $descriptors = [
154✔
295
            1 => ['pipe', 'w'], // stdout
154✔
296
            2 => ['pipe', 'w'], // stderr
154✔
297
        ];
154✔
298

299
        // Mute warnings and back up the current error reporting level.
300
        $errorReportingLevel = error_reporting(0);
154✔
301

302
        try {
303
            $command = [$this->geosopPath, ...$arguments];
154✔
304
            $process = proc_open($command, $descriptors, $pipes);
154✔
305

306
            if (!is_resource($process)) {
154✔
307
                throw new GeometryEngineException("Failed to run geosop at path: $this->geosopPath");
×
308
            }
309

310
            $stdout = stream_get_contents($pipes[1]);
154✔
311
            $stderr = stream_get_contents($pipes[2]);
154✔
312

313
            if ($stdout === false) {
154✔
314
                throw new GeometryEngineException('Failed to read geosop stdout');
×
315
            }
316

317
            if ($stderr === false) {
154✔
318
                throw new GeometryEngineException('Failed to read geosop stderr');
×
319
            }
320

321
            fclose($pipes[1]);
154✔
322
            fclose($pipes[2]);
154✔
323

324
            $exitCode = proc_close($process);
154✔
325
        } finally {
326
            // Restore the error reporting level.
327
            error_reporting($errorReportingLevel);
154✔
328
        }
329

330
        if ($exitCode !== 0 || $stderr !== '') {
154✔
331
            if ($exitCode !== 0) {
60✔
332
                if ($stderr !== '') {
60✔
333
                    $message = sprintf('geosop failed with exit code %d and error: %s', $exitCode, $stderr);
60✔
334
                } else {
335
                    $message = sprintf('geosop failed with exit code %d and no error output', $exitCode);
×
336
                }
337
            } else {
338
                $message = sprintf('geosop failed with error: %s', $stderr);
×
339
            }
340

341
            throw new GeometryEngineException($message);
60✔
342
        }
343

344
        return $stdout;
154✔
345
    }
346

347
    /**
348
     * @param 'wkt'|'txt' $format
349
     * @param list<Geometry|string|float> $arguments
350
     *
351
     * @throws GeometryEngineException
352
     */
353
    private function query(string $operation, array $arguments, string $format) : string
354
    {
355
        $arguments = $this->buildArguments($operation, $format, $arguments);
133✔
356

357
        $output = $this->execute($arguments);
133✔
358
        $output = rtrim($output);
73✔
359

360
        if ($output === '') {
73✔
361
            throw new GeometryEngineException('geosop did not return any output');
×
362
        }
363

364
        return $output;
73✔
365
    }
366

367
    /**
368
     * Examples:
369
     *
370
     * ('length', 'wkt', [Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry', 'length']
371
     * ('union', 'wkt', [Geometry, Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry 1', '-b', 'WKT of Geometry 2', 'union']
372
     * ('buffer', 'txt', [Geometry, float]) => ['-f', 'txt', '-a', 'WKT of Geometry', 'buffer', 'float as string']
373
     *
374
     * @param 'wkt'|'txt' $format
375
     * @param list<Geometry|string|float> $arguments
376
     *
377
     * @return list<string>
378
     */
379
    private function buildArguments(string $operation, string $format, array $arguments): array
380
    {
381
        $geometryArgs = [];
133✔
382
        $otherArgs = [];
133✔
383

384
        $numberOfGeometries = 0;
133✔
385

386
        foreach ($arguments as $argument) {
133✔
387
            if ($argument instanceof Geometry) {
133✔
388
                $geometryArgs[] = match (++$numberOfGeometries) {
133✔
389
                    1 => '-a',
133✔
390
                    2 => '-b',
74✔
391
                };
133✔
392

393
                $geometryArgs[] = $argument->asText();
133✔
394
            } elseif (is_string($argument)) {
5✔
395
                $otherArgs[] = $argument;
×
396
            } else {
397
                $otherArgs[] = (string) $argument;
5✔
398
            }
399
        }
400

401
        return ['-f', $format, ...$geometryArgs, $operation, ...$otherArgs];
133✔
402
    }
403

404
    /**
405
     * @template T of Geometry
406
     *
407
     * @param list<Geometry|string|float> $arguments
408
     * @param class-string<T> $geometryClass
409
     *
410
     * @return T
411
     *
412
     * @throws GeometryEngineException
413
     */
414
    private function queryGeometry(string $operation, array $arguments, string $geometryClass) : Geometry
415
    {
416
        $output = $this->query($operation, $arguments, 'wkt');
29✔
417

418
        try {
419
            return $geometryClass::fromText($output);
27✔
420
        } catch (GeometryException $e) {
×
421
            throw new GeometryEngineException('Failed to parse geosop output as geometry: ' . $output, $e);
×
422
        }
423
    }
424

425
    /**
426
     * @param list<Geometry|string|float> $arguments
427
     *
428
     * @throws GeometryEngineException
429
     */
430
    private function queryBoolean(string $operation, array $arguments) : bool
431
    {
432
        $output = $this->query($operation, $arguments, 'txt');
93✔
433

434
        return match ($output) {
40✔
435
            'true' => true,
29✔
436
            'false' => false,
11✔
437
            default => throw new GeometryEngineException(sprintf(
40✔
438
                'Unexpected geosop output: expected boolean "true" or "false", got "%s"',
40✔
439
                $output,
40✔
440
            )),
40✔
441
        };
40✔
442
    }
443

444
    /**
445
     * @param list<Geometry|string|float> $arguments
446
     *
447
     * @throws GeometryEngineException
448
     */
449
    private function queryFloat(string $operation, array $arguments) : float
450
    {
451
        $output = $this->query($operation, $arguments, 'txt');
21✔
452

453
        if (is_numeric($output)) {
16✔
454
            return (float) $output;
16✔
455
        }
456

457
        throw new GeometryEngineException(sprintf(
×
458
            'Unexpected geosop output: expected float, got "%s"',
×
459
            $output,
×
460
        ));
×
461
    }
462
}
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