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

brick / geo / 13837911222

13 Mar 2025 03:05PM UTC coverage: 48.568% (-1.4%) from 50.0%
13837911222

push

github

BenMorel
Add GeosOpEngine

0 of 104 new or added lines in 1 file covered. (0.0%)

1764 of 3632 relevant lines covered (48.57%)

1005.34 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
    ) {
NEW
30
    }
×
31

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

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

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

50
    #[Override]
51
    public function length(Curve|MultiCurve $g) : float
52
    {
NEW
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).
NEW
60
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
61
    }
62

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

159
    #[Override]
160
    public function overlaps(Geometry $a, Geometry $b) : bool
161
    {
NEW
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.
NEW
169
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
170
    }
171

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

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

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

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

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

202
    #[Override]
203
    public function intersection(Geometry $a, Geometry $b) : Geometry
204
    {
NEW
205
        return $this->queryGeometry('intersection', [$a, $b], Geometry::class);
×
206
    }
207

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

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

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

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

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

238
    #[Override]
239
    public function split(Geometry $g, Geometry $blade) : Geometry
240
    {
NEW
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.
NEW
248
        throw GeometryEngineException::unimplementedMethod(__METHOD__);
×
249
    }
250

251
    #[Override]
252
    public function lineInterpolatePoints(LineString $lineString, float $fraction) : MultiPoint
253
    {
NEW
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!
NEW
263
        $output = $this->execute([]);
×
NEW
264
        $lines = explode("\n", $output);
×
265

NEW
266
        if (count($lines) === 0) {
×
NEW
267
            throw new GeometryEngineException('geosop did not return any output');
×
268
        }
269

NEW
270
        $firstLine = $lines[0];
×
271

NEW
272
        if (preg_match('/^geosop - GEOS (\S+)$/', $firstLine, $matches) !== 1) {
×
NEW
273
            throw new GeometryEngineException('Failed to parse geosop version from output: ' . $firstLine);
×
274
        }
275

NEW
276
        return $matches[1];
×
277
    }
278

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

NEW
291
        $command = [$this->geosopPath, ...$arguments];
×
NEW
292
        $process = proc_open($command, $descriptors, $pipes);
×
293

NEW
294
        if (!is_resource($process)) {
×
NEW
295
            throw new GeometryEngineException("Failed to run geosop at path: $this->geosopPath");
×
296
        }
297

NEW
298
        $stdout = stream_get_contents($pipes[1]);
×
NEW
299
        $stderr = stream_get_contents($pipes[2]);
×
300

NEW
301
        assert($stdout !== false);
×
NEW
302
        assert($stderr !== false);
×
303

NEW
304
        fclose($pipes[1]);
×
NEW
305
        fclose($pipes[2]);
×
306

NEW
307
        $exitCode = proc_close($process);
×
308

NEW
309
        if ($exitCode !== 0) {
×
NEW
310
            throw new GeometryEngineException("geosop exited with code $exitCode: $stderr");
×
311
        }
312

NEW
313
        return $stdout;
×
314
    }
315

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

NEW
324
        $output = $this->execute($arguments);
×
NEW
325
        $output = rtrim($output);
×
326

NEW
327
        if ($output === '') {
×
NEW
328
            throw new GeometryEngineException('geosop did not return any output');
×
329
        }
330

NEW
331
        return $output;
×
332
    }
333

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

NEW
351
        $numberOfGeometries = 0;
×
352

NEW
353
        foreach ($arguments as $argument) {
×
NEW
354
            if ($argument instanceof Geometry) {
×
NEW
355
                $geometryArgs[] = match (++$numberOfGeometries) {
×
NEW
356
                    1 => '-a',
×
NEW
357
                    2 => '-b',
×
NEW
358
                };
×
359

NEW
360
                $geometryArgs[] = $argument->asText();
×
NEW
361
            } elseif (is_string($argument)) {
×
NEW
362
                $otherArgs[] = $argument;
×
363
            } else {
NEW
364
                $otherArgs[] = (string) $argument;
×
365
            }
366
        }
367

NEW
368
        return ['-f', $format, ...$geometryArgs, $operation, ...$otherArgs];
×
369
    }
370

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

383
        try {
NEW
384
            return $geometryClass::fromText($output);
×
NEW
385
        } catch (GeometryException $e) {
×
NEW
386
            throw new GeometryEngineException('Failed to parse geosop output as geometry: ' . $output, 0, $e);
×
387
        }
388
    }
389

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

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

407
    /**
408
     * @param list<Geometry|string|float> $arguments
409
     */
410
    private function queryFloat(string $operation, array $arguments) : float
411
    {
NEW
412
        $output = $this->query($operation, 'txt', $arguments);
×
413

NEW
414
        if (is_numeric($output)) {
×
NEW
415
            return (float) $output;
×
416
        }
417

NEW
418
        throw new GeometryEngineException(sprintf(
×
NEW
419
            'Unexpected geosop output: expected float, got "%s"',
×
NEW
420
            $output,
×
NEW
421
        ));
×
422
    }
423
}
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