Passed
Push — master ( a4f803...3d4b77 )
by smiley
12:11
created

PolylineSimplifyer   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 152
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 25
eloc 72
c 1
b 0
f 0
dl 0
loc 152
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getSqDist() 0 5 1
B simplifyDouglasPeucker() 0 44 7
A __construct() 0 2 1
A simplifyRadialDist() 0 20 4
B simplify() 0 23 7
A getSqSegDist() 0 25 5
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
	protected array $coordKeys;
21
22
	/**
23
	 * PolylineSimplifyer constructor.
24
	 */
25
	public function __construct(array $polylineCoords){
26
		$this->coords = $polylineCoords;
27
	}
28
29
	/**
30
	 * @throws \chillerlan\GeoJSON\GeoJSONException
31
	 */
32
	public function simplify(float $tolerance = 1, bool $highestQuality = false):array{
33
		$coords = array_values($this->coords ?? []);
34
35
		if(count($coords) < 2){
36
			throw new GeoJSONException('not enough points');
37
		}
38
39
		$this->coordKeys = array_keys($this->coords[0]);
40
41
		if(count($this->coordKeys) < 2){
42
			throw new GeoJSONException('invalid coordinate keys');
43
		}
44
45
		foreach($this->coords as $coord){
46
			if(!is_array($coord) || count($coord) < 2){
47
				throw new GeoJSONException('invalid coords found');
48
			}
49
		}
50
51
		$sqTolerance = $tolerance * $tolerance;
52
		$points      = $highestQuality ? $coords : $this->simplifyRadialDist($coords, $sqTolerance);
53
54
		return $this->simplifyDouglasPeucker($points, $sqTolerance);
55
	}
56
57
	/**
58
	 * basic distance-based simplification
59
	 */
60
	protected function simplifyRadialDist(array $points, float $sqTolerance):array{
61
		$prevPoint = $points[0];
62
		$newPoints = [$prevPoint];
63
		$point     = null;
64
		$len       = count($points);
65
66
		for($i = 1; $i < $len; $i++){
67
			$point = $points[$i];
68
69
			if($this->getSqDist(array_values($point), array_values($prevPoint)) > $sqTolerance){
70
				$newPoints[] = $point;
71
				$prevPoint   = $point;
72
			}
73
		}
74
75
		if($prevPoint !== $point){
76
			$newPoints[] = $point;
77
		}
78
79
		return $newPoints;
80
	}
81
82
	/**
83
	 * square distance between 2 points
84
	 */
85
	protected function getSqDist(array $p1, array $p2):float{
86
		$dx = $p1[0] - $p2[0];
87
		$dy = $p1[1] - $p2[1];
88
89
		return $dx * $dx + $dy * $dy;
90
	}
91
92
	/**
93
	 * simplification using optimized Douglas-Peucker algorithm with recursion elimination
94
	 */
95
	protected function simplifyDouglasPeucker(array $points, float $sqTolerance):array{
96
		$len       = count($points);
97
		$markers   = array_fill(0, $len - 1, null);
98
		$first     = 0;
99
		$last      = $len - 1;
100
		$stack     = [];
101
		$newPoints = [];
102
		$index     = 0;
103
104
		$markers[$first] = $markers[$last] = 1;
105
106
		while($last){
107
108
			$maxSqDist = 0;
109
110
			for($i = $first + 1; $i < $last; $i++){
111
				$sqDist = $this->getSqSegDist(
112
					array_values($points[$i]),
113
					array_values($points[$first]),
114
					array_values($points[$last])
115
				);
116
117
				if($sqDist > $maxSqDist){
118
					$index     = $i;
119
					$maxSqDist = $sqDist;
120
				}
121
			}
122
123
			if($maxSqDist > $sqTolerance){
124
				$markers[$index] = 1;
125
				array_push($stack, $first, $index, $index, $last);
126
			}
127
128
			$last  = array_pop($stack);
129
			$first = array_pop($stack);
130
		}
131
132
		foreach($points as $i => $point){
133
			if($markers[$i]){
134
				$newPoints[] = $point;
135
			}
136
		}
137
138
		return $newPoints;
139
	}
140
141
	/**
142
	 * square distance from a point to a segment
143
	 */
144
	protected function getSqSegDist(array $p, array $p1, array $p2):float{
145
		$x  = $p1[0];
146
		$y  = $p1[1];
147
		$dx = $p2[0] - $x;
148
		$dy = $p2[1] - $y;
149
150
		if((int)$dx !== 0 || (int)$dy !== 0){
151
152
			$t = (($p[0] - $x) * $dx + ($p[1] - $y) * $dy) / ($dx * $dx + $dy * $dy);
153
154
			if($t > 1){
155
				$x = $p2[0];
156
				$y = $p2[1];
157
158
			}
159
			elseif($t > 0){
160
				$x += $dx * $t;
161
				$y += $dy * $t;
162
			}
163
		}
164
165
		$dx = $p[0] - $x;
166
		$dy = $p[1] - $y;
167
168
		return $dx * $dx + $dy * $dy;
169
	}
170
171
}
172