]> Raphaël G. Git Repositories - packbundle/blob - Util/OsmUtil.php
df66f2fc7825a02b17638b1d89be79e9fc3767e8
[packbundle] / Util / OsmUtil.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys PackBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\PackBundle\Util;
13
14 use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
15 use Symfony\Component\Filesystem\Filesystem;
16
17 /**
18 * Helps manage osm images
19 */
20 class OsmUtil {
21 /**
22 * The tile size
23 */
24 const tz = 256;
25
26 /**
27 * The cache directory
28 */
29 protected $cache;
30
31 /**
32 * The public path
33 */
34 protected $path;
35
36 /**
37 * The tile server
38 */
39 protected $server;
40
41 /**
42 * The tile servers
43 *
44 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
45 */
46 protected $servers = [
47 'osm' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png',
48 'cycle' => 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png',
49 'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png'
50 ];
51
52 /**
53 * The public url
54 */
55 protected $url;
56
57 /**
58 * Creates a new osm util
59 *
60 * @param string $cache The cache directory
61 * @param string $path The public path
62 * @param string $url The public url
63 * @param string $server The server key
64 */
65 function __construct(string $cache, string $path, string $url, string $server = 'osm') {
66 //Set cache
67 $this->cache = $cache.'/'.$server;
68
69 //Set path
70 $this->path = $path.'/'.$server;
71
72 //Set url
73 $this->url = $url.'/'.$server;
74
75 //Set server key
76 $this->server = $server;
77 }
78
79 /**
80 * Return the image
81 *
82 * @desc Generate image in jpeg format or load it from cache
83 *
84 * @param string $pathInfo The path info
85 * @param string $alt The image alt
86 * @param int $updated The updated timestamp
87 * @param float $latitude The latitude
88 * @param float $longitude The longitude
89 * @param int $zoom The zoom
90 * @param int $width The width
91 * @param int $height The height
92 * @return array The image array
93 */
94 public function getImage(string $pathInfo, string $alt, int $updated, float $latitude, float $longitude, int $zoom = 18, int $width = 1280, int $height = 1280): array {
95 //Set path file
96 $path = $this->path.$pathInfo.'.jpeg';
97
98 //Without existing path
99 if (!is_dir($dir = dirname($path))) {
100 //Create filesystem object
101 $filesystem = new Filesystem();
102
103 try {
104 //Create path
105 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
106 $filesystem->mkdir($dir, 0775);
107 } catch (IOExceptionInterface $e) {
108 //Throw error
109 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
110 }
111 }
112
113 //With path file
114 if (
115 false &&
116 is_file($path) &&
117 ($stat = stat($path)) &&
118 $stat['mtime'] >= $updated
119 ) {
120 //Return image data
121 return [
122 'src' => $this->url.'/'.$stat['mtime'].$pathInfo.'.jpeg',
123 'alt' => $alt,
124 'height' => $height,
125 'width' => $width
126 ];
127 }
128
129 //Create image instance
130 $image = new \Imagick();
131
132 //Add new image
133 $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
134
135 //Create tile instance
136 $tile = new \Imagick();
137
138 //Init context
139 $ctx = stream_context_create(
140 [
141 'http' => [
142 #'header' => ['Referer: https://www.openstreetmap.org/'],
143 'max_redirects' => 5,
144 'timeout' => (int)ini_get('default_socket_timeout'),
145 #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36',
146 'user_agent' => (string)ini_get('user_agent')?:'rapsys_air/2.0.0',
147 ]
148 ]
149 );
150
151 //Get tile xy
152 $tileX = floor($centerX = $this->longitudeToX($longitude, $zoom));
153 $tileY = floor($centerY = $this->latitudeToY($latitude, $zoom));
154
155 //Calculate start xy
156 $startX = floor(($tileX * self::tz - $width) / self::tz);
157 $startY = floor(($tileY * self::tz - $height) / self::tz);
158
159 //Calculate end xy
160 $endX = ceil(($tileX * self::tz + $width) / self::tz);
161 $endY = ceil(($tileY * self::tz + $height) / self::tz);
162
163 for($x = $startX; $x <= $endX; $x++) {
164 for($y = $startY; $y <= $endY; $y++) {
165 //Set cache path
166 $cache = $this->cache.'/'.$zoom.'/'.$x.'/'.$y.'.png';
167
168 //Without cache path
169 if (!is_dir($dir = dirname($cache))) {
170 //Create filesystem object
171 $filesystem = new Filesystem();
172
173 try {
174 //Create path
175 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
176 $filesystem->mkdir($dir, 0775);
177 } catch (IOExceptionInterface $e) {
178 //Throw error
179 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
180 }
181 }
182
183 //Without cache image
184 if (!is_file($cache)) {
185 //Set tile url
186 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->servers[$this->server]);
187
188 //Store tile in cache
189 file_put_contents($cache, file_get_contents($tileUri, false, $ctx));
190 }
191
192 //Set dest x
193 $destX = intval(floor(($width / 2) - self::tz * ($centerX - $x)));
194
195 //Set dest y
196 $destY = intval(floor(($height / 2) - self::tz * ($centerY - $y)));
197
198 //Read tile from cache
199 $tile->readImage($cache);
200
201 //Compose image
202 $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
203
204 //Clear tile
205 $tile->clear();
206 }
207 }
208
209 //Add circle
210 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
211 $draw = new \ImagickDraw();
212
213 //Set text antialias
214 $draw->setTextAntialias(true);
215
216 //Set fill color
217 $draw->setFillColor('#cff');
218
219 //Set stroke color
220 $draw->setStrokeColor('#00c3f9');
221
222 //Set stroke width
223 $draw->setStrokeWidth(2);
224
225 //Draw circle
226 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 + 5, $height/2 + 5);
227 #$draw->circle($tileX/self::tz - 5, $tileY/self::tz - 5, $tileX/self::tz + 5, $tileY/self::tz + 5);
228
229 //Draw on image
230 $image->drawImage($draw);
231
232 //Strip image exif data and properties
233 $image->stripImage();
234
235 //Add latitude
236 //XXX: not supported by imagick :'(
237 $image->setImageProperty('exif:GPSLatitude', $this->latitudeToSexagesimal($latitude));
238
239 //Add longitude
240 //XXX: not supported by imagick :'(
241 $image->setImageProperty('exif:GPSLongitude', $this->longitudeToSexagesimal($longitude));
242
243 //Add description
244 //XXX: not supported by imagick :'(
245 $image->setImageProperty('exif:Description', $alt);
246
247 //Save image
248 if (!$image->writeImage($path)) {
249 //Throw error
250 throw new \Exception(sprintf('Unable to write image "%s"', $dest));
251 }
252
253 //Get dest stat
254 $stat = stat($path);
255
256 //Return image data
257 return [
258 'src' => $this->url.'/'.$stat['mtime'].$pathInfo.'.jpeg',
259 'alt' => $alt,
260 'height' => $height,
261 'width' => $width
262 ];
263 }
264
265 /**
266 * Convert longitude to tile x number
267 *
268 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
269 *
270 * @param float $longitude The longitude
271 * @param int $zoom The zoom
272 *
273 * @return float The tile x
274 */
275 public function longitudeToX(float $longitude, int $zoom): float {
276 return (($longitude + 180) / 360) * pow(2, $zoom);
277 }
278
279 /**
280 * Convert latitude to tile y number
281 *
282 * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
283 *
284 * @param $latitude The latitude
285 * @param $zoom The zoom
286 *
287 * @return float The tile y
288 */
289 public function latitudeToY(float $latitude, int $zoom): float {
290 return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom);
291 }
292
293 /**
294 * Convert tile x to longitude
295 *
296 * @param float $x The tile x
297 * @param int $zoom The zoom
298 *
299 * @return float The longitude
300 */
301 public function xToLongitude(float $x, int $zoom): float {
302 return $x / pow(2, $zoom) * 360.0 - 180.0;
303 }
304
305 /**
306 * Convert tile y to latitude
307 *
308 * @param float $y The tile y
309 * @param int $zoom The zoom
310 *
311 * @return float The latitude
312 */
313 public function yToLatitude(float $y, int $zoom): float {
314 return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom)))));
315 }
316
317 /**
318 * Convert decimal latitude to sexagesimal
319 *
320 * @param float $latitude The decimal latitude
321 *
322 * @return string The sexagesimal longitude
323 */
324 public function latitudeToSexagesimal(float $latitude): string {
325 //Set degree
326 $degree = $latitude % 60;
327
328 //Set minute
329 $minute = ($latitude - $degree) * 60 % 60;
330
331 //Set second
332 $second = ($latitude - $degree - $minute / 60) * 3600 % 3600;
333
334 //Return sexagesimal longitude
335 return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S');
336 }
337
338 /**
339 * Convert decimal longitude to sexagesimal
340 *
341 * @param float $longitude The decimal longitude
342 *
343 * @return string The sexagesimal longitude
344 */
345 public function longitudeToSexagesimal(float $longitude): string {
346 //Set degree
347 $degree = $longitude % 60;
348
349 //Set minute
350 $minute = ($longitude - $degree) * 60 % 60;
351
352 //Set second
353 $second = ($longitude - $degree - $minute / 60) * 3600 % 3600;
354
355 //Return sexagesimal longitude
356 return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');
357 }
358 }