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

brick / geo / 13838248234

13 Mar 2025 03:20PM UTC coverage: 47.59% (-1.0%) from 48.568%
13838248234

push

github

BenMorel
Remove --find-unused-psalm-suppress redundant with config

1728 of 3631 relevant lines covered (47.59%)

862.17 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 does have a relate operation, but has 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 intersection(Geometry $a, Geometry $b) : Geometry
204
    {
205
        return $this->queryGeometry('intersection', [$a, $b], Geometry::class);
×
206
    }
207

208
    #[Override]
209
    public function symDifference(Geometry $a, Geometry $b) : Geometry
210
    {
211
        return $this->queryGeometry('symDifference', [$a, $b], Geometry::class);
×
212
    }
213

214
    #[Override]
215
    public function snapToGrid(Geometry $g, float $size) : Geometry
216
    {
217
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
218
    }
219

220
    #[Override]
221
    public function simplify(Geometry $g, float $tolerance) : Geometry
222
    {
223
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
224
    }
225

226
    #[Override]
227
    public function maxDistance(Geometry $a, Geometry $b) : float
228
    {
229
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
230
    }
231

232
    #[Override]
233
    public function transform(Geometry $g, int $srid) : Geometry
234
    {
235
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
236
    }
237

238
    #[Override]
239
    public function split(Geometry $g, Geometry $blade) : Geometry
240
    {
241
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
242
    }
243

244
    #[Override]
245
    public function lineInterpolatePoint(LineString $lineString, float $fraction) : Point
246
    {
247
        // Unlike the GEOS PHP extension, interpolate has no support normalized=true, which we need here.
248
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
249
    }
250

251
    #[Override]
252
    public function lineInterpolatePoints(LineString $lineString, float $fraction) : MultiPoint
253
    {
254
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
255
    }
256

257
    /**
258
     * @throws GeometryEngineException
259
     */
260
    public function getGeosOpVersion(): string
261
    {
262
        // No --version yet, we have to parse the first line of the output!
263
        $output = $this->execute([]);
×
264
        $lines = explode("\n", $output);
×
265

266
        $firstLine = $lines[0];
×
267

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

272
        return $matches[1];
×
273
    }
274

275
    /**
276
     * @param list<string> $arguments The CLI arguments for geosop.
277
     *
278
     * @return string The stdout output.
279
     */
280
    private function execute(array $arguments) : string
281
    {
282
        $descriptors = [
×
283
            1 => ['pipe', 'w'], // stdout
×
284
            2 => ['pipe', 'w'], // stderr
×
285
        ];
×
286

287
        $command = [$this->geosopPath, ...$arguments];
×
288
        $process = proc_open($command, $descriptors, $pipes);
×
289

290
        if (!is_resource($process)) {
×
291
            throw new GeometryEngineException("Failed to run geosop at path: $this->geosopPath");
×
292
        }
293

294
        $stdout = stream_get_contents($pipes[1]);
×
295
        $stderr = stream_get_contents($pipes[2]);
×
296

297
        assert($stdout !== false);
×
298
        assert($stderr !== false);
×
299

300
        fclose($pipes[1]);
×
301
        fclose($pipes[2]);
×
302

303
        $exitCode = proc_close($process);
×
304

305
        if ($exitCode !== 0) {
×
306
            throw new GeometryEngineException("geosop exited with code $exitCode: $stderr");
×
307
        }
308

309
        return $stdout;
×
310
    }
311

312
    /**
313
     * @param 'wkt'|'txt' $format
314
     * @param list<Geometry|string|float> $arguments
315
     */
316
    private function query(string $operation, string $format, array $arguments) : string
317
    {
318
        $arguments = $this->buildArguments($operation, $format, $arguments);
×
319

320
        $output = $this->execute($arguments);
×
321
        $output = rtrim($output);
×
322

323
        if ($output === '') {
×
324
            throw new GeometryEngineException('geosop did not return any output');
×
325
        }
326

327
        return $output;
×
328
    }
329

330
    /**
331
     * Examples:
332
     *
333
     * ('length', 'wkt', [Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry', 'length']
334
     * ('union', 'wkt', [Geometry, Geometry]) => ['-f', 'wkt', '-a', 'WKT of Geometry 1', '-b', 'WKT of Geometry 2', 'union']
335
     * ('buffer', 'txt', [Geometry, float]) => ['-f', 'txt', '-a', 'WKT of Geometry', 'buffer', 'float as string']
336
     *
337
     * @param 'wkt'|'txt' $format
338
     * @param list<Geometry|string|float> $arguments
339
     *
340
     * @return list<string>
341
     */
342
    private function buildArguments(string $operation, string $format, array $arguments): array
343
    {
344
        $geometryArgs = [];
×
345
        $otherArgs = [];
×
346

347
        $numberOfGeometries = 0;
×
348

349
        foreach ($arguments as $argument) {
×
350
            if ($argument instanceof Geometry) {
×
351
                $geometryArgs[] = match (++$numberOfGeometries) {
×
352
                    1 => '-a',
×
353
                    2 => '-b',
×
354
                };
×
355

356
                $geometryArgs[] = $argument->asText();
×
357
            } elseif (is_string($argument)) {
×
358
                $otherArgs[] = $argument;
×
359
            } else {
360
                $otherArgs[] = (string) $argument;
×
361
            }
362
        }
363

364
        return ['-f', $format, ...$geometryArgs, $operation, ...$otherArgs];
×
365
    }
366

367
    /**
368
     * @template T of Geometry
369
     *
370
     * @param list<Geometry|string|float> $arguments
371
     * @param class-string<T> $geometryClass
372
     *
373
     * @return T
374
     */
375
    private function queryGeometry(string $operation, array $arguments, string $geometryClass) : Geometry
376
    {
377
        $output = $this->query($operation, 'wkt', $arguments);
×
378

379
        try {
380
            return $geometryClass::fromText($output);
×
381
        } catch (GeometryException $e) {
×
382
            throw new GeometryEngineException('Failed to parse geosop output as geometry: ' . $output, 0, $e);
×
383
        }
384
    }
385

386
    /**
387
     * @param list<Geometry|string|float> $arguments
388
     */
389
    private function queryBoolean(string $operation, array $arguments) : bool
390
    {
391
        $output = $this->query($operation, 'txt', $arguments);
×
392

393
        return match ($output) {
×
394
            'true' => true,
×
395
            'false' => false,
×
396
            default => throw new GeometryEngineException(sprintf(
×
397
                'Unexpected geosop output: expected boolean "true" or "false", got "%s"',
×
398
                $output,
×
399
            )),
×
400
        };
×
401
    }
402

403
    /**
404
     * @param list<Geometry|string|float> $arguments
405
     */
406
    private function queryFloat(string $operation, array $arguments) : float
407
    {
408
        $output = $this->query($operation, 'txt', $arguments);
×
409

410
        if (is_numeric($output)) {
×
411
            return (float) $output;
×
412
        }
413

414
        throw new GeometryEngineException(sprintf(
×
415
            'Unexpected geosop output: expected float, got "%s"',
×
416
            $output,
×
417
        ));
×
418
    }
419
}
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