Covivo/mobicoop

View on GitHub
api/src/Geography/Service/ZoneManager.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/**
 * Copyright (c) 2018, MOBICOOP. All rights reserved.
 * This project is dual licensed under AGPL and proprietary licence.
 ***************************
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as
 *    published by the Free Software Foundation, either version 3 of the
 *    License, or (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <gnu.org/licenses>.
 ***************************
 *    Licence MOBICOOP described in the file
 *    LICENSE
 **************************/

namespace App\Geography\Service;

use App\Geography\Entity\Address;
use App\Geography\Entity\Direction;
use App\Geography\Entity\Zone;

/**
 * Zone management service.
 *
 * This service gets the zone and nearby zones for routes (list of addresses) and points (address).
 * The whole world map can be considered as a grid, the precision of the grid can be parametered (1° longitude at the equator represents 111km, 1° latitude is always 111km)
 *
 * @author Sylvain Briat <sylvain.briat@covivo.eu>
 */
class ZoneManager
{
    // zones precisions (in degrees) to generate when adding a direction
    public const THINNESSES = [
        1,
        0.5,
        0.25,
        0.125,
        //0.0625
    ];

    /**
     * Create the zones for a direction.
     *
     * @param Direction $direction The direction.
     * @return Direction The direction with the associated zones.
     */
    public function createZonesForDirection(Direction $direction)
    {
        $zones = [];
        foreach (self::THINNESSES as $thinness) {
            // $zones[$thinness] would be simpler and better... but we can't use a float as a key with php (transformed to string)
            // so we use an inner value for thinness
            $zones[] = [
                'thinness' => $thinness,
                'crossed' => $this->getZonesForAddresses($direction->getPoints(), $thinness, 0)
            ];
        }

        foreach ($zones as $crossed) {
            foreach ($crossed['crossed'] as $zoneCrossed) {
                $zone = new Zone();
                $zone->setZoneid($zoneCrossed);
                $zone->setThinness($crossed['thinness']);
                $direction->addZone($zone);
            }
        }
        return $direction;
    }

    /**
     * Get the zones for a list of addresses.
     *
     * @param array $addresses[]    The array of addresses
     * @param float $precision      The precision of the grid in degrees
     * @param int $deep             The deepness of near zones to retrieve (0 = only the zone, not the near zones)
     * @return array                The zones concerned by the addresses
     * @return array|NULL
     */
    public function getZonesForAddresses(array $addresses, float $precision, int $deep=0): ?array
    {
        $zones = [];
        foreach ($addresses as $address) {
            $zones = array_merge($zones, $this->getZonesForAddress($address, $precision, $deep));
        }
        return array_unique($zones, SORT_REGULAR);
    }
    
    /**
     * Get the zones for an address.
     *
     * @param Address $address  The address
     * @param float $precision  The precision of the grid in degrees
     * @param int $deep         The deepness of near zones to retrieve (0 = only the current zone, not the near zones)
     * @return array|NULL       The zones concerned by the address
     */
    public function getZonesForAddress(Address $address, float $precision, int $deep = 0): ?array
    {
        $zones[] = $this->getZoneForAddress($address, $precision);
        if ($deep == 0) {
            return $zones;
        }
            
        $nearbyZones = $this->getNear($address, $precision, $deep);
        $zones = array_unique(array_merge($zones, $nearbyZones), SORT_REGULAR);
        sort($zones);
        return $zones;
    }

    private function getZoneForAddress(Address $address, float $precision): int
    {
        // we transform longitude and latitude to keep calculation simple :
        // - longitude > 0 => no change
        // - longitude < 0 => we add 360 (-1 will become 359, -179 will become 181...)
        // - we add 90 to the latitude to keep positive values : new latitude now goes from 0 to 180 instead of -90 to 90
        $longitude = ((float)$address->getLongitude()<0) ? 360+(float)$address->getLongitude() : (float)$address->getLongitude();
        $latitude = 90+(float)$address->getLatitude();

        // we search the col and row for the gps point
        $col = (int)($longitude*(1/$precision)+1);
        $row = (int)($latitude*(1/$precision));
        
        // we search the zone
        $zone = $col+360*$row;

        return $zone;
    }

    /**
     * Get near zones of an address.
     *
     * @param Address $address  The address
     * @param float $precision  The precision of the grid in degrees
     * @param int $deep         The deepness of the search (1 = direct nearby zones, 2 = nearby zone and their nearby zones, etc...)
     * @return array|NULL       The list of nearby zones.
     */
    public function getNear(Address $address, float $precision, int $deep): ?array
    {
        if ($deep<0) {
            return null;
        }
        
        // we search for nearby zones
        // we nearby zones of the XX zone are defined like this :
        // X1 X2 X3
        // X4 XX X5
        // X6 X7 X8

        // we search the GPS point of each X? zone
        $x1 = new Address();
        $x2 = new Address();
        $x3 = new Address();
        $x4 = new Address();
        $x5 = new Address();
        $x6 = new Address();
        $x7 = new Address();
        $x8 = new Address();
        $x1->setLatitude((string)((float)($address->getLatitude())+$precision));
        $x1->setLongitude((string)((float)($address->getLongitude())-$precision));
        $x2->setLatitude((string)((float)($address->getLatitude())+$precision));
        $x2->setLongitude($address->getLongitude());
        $x3->setLatitude((string)((float)($address->getLatitude())+$precision));
        $x3->setLongitude((string)((float)($address->getLongitude())+$precision));
        $x4->setLatitude($address->getLatitude());
        $x4->setLongitude((string)((float)($address->getLongitude())-$precision));
        $x5->setLatitude($address->getLatitude());
        $x5->setLongitude((string)((float)($address->getLongitude())+$precision));
        $x6->setLatitude((string)((float)($address->getLatitude())-$precision));
        $x6->setLongitude((string)((float)($address->getLongitude())-$precision));
        $x7->setLatitude((string)((float)($address->getLatitude())-$precision));
        $x7->setLongitude($address->getLongitude());
        $x8->setLatitude((string)((float)($address->getLatitude())-$precision));
        $x8->setLongitude((string)((float)($address->getLongitude())+$precision));

        $nearX1 = $this->getZonesForAddress($x1, $precision, $deep-1);
        $nearX2 = $this->getZonesForAddress($x2, $precision, $deep-1);
        $nearX3 = $this->getZonesForAddress($x3, $precision, $deep-1);
        $nearX4 = $this->getZonesForAddress($x4, $precision, $deep-1);
        $nearX5 = $this->getZonesForAddress($x5, $precision, $deep-1);
        $nearX6 = $this->getZonesForAddress($x6, $precision, $deep-1);
        $nearX7 = $this->getZonesForAddress($x7, $precision, $deep-1);
        $nearX8 = $this->getZonesForAddress($x8, $precision, $deep-1);
        
        return array_merge($nearX1, $nearX2, $nearX3, $nearX4, $nearX5, $nearX6, $nearX7, $nearX8);
    }
}