Implementing work with coordinates in sonata-admin

    Good day,% habrauser%!

    Recently, the task arose to store GPS data in the database with the further possibility of using various mysql geometric functions. Coordinates should be managed from sonata-admin. What came of this can be read under the cut.

    The first thing I had to worry about was finding a suitable extension for the doctrine. Having spent some time googling, I found the doctrine2-mysql-spatial library (in the forks you can find versions with pgsql included). Everything is good in it, but there is no support for the ST_ functions (mysql 5.6 and higher). Without hesitation, he made a fork , added the necessary functions and built a package for the composer. I won’t dwell on installing and configuring the library, everything is corny and is in the fileinstallation .

    The second is the tuning of the sonata. By some manipulations using a fraction of magic, it was possible to realize the output and storage of data for the corresponding entity.
    In the entity class, we need one fake field for pre / post processing of coordinates, getter and setter. In the admin class, overload the create and update methods. So, let's begin.

    The coordinates will be stored in a field of type polygon. A sample description of the entity looks like this:
    show code
    #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
    


    After generation, open the resulting entity class and add there:
    show code
    //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;
    	}
    


    Next, open the admin class, which is responsible for the conclusion of the desired entity in the sonata. We go to the configureFormFields method (FormMapper $ formMapper) and add our field, which will be responsible for working with coordinates:
    show code
     $formMapper
    ....
    		->add('__area','sonata_type_native_collection',[
    			'options'=>['label'=>'Точка(GpsX;GpsY)'],
    			'allow_add'=>true,
    			'allow_delete'=>true,
    			'label' => 'Полигон координат'
    		])
    ....
    


    We also overload the base class methods:
    show code
    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];
    		}
    	}
    



    The result looks something like this:
    show picture


    In the repository class, you can use the following query:
    show code
    		$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();
    


    Thank you all for your attention. Text and code prepared in conjunction with scooty

    Also popular now: