Passed
Push — master ( 3d4b77...c34803 )
by smiley
01:33
created

PolylineSimplifyer::simplify()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 9
c 2
b 0
f 0
dl 0
loc 17
rs 9.2222
cc 6
nc 6
nop 2
1
<?php
2
/**
3
 * Class PolylineSimplifyer
4
 *
5
 * @link https://github.com/mourner/simplify-js
6
 *
7
 * @created      04.10.2019
8
 * @author       smiley <[email protected]>
9
 * @copyright    2019 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\GeoJSON;
14
15
use function array_fill, array_keys, array_pop, array_push, array_values, count, is_array;
16
17
class PolylineSimplifyer{
18
19
	protected array $coords;
20
21
	/**
22
	 * PolylineSimplifyer constructor.
23
	 */
24
	public function __construct(array $polylineCoords){
25
		$this->coords = $polylineCoords;
26
	}
27
28
	/**
29
	 * @throws \chillerlan\GeoJSON\GeoJSONException
30
	 */
31
	public function simplify(float $tolerance = 1, bool $highestQuality = false):array{
32
		$coords = array_values($this->coords ?? []);
33
34
		if(count($coords) < 2){
35
			throw new GeoJSONException('not enough points');
36
		}
37
38
		foreach($this->coords as $coord){
39
			if(!is_array($coord) || count($coord) < 2){
40
				throw new GeoJSONException('invalid coords found');
41
			}
42
		}
43
44
		$sqTolerance = $tolerance * $tolerance;
45
		$points      = $highestQuality ? $coords : $this->simplifyRadialDist($coords, $sqTolerance);
46
47
		return $this->simplifyDouglasPeucker($points, $sqTolerance);
48
	}
49
50
	/**
51
	 * basic distance-based simplification
52
	 */
53
	protected function simplifyRadialDist(array $points, float $sqTolerance):array{
54
		$prevPoint = $points[0];
55
		$newPoints = [$prevPoint];
56
		$point     = null;
57
		$len       = count($points);
58
59
		for($i = 1; $i < $len; $i++){
60
			$point = $points[$i];
61
62
			if($this->getSqDist(array_values($point), array_values($prevPoint)) > $sqTolerance){
63
				$newPoints[] = $point;
64
				$prevPoint   = $point;
65
			}
66
		}
67
68
		if($prevPoint !== $point){
69
			$newPoints[] = $point;
70
		}
71
72
		return $newPoints;
73
	}
74
75
	/**
76
	 * square distance between 2 points
77
	 */
78
	protected function getSqDist(array $p1, array $p2):float{
79
		$dx = $p1[0] - $p2[0];
80
		$dy = $p1[1] - $p2[1];
81
82
		return $dx * $dx + $dy * $dy;
83
	}
84
85
	/**
86
	 * simplification using optimized Douglas-Peucker algorithm with recursion elimination
87
	 */
88
	protected function simplifyDouglasPeucker(array $points, float $sqTolerance):array{
89
		$len       = count($points);
90
		$markers   = array_fill(0, $len - 1, null);
91
		$first     = 0;
92
		$last      = $len - 1;
93
		$stack     = [];
94
		$newPoints = [];
95
		$index     = 0;
96
97
		$markers[$first] = $markers[$last] = 1;
98
99
		while($last){
100
101
			$maxSqDist = 0;
102
103
			for($i = $first + 1; $i < $last; $i++){
104
				$sqDist = $this->getSqSegDist(
105
					array_values($points[$i]),
106
					array_values($points[$first]),
107
					array_values($points[$last])
108
				);
109
110
				if($sqDist > $maxSqDist){
111
					$index     = $i;
112
					$maxSqDist = $sqDist;
113
				}
114
			}
115
116
			if($maxSqDist > $sqTolerance){
117
				$markers[$index] = 1;
118
				array_push($stack, $first, $index, $index, $last);
119
			}
120
121
			$last  = array_pop($stack);
122
			$first = array_pop($stack);
123
		}
124
125
		foreach($points as $i => $point){
126
			if($markers[$i]){
127
				$newPoints[] = $point;
128
			}
129
		}
130
131
		return $newPoints;
132
	}
133
134
	/**
135
	 * square distance from a point to a segment
136
	 */
137
	protected function getSqSegDist(array $p, array $p1, array $p2):float{
138
		$x  = $p1[0];
139
		$y  = $p1[1];
140
		$dx = $p2[0] - $x;
141
		$dy = $p2[1] - $y;
142
143
		if((int)$dx !== 0 || (int)$dy !== 0){
144
145
			$t = (($p[0] - $x) * $dx + ($p[1] - $y) * $dy) / ($dx * $dx + $dy * $dy);
146
147
			if($t > 1){
148
				$x = $p2[0];
149
				$y = $p2[1];
150
151
			}
152
			elseif($t > 0){
153
				$x += $dx * $t;
154
				$y += $dy * $t;
155
			}
156
		}
157
158
		$dx = $p[0] - $x;
159
		$dy = $p[1] - $y;
160
161
		return $dx * $dx + $dy * $dy;
162
	}
163
164
}
165