Completed
Pull Request — master (#3)
by
unknown
01:56
created

Clusterer::addCoordinate()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 31
rs 6.7272
cc 7
eloc 14
nc 10
nop 1
1
<?php
2
namespace MatthiasMullie\Geo;
3
4
/**
5
 * Please report bugs on https://github.com/matthiasmullie/geo/issues
6
 *
7
 * @author Matthias Mullie <[email protected]>
8
 *
9
 * @copyright Copyright (c) 2013, Matthias Mullie. All rights reserved.
10
 * @license MIT License
11
 */
12
class Clusterer
13
{
14
    /**
15
     * @var Bounds
16
     */
17
    protected $bounds;
18
19
    /**
20
     * Amount of coordinates in one cell needed to start clustering.
21
     *
22
     * @var int
23
     */
24
    protected $minLocations = 2;
25
26
    /**
27
     * @var int
28
     */
29
    protected $numberOfClusters = 50;
30
31
    /**
32
     * @var Cluster[][]
33
     */
34
    protected $clusters = array();
35
36
    /**
37
     * @var Coordinate[][][]
38
     */
39
    protected $coordinates = array();
40
41
    /**
42
     * @var int
43
     */
44
    protected $coefficientLat = 0;
45
46
    /**
47
     * @var int
48
     */
49
    protected $coefficientLng = 0;
50
51
    /**
52
     * @var bool
53
     */
54
    protected $spanBoundsLat = false;
55
56
    /**
57
     * @var bool
58
     */
59
    protected $spanBoundsLng = false;
60
61
    /**
62
     * @var bool
63
     */
64
    protected $saveCoordinates = false;
65
66
    /**
67
     * @param Bounds $bounds
68
     */
69
    public function __construct(Bounds $bounds)
70
    {
71
        // determine if bounds span 360 to -360 degrees gap
72
        $this->spanBoundsLng = $bounds->ne->longitude < $bounds->sw->longitude;
73
        $this->spanBoundsLat = $bounds->ne->latitude < $bounds->sw->latitude;
74
75
        $this->bounds = $this->fixBounds($bounds);
76
        $this->createMatrix();
77
    }
78
79
    /**
80
     * Enables coordinate saving in clusters.
81
     *
82
     * Note that while it allows you to retrieve all Coordinate objects
83
     * in a cluster, this does not scale. At some point, if you keep
84
     * adding coordinates into clusters, you'll run out of memory
85
     * because we're saving all those coordinates.
86
     * If you don't need the exact information of coordinates in a
87
     * cluster, leave this disabled.
88
     *
89
     * @param  bool      $save
90
     * @throws Exception
91
     */
92
    public function setSaveCoordinates($save)
93
    {
94
        if (count($this->clusters) || count($this->coordinates)) {
95
            throw new Exception('Sorry, it is not possible to change coordinate saving policy after you have already added coordinates.');
96
        }
97
98
        $this->saveCoordinates = $save;
99
    }
100
101
    /**
102
     * Set the minimum amount of locations before clustering.
103
     *
104
     * @param int $limit
105
     */
106
    public function setMinClusterLocations($limit)
107
    {
108
        // simple sanity check. It doesn't make sense to have clusters with less than 2 locations
109
        $this->minLocations = (int)($limit > 2 ? $limit : 0); 
110
    }
111
112
    /**
113
     * Set an approximate amount of clusters.
114
     * Approximate in that it also depends on the viewport:
115
     * less square = less clusters.
116
     *
117
     * @param int $number
118
     */
119
    public function setNumberOfClusters($number)
120
    {
121
        $this->numberOfClusters = $number;
122
    }
123
124
    /**
125
     * @param Coordinate $coordinate
126
     */
127
    public function addCoordinate(Coordinate $coordinate)
128
    {
129
        list($latIndex, $lngIndex) = $this->findCell($coordinate);
130
        $coordinateCount = isset($this->coordinates[$latIndex][$lngIndex]) ? count($this->coordinates[$latIndex][$lngIndex]) : 0;
131
132
        // cluster already exists, add coordinate to it
133
        if (isset($this->clusters[$latIndex][$lngIndex])) {
134
            $this->clusters[$latIndex][$lngIndex]->addCoordinate($coordinate, $this->saveCoordinates);
135
136
        // there's no cluster yet, but entry limit reached = cluster now, as long as we have more than one location/coordinate
137
        } elseif ($coordinateCount >= $this->minLocations - 1 && $coordinateCount > 1) { 
138
            // initialise cluster with given coordinate
139
            $this->clusters[$latIndex][$lngIndex] = new Cluster();
140
            $this->clusters[$latIndex][$lngIndex]->addCoordinate($coordinate, $this->saveCoordinates);
141
142
            if ($coordinateCount) {
143
                // add existing coordinates
144
                foreach ($this->coordinates[$latIndex][$lngIndex] as $coordinate) {
145
                    $this->clusters[$latIndex][$lngIndex]->addCoordinate($coordinate, $this->saveCoordinates);
146
                }
147
148
                // save cluster & clear array of individual coordinates (to free up
149
                // memory, in case we're dealing with lots of coordinates)
150
                unset($this->coordinates[$latIndex][$lngIndex]);
151
            }
152
153
        // entry limit for clustering not yet reached, save coordinate
154
        } else {
155
            $this->coordinates[$latIndex][$lngIndex][] = $coordinate;
156
        }
157
    }
158
159
    /**
160
     * @return Coordinate[]
161
     */
162
    public function getCoordinates()
163
    {
164
        // flatten matrix of coordinates
165
        $coordinates = $this->coordinates ? call_user_func_array('array_merge', $this->coordinates) : array();
166
167
        return $coordinates ? call_user_func_array('array_merge', $coordinates) : array();
168
    }
169
170
    /**
171
     * @return Cluster[]
172
     */
173
    public function getClusters()
174
    {
175
        // flatten matrix of clusters
176
        return $this->clusters ? call_user_func_array('array_merge', $this->clusters) : array();
177
    }
178
179
    /**
180
     * Based on given bounds, determine matrix size/structure.
181
     */
182
    protected function createMatrix()
183
    {
184
        $totalLat = $this->bounds->ne->latitude - $this->bounds->sw->latitude;
185
        $totalLng = $this->bounds->ne->longitude - $this->bounds->sw->longitude;
186
187
        $approxMiddle = round(sqrt($this->numberOfClusters));
188
        // the smaller one wins
189
        $func = $totalLat > $totalLng ? 'floor' : 'ceil';
190
        $numLat = $func($totalLat / ($totalLat + $totalLng) * $approxMiddle * 2);
191
        $numLng = $approxMiddle * 2 - $numLat;
192
193
        // this will be used later to calculate exactly which sector a
194
        // coordinate falls into (see findCell)
195
        $this->coefficientLat = 1 / ($totalLat) * $numLat;
0 ignored issues
show
Documentation Bug introduced by
The property $coefficientLat was declared of type integer, but 1 / $totalLat * $numLat is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
196
        $this->coefficientLng = 1 / ($totalLng) * $numLng;
0 ignored issues
show
Documentation Bug introduced by
The property $coefficientLng was declared of type integer, but 1 / $totalLng * $numLng is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
197
    }
198
199
    /**
200
     * Find the lat & lng indices of the matrix cell
201
     * the given coordinate fits into.
202
     *
203
     * @param  Coordinate $coordinate
204
     * @return array
205
     */
206
    protected function findCell(Coordinate $coordinate)
207
    {
208
        $coordinate = $this->fixCoordinates($coordinate);
209
210
        return array(
211
            floor(($coordinate->latitude - $this->bounds->sw->latitude) * $this->coefficientLat),
212
            floor(($coordinate->longitude - $this->bounds->sw->longitude) * $this->coefficientLng),
213
        );
214
    }
215
216
    /**
217
     * "Fix" coordinates - when leaping from east 360 to west -359, increase
218
     * the west coordinated by 360 to make calculating easier.
219
     *
220
     * @param  Coordinate $coordinate
221
     * @return Coordinate
222
     */
223
    protected function fixCoordinates(Coordinate $coordinate)
224
    {
225
        // create a new copy of this object, don't just change existing one
226
        $coordinate = clone $coordinate;
227
228
        if ($this->spanBoundsLat && $coordinate->latitude < $this->bounds->sw->latitude) {
229
            $coordinate->latitude += 180;
230
        }
231
232
        if ($this->spanBoundsLng && $coordinate->longitude < $this->bounds->sw->longitude) {
233
            $coordinate->longitude += 360;
234
        }
235
236
        return $coordinate;
237
    }
238
    /**
239
     * North and east coordinates can actually be lower than south & west.
240
     * This will happen when the left side of a map is displaying east and
241
     * the right side is displaying west. At the center of the map, we'll
242
     * suddenly have coordinates jumping from 360 to -359.
243
     * To make calculating things easier, we'll just increase the west
244
     * (= negative) coordinates by 360, and consider those to now be east
245
     * (and east as west). Now, coordinates will go from 360 to 361.
246
     *
247
     * @param  Bounds $bounds
248
     * @return Bounds
249
     */
250
    protected function fixBounds(Bounds $bounds)
251
    {
252
        // create a new copy of this object, don't just change existing one
253
        $bounds = clone $bounds;
254
255 View Code Duplication
        if ($this->spanBoundsLat) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
256
            // workaround for crossover bounds being rounded too aggressively
257
            if ($bounds->sw->latitude == 0) {
258
                $bounds->sw->latitude += 180;
259
            }
260
261
            $neLat = max(180 + $bounds->ne->latitude, 180 + $bounds->sw->latitude);
262
            $bounds->sw->latitude = $bounds->ne->latitude;
263
            $bounds->ne->latitude = $neLat;
264
        }
265 View Code Duplication
        if ($this->spanBoundsLng) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
            // workaround for crossover bounds being rounded too aggressively
267
            if ($bounds->sw->longitude == 0) {
268
                $bounds->sw->longitude += 360;
269
            }
270
271
            $neLng = max(360 + $bounds->ne->longitude, 360 + $bounds->sw->longitude);
272
            $bounds->sw->longitude = $bounds->ne->longitude;
273
            $bounds->ne->longitude = $neLng;
274
        }
275
276
        return $bounds;
277
    }
278
}
279