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

brick / geo / 17456208570

04 Sep 2025 07:10AM UTC coverage: 50.432%. Remained the same
17456208570

push

github

BenMorel
Use @extends and @implements instead of @template-* variants

For consistency with the rest of the project.

1867 of 3702 relevant lines covered (50.43%)

1140.21 hits per line

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

85.09
/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
use function error_reporting;
20
use function explode;
21
use function fclose;
22
use function is_numeric;
23
use function is_resource;
24
use function is_string;
25
use function preg_match;
26
use function proc_close;
27
use function proc_open;
28
use function rtrim;
29
use function sprintf;
30
use function stream_get_contents;
31

32
/**
33
 * GeometryEngine implementation based on the geosop binary.
34
 *
35
 * https://libgeos.org/usage/tools/#geosop
36
 */
37
final class GeosOpEngine implements GeometryEngine
38
{
39
    public function __construct(
40
        /** Path to the geosop binary. */
41
        private readonly string $geosopPath,
42
    ) {
43
    }
×
44

45
    #[Override]
46
    public function union(Geometry $a, Geometry $b): Geometry
47
    {
48
        return $this->queryGeometry('union', [$a, $b], Geometry::class);
3✔
49
    }
50

51
    #[Override]
52
    public function difference(Geometry $a, Geometry $b): Geometry
53
    {
54
        return $this->queryGeometry('difference', [$a, $b], Geometry::class);
1✔
55
    }
56

57
    #[Override]
58
    public function envelope(Geometry $g): Geometry
59
    {
60
        return $this->queryGeometry('envelope', [$g], Geometry::class);
×
61
    }
62

63
    #[Override]
64
    public function length(Curve|MultiCurve $g): float
65
    {
66
        return $this->queryFloat('length', [$g]);
16✔
67
    }
68

69
    #[Override]
70
    public function area(Surface|MultiSurface $g): float
71
    {
72
        // geosop does have an area operation, but it is broken (return a geometry).
73
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
11✔
74
    }
75

76
    #[Override]
77
    public function azimuth(Point $observer, Point $subject): float
78
    {
79
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
6✔
80
    }
81

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

88
    #[Override]
89
    public function pointOnSurface(Surface|MultiSurface $g): Point
90
    {
91
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
7✔
92
    }
93

94
    #[Override]
95
    public function boundary(Geometry $g): Geometry
96
    {
97
        return $this->queryGeometry('boundary', [$g], Geometry::class);
6✔
98
    }
99

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

106
    #[Override]
107
    public function isClosed(Geometry $g): bool
108
    {
109
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
49✔
110
    }
111

112
    #[Override]
113
    public function isSimple(Geometry $g): bool
114
    {
115
        return $this->queryBoolean('isSimple', [$g]);
16✔
116
    }
117

118
    #[Override]
119
    public function isRing(Curve $curve): bool
120
    {
121
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
122
    }
123

124
    #[Override]
125
    public function makeValid(Geometry $g): Geometry
126
    {
127
        return $this->queryGeometry('makeValid', [$g], Geometry::class);
4✔
128
    }
129

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

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

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

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

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

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

166
    #[Override]
167
    public function contains(Geometry $a, Geometry $b): bool
168
    {
169
        return $this->queryBoolean('contains', [$a, $b]);
8✔
170
    }
171

172
    #[Override]
173
    public function overlaps(Geometry $a, Geometry $b): bool
174
    {
175
        return $this->queryBoolean('overlaps', [$a, $b]);
2✔
176
    }
177

178
    #[Override]
179
    public function relate(Geometry $a, Geometry $b, string $matrix): bool
180
    {
181
        // geosop has a relate operation, but no support for matrix.
182
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
4✔
183
    }
184

185
    #[Override]
186
    public function locateAlong(Geometry $g, float $mValue): Geometry
187
    {
188
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
2✔
189
    }
190

191
    #[Override]
192
    public function locateBetween(Geometry $g, float $mStart, float $mEnd): Geometry
193
    {
194
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
2✔
195
    }
196

197
    #[Override]
198
    public function distance(Geometry $a, Geometry $b): float
199
    {
200
        return $this->queryFloat('distance', [$a, $b]);
5✔
201
    }
202

203
    #[Override]
204
    public function buffer(Geometry $g, float $distance): Geometry
205
    {
206
        return $this->queryGeometry('buffer', [$g, $distance], Geometry::class);
3✔
207
    }
208

209
    #[Override]
210
    public function convexHull(Geometry $g): Geometry
211
    {
212
        return $this->queryGeometry('convexHull', [$g], Geometry::class);
3✔
213
    }
214

215
    #[Override]
216
    public function concaveHull(Geometry $g, float $convexity, bool $allowHoles): Geometry
217
    {
218
        if ($allowHoles) {
5✔
219
            throw new GeometryEngineException('geosop does not support concaveHull with holes.');
3✔
220
        }
221

222
        return $this->queryGeometry('concaveHull', [$g, $convexity], Geometry::class);
2✔
223
    }
224

225
    #[Override]
226
    public function intersection(Geometry $a, Geometry $b): Geometry
227
    {
228
        return $this->queryGeometry('intersection', [$a, $b], Geometry::class);
1✔
229
    }
230

231
    #[Override]
232
    public function symDifference(Geometry $a, Geometry $b): Geometry
233
    {
234
        return $this->queryGeometry('symDifference', [$a, $b], Geometry::class);
×
235
    }
236

237
    #[Override]
238
    public function snapToGrid(Geometry $g, float $size): Geometry
239
    {
240
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
4✔
241
    }
242

243
    #[Override]
244
    public function simplify(Geometry $g, float $tolerance): Geometry
245
    {
246
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
2✔
247
    }
248

249
    #[Override]
250
    public function maxDistance(Geometry $a, Geometry $b): float
251
    {
252
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
3✔
253
    }
254

255
    #[Override]
256
    public function transform(Geometry $g, int $srid): Geometry
257
    {
258
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
7✔
259
    }
260

261
    #[Override]
262
    public function split(Geometry $g, Geometry $blade): Geometry
263
    {
264
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
4✔
265
    }
266

267
    #[Override]
268
    public function lineInterpolatePoint(LineString $lineString, float $fraction): Point
269
    {
270
        // Unlike the GEOS PHP extension, interpolate has no support normalized=true, which we need here.
271
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
21✔
272
    }
273

274
    #[Override]
275
    public function lineInterpolatePoints(LineString $lineString, float $fraction): MultiPoint
276
    {
277
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
30✔
278
    }
279

280
    /**
281
     * @throws GeometryEngineException
282
     */
283
    public function getGeosOpVersion(): string
284
    {
285
        // No --version yet, we have to parse the first line of the output!
286
        $output = $this->execute([]);
81✔
287
        $lines = explode("\n", $output);
81✔
288

289
        $firstLine = $lines[0];
81✔
290

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

295
        return $matches[1];
81✔
296
    }
297

298
    /**
299
     * @param list<string> $arguments The CLI arguments for geosop.
300
     *
301
     * @return string The stdout output.
302
     *
303
     * @throws GeometryEngineException
304
     */
305
    private function execute(array $arguments): string
306
    {
307
        $descriptors = [
154✔
308
            1 => ['pipe', 'w'], // stdout
154✔
309
            2 => ['pipe', 'w'], // stderr
154✔
310
        ];
154✔
311

312
        // Mute warnings and back up the current error reporting level.
313
        $errorReportingLevel = error_reporting(0);
154✔
314

315
        try {
316
            $command = [$this->geosopPath, ...$arguments];
154✔
317
            $process = proc_open($command, $descriptors, $pipes);
154✔
318

319
            if (! is_resource($process)) {
154✔
320
                throw new GeometryEngineException("Failed to run geosop at path: $this->geosopPath");
×
321
            }
322

323
            $stdout = stream_get_contents($pipes[1]);
154✔
324
            $stderr = stream_get_contents($pipes[2]);
154✔
325

326
            if ($stdout === false) {
154✔
327
                throw new GeometryEngineException('Failed to read geosop stdout');
×
328
            }
329

330
            if ($stderr === false) {
154✔
331
                throw new GeometryEngineException('Failed to read geosop stderr');
×
332
            }
333

334
            fclose($pipes[1]);
154✔
335
            fclose($pipes[2]);
154✔
336

337
            $exitCode = proc_close($process);
154✔
338
        } finally {
339
            // Restore the error reporting level.
340
            error_reporting($errorReportingLevel);
154✔
341
        }
342

343
        if ($exitCode !== 0 || $stderr !== '') {
154✔
344
            if ($exitCode !== 0) {
60✔
345
                if ($stderr !== '') {
60✔
346
                    $message = sprintf('geosop failed with exit code %d and error: %s', $exitCode, $stderr);
60✔
347
                } else {
348
                    $message = sprintf('geosop failed with exit code %d and no error output', $exitCode);
60✔
349
                }
350
            } else {
351
                $message = sprintf('geosop failed with error: %s', $stderr);
×
352
            }
353

354
            throw new GeometryEngineException($message);
60✔
355
        }
356

357
        return $stdout;
154✔
358
    }
359

360
    /**
361
     * @param 'wkt'|'txt'                 $format
362
     * @param list<Geometry|string|float> $arguments
363
     *
364
     * @throws GeometryEngineException
365
     */
366
    private function query(string $operation, array $arguments, string $format): string
367
    {
368
        $arguments = $this->buildArguments($operation, $format, $arguments);
133✔
369

370
        $output = $this->execute($arguments);
133✔
371
        $output = rtrim($output);
73✔
372

373
        if ($output === '') {
73✔
374
            throw new GeometryEngineException('geosop did not return any output');
×
375
        }
376

377
        return $output;
73✔
378
    }
379

380
    /**
381
     * Examples:
382
     *
383
     * ('length', 'wkt', [Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry', 'length']
384
     * ('union', 'wkt', [Geometry, Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry 1', '-b', 'WKT of Geometry 2', 'union']
385
     * ('buffer', 'txt', [Geometry, float]) => ['-f', 'txt', '-a', 'WKT of Geometry', 'buffer', 'float as string']
386
     *
387
     * @param 'wkt'|'txt'                 $format
388
     * @param list<Geometry|string|float> $arguments
389
     *
390
     * @return list<string>
391
     */
392
    private function buildArguments(string $operation, string $format, array $arguments): array
393
    {
394
        $geometryArgs = [];
133✔
395
        $otherArgs = [];
133✔
396

397
        $numberOfGeometries = 0;
133✔
398

399
        foreach ($arguments as $argument) {
133✔
400
            if ($argument instanceof Geometry) {
133✔
401
                $geometryArgs[] = match (++$numberOfGeometries) {
133✔
402
                    1 => '-a',
133✔
403
                    2 => '-b',
74✔
404
                };
133✔
405

406
                $geometryArgs[] = $argument->asText();
133✔
407
            } elseif (is_string($argument)) {
5✔
408
                $otherArgs[] = $argument;
×
409
            } else {
410
                $otherArgs[] = (string) $argument;
5✔
411
            }
412
        }
413

414
        return ['-f', $format, ...$geometryArgs, $operation, ...$otherArgs];
133✔
415
    }
416

417
    /**
418
     * @template T of Geometry
419
     *
420
     * @param list<Geometry|string|float> $arguments
421
     * @param class-string<T>             $geometryClass
422
     *
423
     * @return T
424
     *
425
     * @throws GeometryEngineException
426
     */
427
    private function queryGeometry(string $operation, array $arguments, string $geometryClass): Geometry
428
    {
429
        $output = $this->query($operation, $arguments, 'wkt');
29✔
430

431
        try {
432
            return $geometryClass::fromText($output);
27✔
433
        } catch (GeometryException $e) {
×
434
            throw new GeometryEngineException('Failed to parse geosop output as geometry: ' . $output, $e);
×
435
        }
436
    }
437

438
    /**
439
     * @param list<Geometry|string|float> $arguments
440
     *
441
     * @throws GeometryEngineException
442
     */
443
    private function queryBoolean(string $operation, array $arguments): bool
444
    {
445
        $output = $this->query($operation, $arguments, 'txt');
93✔
446

447
        return match ($output) {
40✔
448
            'true' => true,
29✔
449
            'false' => false,
11✔
450
            default => throw new GeometryEngineException(sprintf(
40✔
451
                'Unexpected geosop output: expected boolean "true" or "false", got "%s"',
40✔
452
                $output,
40✔
453
            )),
40✔
454
        };
40✔
455
    }
456

457
    /**
458
     * @param list<Geometry|string|float> $arguments
459
     *
460
     * @throws GeometryEngineException
461
     */
462
    private function queryFloat(string $operation, array $arguments): float
463
    {
464
        $output = $this->query($operation, $arguments, 'txt');
21✔
465

466
        if (is_numeric($output)) {
16✔
467
            return (float) $output;
16✔
468
        }
469

470
        throw new GeometryEngineException(sprintf(
×
471
            'Unexpected geosop output: expected float, got "%s"',
×
472
            $output,
×
473
        ));
×
474
    }
475
}
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