allebb/distical

View on GitHub
src/Calculator.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
namespace Ballen\Distical;

use Ballen\Distical\Entities\LatLong;
use Ballen\Distical\Entities\Distance;

/**
 * Distical
 *
 * Distical is a simple distance calculator library for PHP 5.3+ which
 * amongst other things can calculate the distance between two or more lat/long
 * co-ordinates.
 *
 * @author Bobby Allen <ballen@bobbyallen.me>
 * @license http://opensource.org/licenses/MIT
 * @link https://github.com/allebb/distical
 * @link http://bobbyallen.me
 *
 */
class Calculator
{

    /**
     * Stores the earth's mean radius, used by the calculate() method.
     */
    const MEAN_EARTH_RADIUS = 6372.797;

    /**
     * LatLon points to measure between.
     * @var Entities\LatLong[]
     */
    private $points;

    /**
     * The constructor
     * @param Entities\LatLong $pointA Optional initial point.
     * @param Entities\LatLong $pointB Optional final point.
     */
    public function __construct($pointA = null, $pointB = null)
    {
        if (($pointA instanceof LatLong) && ($pointB instanceof LatLong)) {
            $this->between($pointA, $pointB);
        }
    }

    /**
     * Adds a new lat/long co-ordinate to measure.
     * @param LatLong $point The LatLong co-ordinate object.
     * @param string $key Optional co-ordinate key (name).
     * @return \Ballen\Distical\Calculator
     */
    public function addPoint(LatLong $point, $key = null)
    {
        if (is_null($key)) {
            $this->points[] = $point;
            return $this;
        }
        $this->points[$key] = $point;
        return $this;
    }

    /**
     * Remove a lat/long co-ordinate from the points collection.
     * @param int|string $key The name or ID of the point key.
     * @throws \InvalidArgumentException
     * @return \Ballen\Distical\Calculator
     */
    public function removePoint($key = null)
    {
        if (isset($this->points[$key])) {
            unset($this->points[$key]);
            return $this;
        }
        throw new \InvalidArgumentException('The point key does not exist.');
    }

    /**
     * Resets the points collection.
     * @return \Ballen\Distical\Calculator
     */
    public function resetPoints()
    {
        $this->points = [];
        return $this;        
    }

    /**
     * Helper method to get distance between two points.
     * @param LatLong $pointA Point A (eg. Departure point)
     * @param LatLong $pointB Point B (eg. Arrival point)
     * @return \Ballen\Distical\Calculator
     * @throws \RuntimeException
     */
    public function between(LatLong $pointA, LatLong $pointB)
    {
        if (!empty($this->points)) {
            throw new \RuntimeException('The between() method can only be called when it is the first set or co-ordinates.');
        }
        $this->addPoint($pointA);
        $this->addPoint($pointB);
        return $this;
    }

    /**
     * Calculates the distance between two lat/lng posistions.
     * @param LatLong $pointA Point A (eg. Departure point)
     * @param LatLong $pointB Point B (eg. Arrival point)
     * @return double
     */
    private function distanceBetweenPoints(LatLong $pointA, LatLong $pointB)
    {
        $pi180 = M_PI / 180;
        $latA = $pointA->lat() * $pi180;
        $lngA = $pointA->lng() * $pi180;
        $latB = $pointB->lat() * $pi180;
        $lngB = $pointB->lng() * $pi180;
        $dlat = $latB - $latA;
        $dlng = $lngB - $lngA;
        $calcA = sin($dlat / 2) * sin($dlat / 2) + cos($latA) * cos($latB) * sin($dlng / 2) * sin($dlng / 2);
        $calcB = 2 * atan2(sqrt($calcA), sqrt(1 - $calcA));
        return self::MEAN_EARTH_RADIUS * $calcB;
    }

    /**
     * Calculates the distance between each of the points.
     * @param bool $shouldResetPoints Clears points collection after calculating distance if true
     * @return double Distance in kilometres.
     */
    private function calculate(bool $shouldResetPoints = false)
    {
        if (count($this->points) < 2) {
            throw new \RuntimeException('There must be two or more points (co-ordinates) before a calculation can be performed.');
        }
        $total = 0;
        foreach ($this->points as $point) {
            if (isset($previous)) {
                $total += $this->distanceBetweenPoints($previous, $point);
            }
            $previous = $point;
        }

        if ($shouldResetPoints === true) {
            $this->resetPoints();
        }
        
        return $total;
    }

    /**
     * Returns the total distance between the two lat/lng points.
     * @param bool $shouldResetPoints Resets points collection after calculating distance if true
     * @return Distance
     */
    public function get(bool $shouldResetPoints = false)
    {
        return new Distance($this->calculate($shouldResetPoints));
    }
}