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

brick / geo / 13861125046

14 Mar 2025 04:33PM UTC coverage: 48.122% (-2.9%) from 51.017%
13861125046

push

github

BenMorel
Add more tests for spatial equality

This engine method is the foundation for other tests, so we need to ensure that it works fine in more complex cases.

1755 of 3647 relevant lines covered (48.12%)

863.52 hits per line

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

0.0
/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
    {
35
        return $this->queryGeometry('union', [$a, $b], Geometry::class);
×
36
    }
37

38
    #[Override]
39
    public function difference(Geometry $a, Geometry $b) : Geometry
40
    {
41
        return $this->queryGeometry('difference', [$a, $b], Geometry::class);
×
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]);
×
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__);
×
61
    }
62

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

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

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

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

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

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

99
    #[Override]
100
    public function isSimple(Geometry $g) : bool
101
    {
102
        return $this->queryBoolean('isSimple', [$g]);
×
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);
×
115
    }
116

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

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

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

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

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

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

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

159
    #[Override]
160
    public function overlaps(Geometry $a, Geometry $b) : bool
161
    {
162
        return $this->queryBoolean('overlaps', [$a, $b]);
×
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__);
×
170
    }
171

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

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

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

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

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

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

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

212
    #[Override]
213
    public function intersection(Geometry $a, Geometry $b) : Geometry
214
    {
215
        return $this->queryGeometry('intersection', [$a, $b], Geometry::class);
×
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__);
×
228
    }
229

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

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

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

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

261
    #[Override]
262
    public function lineInterpolatePoints(LineString $lineString, float $fraction) : MultiPoint
263
    {
264
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
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([]);
×
274
        $lines = explode("\n", $output);
×
275

276
        $firstLine = $lines[0];
×
277

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

282
        return $matches[1];
×
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 = [
×
295
            1 => ['pipe', 'w'], // stdout
×
296
            2 => ['pipe', 'w'], // stderr
×
297
        ];
×
298

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

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

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

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

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

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

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

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

330
        if ($exitCode !== 0 || $stderr !== '') {
×
331
            if ($exitCode !== 0) {
×
332
                if ($stderr !== '') {
×
333
                    $message = sprintf('geosop failed with exit code %d and error: %s', $exitCode, $stderr);
×
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);
×
342
        }
343

344
        return $stdout;
×
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);
×
356

357
        $output = $this->execute($arguments);
×
358
        $output = rtrim($output);
×
359

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

364
        return $output;
×
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 = [];
×
382
        $otherArgs = [];
×
383

384
        $numberOfGeometries = 0;
×
385

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

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

401
        return ['-f', $format, ...$geometryArgs, $operation, ...$otherArgs];
×
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');
×
417

418
        try {
419
            return $geometryClass::fromText($output);
×
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');
×
433

434
        return match ($output) {
×
435
            'true' => true,
×
436
            'false' => false,
×
437
            default => throw new GeometryEngineException(sprintf(
×
438
                'Unexpected geosop output: expected boolean "true" or "false", got "%s"',
×
439
                $output,
×
440
            )),
×
441
        };
×
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');
×
452

453
        if (is_numeric($output)) {
×
454
            return (float) $output;
×
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