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

longitude-one / wkt-parser / 10407526443

16 May 2024 07:54AM UTC coverage: 99.145% (-0.9%) from 100.0%
10407526443

push

github

web-flow
Merge pull request #22 from longitude-one/20-implement-triangle

Triangle implemented #20

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

2 existing lines in 2 files now uncovered.

232 of 234 relevant lines covered (99.15%)

70.09 hits per line

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

99.4
/lib/LongitudeOne/Geo/WKT/Parser.php
1
<?php
2

3
/**
4
 * This file is part of the LongitudeOne WKT-Parser project.
5
 *
6
 * PHP 8.1 | 8.2 | 8.3
7
 *
8
 * Copyright LongitudeOne - Alexandre Tranchant - Derek J. Lambert.
9
 * Copyright 2024.
10
 *
11
 */
12

13
namespace LongitudeOne\Geo\WKT;
14

15
use LongitudeOne\Geo\WKT\Exception\NotExistentException;
16
use LongitudeOne\Geo\WKT\Exception\NotInstantiableException;
17
use LongitudeOne\Geo\WKT\Exception\NotYetImplementedException;
18
use LongitudeOne\Geo\WKT\Exception\UnexpectedValueException;
19

20
/**
21
 * Parse WKT/EWKT spatial object strings.
22
 *
23
 * @see ISO13249 Chapter 4.2
24
 * @see https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry
25
 */
26
class Parser
27
{
28
    public const BREP_SOLID = 'BREPSOLID';
29
    public const CIRCLE = 'CIRCLE';
30
    public const CIRCULAR_STRING = 'CIRCULARSTRING';
31
    public const CLOTHOID = 'CLOTHOID';
32
    public const COMPOUND_CURVE = 'COMPOUNDCURVE';
33
    public const COMPOUND_SURFACE = 'COMPOUNDSURFACE';
34
    public const CURVE = 'CURVE';
35
    public const CURVE_POLYGON = 'CURVEPOLYGON';
36
    public const ELLIPTICAL_CURVE = 'ELLIPTICALCURVE';
37
    public const GEODESIC_STRING = 'GEODESICSTRING';
38
    public const GEOMETRY = 'GEOMETRY';
39
    public const GEOMETRY_COLLECTION = 'GEOMETRYCOLLECTION';
40
    public const LINE_STRING = 'LINESTRING';
41
    public const MULTI_CURVE = 'MULTICURVE';
42
    public const MULTI_LINE_STRING = 'MULTILINESTRING';
43
    public const MULTI_POINT = 'MULTIPOINT';
44
    public const MULTI_POLYGON = 'MULTIPOLYGON';
45
    public const MULTI_SURFACE = 'MULTISURFACE';
46
    public const NURBS_CURVE = 'NURBSCURVE';
47
    public const POINT = 'POINT';
48
    public const POLYGON = 'POLYGON';
49
    public const POLYHEDRAL_SURFACE = 'POLYHDRLSURFACE';
50
    public const SOLID = 'SOLID';
51
    public const SPIRAL_CURVE = 'SPIRALCURVE';
52
    public const SURFACE = 'SURFACE';
53
    public const TIN = 'TIN';
54
    public const TRIANGLE = 'TRIANGLE';
55

56
    private ?string $dimension = null;
57
    private ?string $input = null;
58
    private Lexer $lexer;
59

60
    public function __construct(?string $input = null)
144✔
61
    {
62
        $this->lexer = new Lexer();
144✔
63

64
        if (null !== $input) {
144✔
65
            $this->input = $input;
39✔
66
        }
67
    }
68

69
    /**
70
     * Parse WKT/EWKT string.
71
     *
72
     * return an array of                point            ,linestring|multipoint,multilinestring|polygon, multipolygon      , geometry collection.
73
     *
74
     * @return array{type:string, value: array<int|string>|array<int|string>[]|array<int|string>[][]|array<int|string>[][][]|array{'type':string, 'value':array<int|string>|array<int|string>[]|array<int|string>[][]|array<int|string>[][][]}[]}
75
     */
76
    public function parse(?string $input = null): array
144✔
77
    {
78
        if (null !== $input) {
144✔
79
            $this->input = $input;
104✔
80
        }
81

82
        if (null === $this->input) {
144✔
83
            throw new UnexpectedValueException('No value provided');
1✔
84
        }
85

86
        $this->lexer->setInput($this->input);
143✔
87
        $this->lexer->moveNext();
143✔
88

89
        $srid = null;
143✔
90
        $this->dimension = null;
143✔
91

92
        if ($this->lexer->isNextToken(Lexer::T_SRID)) {
143✔
93
            $srid = $this->srid();
41✔
94
        }
95

96
        $geometry = $this->geometry();
142✔
97
        $geometry['srid'] = $srid;
80✔
98
        $geometry['dimension'] = empty($this->dimension) ? null : $this->dimension;
80✔
99

100
        return $geometry;
80✔
101
    }
102

103
    /**
104
     * Match CIRCULARSTRING value.
105
     *
106
     * @return (int|string)[][]
107
     */
108
    protected function circularString(): array
9✔
109
    {
110
        return $this->pointList();
9✔
111
    }
112

113
    /**
114
     * Match a number and optional exponent.
115
     */
116
    protected function coordinate(): string|int
115✔
117
    {
118
        $this->match($this->lexer->isNextToken(Lexer::T_FLOAT) ? Lexer::T_FLOAT : Lexer::T_INTEGER);
115✔
119

120
        return $this->lexer->value();
115✔
121
    }
122

123
    /**
124
     * @return array<string|int>
125
     */
126
    protected function coordinates(int $count): array
115✔
127
    {
128
        $values = [];
115✔
129

130
        for ($i = 1; $i <= $count; ++$i) {
115✔
131
            $values[] = $this->coordinate();
115✔
132
        }
133

134
        return $values;
109✔
135
    }
136

137
    /**
138
     * Match a spatial geometry object.
139
     * return an array of                point            ,linestring|multipoint,multilinestring|polygon, multipolygon      , geometry collection.
140
     *
141
     * @return array{type:string, value: array<int|string>|array<int|string>[]|array<int|string>[][]|array<int|string>[][][]|array{'type':string, 'value':array<int|string>|array<int|string>[]|array<int|string>[][]|array<int|string>[][][]}[]}
142
     */
143
    protected function geometry(): array
142✔
144
    {
145
        try {
146
            $type = $this->type();
142✔
147
        } catch (UnexpectedValueException $e) {
4✔
148
            throw new NotExistentException((string) $this->lexer->lookahead?->value, $e->getCode(), $e);
4✔
149
        }
150

151
        if ($this->lexer->isNextTokenAny([Lexer::T_Z, Lexer::T_M, Lexer::T_ZM])) {
139✔
152
            /* @phpstan-ignore-next-line TOKEN is T_Z, T_M, or T_ZM so type is set */
153
            $this->match($this->lexer->lookahead->type);
66✔
154
            /* @phpstan-ignore-next-line TOKEN is T_Z, T_M, or T_ZM, so value is returning a string: 'Z', 'M' or 'ZM' */
155
            $this->dimension = $this->lexer->value();
66✔
156
        }
157

158
        $this->match(Lexer::T_OPEN_PARENTHESIS);
139✔
159

160
        $value = match ($type) {
139✔
161
            self::CIRCULAR_STRING => $this->circularString(),
9✔
162
            self::GEOMETRY_COLLECTION => $this->geometryCollection(),
11✔
163
            self::LINE_STRING => $this->lineString(),
19✔
164
            self::MULTI_LINE_STRING => $this->multiLineString(),
10✔
165
            self::MULTI_POINT => $this->multiPoint(),
9✔
166
            self::MULTI_POLYGON => $this->multiPolygon(),
10✔
167
            self::POINT => $this->point(),
30✔
168
            self::POLYGON => $this->polygon(),
14✔
169
            self::TRIANGLE => $this->triangle(), // PostGis ✅, MySQL ❌
28✔
170

171
            // ❌ Not implemented types in longitude-one/geo-parser
172
            self::BREP_SOLID, // PostGis ❌, MySQL ❌
139✔
173
            self::CIRCLE, // PostGis ❌, MySQL ❌
139✔
174
            self::CLOTHOID, // PostGis ❌, MySQL ❌
139✔
175
            self::COMPOUND_CURVE, // PostGis ❌, MySQL ❌
139✔
176
            self::COMPOUND_SURFACE, // PostGis ❌, MySQL ❌
139✔
177
            self::CURVE_POLYGON, // PostGis ✅, MySQL ❌
139✔
178
            self::ELLIPTICAL_CURVE, // PostGis ❌, MySQL ❌
139✔
179
            self::GEODESIC_STRING, // PostGis ❌, MySQL ❌
139✔
180
            self::MULTI_CURVE, // PostGis ✅, MySQL ✅
139✔
181
            self::MULTI_SURFACE, // PostGis ✅, MySQL ✅
139✔
182
            self::NURBS_CURVE, // PostGis ❌, MySQL ❌
139✔
183
            self::SPIRAL_CURVE, // PostGis ❌, MySQL ❌
139✔
184
            self::POLYHEDRAL_SURFACE, // PostGis ✅, MySQL ❌
139✔
185
            self::TIN, => throw new NotYetImplementedException($type), // PostGis ✅, MySQL ❌
14✔
186

187
            // @see ISO13249-3 Chapter 4.2 §2 page 11
188
            // Curve, geometry, solid and surface aren't instantiable!
189
            self::CURVE,
139✔
190
            self::GEOMETRY,
139✔
191
            self::SOLID,
139✔
192
            self::SURFACE => throw new NotInstantiableException($type),
8✔
193

194
            // This should never happen, because Lexer will throw an UnexpectedValueException
UNCOV
195
            default => throw new NotExistentException($type),
×
196
        };
139✔
197

198
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
86✔
199

200
        return [
81✔
201
            'type' => $type,
81✔
202
            'value' => $value,
81✔
203
        ];
81✔
204
    }
205

206
    /**
207
     * Match GEOMETRYCOLLECTION value.
208
     *
209
     * no recursive here, only one level of geometry collection.
210
     *
211
     * @return array{'type':string, 'value':array<int|string>|array<int|string>[]|array<int|string>[][]|array<int|string>[][][]}[]
212
     */
213
    protected function geometryCollection(): array
11✔
214
    {
215
        $collection = [$this->geometry()];
11✔
216

217
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
10✔
218
            $this->match(Lexer::T_COMMA);
10✔
219

220
            $collection[] = $this->geometry();
10✔
221
        }
222

223
        return $collection;
9✔
224
    }
225

226
    /**
227
     * Match LINESTRING value.
228
     *
229
     * @return array<int|string>[]
230
     */
231
    protected function lineString(): array
19✔
232
    {
233
        return $this->pointList();
19✔
234
    }
235

236
    /**
237
     * Match token at current position in input.
238
     *
239
     * @throw UnexpectedValueException
240
     */
241
    protected function match(int $token): void
143✔
242
    {
243
        if (null !== $this->lexer->lookahead) {
143✔
244
            $lookaheadType = $this->lexer->lookahead->type;
143✔
245
        }
246

247
        if (!isset($lookaheadType) || ($lookaheadType !== $token && (Lexer::T_TYPE !== $token || $lookaheadType <= Lexer::T_TYPE))) {
143✔
248
            throw $this->syntaxError((string) $this->lexer->getLiteral($token));
21✔
249
        }
250

251
        $this->lexer->moveNext();
140✔
252
    }
253

254
    /**
255
     * Match MULTILINESTRING value.
256
     *
257
     * @return array<int|string>[][]
258
     */
259
    protected function multiLineString(): array
10✔
260
    {
261
        return $this->pointLists();
10✔
262
    }
263

264
    /**
265
     * Match MULTIPOINT value.
266
     *
267
     * @return array<int|string>[]
268
     */
269
    protected function multiPoint(): array
9✔
270
    {
271
        return $this->pointList();
9✔
272
    }
273

274
    /**
275
     * Match MULTIPOLYGON value.
276
     *
277
     * @return array<int|string>[][][]
278
     */
279
    protected function multiPolygon(): array
10✔
280
    {
281
        $this->match(Lexer::T_OPEN_PARENTHESIS);
10✔
282

283
        $polygons = [$this->polygon()];
10✔
284

285
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
10✔
286

287
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
10✔
288
            $this->match(Lexer::T_COMMA);
10✔
289
            $this->match(Lexer::T_OPEN_PARENTHESIS);
10✔
290

291
            $polygons[] = $this->polygon();
10✔
292

293
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
9✔
294
        }
295

296
        return $polygons;
9✔
297
    }
298

299
    /**
300
     * Match a coordinate pair.
301
     *
302
     * @return array<int|string>
303
     */
304
    protected function point(): array
115✔
305
    {
306
        if (null !== $this->dimension) {
115✔
307
            return $this->coordinates(2 + strlen($this->dimension));
102✔
308
        }
309

310
        $values = $this->coordinates(2);
50✔
311

312
        for ($i = 3; $i <= 4 && $this->lexer->isNextTokenAny([Lexer::T_FLOAT, Lexer::T_INTEGER]); ++$i) {
46✔
313
            $values[] = $this->coordinate();
11✔
314
        }
315

316
        switch (count($values)) {
46✔
317
            case 2:
46✔
318
                $this->dimension = '';
36✔
319
                break;
36✔
320
            case 3:
11✔
321
                $this->dimension = 'Z';
9✔
322
                break;
9✔
323
            case 4:
3✔
324
                $this->dimension = 'ZM';
3✔
325
                break;
3✔
326
        }
327

328
        return $values;
46✔
329
    }
330

331
    /**
332
     * Match a list of coordinates.
333
     *
334
     * @return (int|string)[][]
335
     */
336
    protected function pointList(): array
94✔
337
    {
338
        $points = [$this->point()];
94✔
339

340
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
94✔
341
            $this->match(Lexer::T_COMMA);
90✔
342

343
            $points[] = $this->point();
90✔
344
        }
345

346
        return $points;
93✔
347
    }
348

349
    /**
350
     * Match nested lists of coordinates.
351
     *
352
     * @return array<int|string>[][]
353
     */
354
    protected function pointLists(): array
32✔
355
    {
356
        $this->match(Lexer::T_OPEN_PARENTHESIS);
32✔
357

358
        $pointLists = [$this->pointList()];
31✔
359

360
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
31✔
361

362
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
30✔
363
            $this->match(Lexer::T_COMMA);
20✔
364
            $this->match(Lexer::T_OPEN_PARENTHESIS);
20✔
365

366
            $pointLists[] = $this->pointList();
20✔
367

368
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
20✔
369
        }
370

371
        return $pointLists;
30✔
372
    }
373

374
    /**
375
     * Match POLYGON value.
376
     *
377
     * @return array<int|string>[][]
378
     */
379
    protected function polygon(): array
23✔
380
    {
381
        return $this->pointLists();
23✔
382
    }
383

384
    /**
385
     * Match SRID in EWKT object.
386
     */
387
    protected function srid(): int
41✔
388
    {
389
        $this->match(Lexer::T_SRID);
41✔
390
        $this->match(Lexer::T_EQUALS);
41✔
391
        $this->match(Lexer::T_INTEGER);
41✔
392

393
        /** @var int $srid because we just match a Lexer::T_INTEGER */
394
        $srid = $this->lexer->value();
40✔
395

396
        $this->match(Lexer::T_SEMICOLON);
40✔
397

398
        return $srid;
40✔
399
    }
400

401
    /**
402
     * Match TRIANGLE value.
403
     *
404
     * @return (int|string)[][]
405
     */
406
    protected function triangle(): array
28✔
407
    {
408
        // TRIANGLE ((0 0, 0 9, 9 0, 0 0))
409
        $this->match(Lexer::T_OPEN_PARENTHESIS);
28✔
410
        $pointList = $this->pointList();
28✔
411
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
28✔
412

413
        if (4 !== count($pointList)) {
28✔
414
            throw new UnexpectedValueException(sprintf('According to the ISO-13249 specification, a triangle is a closed ring with fourth points, you provided %d.', count($pointList)));
16✔
415
        }
416

417
        if ($pointList[0] !== $pointList[3]) {
12✔
418
            throw new UnexpectedValueException(sprintf('According to the ISO-13249 specification, a triangle is a closed ring with fourth points. Your first point is "%s", the fourth is "%s"', implode(' ', $pointList[0]), implode(' ', $pointList[3])));
4✔
419
        }
420

421
        return $pointList;
8✔
422
    }
423

424
    /**
425
     * Match a spatial data type.
426
     */
427
    protected function type(): string
142✔
428
    {
429
        $this->match(Lexer::T_TYPE);
142✔
430

431
        return (string) $this->lexer->value();
139✔
432
    }
433

434
    /**
435
     * Create exception with a descriptive error message.
436
     */
437
    private function syntaxError(string $expected): UnexpectedValueException
21✔
438
    {
439
        $expected = sprintf('Expected %s, got', $expected);
21✔
440
        $token = $this->lexer->lookahead;
21✔
441
        $found = null === $this->lexer->lookahead ? 'end of string.' : sprintf('"%s"', $token?->value);
21✔
442
        $message = sprintf(
21✔
443
            '[Syntax Error] line 0, col %d: Error: %s %s in value "%s"',
21✔
444
            $token->position ?? -1,
21✔
445
            $expected,
21✔
446
            $found,
21✔
447
            $this->input,
21✔
448
        );
21✔
449

450
        return new UnexpectedValueException($message);
21✔
451
    }
452
}
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

© 2025 Coveralls, Inc