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

    Доброго времени суток, %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
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 0

    Only users with full accounts can post comments. Log in, please.