KD2 Framework  Check-in [d5252aa931]

Overview
Comment:Remove Karto, update Karto\Point and Karto\Set
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | 7.3
Files: files | file ages | folders
SHA1: d5252aa9315ab2fa992e1b86e024051242dc04a7
User & Date: bohwaz on 2019-12-18 11:56:37
Other Links: branch diff | manifest | tags
Context
2019-12-18
12:01
Rename graphics libraries check-in: 70c87a9b18 user: bohwaz tags: 7.3
11:56
Remove Karto, update Karto\Point and Karto\Set check-in: d5252aa931 user: bohwaz tags: 7.3
11:42
Move Karto libraries check-in: ac4c400e8b user: bohwaz tags: 7.3
Changes

Deleted src/lib/KD2/Karto/Karto.php version [d616b5b332].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
<?php
/*
    This file is part of KD2FW -- <http://dev.kd2.org/>

    Copyright (c) 2001-2019 BohwaZ <http://bohwaz.net/>
    All rights reserved.

    KD2FW is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Foobar is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Foobar.  If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * Karto: an independent PHP library providing basic mapping tools
 */

namespace KD2\Karto;

// From http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps

class Karto
{
    const PIXELS_OFFSET = 268435456;
    // You might wonder where did number 268435456 come from?
    // It is half of the earth circumference in pixels at zoom level 21.
    // You can visualize it by thinking of full map.
    // Full map size is 536870912 × 536870912 pixels.
    // Center of the map in pixel coordinates is 268435456,268435456
    // which in latitude and longitude would be 0,0.
    const PIXELS_RADIUS = 85445659.4471; /* PIXELS_OFFSET / pi() */

    const RADIUS = 6371.0; // Average radius of earth (in kilometers)
    // FIXME: or 6378.1 km?

    /**
     * Distance from one point to another
     * @param  float $lat1 Latitude of starting point (decimal)
     * @param  float $lon1 Longitude of starting point (decimal)
     * @param  float $lat2 Latitude of destination (decimal)
     * @param  float $lon2 Longitude of destination (decimal)
     * @return float Distance in kilometers
     */
    public function haversineDistance($lat1, $lon1, $lat2, $lon2)
    {
        $latd = deg2rad($lat2 - $lat1);
        $lond = deg2rad($lon2 - $lon1);
        $a = sin($latd / 2) * sin($latd / 2) +
             cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
             sin($lond / 2) * sin($lond / 2);
             $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
        return self::RADIUS * $c; // average radius of earth
    }

    /**
     * Get horizontal position in pixels for map from longitude
     * @param  float $lon Longitude (decimal)
     * @return int Horizontal position on map in pixels
     */
    public function lonToX($lon)
    {
        return round(self::PIXELS_OFFSET + self::PIXELS_RADIUS * $lon * pi() / 180);
    }

    /**
     * Get vertical position in pixels for map from latitude
     * @param  float $lat Latitude (decimal)
     * @return int Vertical position on map in pixels
     */
    public function latToY($lat)
    {
        return round(self::PIXELS_OFFSET - self::PIXELS_RADIUS *
                    log((1 + sin($lat * pi() / 180)) /
                    (1 - sin($lat * pi() / 180))) / 2);
    }

    /**
     * Is this latitude/longitude contained inside the given NW/SE bounds?
     * @param  double  $lat        Point latitude
     * @param  double  $lon        Point longitude
     * @param  array   $north_west NW boundary ['lat' => 0.0, 'lon' => 0.0]
     * @param  array   $south_east SW boundary
     * @return boolean             true if point is inside the boundaries, false if it is outside
     */
    public function isContainedInBounds($lat, $lon, $north_west, $south_east)
    {
        if ($lon < max($north_west['lon'], $south_east['lon'])
            && $lon > min($north_west['lon'], $south_east['lon'])
            && $lat < max($north_west['lat'], $south_east['lon'])
            && $lat > min($north_west['lat'], $south_east['lat']))
        {
            return true;
        }

        return false;
    }

    /**
     * Distance in pixels between two points at a specific zoom level
     * @param  float $lat1 Latitude of starting point (decimal)
     * @param  float $lon1 Longitude of starting point (decimal)
     * @param  float $lat2 Latitude of destination (decimal)
     * @param  float $lon2 Longitude of destination (decimal)
     * @param  integer $zoom Zoom level
     * @return int Distance in pixels
     */
    public function pixelDistance($lat1, $lon1, $lat2, $lon2, $zoom)
    {
        $x1 = $this->lonToX($lon1);
        $y1 = $this->latToY($lat1);

        $x2 = $this->lonToX($lon2);
        $y2 = $this->latToY($lat2);

        return sqrt(pow(($x1-$x2),2) + pow(($y1-$y2),2)) >> (21 - $zoom);
    }

    /**
     * Cluster points to avoid having too much points
     * @param  array $points Array of points, each item MUST contain a 'lat' and 'lon' keys containing
     * latitude and longitude in decimal notation. All data from each item will be kept in resulting cluster array.
     * @param  integer $distance Maximum distance between two points to cluster them
     * @param  integer $zoom Map zoom level
     * @return array Each row will contain a key named 'points' containing all the points in the cluster and
     * a key named 'center' containing the center coordinates of the cluster.
     */
    public function cluster($points, $distance, $zoom)
    {
        $clustered = [];

        /* Loop until all points have been compared. */
        while (count($points))
        {
            $point  = array_pop($points);
            $cluster = ['points' => [], 'center' => null];
            /* Compare against all points which are left. */
            foreach ($points as $key => $target)
            {
                $pixels = $this->pixelDistance($point['lat'], $point['lon'],
                                        $target['lat'], $target['lon'],
                                        $zoom);

                /* If two points are closer than given distance remove */
                /* target point from array and add it to cluster.      */
                if ($distance > $pixels)
                {
                    unset($points[$key]);
                    $cluster['points'][] = $target;
                }
            }

            /* If a point has been added to cluster, add also the one  */
            /* we were comparing to and remove the original from array. */
            if (count($cluster) > 0)
            {
                $cluster['points'][] = $point;

                $cluster['center'] = $this->calculateCenter($cluster['points']);

                $clustered[] = $cluster;
            }
            else
            {
                $clustered[] = $point;
            }
        }

        return $clustered;
    }

    /**
     * Calculate average latitude and longitude of supplied array of points
     * @param  array  $points Each array item MUST have at least two keys named 'lat' and 'lon'
     * @return array  ['lat' => x.xxxx, 'lon' => y.yyyy]
     */
    public function calculateCenter($points)
    {
        /* Calculate average lat and lon of points. */
        $lat_sum = $lon_sum = 0;
        foreach ($points as $point)
        {
           $lat_sum += $point['lat'];
           $lon_sum += $point['lon'];
        }

        $lat_avg = $lat_sum / count($points);
        $lon_avg = $lon_sum / count($points);

        return ['lat' => $lat_avg, 'lon' => $lon_avg];
    }

    /**
     * Gets distance between two points
     * @param  float $lat1 Latitude of starting point (decimal)
     * @param  float $lon1 Longitude of starting point (decimal)
     * @param  float $lat2 Latitude of destination (decimal)
     * @param  float $lon2 Longitude of destination (decimal)
     * @return [type]       [description]
     */
	public function distance($lat1, $lon1, $lat2, $lon2)
	{
		if (is_null($lat1) || is_null($lon1) || is_null($lat2) || is_null($lon2))
			return null;

		$lat1 = (double) $lat1;
		$lon1 = (double) $lon1;
		$lat2 = (double) $lat2;
		$lon2 = (double) $lon2;

		// convert lat1 and lat2 into radians now, to avoid doing it twice below
		$lat1rad = deg2rad($lat1);
		$lat2rad = deg2rad($lat2);

		// apply the spherical law of cosines to our latitudes and longitudes, and set the result appropriately
		return (acos(sin($lat1rad) * sin($lat2rad) + cos($lat1rad) * cos($lat2rad) * cos(deg2rad($lon2) - deg2rad($lon1))) * self::RADIUS);
	}

	/**
	 * Converts a latitude and longitude from decimal to DMS notation
	 * @param  float $lat Decimal latitude
	 * @param  float $lon Decimal longitude
	 * @return string Latitude / Longitude in DMS notation, eg. 45 5 56 S 174 11 37 E
	 */
	static function notationDecToDMS($lat, $lon)
	{
		$convert = function ($dec)
		{
			$dec = explode('.', $dec);
			if (count($dec) != 2) return [0, 0, 0];
			$m = $dec[1];
			$h = $dec[0];
			$m = round('0.' . $m, 6) * 3600;
			$min = floor($m / 60);
			$sec = round($m - ($min*60));
			return [$h, $min, $sec];
		};

		$lat = $convert($lat);
		$lat = abs($lat[0]) . ' ' . abs($lat[1]) . ' ' . abs($lat[2]) . ' ' . ($lat[0] > 0 ? 'N' : 'S');

		$lon = $convert($lon);
		$lon = abs($lon[0]) . ' ' . abs($lon[1]) . ' ' . abs($lon[2]) . ' ' . ($lon[0] > 0 ? 'E' : 'W');

	    return $lat . ' ' . $lon;
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































































































































































































































































































































































































Modified src/lib/KD2/Karto/Point.php from [79ddb58824] to [77f4e12bb5].

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46





47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
 * Karto: an independent PHP library providing basic mapping tools
 */

namespace KD2\Karto;

use ArrayAccess;
use \KD2\Karto\Point_Set;

class Point implements ArrayAccess
{
	/**
	 * Half of the earth circumference in pixels at zoom level 21
	 *
	 * Full map size is 536870912 × 536870912 pixels.
	 * Center of the map in pixel coordinates is 268435456,268435456
	 * which in latitude and longitude would be 0,0.
	 */
	const PIXELS_OFFSET = 268435456;

	/**
	 * PIXELS_OFFSET / pi()
	 */
	const PIXELS_RADIUS = 85445659.4471;






	/**
	 * Latitude value
	 * @var float
	 */
	protected $lat = null;

	/**
	 * Longitude value
	 * @var float
	 */	
	protected $lon = null;

	/**
	 * "Magic" constructor  will accept either float coordinates, an array, a Karto_Point object, or a string
	 * @param	float	$lat	Latitude
	 * @param	float	$lon	Longitude
	 */
	public function __construct($lat = null, $lon = null)
	{
		$self = __CLASS__;

		if (is_object($lat) && $lat instanceof $self)
		{







|

















>
>
>
>
>









|



|
|
|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
 * Karto: an independent PHP library providing basic mapping tools
 */

namespace KD2\Karto;

use ArrayAccess;
use KD2\Karto\Set;

class Point implements ArrayAccess
{
	/**
	 * Half of the earth circumference in pixels at zoom level 21
	 *
	 * Full map size is 536870912 × 536870912 pixels.
	 * Center of the map in pixel coordinates is 268435456,268435456
	 * which in latitude and longitude would be 0,0.
	 */
	const PIXELS_OFFSET = 268435456;

	/**
	 * PIXELS_OFFSET / pi()
	 */
	const PIXELS_RADIUS = 85445659.4471;

	/**
	 * Average radius of earth (in kilometers)
	 */
	const RADIUS = 6371;

	/**
	 * Latitude value
	 * @var float
	 */
	protected $lat = null;

	/**
	 * Longitude value
	 * @var float
	 */
	protected $lon = null;

	/**
	 * "Magic" constructor  will accept either float coordinates, an array, a Point object, or a string
	 * @param	float|Point|string|array|null	$lat	Latitude
	 * @param	float|null	$lon	Longitude
	 */
	public function __construct($lat = null, $lon = null)
	{
		$self = __CLASS__;

		if (is_object($lat) && $lat instanceof $self)
		{
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
	 */
	public function offsetGet($offset) {
		return $this->__get($offset);
	}

	/**
	 * Gets distance between this point and another
	 * @param	Karto_Point	$point	Distant point
	 * @return	float				Distance in KM
	 */
	public function distanceTo(Karto_Point $point)
	{
		$lat1 = $this->lat;
		$lon1 = $this->lon;
		$lat2 = $point->lat;
		$lon2 = $point->lon;

		// convert lat1 and lat2 into radians now, to avoid doing it twice below
		$lat1rad = deg2rad($lat1);
		$lat2rad = deg2rad($lat2);

		// apply the spherical law of cosines to our latitudes and longitudes, and set the result appropriately
		return (acos(sin($lat1rad) * sin($lat2rad) + cos($lat1rad) * cos($lat2rad) * cos(deg2rad($lon2) - deg2rad($lon1))) * 6371);
	}

	/**
	 * Distance in pixels between two points at a specific zoom level
	 * @param	Karto_Point	$point	Distant point
	 * @param	integer		$zoom	Zoom level
	 * @param	mixed		$restrict	Restrict direction (x or y, or false to have the diagonal)
	 * @return	int					Distance in pixels
	 */
	public function pixelDistanceTo(Karto_Point $point, $zoom, $restrict = false)
	{
		list($x1, $y1) = $this->XY();
		list($x2, $y2) = $point->XY();

		// Restrict direction
		if ($restrict == 'x')
		{







|


|











|




|

|


|







191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
	 */
	public function offsetGet($offset) {
		return $this->__get($offset);
	}

	/**
	 * Gets distance between this point and another
	 * @param	Point	$point	Distant point
	 * @return	float				Distance in KM
	 */
	public function distanceTo(Point $point): float
	{
		$lat1 = $this->lat;
		$lon1 = $this->lon;
		$lat2 = $point->lat;
		$lon2 = $point->lon;

		// convert lat1 and lat2 into radians now, to avoid doing it twice below
		$lat1rad = deg2rad($lat1);
		$lat2rad = deg2rad($lat2);

		// apply the spherical law of cosines to our latitudes and longitudes, and set the result appropriately
		return (acos(sin($lat1rad) * sin($lat2rad) + cos($lat1rad) * cos($lat2rad) * cos(deg2rad($lon2) - deg2rad($lon1))) * self::RADIUS);
	}

	/**
	 * Distance in pixels between two points at a specific zoom level
	 * @param	Point	$point	Distant point
	 * @param	integer		$zoom	Zoom level
	 * @param	mixed		$restrict	Restrict direction (x or y, or null to have the diagonal)
	 * @return	int					Distance in pixels
	 */
	public function pixelDistanceTo(Point $point, int $zoom, ?string $restrict = null): int
	{
		list($x1, $y1) = $this->XY();
		list($x2, $y2) = $point->XY();

		// Restrict direction
		if ($restrict == 'x')
		{
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
		return sqrt(pow(($x1-$x2),2) + pow(($y1-$y2),2)) >> (21 - $zoom);
	}

	/**
	 * Get position in pixels for map
	 * @return	array	[int X, int Y] position on map in pixels at zoom 21
	 */
	public function XY()
	{
		$x = round(self::PIXELS_OFFSET + self::PIXELS_RADIUS * $this->lon * pi() / 180);
		$y = round(self::PIXELS_OFFSET - self::PIXELS_RADIUS * 
					log((1 + sin($this->lat * pi() / 180)) / 
					(1 - sin($this->lat * pi() / 180))) / 2);
		return [$x, $y];
	}

	/**
	 * Converts a latitude and longitude from decimal to DMS notation
	 * @return	array	Latitude / Longitude in DMS notation, eg. [45 5 56 S, 174 11 37 E]
	 */
	public function toDMS()
	{
		$convert = function ($dec)
		{
			$dec = explode('.', $dec);
			if (count($dec) != 2) return [0, 0, 0];
			$m = $dec[1];
			$h = $dec[0];







|












|







238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
		return sqrt(pow(($x1-$x2),2) + pow(($y1-$y2),2)) >> (21 - $zoom);
	}

	/**
	 * Get position in pixels for map
	 * @return	array	[int X, int Y] position on map in pixels at zoom 21
	 */
	public function XY(): array
	{
		$x = round(self::PIXELS_OFFSET + self::PIXELS_RADIUS * $this->lon * pi() / 180);
		$y = round(self::PIXELS_OFFSET - self::PIXELS_RADIUS * 
					log((1 + sin($this->lat * pi() / 180)) / 
					(1 - sin($this->lat * pi() / 180))) / 2);
		return [$x, $y];
	}

	/**
	 * Converts a latitude and longitude from decimal to DMS notation
	 * @return	array	Latitude / Longitude in DMS notation, eg. [45 5 56 S, 174 11 37 E]
	 */
	public function toDMS(): array
	{
		$convert = function ($dec)
		{
			$dec = explode('.', $dec);
			if (count($dec) != 2) return [0, 0, 0];
			$m = $dec[1];
			$h = $dec[0];
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
		$lon = abs($lon[0]) . ' ' . abs($lon[1]) . ' ' . abs($lon[2]) . ' ' . ($lon[0] > 0 ? 'E' : 'W');

		return [$lat, $lon];
	}

	/**
	 * Is this latitude/longitude contained inside the given set bounds?
	 * @param	Karto_Point_Set	$set	Set of points
	 * @return	boolean					true if point is inside the boundaries, false if it is outside
	 */
	public function isContainedIn(Karto_Point_Set $set)
	{
		$bbox = $set->getBBox();

		if ($this->lon >= $bbox->minLon
			&& $this->lon <= $bbox->maxLon
			&& $this->lat >= $bbox->minLat
			&& $this->lat <= $bbox->maxLat)







|


|







276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
		$lon = abs($lon[0]) . ' ' . abs($lon[1]) . ' ' . abs($lon[2]) . ' ' . ($lon[0] > 0 ? 'E' : 'W');

		return [$lat, $lon];
	}

	/**
	 * Is this latitude/longitude contained inside the given set bounds?
	 * @param	Set	$set	Set of points
	 * @return	boolean					true if point is inside the boundaries, false if it is outside
	 */
	public function isContainedIn(Set $set): bool
	{
		$bbox = $set->getBBox();

		if ($this->lon >= $bbox->minLon
			&& $this->lon <= $bbox->maxLon
			&& $this->lat >= $bbox->minLat
			&& $this->lat <= $bbox->maxLat)

Modified src/lib/KD2/Karto/Set.php from [e31647690f] to [fff9bc50c1].

24
25
26
27
28
29
30

31
32
33
34
35
36
37
38
39
 *
 * @author	bohwaz	http://bohwaz.net/
 */

namespace KD2;

use KD2\Karto\Point;


class Point_Set
{
	/**
	 * Internal storage of set of points
	 * @var array
	 */
	protected $points = [];








>

|







24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 *
 * @author	bohwaz	http://bohwaz.net/
 */

namespace KD2;

use KD2\Karto\Point;
use stdClass;

class Set
{
	/**
	 * Internal storage of set of points
	 * @var array
	 */
	protected $points = [];

47
48
49
50
51
52
53
54
55
56
57




58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
		{
			$this->add($point);
		}
	}

	/**
	 * Add a new point to the set
	 * @param mixed $point Could be a Karto_Point object, or an array like [85, 180] or [lat => 85, lon => 180]
	 */
	public function add($point)
	{




		$this->points[] = new Karto_Point($point);
	}

	/**
	 * Returns all the points of the set
	 * @return array list of Karto_Point points
	 */
	public function getPoints()
	{
		return $this->points;
	}

	/**
	 * Returns the number of points in the current set
	 * @return integer Number of points
	 */
	public function count()
	{
		return count($this->points);
	}

	/**
	 * Returns the bounding box of the current set of coordinates
	 * @return \stdClass {float minLat, float maxLat, float minLon, float maxLon, Karto_Point northEast, Karto_Point southWest}
	 */
	public function getBBox()
	{
		if (count($this->points) < 1)
			throw new \OutOfRangeException('Empty point set');

		$bbox = new \stdClass;

		$point = $this->points[0];

		$bbox->minLat = $bbox->maxLat = $point->lat;
		$bbox->minLon = $bbox->maxLon = $point->lon;

		foreach ($this->points as $point)
		{
			$bbox->minLat = min($bbox->minLat, $point->lat);
			$bbox->maxLat = max($bbox->maxLat, $point->lat);
			$bbox->minLon = min($bbox->minLon, $point->lon);
			$bbox->maxLon = max($bbox->maxLon, $point->lon);
		}

		$bbox->northEast = new Karto_Point($bbox->maxLat, $bbox->minLon);
		$bbox->southWest = new Karto_Point($bbox->minLat, $bbox->maxLon);

		return $bbox;
	}

	/**
	 * Calculate average latitude and longitude of supplied array of points
	 * @param  array  $points Each array item MUST have at least two keys named 'lat' and 'lon'
	 * @return object (obj)->lat = x.xxxx, ->lon => y.yyyy
	 */
	public function getCenter()
	{
		if (count($this->points) < 1)
			throw new \OutOfRangeException('Empty point set');

		/* Calculate average lat and lon of points. */
		$lat_sum = $lon_sum = 0;
		foreach ($this->points as $point)
		{
		   $lat_sum += $point->lat;
		   $lon_sum += $point->lon;
		}

		$lat_avg = $lat_sum / count($this->points);
		$lon_avg = $lon_sum / count($this->points);

		return new Karto_Point($lat_avg, $lon_avg);
	}


	/**
	 * Cluster points to avoid having too much points
	 * @param  integer $distance Maximum distance between two points to cluster them
	 * @param  integer $zoom Map zoom level
	 * @return array Each row will contain a key named 'points' containing all the points in the cluster and
	 * a key named 'center' containing the center coordinates of the cluster.
	 */
	public function cluster($distance, $zoom)
	{
		$clustered = [];
		$points = $this->points;

		/* Loop until all points have been compared. */
		while (count($points))
		{
			$point  = array_pop($points);
			$cluster = new Karto_Point_Set;

			/* Compare against all points which are left. */
			foreach ($points as $key => $target)
			{
				$pixels = $point->pixelDistanceTo($target, $zoom);

				/* If two points are closer than given distance remove */







|



>
>
>
>
|




|

|








|






|

|




|














|
|









|















|










|








|







48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
		{
			$this->add($point);
		}
	}

	/**
	 * Add a new point to the set
	 * @param mixed $point Could be a Point object, or an array like [85, 180] or [lat => 85, lon => 180]
	 */
	public function add($point)
	{
		if (!is_object($point) || !($point instanceof Point)) {
			$point = new Point($point);
		}

		$this->points[] = $point;
	}

	/**
	 * Returns all the points of the set
	 * @return array list of Point points
	 */
	public function getPoints(): array
	{
		return $this->points;
	}

	/**
	 * Returns the number of points in the current set
	 * @return integer Number of points
	 */
	public function count(): int
	{
		return count($this->points);
	}

	/**
	 * Returns the bounding box of the current set of coordinates
	 * @return \stdClass {float minLat, float maxLat, float minLon, float maxLon, Point northEast, Point southWest}
	 */
	public function getBBox(): stdClass
	{
		if (count($this->points) < 1)
			throw new \OutOfRangeException('Empty point set');

		$bbox = new stdClass;

		$point = $this->points[0];

		$bbox->minLat = $bbox->maxLat = $point->lat;
		$bbox->minLon = $bbox->maxLon = $point->lon;

		foreach ($this->points as $point)
		{
			$bbox->minLat = min($bbox->minLat, $point->lat);
			$bbox->maxLat = max($bbox->maxLat, $point->lat);
			$bbox->minLon = min($bbox->minLon, $point->lon);
			$bbox->maxLon = max($bbox->maxLon, $point->lon);
		}

		$bbox->northEast = new Point($bbox->maxLat, $bbox->minLon);
		$bbox->southWest = new Point($bbox->minLat, $bbox->maxLon);

		return $bbox;
	}

	/**
	 * Calculate average latitude and longitude of supplied array of points
	 * @param  array  $points Each array item MUST have at least two keys named 'lat' and 'lon'
	 * @return object (obj)->lat = x.xxxx, ->lon => y.yyyy
	 */
	public function getCenter(): Point
	{
		if (count($this->points) < 1)
			throw new \OutOfRangeException('Empty point set');

		/* Calculate average lat and lon of points. */
		$lat_sum = $lon_sum = 0;
		foreach ($this->points as $point)
		{
		   $lat_sum += $point->lat;
		   $lon_sum += $point->lon;
		}

		$lat_avg = $lat_sum / count($this->points);
		$lon_avg = $lon_sum / count($this->points);

		return new Point($lat_avg, $lon_avg);
	}


	/**
	 * Cluster points to avoid having too much points
	 * @param  integer $distance Maximum distance between two points to cluster them
	 * @param  integer $zoom Map zoom level
	 * @return array Each row will contain a key named 'points' containing all the points in the cluster and
	 * a key named 'center' containing the center coordinates of the cluster.
	 */
	public function cluster(int $distance, int $zoom): array
	{
		$clustered = [];
		$points = $this->points;

		/* Loop until all points have been compared. */
		while (count($points))
		{
			$point  = array_pop($points);
			$cluster = new Set;

			/* Compare against all points which are left. */
			foreach ($points as $key => $target)
			{
				$pixels = $point->pixelDistanceTo($target, $zoom);

				/* If two points are closer than given distance remove */
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
		return $clustered;
	}

	/**
	 * Decode a polyline into a set of coordinates
	 * @link   https://developers.google.com/maps/documentation/utilities/polylinealgorithm Polyline algorithm
	 * @param  string $line   Polyline encoded string
	 * @return array          A list of lat/lon tuples
	 */
	static public function fromPolyline($line)
	{
		$precision = 5;
		$index = $i = 0;
		$points = [];
		$previous = [0,0];

		while ($i < strlen($line))







|

|







184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
		return $clustered;
	}

	/**
	 * Decode a polyline into a set of coordinates
	 * @link   https://developers.google.com/maps/documentation/utilities/polylinealgorithm Polyline algorithm
	 * @param  string $line   Polyline encoded string
	 * @return Set          A set of lat/lon tuples
	 */
	static public function fromPolyline(string $line): Set
	{
		$precision = 5;
		$index = $i = 0;
		$points = [];
		$previous = [0,0];

		while ($i < strlen($line))
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
			$number = $previous[$index % 2] + $diff;
			$previous[$index % 2] = $number;
			$index++;

			$points[] = $number * 1 / pow(10, $precision);
		}

		return new Karto_Point_Set(array_chunk($points, 2));
	}

	/**
	 * Encode the current set of coordinates as a polyline
	 * @link	https://developers.google.com/maps/documentation/utilities/polylinealgorithm Polyline algorithm
	 * @return	string	A polyline encoded string
	 */
	public function toPolyline()
	{
		// Flatten array
		$points = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($this->points));

		$precision = 5;
		$encodedString = '';
		$index = 0;







|







|







211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
			$number = $previous[$index % 2] + $diff;
			$previous[$index % 2] = $number;
			$index++;

			$points[] = $number * 1 / pow(10, $precision);
		}

		return new Set(array_chunk($points, 2));
	}

	/**
	 * Encode the current set of coordinates as a polyline
	 * @link	https://developers.google.com/maps/documentation/utilities/polylinealgorithm Polyline algorithm
	 * @return	string	A polyline encoded string
	 */
	public function toPolyline(): string
	{
		// Flatten array
		$points = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($this->points));

		$precision = 5;
		$encodedString = '';
		$index = 0;
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
	/**
	 * Returns mercator suitable zoom level according to the set of points
	 * @param	integer	$mapWidth	Target map width in pixels
	 * @param	integer	$mapHeight	Target map height in pixels
	 * @return	integer			Zoom level between 0 and 21
	 * @link http://stackoverflow.com/a/15397775
	 */
	public function getZoomLevel($mapWidth, $mapHeight)
	{
		$bbox = $this->getBBox();

		for ($zoom = 21; $zoom >= 0; --$zoom)
		{
			$distance_w = $bbox->northEast->pixelDistanceTo($bbox->southWest, $zoom, 'x');
			$distance_h = $bbox->northEast->pixelDistanceTo($bbox->southWest, $zoom, 'y');







|







261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
	/**
	 * Returns mercator suitable zoom level according to the set of points
	 * @param	integer	$mapWidth	Target map width in pixels
	 * @param	integer	$mapHeight	Target map height in pixels
	 * @return	integer			Zoom level between 0 and 21
	 * @link http://stackoverflow.com/a/15397775
	 */
	public function getZoomLevel(int $mapWidth, int $mapHeight): int
	{
		$bbox = $this->getBBox();

		for ($zoom = 21; $zoom >= 0; --$zoom)
		{
			$distance_w = $bbox->northEast->pixelDistanceTo($bbox->southWest, $zoom, 'x');
			$distance_h = $bbox->northEast->pixelDistanceTo($bbox->southWest, $zoom, 'y');