Рисуем тайлы с данными для GoogleMap на PHP

в 13:08, , рубрики: Google Maps, php, метки: ,
Преамбула

В настоящее время очень популярно заниматься визуализацией каких-либо данных на картах. Да прочем и не только визуализацией, применений множество: игры, гео-сервисы, визуализация, статистика и многое-многое другое. С одной стороны, применение canvas это хорошо и современно, с другой же — количество объектов может превышать все мыслимые и немыслимые пределы, что ведет к уменьшению скорости работы пользователя с такими сервисами, тысячи полигонов на canvas «тормозят клиента», браузеры «жрут» память в огромных количествах и т.п. Это не говоря уже о том, что хоть и редко, но необходима поддержка «старых» браузеров, не поддерживающих canvas/html5.

Простой пример

Рисуем тайлы с данными для GoogleMap на PHP
Представьте что-то подобное этой картинке, уменьшите масштаб и увеличьте тем самым количество полигонов в «кадре» до 5 000. Офисный компьютер двух- или трех-летней давности может и умереть на отрисовке такой карты. Бороться с этим можно просто добавив оверлей слой на карту со своими тайлами.

Исходные данные

Предположим, что у нас имеется таблица в MySQL базе данных, в которой описаны некие блоки, представленные координатами вершин полигона. В нашем примере, на картинке выше, это отрисованные вручную контуры кварталов города Екатеринбурга. Для каждого полигона есть дистанция от центра города, ее мы будем использовать для раскрашивания блоков, как пример визуализации каких-то данных (вариантов масса: плотность населения, загрязнение окружающей среды и т.п.)

Код

Код я постарался задокументировать как можно подробнее, чтоб было понятно новичкам. Код далеко не идеальный, написан за час, и претендует только для иллюстрации того, как это можно заставить работать.

<?php
	// Подключаемся к базе данных, в нашем случае в файле mysql.php 
	// находится ООП-обертка для работы с mysql,
	// которая сразу возвращяет объект $db.
	require('mysql.php');
		
	// Путь к папке с тайлами
	$tiles_path = '/some/path/to/web/site/root/poly-tiles/';
	
	// На всякий случай проверим если ли папка, в случае отсутствия создадим ее.
	if (!file_exists($tiles_path)) {
		mkdir($tiles_path, 0755);
	}
	
	// Забъем в массив все необходимые уровни зума на карте, 
	// для которых мы будем генерить наши тайлы
	$zooms = array(12,13,14,15,16);
	
	// Запрос на получение данных из БД, наши полигоны хранятся в ввиде кооринат 
	// вершин в колонке 'vertices', разделенные символом '|'
	$query = 'SELECT * FROM map_blocks';
	// Выполним запрос и выгребем все данные, тут использовата простая ООП обертка для БД
	$result = $db->query($query);
	// Переберем все блоки
	while ($block = $db->fetch_array($result,1)) {
		// Сохраним все наши блоки в массив с id блоков в качестве ключей
		$blocks[$block['blockid']] = $block;
		// Выдернем все вершины для блока и разделим их в список
		$verticles = explode('|',$block['vertices']);
		// Для каждой вершины блока выполним действия:
		foreach ($verticles as $verticle) {
			// Разделим на широту и долготу
			$v_coord = explode(',',$verticle);
			// и заполним ими два массива, делаем это здесь,
			// чтоб не захламлять этим кодом функцию генерации
			// и не повторять это для каждого зума
			$lats[] = $v_coord[0];
			$long[] = $v_coord[1];
		}
	}
	// Теперь у нас есть массив с блоками и списки всех широт и долгот их вершин, работаем дальше
	
	// Для каждого уровня зума:
	foreach ($zooms as $zoom) {
		// Создадим папку по имени уровня зума (вынесено в функцию для удобства)
		make_zoom_dir ($zoom);
		// Сгенерим для этого уровня зума большую картинку (описание внутри функции)
		$bigimg = gen_map ($zoom,$blocks,$lats,$long);
		// Сохраним на всякий случай в папку нужного зума как отдельное изображение
		imagepng($bigimg,$tiles_path.$zoom.'/all.png');
		// Ну и наконец, порежем это большое изображение на отдельные тайлы (описание внутри функции)
		tile_map ($zoom,$bigimg,$blocks,$lats,$long);
	}
	
	// просто выход, делать больше нечего
	exit;
	
	/**
	*  gen_map
	* 
	*  Функция создает изображение с блоками для указанного зума.
	*
	*  @param integer $zoom Уровень зума
	*  @param array $blocks Массив с блоками
	*  @param array $lats Массив с широтами
	*  @param array $long Массив с долготами
	*  @return  gd_image $image Большое изображение с нашими блоками
	*/
	function gen_map ($zoom,$blocks,$lats,$long) {
		
		// Вычислим координаты в зону которых помещаются ВСЕ наши блоки
		$x['min'] = min($long);
		$y['min'] = max($lats);
		$x['max'] = max($long);
		$y['max'] = min($lats);
		
		// Получим номера тайлов для этих границ (getTile вернет x & y)
		$tiles['tl'] = getTile ($zoom,$y['min'],$x['min']);
		$tiles['rb'] = getTile ($zoom,$y['max'],$x['max']);
		
		// Получим размер нашего изображение в количестве тайлов +1 (чтобы точно все влезло)
		$picsize_blocks['x'] = $tiles['rb']['x'] - $tiles['tl']['x'] + 1;
		$picsize_blocks['y'] = $tiles['rb']['y'] - $tiles['tl']['y'] + 1;
		
		// Посчитаем размер нашего изображения в пикселах
		$pict_w = $picsize_blocks['x'] * 256;
		$pict_h = $picsize_blocks['y'] * 256;
		
		// Так как номера тайлов считаются от 180/85 градусов долготы и широты, 
		// то посчитаем сдвиг от начала координат в пикселах
		$world_shift['x'] = $tiles['tl']['x'] * 256;
		$world_shift['y'] = $tiles['tl']['y'] * 256;
		
		// Создаем GD-image нужного размера
		$image = imagecreatetruecolor($pict_w, $pict_h);
		// Делаем белый прозрачный цвет для подложки
		$bg = imagecolorallocatealpha($image, 255, 255, 255, 0);
		// Делаем его прозрачным в изображении
		imagecolortransparent($image, $bg);
		// Добавляем черный цвет
		$black = imagecolorallocate($image, 0, 0, 0);
		
		// Делаем набор цветов, будем красить блоки в зависимости расстояния от центра города
		$color1 = imagecolorallocatealpha($image, 255, 0, 0, 50);
		$color2 = imagecolorallocatealpha($image, 204, 0, 51, 50);
		$color3 = imagecolorallocatealpha($image, 153, 0, 102, 50);
		$color4 = imagecolorallocatealpha($image, 102, 0, 153, 50);
		$color5 = imagecolorallocatealpha($image, 51, 0, 204, 50);
		$color6 = imagecolorallocatealpha($image, 0, 0, 255, 50);
		
		// Красим изображение в белый продрачный цвет подложки
		imagefilledrectangle($image, 0, 0, $pict_w-1, $pict_h-1, $bg);
		
		// Обрабатываем блоки:
		foreach ($blocks as $block_id=>$block_data) {
			// Выдергиваем из элемента массива строку с вершинами блока
			$vertices = $block_data['vertices'];
			// Раздергиваем в список координат вершин
			$verticles_data = explode('|',$vertices);
			// Для каждого набора вершин:
			foreach ($verticles_data as $vert) {
				// Раздергиваем координаты вершины на широту и долготу
				$b_coord = explode(',',$vert);
				// Вычисляем координаты в пикселах для глобального начала координат, для текущего зума
				$vx = lonToX($b_coord[1], $zoom);
				$vy = latToY($b_coord[0], $zoom);
				// Заполняем массив вершин блоков в ключ 'verts' вычитая сдвиг
				$vershiny[$block_id]['verts'][] = $vx - $world_shift['x'];
				$vershiny[$block_id]['verts'][] = $vy - $world_shift['y'];
			}
			// Заполняем количество вершин блока в ключе 'vcount' 
			// (это может понадобиться если вершины всего две - то можно рисовать линию, 
			// сдесь мы это не использовали потому, что линий у нас нет). Кроме того,
			// для функции рисования полигона необходимо количество вершин.
			$vershiny[$block_id]['vcount'] = intval(count($vershiny[$block_id]['verts'])/2);
		}
		
		// И наконец то рисуем блоки полигонами закрашенными нужным цветом
		// На самом деле вы можете в зависимости от каких-то параметров блока менять цвета и т.п.
		foreach ($vershiny as $block_id=>$b_data) {
			// Неоптимальный выбор с пачкой if, сделано для быстроты.
			$block_dist = $blocks[$block_id]['distance'];
			if ( $block_dist >= 0 && $block_dist < 1000 ) {
				imagefilledpolygon($image, $b_data['verts'], $b_data['vcount'], $color1);
			}
			if ( $block_dist >= 1000 && $block_dist < 2000 ) {
				imagefilledpolygon($image, $b_data['verts'], $b_data['vcount'], $color2);
			}
			if ( $block_dist >= 2000 && $block_dist < 4000 ) {
				imagefilledpolygon($image, $b_data['verts'], $b_data['vcount'], $color3);
			}
			if ( $block_dist >= 4000 && $block_dist < 7000 ) {
				imagefilledpolygon($image, $b_data['verts'], $b_data['vcount'], $color4);
			}
			if ( $block_dist >= 7000 && $block_dist < 10000 ) {
				imagefilledpolygon($image, $b_data['verts'], $b_data['vcount'], $color5);
			}
			if ( $block_dist >= 10000 && $block_dist < 15000) {
				imagefilledpolygon($image, $b_data['verts'], $b_data['vcount'], $color6);
			}
			if ( $block_dist >= 15000) {
				imagefilledpolygon($image, $b_data['verts'], $b_data['vcount'], $black);
			}
		}
		
		// Возвращаем большую отрисованную картинку
		return $image;
	}
	
	/**
	*  tile_map
	* 
	*  Функция режет большую картинку на тайлы
	*
	*  @param integer $zoom Уровень зума
	*  @param gd_image $img Боьшая картинка, которую надо порезать
	*  @param array $blocks Массив с блоками
	*  @param array $lats Массив с широтами
	*  @param array $long Массив с долготами
	*/
	function tile_map ($zoom,$img,$blocks,$lats,$long) {
		global $tiles_path;
		
		// Вычислим координаты в зону которых помещаются ВСЕ наши блоки
		$x['min'] = min($long);
		$y['min'] = max($lats);
		$x['max'] = max($long);
		$y['max'] = min($lats);
		
		// Получим номера тайлов для этих границ (getTile вернет x & y)
		$tiles['tl'] = getTile ($zoom,$y['min'],$x['min']);
		$tiles['rb'] = getTile ($zoom,$y['max'],$x['max']);
		
		// Обычный вложенный цыкл по двум осям:
		// Бежим по горизонтали
		for($x = $tiles['tl']['x']; $x<=$tiles['rb']['x']; $x++) {
			// Бежим по вертикали
			for($y = $tiles['tl']['y']; $y <= $tiles['rb']['y']; $y++) {
				
				// Вычисляем позицию в тайлах
				$from_position_x = $x - $tiles['tl']['x'];
				$from_position_y = $y - $tiles['tl']['y'];
				
				// Вычисляем позицию в пикселах
				$from_x = $from_position_x * 256;
				$from_y = $from_position_y * 256;
				
				// Создаем GD-image тайл
				$tile = imagecreatetruecolor(256, 256);
				// Делаем белый прозрачный цвет для подложки
				$bg   = imagecolorallocatealpha($tile, 255, 255, 255, 0);
				// Копируем с наложением из большой картинки нужный кусок 
				// (для сохранения прозрачности)
				imagecopymerge($tile,$img,0,0,$from_x,$from_y,256,256,100);
				// Добавляем белый цвет
				$white = imagecolorclosest ($tile, 255,255,255);
				// Добавляем черный цвет
				$black = imagecolorclosest ($tile, 0,0,0);
				// Делаем цвет подложки прозрачным
				imagecolortransparent($tile, $bg);
				// Можно сделать так же белый и черный, приведено как пример
				imagecolortransparent($tile, $white);
				imagecolortransparent($tile, $black);
				
				// Создаем в папке нужного уровня зума папку для X тайлов
				make_zoom_x_dir ($zoom,$x);
				// Генерируем имя тайла 
				// оно будет что-то вроде: {$tiles_path}/{$zoom_dir}/{$x}/{$x}x{$t}.png
				$tile_name = make_tile_name ($zoom,$x,$y);
				// Это некоторая отладка, чтоб видеть прогресс генерации
				echo "Zoom: $zoom, $x x $y -> $tile_namen";
				// Сохраняем тайл в файл
				imagepng($tile,$tile_name);
				// И уничтожаем GD-image тайла, чтоб не память не жрать :-)
				imagedestroy($tile);
			}
		}
		// И уничтожаем GD-image большой картинки, чтоб не память тоже не жрать :-)
		imagedestroy($img);
	}
	
	
	/**
	*  make_tile_name
	* 
	*  Функция возвращает имя файла для тайла в нужном уровне зума 
	*  и порядковыми номерами тайла в x & y
	*
	*  @param integer $zoom Уровень зума
	*  @param integer $x Номер тайла X
	*  @param integer $y Номер тайла Y
	*  @return  string Полный путь к файлу тайла
	*/
	function make_tile_name ($zoom,$x,$y) {
		global $tiles_path;
		return $tiles_path.$zoom.'/'.$x.'/'.$y.'.png';
	}
	
	/**
	*  make_zoom_dir
	* 
	*  Функция создает папку для необходимого уровня зума
	*
	*  @param integer $zoom Уровень зума
	*/
	function make_zoom_dir ($zoom) {
		global $tiles_path;
		if (!file_exists($tiles_path.$zoom)) {
			mkdir($tiles_path.$zoom, 0755);
		}
	}
	
	/**
	*  make_zoom_x_dir
	* 
	*  Функция создает папку для необходимого уровня зума и X тайлов
	*
	*  @param integer $zoom Уровень зума
	*  @param integer $x Номер тайла X
	*/
	function make_zoom_x_dir ($zoom,$x) {
		global $tiles_path;
		if (!file_exists($tiles_path.$zoom.'/'.$x.'/')) {
			mkdir($tiles_path.$zoom.'/'.$x.'/', 0755);
		}
	}

	/**
	*  lonToX
	* 
	*  Returns longitude in pixels at a certain zoom level
	*
	*  @param float $lon longitude
	*  @param integer $zoom Уровень зума
	*/
	function lonToX($lon, $zoom) {
	    $offset = 256 << ($zoom-1);
	    $x = round($offset + ($offset * $lon / 180));
	    return $x;
	}
	
	/**
	*  lonToX
	* 
	*  Returns latitude in pixels at a certain zoom level
	* 
	*  @param float $lat latitude
	*  @param integer $zoom Уровень зума
	*/
	function latToY($lat, $zoom) {
	    $offset = 256 << ($zoom-1);
	    $y = round($offset - $offset/pi() * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2);
	    return $y;
	}
	
	/**
	*  getTile
	* 
	*  Returns tile x & y numbers at a certain zoom level, latitude & longitude
	* 
	*  @param integer $zoom Уровень зума
	*  @param float $lat latitude
	*  @param float $lon longitude
	*/
	function getTile ($zoom,$lat,$lon) {
		
		$tile['x'] = floor((($lon + 180) / 360) * pow(2, $zoom));
		$tile['y'] = floor((1 - log(tan(deg2rad($lat)) + 1 / cos(deg2rad($lat))) / pi()) /2 * pow(2, $zoom));
		
		return $tile;
	}
	
	/**
	*  tilenums2latlon
	* 
	*  Convert tile coordinates pair to latitude, longitude.
	* 
	*  @param int $_xtile X coordinate of the tile.
	*  @param int $_ytile Y coordinate of the tile.
	*  @param itn $_zoom Zoom level.
	*  @return Point Returns latitude and longitude as a {<hh user=link> Point} object.
	*/
	function tilenums2latlon($_xtile, $_ytile, $_zoom)
	{
		$factor = pow(2.0, floatval($_zoom));
		$coord['lon'] = ($_xtile * 360 / $factor) - 180.0;
		$lat = atan(sinh(M_PI * (1 - 2 * $_ytile / $factor)));
		
		$coord['lat'] = degrees($lat);
		
		return $coord;
	}

	/**
	*  Utility function. Transforms degree value to radian one.
	*
	*  @param float $_degrees Degree value.
	*  @return float Radian value.
	*/
	function radians($_degrees)
	{
		return M_PI * $_degrees / 180;
	}

	/**
	*  Utility function. Converts radians to degrees.
	*
	*  @param float $_radians Radian value.
	*  @return float Degree value.
	*/
	function degrees($_radians)
	{
		return $_radians * 180 / M_PI;
	}
	
?>
Как это работает

  1. Вычисляем границы зоны, в которую поместятся все наши данные
  2. Генерим большую картинку для каждого уровня зума
  3. Рисуем в ней наши данные
  4. Разрезаем ее на мелкие кусочки 256х256
  5. Раскладываем их по папочкам

Далее все просто, создаем в Google Map API дополнительный Map Type

var BWPolygonsOptions = {
	getTileUrl: function(ll, z) {
		var X = ll.x % (1 << z);  // wrap
		return "http://some.host.com/poly-tiles/" + z + "/" + X + "/" + ll.y + ".png";
	},
	tileSize: new google.maps.Size(256, 256),
	isPng: true,
	minZoom: 12,
	maxZoom: 16,
	name: "BWPolygons",
	alt: "BWPolygons"
	}; 
var BWPolygonsMapType = new google.maps.ImageMapType(BWPolygonsOptions);

И внедряем как оверлей слой

	map.overlayMapTypes.insertAt(0, BWPolygonsMapType);

Хорошо разбирающиеся в Google Map API могут обвешивать этот слой по желанию выключателями и другими украшениями, мы же остановимся на этом.

Демо

Результат работы здесь.

Скорость

Для примера использовалось 2 873 блоков, находящихся в пределах границ города Екатеринбурга.
Количество тайлов для зумов с 12 по 16 — 5 118.
Время работы данного скрипта 1 минута 11 секунд.
Генерация производилась на сервере HP Proliant DL 360 G5 (1 Intel Xeon E5420 @ 2.50GHz, 4 Gb RAM)

С блогом определиться затруднился, выставил в PHP, желающие перенести в более подходящий — велком.

Автор: NickyX3

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js