Passed
Pull Request — master (#4)
by Mark
01:50
created

LineString   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 503
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 233
dl 0
loc 503
rs 2
c 0
b 0
f 0
wmc 88

23 Methods

Rating   Name   Duplication   Size   Complexity  
A elevationGain() 0 14 5
A maximumM() 0 10 5
A length3D() 0 16 3
A haversineLength() 0 20 3
A pointN() 0 5 2
A greatCircleLength() 0 34 3
A __construct() 0 3 1
A geometryType() 0 3 1
A maximumZ() 0 10 5
A zDifference() 0 6 3
A explode() 0 13 4
A numPoints() 0 3 1
B distance() 0 42 10
A centroid() 0 3 1
A isSimple() 0 29 6
B getCentroidAndLength() 0 35 6
B vincentyLength() 0 65 7
A elevationLoss() 0 14 5
A minimumZ() 0 9 5
A minimumM() 0 9 5
A lineSegmentIntersect() 0 7 1
A fromArray() 0 7 2
A length() 0 21 4

How to fix   Complexity   

Complex Class

Complex classes like LineString often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LineString, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace geoPHP\Geometry;
4
5
use geoPHP\Exception\InvalidGeometryException;
6
use geoPHP\geoPHP;
7
8
/**
9
 * A LineString is defined by a sequence of points, (X,Y) pairs, which define the reference points of the line string.
10
 * Linear interpolation between the reference points defines the resulting linestring.
11
 *
12
 * @method Point[] getComponents()
13
 * @property Point[] $components
14
 * @method Point geometryN($n)
15
 */
16
class LineString extends Curve
17
{
18
19
    public function geometryType()
20
    {
21
        return Geometry::LINE_STRING;
22
    }
23
24
    /**
25
     * Constructor
26
     *
27
     * @param Point[] $points An array of at least two points with
28
     * which to build the LineString
29
     * @throws \Exception
30
     */
31
    public function __construct($points = [])
32
    {
33
        parent::__construct($points);
34
    }
35
36
    public static function fromArray($array)
37
    {
38
        $points = [];
39
        foreach ($array as $point) {
40
            $points[] = Point::fromArray($point);
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Point was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
41
        }
42
        return new static($points);
43
    }
44
45
    /**
46
     * Returns the number of points of the LineString
47
     *
48
     * @return int
49
     */
50
    public function numPoints()
51
    {
52
        return count($this->components);
53
    }
54
55
    /**
56
     * Returns the 1-based Nth point of the LineString.
57
     * Negative values are counted backwards from the end of the LineString.
58
     *
59
     * @param int $n Nth point of the LineString
60
     * @return Point|null
61
     */
62
    public function pointN($n)
63
    {
64
        return $n >= 0
65
                ? $this->geometryN($n)
66
                : $this->geometryN(count($this->components) - abs($n + 1));
67
    }
68
69
    public function centroid()
70
    {
71
        return $this->getCentroidAndLength();
72
    }
73
74
    public function getCentroidAndLength(&$length = 0.0)
75
    {
76
        if ($this->isEmpty()) {
77
            return new Point();
78
        }
79
80
        if ($this->getGeos()) {
81
            // @codeCoverageIgnoreStart
82
            /** @noinspection PhpUndefinedMethodInspection */
83
            return geoPHP::geosToGeometry($this->getGeos()->centroid());
84
            // @codeCoverageIgnoreEnd
85
        }
86
87
        $x = 0;
88
        $y = 0;
89
        $length = 0.0;
90
        /** @var Point $previousPoint */
91
        $previousPoint = null;
92
        foreach ($this->getPoints() as $point) {
93
            if ($previousPoint) {
94
                // Equivalent to $previousPoint->distance($point) but much faster
95
                $segmentLength = sqrt(
96
                    pow(($previousPoint->x() - $point->x()), 2) +
97
                        pow(($previousPoint->y() - $point->y()), 2)
98
                );
99
                $length += $segmentLength;
100
                $x += ($previousPoint->x() + $point->x()) / 2 * $segmentLength;
101
                $y += ($previousPoint->y() + $point->y()) / 2 * $segmentLength;
102
            }
103
            $previousPoint = $point;
104
        }
105
        if ($length === 0.0) {
0 ignored issues
show
introduced by
The condition $length === 0.0 is always true.
Loading history...
106
            return $this->startPoint();
107
        }
108
        return new Point($x / $length, $y / $length);
109
    }
110
111
    /**
112
     *  Returns the length of this Curve in its associated spatial reference.
113
     * Eg. if Geometry is in geographical coordinate system it returns the length in degrees
114
     * @return float|int
115
     */
116
    public function length()
117
    {
118
        if ($this->getGeos()) {
119
            // @codeCoverageIgnoreStart
120
            /** @noinspection PhpUndefinedMethodInspection */
121
            return $this->getGeos()->length();
122
            // @codeCoverageIgnoreEnd
123
        }
124
        $length = 0.0;
125
        /** @var Point $previousPoint */
126
        $previousPoint = null;
127
        foreach ($this->getPoints() as $point) {
128
            if ($previousPoint) {
129
                $length += sqrt(
130
                    pow(($previousPoint->x() - $point->x()), 2) +
131
                        pow(($previousPoint->y() - $point->y()), 2)
132
                );
133
            }
134
            $previousPoint = $point;
135
        }
136
        return $length;
137
    }
138
139
    public function length3D()
140
    {
141
        $length = 0.0;
142
        /** @var Point $previousPoint */
143
        $previousPoint = null;
144
        foreach ($this->getPoints() as $point) {
145
            if ($previousPoint) {
146
                $length += sqrt(
147
                    pow(($previousPoint->x() - $point->x()), 2) +
148
                        pow(($previousPoint->y() - $point->y()), 2) +
149
                        pow(($previousPoint->z() - $point->z()), 2)
150
                );
151
            }
152
            $previousPoint = $point;
153
        }
154
        return $length;
155
    }
156
157
    /**
158
     * @param float|null $radius Earth radius
159
     * @return float Length in meters
160
     */
161
    public function greatCircleLength($radius = geoPHP::EARTH_WGS84_SEMI_MAJOR_AXIS)
162
    {
163
        $length = 0.0;
164
        $rad = M_PI / 180;
165
        $points = $this->getPoints();
166
        $numPoints = $this->numPoints() - 1;
167
        for ($i = 0; $i < $numPoints; ++$i) {
168
            // Simplified Vincenty formula with equal major and minor axes (a sphere)
169
            $lat1 = $points[$i]->y() * $rad;
170
            $lat2 = $points[$i + 1]->y() * $rad;
171
            $lon1 = $points[$i]->x() * $rad;
172
            $lon2 = $points[$i + 1]->x() * $rad;
173
            $deltaLon = $lon2 - $lon1;
174
            $d =
175
                    $radius *
176
                    atan2(
177
                        sqrt(
178
                            pow(cos($lat2) * sin($deltaLon), 2) +
179
                                    pow(cos($lat1) * sin($lat2) - sin($lat1) * cos($lat2) * cos($deltaLon), 2)
180
                        ),
181
                        sin($lat1) * sin($lat2) +
182
                            cos($lat1) * cos($lat2) * cos($deltaLon)
183
                    );
184
            if ($points[$i]->is3D()) {
185
                $d = sqrt(
186
                    pow($d, 2) +
187
                        pow($points[$i + 1]->z() - $points[$i]->z(), 2)
188
                );
189
            }
190
191
            $length += $d;
192
        }
193
        // Returns length in meters.
194
        return $length;
195
    }
196
197
    /**
198
     * @return float Haversine length of geometry in degrees
199
     */
200
    public function haversineLength()
201
    {
202
        $distance = 0.0;
203
        $points = $this->getPoints();
204
        $numPoints = $this->numPoints() - 1;
205
        for ($i = 0; $i < $numPoints; ++$i) {
206
            $point = $points[$i];
207
            $nextPoint = $points[$i + 1];
208
            $degree = (geoPHP::EARTH_WGS84_SEMI_MAJOR_AXIS *
209
                    acos(
210
                        sin(deg2rad($point->y())) * sin(deg2rad($nextPoint->y())) +
211
                            cos(deg2rad($point->y())) * cos(deg2rad($nextPoint->y())) *
212
                            cos(deg2rad(abs($point->x() - $nextPoint->x())))
213
                    )
214
            );
215
            if (!is_nan($degree)) {
216
                $distance += $degree;
217
            }
218
        }
219
        return $distance;
220
    }
221
222
    /**
223
     * @source https://github.com/mjaschen/phpgeo/blob/master/src/Location/Distance/Vincenty.php
224
     * @author Marcus Jaschen <[email protected]>
225
     * @license https://opensource.org/licenses/GPL-3.0 GPL
226
     * (note: geoPHP uses "GPL version 2 (or later)" license which is compatible with GPLv3)
227
     *
228
     * @return float Length in meters
229
     */
230
    public function vincentyLength()
231
    {
232
        $length = 0.0;
233
        $rad = M_PI / 180;
234
        $points = $this->getPoints();
235
        $numPoints = $this->numPoints() - 1;
236
        for ($i = 0; $i < $numPoints; ++$i) {
237
            // Inverse Vincenty formula
238
            $lat1 = $points[$i]->y() * $rad;
239
            $lat2 = $points[$i + 1]->y() * $rad;
240
            $lng1 = $points[$i]->x() * $rad;
241
            $lng2 = $points[$i + 1]->x() * $rad;
242
243
            $a = geoPHP::EARTH_WGS84_SEMI_MAJOR_AXIS;
244
            $b = geoPHP::EARTH_WGS84_SEMI_MINOR_AXIS;
245
            $f = 1 / geoPHP::EARTH_WGS84_FLATTENING;
246
            $L  = $lng2 - $lng1;
247
            $U1 = atan((1 - $f) * tan($lat1));
248
            $U2 = atan((1 - $f) * tan($lat2));
249
            $iterationLimit = 100;
250
            $lambda         = $L;
251
            $sinU1 = sin($U1);
252
            $sinU2 = sin($U2);
253
            $cosU1 = cos($U1);
254
            $cosU2 = cos($U2);
255
            do {
256
                $sinLambda = sin($lambda);
257
                $cosLambda = cos($lambda);
258
                $sinSigma = sqrt(
259
                    ($cosU2 * $sinLambda) *
260
                    ($cosU2 * $sinLambda) +
261
                    ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda) *
262
                    ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda)
263
                );
264
                if ($sinSigma == 0) {
265
                    return 0.0;
266
                }
267
                $cosSigma = $sinU1 * $sinU2 + $cosU1 * $cosU2 * $cosLambda;
268
                $sigma = atan2($sinSigma, $cosSigma);
269
                $sinAlpha = $cosU1 * $cosU2 * $sinLambda / $sinSigma;
270
                $cosSqAlpha = 1 - $sinAlpha * $sinAlpha;
271
                $cos2SigmaM = 0;
272
                if ($cosSqAlpha <> 0) {
273
                    $cos2SigmaM = $cosSigma - 2 * $sinU1 * $sinU2 / $cosSqAlpha;
274
                }
275
                $C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
276
                $lambdaP = $lambda;
277
                $lambda = $L + (1 - $C) * $f * $sinAlpha *
278
                    ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * $cosSigma * (- 1 + 2 * $cos2SigmaM * $cos2SigmaM)));
279
            } while (abs($lambda - $lambdaP) > 1e-12 && --$iterationLimit > 0);
280
            if ($iterationLimit == 0) {
281
                return null; // not converging
282
            }
283
            $uSq        = $cosSqAlpha * ($a * $a - $b * $b) / ($b * $b);
284
            $A          = 1 + $uSq / 16384 * (4096 + $uSq * (- 768 + $uSq * (320 - 175 * $uSq)));
285
            $B          = $uSq / 1024 * (256 + $uSq * (- 128 + $uSq * (74 - 47 * $uSq)));
286
            $deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 *
287
                    ($cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM) - $B / 6
288
                        * $cos2SigmaM * (-3 + 4 * $sinSigma * $sinSigma)
289
                        * (-3 + 4 * $cos2SigmaM * $cos2SigmaM)));
290
291
            $length += $b * $A * ($sigma - $deltaSigma);
292
        }
293
        // Returns length in meters.
294
        return $length;
295
    }
296
297
    public function minimumZ()
298
    {
299
        $min = PHP_INT_MAX;
300
        foreach ($this->getPoints() as $point) {
301
            if ($point->hasZ() && $point->z() < $min) {
302
                $min = $point->z();
303
            }
304
        }
305
        return $min < PHP_INT_MAX ? $min : null;
306
    }
307
308
    public function maximumZ()
309
    {
310
        $max = ~PHP_INT_MAX;
311
        foreach ($this->getPoints() as $point) {
312
            if ($point->hasZ() && $point->z() > $max) {
313
                $max = $point->z();
314
            }
315
        }
316
317
        return $max > ~PHP_INT_MAX ? $max : null;
318
    }
319
320
    public function zDifference()
321
    {
322
        if ($this->startPoint()->hasZ() && $this->endPoint()->hasZ()) {
323
            return abs($this->startPoint()->z() - $this->endPoint()->z());
324
        } else {
325
            return null;
326
        }
327
    }
328
329
    /**
330
     * Returns the cumulative elevation gain of the LineString
331
     *
332
     * @param int|float|null $verticalTolerance Smoothing factor filtering noisy elevation data.
333
     *      Its unit equals to the z-coordinates unit (meters for geographical coordinates)
334
     *      If the elevation data comes from a DEM, a value around 3.5 can be acceptable.
335
     *
336
     * @return float
337
     */
338
    public function elevationGain($verticalTolerance = 0)
339
    {
340
        $gain = 0.0;
341
        $lastEle = $this->startPoint()->z();
342
        $pointCount = $this->numPoints();
343
        foreach ($this->getPoints() as $i => $point) {
344
            if (abs($point->z() - $lastEle) > $verticalTolerance || $i === $pointCount - 1) {
345
                if ($point->z() > $lastEle) {
346
                    $gain += $point->z() - $lastEle;
347
                }
348
                $lastEle = $point->z();
349
            }
350
        }
351
        return $gain;
352
    }
353
354
    /**
355
     * Returns the cumulative elevation loss of the LineString
356
     *
357
     * @param int|float|null $verticalTolerance Smoothing factor filtering noisy elevation data.
358
     *      Its unit equals to the z-coordinates unit (meters for geographical coordinates)
359
     *      If the elevation data comes from a DEM, a value around 3.5 can be acceptable.
360
     *
361
     * @return float
362
     */
363
    public function elevationLoss($verticalTolerance = 0)
364
    {
365
        $loss = 0.0;
366
        $lastEle = $this->startPoint()->z();
367
        $pointCount = $this->numPoints();
368
        foreach ($this->getPoints() as $i => $point) {
369
            if (abs($point->z() - $lastEle) > $verticalTolerance || $i === $pointCount - 1) {
370
                if ($point->z() < $lastEle) {
371
                    $loss += $lastEle - $point->z();
372
                }
373
                $lastEle = $point->z();
374
            }
375
        }
376
        return $loss;
377
    }
378
379
    public function minimumM()
380
    {
381
        $min = PHP_INT_MAX;
382
        foreach ($this->getPoints() as $point) {
383
            if ($point->isMeasured() && $point->m() < $min) {
384
                $min = $point->m();
385
            }
386
        }
387
        return $min < PHP_INT_MAX ? $min : null;
388
    }
389
390
    public function maximumM()
391
    {
392
        $max = ~PHP_INT_MAX;
393
        foreach ($this->getPoints() as $point) {
394
            if ($point->isMeasured() && $point->m() > $max) {
395
                $max = $point->m();
396
            }
397
        }
398
399
        return $max > ~PHP_INT_MAX ? $max : null;
400
    }
401
402
    /**
403
     * Get all line segments
404
     * @param bool $toArray return segments as LineString or array of start and end points
405
     *
406
     * @return LineString[]|array[Point]
0 ignored issues
show
Documentation Bug introduced by
The doc comment LineString[]|array[Point] at position 5 could not be parsed: Expected ']' at position 5, but found '['.
Loading history...
407
     */
408
    public function explode($toArray = false)
409
    {
410
        $points = $this->getPoints();
411
        $numPoints = count($points);
412
        if ($numPoints < 2) {
413
            return [];
414
        }
415
        $parts = [];
416
        for ($i = 1; $i < $numPoints; ++$i) {
417
            $segment = [$points[$i - 1], $points[$i]];
418
            $parts[] = $toArray ? $segment : new LineString($segment);
419
        }
420
        return $parts;
421
    }
422
423
    /**
424
     * Checks that LineString is a Simple Geometry
425
     *
426
     * @return boolean
427
     */
428
    public function isSimple()
429
    {
430
        if ($this->getGeos()) {
431
            // @codeCoverageIgnoreStart
432
            /** @noinspection PhpUndefinedMethodInspection */
433
            return $this->getGeos()->isSimple();
434
            // @codeCoverageIgnoreEnd
435
        }
436
437
        // As of OGR specification a ring is simple only if its start and end points equals in all coordinates
438
        // Neither GEOS, nor PostGIS support it
439
//        if ($this->hasZ()
440
//                && $this->startPoint()->equals($this->endPoint())
441
//                && $this->startPoint()->z() !== $this->endPoint()->z()
442
//        ) {
443
//            return false;
444
//        }
445
446
        $segments = $this->explode(true);
447
        foreach ($segments as $i => $segment) {
448
            foreach ($segments as $j => $checkSegment) {
449
                if ($i != $j) {
450
                    if (Geometry::segmentIntersects($segment[0], $segment[1], $checkSegment[0], $checkSegment[1])) {
451
                        return false;
452
                    }
453
                }
454
            }
455
        }
456
        return true;
457
    }
458
459
    /**
460
     * @param LineString $segment
461
     * @return bool
462
     */
463
    public function lineSegmentIntersect($segment)
464
    {
465
        return Geometry::segmentIntersects(
466
            $this->startPoint(),
467
            $this->endPoint(),
468
            $segment->startPoint(),
469
            $segment->endPoint()
470
        );
471
    }
472
473
    /**
474
     * @param Geometry|Collection $geometry
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Collection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
475
     * @return float|null
476
     */
477
    public function distance($geometry)
478
    {
479
        if ($this->getGeos()) {
480
            // @codeCoverageIgnoreStart
481
            /** @noinspection PhpUndefinedMethodInspection */
482
            return $this->getGeos()->distance($geometry->getGeos());
483
            // @codeCoverageIgnoreEnd
484
        }
485
486
        if ($geometry->geometryType() == Geometry::POINT) {
487
            // This is defined in the Point class nicely
488
            return $geometry->distance($this);
489
        }
490
        if ($geometry->geometryType() == Geometry::LINE_STRING) {
491
            $distance = null;
492
            $geometrySegments = $geometry->explode();
493
            foreach ($this->explode() as $seg1) {
494
                /** @var LineString $seg2 */
495
                foreach ($geometrySegments as $seg2) {
496
                    if ($seg1->lineSegmentIntersect($seg2)) {
497
                        return 0.0;
498
                    }
499
                    // Because line-segments are straight, the shortest distance will occur at an endpoint.
500
                    // If they are parallel an endpoint calculation is still accurate.
501
                    $checkDistance1 = $seg1->startPoint()->distance($seg2);
502
                    $checkDistance2 = $seg1->endPoint()->distance($seg2);
503
                    $checkDistance3 = $seg2->startPoint()->distance($seg1);
504
                    $checkDistance4 = $seg2->endPoint()->distance($seg1);
505
506
                    $checkDistance = min($checkDistance1, $checkDistance2, $checkDistance3, $checkDistance4);
507
                    if ($checkDistance === 0.0) {
508
                        return 0.0;
509
                    }
510
                    if ($distance === null || $checkDistance < $distance) {
511
                        $distance = $checkDistance;
512
                    }
513
                }
514
            }
515
            return $distance;
516
        } else {
517
            // It can be treated as collection
518
            return parent::distance($geometry);
519
        }
520
    }
521
}
522