Pull to refresh

Внедряем работу с координатами в sonata-admin

PHP *Symfony *Doctrine ORM *
Доброго времени суток, %habrauser%!

Недавно встала задача, хранить в базе данные GPS с дальнейшей возможностью применения различных геометрических функций mysql. Управление координатами должно осуществляться из sonata-admin. Что из этого получилось можно прочитать под катом.

Первое, чем пришлось озаботиться это найти подходящее расширение для доктрины. Потратив некоторое время на гугление нашёл библиотеку doctrine2-mysql-spatial(в форках можно найти версии с включённым в состав pgsql). Всё в ней хорошо, но вот поддержки функций ST_(mysql 5.6 и выше) нет. Не долго думая сделал форк, добавил нужные функции и соорудил пакет для композера. На установке и настройке библиотеки подробно останавливаться не буду, всё банально и есть в файле установки.

Второе это настройка сонаты. Путём некоторых манипуляций с применением доли магии удалось реализовать вывод и сохранение данных для соответствующей сущности.
В классе сущности нам понадобится одно фэйковое поле для пре/пост обработки координат, геттер и сеттер. В админсокм классе перегрузить методы create и update. Итак, начнём.

Координаты будем хранить в поле типа polygon. Примерное описание сущности выглядит так:
показать код
#Bundle\Resources\config\doctrine\Location.orm.yml
Location:
    type: entity
    table: Location
    repositoryClass: LocationRepository
    id:
        id:
            type: integer
            nullable: false
            generator:
                strategy: AUTO
    fields:
        title:
            type: string
            length: 500
            nullable: false
        area:
            type: polygon
            nullable: false


После генерации открываем получившийся класс сущности и добавляем туда:
показать код
//Bundle\Entity\Location.php
private $__area;
	const COORDS_SEPARATOR = '; ';
	const POLYGON_SEPARATOR = '[]';
	
	public function get_Area($raw = false)
	{
		if ($raw)
		{
			return $this->__area;
		}

		$result = array();
		if (is_null($this->getArea()))
		{
			return $result;
		}
		$rings = $this->getArea()->getRings();
		$count_rings = count($rings) -1;

		foreach ($rings as $key => $line)
		{
			foreach ($line->getPoints() as $point)
			{
				$result[] = $point->getX() . self::COORDS_SEPARATOR . $point->getY();
			}

			if($count_rings != $key)
			{
				$result[] = Task::POLYGON_SEPARATOR;
			}
		}
		return $result;
	}

	public function set_Area($__area) {
		$this->__area = $__area;

        return $this;
	}


Далее открываем админский класс, который отвечает за вывод нужной сущности в сонате. Идём в метод configureFormFields(FormMapper $formMapper) и добавляем наше поле, которое будет отвечать за работу с координатами:
показать код
 $formMapper
....
		->add('__area','sonata_type_native_collection',[
			'options'=>['label'=>'Точка(GpsX;GpsY)'],
			'allow_add'=>true,
			'allow_delete'=>true,
			'label' => 'Полигон координат'
		])
....


Также перегружаем методы базового класса:
показать код
public function update($object)
	{
		$object = $this->prepareTask($object);
		return parent::update($object);
	}

	public function create($object)
	{
		$object = $this->prepareTask($object);
		return parent::create($object);
	}

	protected function prepareTask($object)
	{
		$res = array();
		if (count($object->get_Area(true)))
		{
			$flb = $this->getConfigurationPool()->getContainer()->get('session')->getFlashBag();

			$i = 0;
			foreach ($object->get_Area(true) as $point)
			{
				if((string) $point === Task::POLYGON_SEPARATOR)
				{
					if(count($res[$i]) > 2)
					{
						$this->fillLastPoint($res[$i]);
					}

					$i++;
					continue;
				}

				if (!preg_match('/[\d]+[.]{0,1}[\d]{0,}' . preg_quote(Task::COORDS_SEPARATOR) . '[\d]+[.]{0,1}[\d]{0,}/', (string) $point))
				{
					$flb->add(
						'danger', 'Не верный формат точки ' . (string) $point . '. Должны быть два целых или дробных числа(разделитель .) через ";пробел"'
					);
					return $object;
				}
				list($x, $y) = explode(Task::COORDS_SEPARATOR, $point);
				$res[$i][] = new Point($x, $y);
			}

			foreach ($res as $key => $ring)
			{
				if(count($ring) < 3)
				{
					$flb->add(
						'danger', "Полигон координат №" . $key + 1 . " не был изменен, т.к. для построение полигона необходимо минимум 3 точки, вы указали лишь " . count($ring) . '.'
					);
					unset($res[$key]);
					continue;
				}
			}
			
			if(count($res))
			{
				end($res);
				$key = key($res);
				$this->fillLastPoint($res[$key]);
				$object->setArea(new Polygon($res));
			}
		}

		return $object;
	}

	private function fillLastPoint(&$arr)
	{
		if ($arr[0]->getX() !== end($arr)->getX() || $arr[0]->getY() !== end($arr)->getY())
		{
			$arr[] = $arr[0];
		}
	}



Результат выглядит примерно так:
показать картинку


В репозиторном классе можно использовать следующий запрос:
показать код
		$query = $this->getEntityManager()
			->createQuery(
				'SELECT
					t
				FROM
					Bundle:Location t
				where
					ST_CONTAINS(t.area, Point(:x,:y)) = 1'
			)
			->setParameter('x', $x)
			->setParameter('y', $y)
		;
		$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'CrEOF\Spatial\ORM\Query\GeometryWalker');
		$result = $query->getResult();


Всем спасибо за внимание. Текст и код подготовлены совместно с хабраюзером scooty
Tags:
Hubs:
Total votes 8: ↑8 and ↓0 +8
Views 4K
Comments Leave a comment