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

brick / geo / 13846298605

13 Mar 2025 11:05PM UTC coverage: 51.017% (+0.03%) from 50.992%
13846298605

push

github

BenMorel
Replace if/else with match

5 of 5 new or added lines in 1 file covered. (100.0%)

233 existing lines in 7 files now uncovered.

1856 of 3638 relevant lines covered (51.02%)

1141.32 hits per line

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

84.91
/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 class GeosOpEngine implements GeometryEngine
25
{
26
    public function __construct(
27
        /** Path to the geosop binary. */
28
        private readonly string $geosopPath,
29
    ) {
30
    }
×
31

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

38
    #[Override]
39
    public function difference(Geometry $a, Geometry $b) : Geometry
40
    {
UNCOV
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
    {
UNCOV
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).
UNCOV
60
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
11✔
61
    }
62

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

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

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

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

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

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

99
    #[Override]
100
    public function isSimple(Geometry $g) : bool
101
    {
UNCOV
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
    {
UNCOV
114
        return $this->queryGeometry('makeValid', [$g], Geometry::class);
4✔
115
    }
116

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

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

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

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

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

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

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

159
    #[Override]
160
    public function overlaps(Geometry $a, Geometry $b) : bool
161
    {
UNCOV
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 does have a relate operation, but has no support for matrix.
UNCOV
169
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
4✔
170
    }
171

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

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

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

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

196
    #[Override]
197
    public function convexHull(Geometry $g) : Geometry
198
    {
UNCOV
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
        // geosop has no support for the allowHoles argument, and behaves as if it was always false.
206
        // I think that not returning holes when $allowHoles is true still respects the contract,
207
        // so let's not throw an exception in this case.
208

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

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

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

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

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

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

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

248
    #[Override]
249
    public function split(Geometry $g, Geometry $blade) : Geometry
250
    {
UNCOV
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.
UNCOV
258
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
21✔
259
    }
260

261
    #[Override]
262
    public function lineInterpolatePoints(LineString $lineString, float $fraction) : MultiPoint
263
    {
UNCOV
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!
UNCOV
273
        $output = $this->execute([]);
65✔
UNCOV
274
        $lines = explode("\n", $output);
65✔
275

UNCOV
276
        $firstLine = $lines[0];
65✔
277

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

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

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

UNCOV
297
        $command = [$this->geosopPath, ...$arguments];
138✔
UNCOV
298
        $process = proc_open($command, $descriptors, $pipes);
138✔
299

UNCOV
300
        if (!is_resource($process)) {
138✔
UNCOV
301
            throw new GeometryEngineException("Failed to run geosop at path: $this->geosopPath");
×
302
        }
303

UNCOV
304
        $stdout = stream_get_contents($pipes[1]);
138✔
UNCOV
305
        $stderr = stream_get_contents($pipes[2]);
138✔
306

UNCOV
307
        assert($stdout !== false);
UNCOV
308
        assert($stderr !== false);
309

UNCOV
310
        fclose($pipes[1]);
138✔
UNCOV
311
        fclose($pipes[2]);
138✔
312

UNCOV
313
        $exitCode = proc_close($process);
138✔
314

UNCOV
315
        if ($exitCode !== 0 || $stderr !== '') {
138✔
UNCOV
316
            if ($exitCode !== 0) {
47✔
UNCOV
317
                if ($stderr !== '') {
47✔
UNCOV
318
                    $message = sprintf('geosop failed with exit code %d and error: %s', $exitCode, $stderr);
47✔
319
                } else {
UNCOV
320
                    $message = sprintf('geosop failed with exit code %d and no error output', $exitCode);
47✔
321
                }
322
            } else {
UNCOV
323
                $message = sprintf('geosop failed with error: %s', $stderr);
×
324
            }
325

UNCOV
326
            throw new GeometryEngineException($message);
47✔
327
        }
328

UNCOV
329
        return $stdout;
138✔
330
    }
331

332
    /**
333
     * @param 'wkt'|'txt' $format
334
     * @param list<Geometry|string|float> $arguments
335
     */
336
    private function query(string $operation, string $format, array $arguments) : string
337
    {
UNCOV
338
        $arguments = $this->buildArguments($operation, $format, $arguments);
120✔
339

UNCOV
340
        $output = $this->execute($arguments);
120✔
UNCOV
341
        $output = rtrim($output);
73✔
342

UNCOV
343
        if ($output === '') {
73✔
UNCOV
344
            throw new GeometryEngineException('geosop did not return any output');
×
345
        }
346

UNCOV
347
        return $output;
73✔
348
    }
349

350
    /**
351
     * Examples:
352
     *
353
     * ('length', 'wkt', [Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry', 'length']
354
     * ('union', 'wkt', [Geometry, Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry 1', '-b', 'WKT of Geometry 2', 'union']
355
     * ('buffer', 'txt', [Geometry, float]) => ['-f', 'txt', '-a', 'WKT of Geometry', 'buffer', 'float as string']
356
     *
357
     * @param 'wkt'|'txt' $format
358
     * @param list<Geometry|string|float> $arguments
359
     *
360
     * @return list<string>
361
     */
362
    private function buildArguments(string $operation, string $format, array $arguments): array
363
    {
UNCOV
364
        $geometryArgs = [];
120✔
UNCOV
365
        $otherArgs = [];
120✔
366

UNCOV
367
        $numberOfGeometries = 0;
120✔
368

UNCOV
369
        foreach ($arguments as $argument) {
120✔
UNCOV
370
            if ($argument instanceof Geometry) {
120✔
UNCOV
371
                $geometryArgs[] = match (++$numberOfGeometries) {
120✔
UNCOV
372
                    1 => '-a',
120✔
UNCOV
373
                    2 => '-b',
63✔
UNCOV
374
                };
120✔
375

UNCOV
376
                $geometryArgs[] = $argument->asText();
120✔
UNCOV
377
            } elseif (is_string($argument)) {
3✔
UNCOV
378
                $otherArgs[] = $argument;
×
379
            } else {
UNCOV
380
                $otherArgs[] = (string) $argument;
3✔
381
            }
382
        }
383

UNCOV
384
        return ['-f', $format, ...$geometryArgs, $operation, ...$otherArgs];
120✔
385
    }
386

387
    /**
388
     * @template T of Geometry
389
     *
390
     * @param list<Geometry|string|float> $arguments
391
     * @param class-string<T> $geometryClass
392
     *
393
     * @return T
394
     */
395
    private function queryGeometry(string $operation, array $arguments, string $geometryClass) : Geometry
396
    {
UNCOV
397
        $output = $this->query($operation, 'wkt', $arguments);
27✔
398

399
        try {
UNCOV
400
            return $geometryClass::fromText($output);
27✔
UNCOV
401
        } catch (GeometryException $e) {
×
UNCOV
402
            throw new GeometryEngineException('Failed to parse geosop output as geometry: ' . $output, 0, $e);
×
403
        }
404
    }
405

406
    /**
407
     * @param list<Geometry|string|float> $arguments
408
     */
409
    private function queryBoolean(string $operation, array $arguments) : bool
410
    {
UNCOV
411
        $output = $this->query($operation, 'txt', $arguments);
82✔
412

UNCOV
413
        return match ($output) {
40✔
414
            'true' => true,
29✔
415
            'false' => false,
11✔
416
            default => throw new GeometryEngineException(sprintf(
40✔
417
                'Unexpected geosop output: expected boolean "true" or "false", got "%s"',
40✔
UNCOV
418
                $output,
40✔
UNCOV
419
            )),
40✔
UNCOV
420
        };
40✔
421
    }
422

423
    /**
424
     * @param list<Geometry|string|float> $arguments
425
     */
426
    private function queryFloat(string $operation, array $arguments) : float
427
    {
UNCOV
428
        $output = $this->query($operation, 'txt', $arguments);
21✔
429

UNCOV
430
        if (is_numeric($output)) {
16✔
UNCOV
431
            return (float) $output;
16✔
432
        }
433

UNCOV
434
        throw new GeometryEngineException(sprintf(
×
UNCOV
435
            'Unexpected geosop output: expected float, got "%s"',
×
UNCOV
436
            $output,
×
UNCOV
437
        ));
×
438
    }
439
}
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