Travel time

Provides travel distance and time to your favorite destination, with traffic conditions. A Google Developer Account is required with access to "Google Maps Distance Matrix API" and "Google Maps Geolocation API"

Image preview of Travel time plugin.

travel-time.5m.php

Edit
Open on GitHub
#!/usr/bin/env php
<?php
/**
 * Provides travel distance and time for your favorite destination, with traffic conditions.
 *
 * A Google Developer Account is required with access to "Google Maps Distance Matrix API" and "Google Maps Geolocation API"
 *
 * How does it work:
 * - perform a Wifi Access Point scan using airport utility command
 * - Get current position (lat,lng) with Geolocation API using collected AP data
 * - Get distance, time and traffic delay using the Distance Matrix API
 *
 * @link https://console.developers.google.com/apis/enabled
 * @link https://developers.google.com/maps/documentation/geolocation/
 * @link https://developers.google.com/maps/documentation/distance-matrix/
 *
 * <xbar.title>Travel time</xbar.title>
 * <xbar.version>1.1</xbar.version>
 * <xbar.author>Yann Milin</xbar.author>
 * <xbar.author.github>ymilin</xbar.author.github>
 * <xbar.desc>Provides travel distance and time to your favorite destination, with traffic conditions. A Google Developer Account is required with access to "Google Maps Distance Matrix API" and "Google Maps Geolocation API"</xbar.desc>
 * <xbar.image>http://i.imgur.com/Ui6I4YH.png</xbar.image>
 * <xbar.dependencies>php >= 5.4.0</xbar.dependencies>
 */

namespace BitbarPlugins\Travel;

// Required : Your Google Developer Project's API Key
const API_KEY = "YOUR_API_KEY";
const DESTINATION = "Tour Eiffel";

/*
 * Use airport to perform a scan of nearby wifi access points, provides better geolocation accuracy but makes script
 * slower by a few seconds.
 * false: fallback to current IP, resulting in less accurate geolocation
 */
const SCAN_NEARBY_WIFI_ACCESS_POINTS = true;
const LANGUAGE = "en"; // list of supported languages https://developers.google.com/maps/faq#languagesupport
const UNITS = "metric"; // metric, imperial

const AIRPORT_PATH = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport";
const DEBUG = false; // optimize output for console instead of bitbar

/**
 * Class TravelTimePlugin
 *
 * Renders plugin output
 *
 * @package BitbarPlugins\Travel
 */
class TravelTimePlugin
{
    const GOOGLE_MAP_URL                     = "https://www.google.com/maps";
    const GOOGLE_MAP_URL_SOURCE_ADDRESS      = 'saddr';
    const GOOGLE_MAP_URL_DESTINATION_ADDRESS = 'daddr';

    const ICON_PIN     = "📍";
    const ICON_FLAG    = "🏁";
    const ICON_CIRCLE  = "⭕️";
    const ICON_WARNING = "⚠️";
    const COLOR_BLACK  = "#000000";
    const COLOR_ORANGE = "#FF9900";
    const COLOR_RED    = "#FF0000";

    const ONE_MINUTES_IN_SECONDS     = 60;
    const FIVE_MINUTES_IN_SECONDS    = 300;
    const FIFTEEN_MINUTES_IN_SECONDS = 900;

    private $durationInTraffic = [];
    private $duration = [];
    private $distance = [];
    private $delay = null;
    private $originAddress = null;
    private $destinationAddress = null;
    private $latitude = null;
    private $longitude = null;
    private $accuracy = null;
    private $googleMapsLink = null;
    private $accessPointCount = 0;
    private $errors = [];
    private $warnings = [];

    private $distanceMatrixErrorMessages = [
        DistanceMatrixResponse::STATUS_CODE_INVALID_REQUEST => "Invalid Request.",
        DistanceMatrixResponse::STATUS_CODE_MAX_ELEMENTS_EXCEEDED => "Origins and destinations per-query limit exceeded.",
        DistanceMatrixResponse::STATUS_CODE_OVER_QUERY_LIMIT => "Too many requests.",
        DistanceMatrixResponse::STATUS_CODE_REQUEST_DENIED => "Service denied",
        DistanceMatrixResponse::STATUS_CODE_UNKNOWN_ERROR => "Server error, try again.",
    ];

    private $distanceMatrixElementErrorMessages = [
        DistanceMatrixResponseElement::STATUS_CODE_NOT_FOUND => "Destination could not be geocoded.",
        DistanceMatrixResponseElement::STATUS_CODE_ZERO_RESULTS => "No route could be found.",
    ];

    public function __construct()
    {
        // Scan and create WifiAccessPoints
        $wifiAccessPoints = [];
        if (SCAN_NEARBY_WIFI_ACCESS_POINTS === true) {
            try {
                $wifiAccessPoints = WifiAccessPoints::fromAccessPointScannerResponse(AccessPointScanner::scan());
                $this->accessPointCount = count($wifiAccessPoints);
            } catch (AccessPointScannerException $apse) {
                $this->warnings[] = $apse->getMessage();
            }

            if (count($wifiAccessPoints) < 2) {
                $this->warnings[] = "Less than two Wifi Access Points in proximity.";
            }
        }

        // Geolocation
        try {
            $geolocationApi = new GeolocationAPI();
            $geolocationResponse = $geolocationApi->call(GeolocationRequest::fromArrayDefinition([
                GeolocationRequest::DEFINITION_WIFI_ACCESS_POINTS => $wifiAccessPoints
            ]));

            $this->latitude = $geolocationResponse->getLatitude();
            $this->longitude = $geolocationResponse->getLongitude();
            $this->accuracy = $geolocationResponse->getAccuracy();
        } catch (GeolocationResponseException $gre) {
            $this->errors[] = "Geolocation API Error status {$gre->getStatusCode()}: {$gre->getMessage()}";
        }

        if ($this->latitude && $this->longitude) {
            // distanceMatrix
            try {
                $distanceMatrixAPI = new DistanceMatrixAPI();
                $distanceMatrixResponse = $distanceMatrixAPI->call(DistanceMatrixRequest::fromArrayDefinition([
                    DistanceMatrixRequest::DEFINITION_ORIGINS => $this->latitude . ',' . $this->longitude,
                    DistanceMatrixRequest::DEFINITION_DESTINATIONS => DESTINATION,
                    DistanceMatrixRequest::DEFINITION_KEY => API_KEY,
                    DistanceMatrixRequest::DEFINITION_LANGUAGE => LANGUAGE,
                    DistanceMatrixRequest::DEFINITION_UNITS => UNITS,
                ]));

                $this->durationInTraffic = $distanceMatrixResponse->getRows()->getDurationInTraffic();
                $this->duration = $distanceMatrixResponse->getRows()->getDuration();
                $this->distance = $distanceMatrixResponse->getRows()->getDistance();
                $this->originAddress = $distanceMatrixResponse->getOriginAddresses();
                $this->destinationAddress = $distanceMatrixResponse->getDestinationAddresses();

                $this->computeDelay();
                $this->computeGoogleMapsLink();
            } catch (DistanceMatrixResponseException $e) {
                $this->errors[] = "Distance Matrix API Error: " . $this->distanceMatrixErrorMessages[$e->getMessage()];
            } catch (DistanceMatrixResponseElementException $e) {
                $this->errors[] = "Distance Matrix API Error: " . $this->distanceMatrixElementErrorMessages[$e->getMessage()];
            }
        }

    }

    private function computeDelay()
    {
        if (is_array($this->duration)
            && is_array($this->durationInTraffic)
            && array_key_exists('value', $this->duration)
            && array_key_exists('value', $this->durationInTraffic)
        ) {
            $delay = $this->durationInTraffic['value'] - $this->duration['value'];
            $this->delay = $delay > 0 ? $delay : null;
        } else {
            $this->delay = null;
        }
    }

    private function computeGoogleMapsLink()
    {
        if (isset($this->originAddress)
            && is_string($this->originAddress)
            && isset($this->destinationAddress)
            && is_string($this->destinationAddress)
        ) {
            $this->googleMapsLink = self::GOOGLE_MAP_URL . "?" . http_build_query([
                    self::GOOGLE_MAP_URL_SOURCE_ADDRESS => $this->originAddress,
                    self::GOOGLE_MAP_URL_DESTINATION_ADDRESS => $this->destinationAddress,
                ]);
        } else {
            $this->googleMapsLink = null;
        }
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->render();
    }

    private function render()
    {
        return DEBUG ? $this->renderConsole() : $this->renderBitbar();
    }

    /**
     * render with Bitbar metadata
     * @return string
     */
    private function renderBitbar()
    {
        $return = "";

        if ($this->errors) {
            $return .= self::ICON_WARNING . " Error|color=" . self::COLOR_RED . "\n";
            $return .= "---\n";
            $return .= implode("|color=" . self::COLOR_RED . "\n", $this->errors) . "\n";
            $return .= "---\n";
            $return .= "Refresh | refresh=true \n";
            return $return;
        }

        $durationColor = self::COLOR_BLACK;
        if ($this->delay > self::FIVE_MINUTES_IN_SECONDS) {
            $durationColor = self::COLOR_ORANGE;
        }
        if ($this->delay > self::FIFTEEN_MINUTES_IN_SECONDS) {
            $durationColor = self::COLOR_RED;
        }

        $return .= self::ICON_PIN . " {$this->durationInTraffic['text']}|color=$durationColor\n";
        $return .= "---\n";

        if ($this->warnings) {
            foreach ($this->warnings as $warning) {
                $return .= self::ICON_WARNING . " $warning\n";
            }
            $return .= "---\n";
        }

        $return .= "{$this->durationInTraffic['text']} ({$this->distance['text']})|color=$durationColor\n";
        if ($this->delay > self::ONE_MINUTES_IN_SECONDS) {
            $return .= "{$this->duration['text']} without traffic\n";
        }
        $return .= "---\n";

        $return .= "Directions\n";
        $return .= self::ICON_CIRCLE . " {$this->originAddress} | color=" . self::COLOR_BLACK . "\n";
        $return .= self::ICON_FLAG . " {$this->destinationAddress} | color=" . self::COLOR_BLACK . "\n";
        $return .= "---\n";

        $return .= "Geolocation\n";
        $return .= "Latitude: {$this->latitude} | color=" . self::COLOR_BLACK . "\n";
        $return .= "Longitude: {$this->longitude} | color=" . self::COLOR_BLACK . "\n";
        $return .= "Accuracy: {$this->accuracy}m | color=" . self::COLOR_BLACK . "\n";
        $return .= "---\n";

        if ($this->googleMapsLink) {
            $return .= "View on Google maps|href={$this->googleMapsLink}\n";
        }
        $return .= "Refresh | refresh=true \n";

        return $return;
    }

    /**
     * render for console output
     * @return string
     */
    private function renderConsole()
    {
        $return = "";
        if ($this->errors) {
            return implode("\n", $this->errors);
        }

        if ($this->warnings) {
            $return .= "---\nWarning(s):\n";
            $return .= implode("\n", $this->warnings);
            $return .= "\n---\n\n";
        }

        $return .= "Duration: {$this->durationInTraffic['text']}\n";
        $return .= "Distance: {$this->distance['text']}\n";
        $return .= $this->delay ? "Traffic Delay: {$this->delay}s" : "No Traffic Delay";
        $return .= "\n\n";

        $return .= "Directions:\n";
        $return .= "\tFrom: {$this->originAddress}\n";
        $return .= "\tTo: {$this->destinationAddress}\n\n";

        $return .= "Geolocation:\n";
        $return .= "\tLatitude: {$this->latitude}\n";
        $return .= "\tLongitude: {$this->longitude}\n";
        $return .= "\tAccuracy: {$this->accuracy}\n\n";

        if ($this->googleMapsLink) {
            $return .= "Google Maps URL: {$this->googleMapsLink}\n\n";
        };

        return $return;
    }

}

/**
 * Class AccessPointScanner
 * @package BitbarPlugins\Travel
 */
final class AccessPointScanner
{
    /**
     * use `airport` utility command with --scan option : Perform a wireless broadcast scan.
     *
     * @return string raw command output
     * @throws AccessPointScannerException
     */
    public static function scan()
    {
        if (!self::airportProgramFound()) {
            throw new AccessPointScannerException("Airport utility command not found. Please check path.");
        }

        return shell_exec(AIRPORT_PATH . " -s");
    }

    private static function airportProgramFound()
    {
        if (!(is_file(AIRPORT_PATH)
            && file_exists(AIRPORT_PATH)
            && is_executable(AIRPORT_PATH))
        ) {
            return false;
        }

        return true;
    }
}

final class AccessPointScannerException extends \Exception
{

}

final class WifiAccessPoints implements \Countable, \JsonSerializable
{

    const PATTERN_AIRPORT_LINE_SCAN = '/^\s*(.+?)\s((?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2}))\s(.+?)\s+(\d+).+$/';

    /**
     * @var WifiAccessPoint[]
     */
    private $accessPoints = [];

    /**
     * {@inheritDoc}
     */
    public function count()
    {
        return count($this->accessPoints);
    }

    /**
     * {@inheritDoc}
     */
    function jsonSerialize()
    {
        return $this->accessPoints;
    }

    /**
     * @param string $response
     *
     * @return self
     */
    public static function fromAccessPointScannerResponse($response)
    {
        $instance = new self();

        foreach (explode("\n", $response) as $airportAccessPoint) {
            if (preg_match(self::PATTERN_AIRPORT_LINE_SCAN, $airportAccessPoint, $matches)) {
                $instance->accessPoints[] = WifiAccessPoint::fromArrayDefinition([
                    WifiAccessPoint::DEFINITION_MAC_ADDRESS => $matches[2],
                    WifiAccessPoint::DEFINITION_SIGNAL_TO_NOISE_RATION => intval($matches[3]),
                    WifiAccessPoint::DEFINITION_CHANNEL => intval($matches[4]),
                ]);
            }
        }

        return $instance;
    }

}

final class WifiAccessPoint implements \JsonSerializable
{
    const DEFINITION_MAC_ADDRESS = "macAddress";
    const DEFINITION_SIGNAL_STRENGTH = "signalStrength";
    const DEFINITION_AGE = "age";
    const DEFINITION_CHANNEL = "channel";
    const DEFINITION_SIGNAL_TO_NOISE_RATION = "signalToNoiseRatio";

    const PATTERN_MAC_ADDRESS = '/^(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})$/';

    /**
     * @var string
     */
    private $macAddress = null;

    /**
     * @var int
     */
    private $signalStrength = null;

    /**
     * @var int
     */
    private $age = null;

    /**
     * @var int
     */
    private $channel = null;

    /**
     * @var int
     */
    private $signalToNoiseRatio = null;

    /**
     * {@inheritDoc}
     */
    function jsonSerialize()
    {
        $return = [self::DEFINITION_MAC_ADDRESS => $this->macAddress];

        if ($this->signalStrength !== null) {
            $return[self::DEFINITION_SIGNAL_STRENGTH] = $this->signalStrength;
        }

        if ($this->age !== null) {
            $return[self::DEFINITION_AGE] = $this->age;
        }

        if ($this->channel !== null) {
            $return[self::DEFINITION_CHANNEL] = $this->channel;
        }

        if ($this->signalToNoiseRatio !== null) {
            $return[self::DEFINITION_SIGNAL_TO_NOISE_RATION] = $this->signalToNoiseRatio;
        }

        return $return;
    }

    /**
     * @param array $definition
     * @return WifiAccessPoint
     */
    public static function fromArrayDefinition(array $definition)
    {
        $instance = new self();

        $instance->macAddress = self::getMacAddressFromDefinition($definition);
        $instance->signalStrength = self::getSignalStrengthFromDefinition($definition);
        $instance->age = self::getAgeFromDefinition($definition);
        $instance->channel = self::getChannelFromDefinition($definition);
        $instance->signalToNoiseRatio = self::getSignalToNoiseRatioFromDefinition($definition);

        return $instance;
    }

    private static function getMacAddressFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_MAC_ADDRESS])
            && is_string($definition[self::DEFINITION_MAC_ADDRESS])
            && preg_match(self::PATTERN_MAC_ADDRESS, $definition[self::DEFINITION_MAC_ADDRESS]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_MAC_ADDRESS];
    }

    private static function getSignalStrengthFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_SIGNAL_STRENGTH])
            && is_int($definition[self::DEFINITION_SIGNAL_STRENGTH]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_SIGNAL_STRENGTH];
    }

    private static function getAgeFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_AGE])
            && is_int($definition[self::DEFINITION_AGE]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_AGE];
    }

    private static function getChannelFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_CHANNEL])
            && is_int($definition[self::DEFINITION_CHANNEL]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_CHANNEL];
    }

    private static function getSignalToNoiseRatioFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_SIGNAL_TO_NOISE_RATION])
            && is_int($definition[self::DEFINITION_SIGNAL_TO_NOISE_RATION]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_SIGNAL_TO_NOISE_RATION];
    }


}

/**
 * Class GeolocationAPI
 *
 * @link https://developers.google.com/maps/documentation/geolocation/
 * @package BitbarPlugins\Travel
 */
final class GeolocationAPI
{
    const GEOLOCATION_URL = "https://www.googleapis.com/geolocation/v1/geolocate";
    const METHOD = "POST";

    /**
     * @var resource curl handler
     */
    private $ch;

    private $headers = [
        "content-type: application/json",
        "Accept: application/json",
        "Cache-Control: no-cache",
        "Pragma: no-cache",
    ];

    public function __construct()
    {
        $this->initCurl();
    }

    private function initCurl()
    {
        $this->ch = curl_init();

        curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->headers);
        curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, self::METHOD);
        curl_setopt($this->ch, CURLOPT_POSTFIELDS, $this->headers);
    }

    /**
     * Sends a Geolocation request
     *
     * @param GeolocationRequest $request
     * @return GeolocationResponse
     */
    public function call(GeolocationRequest $request)
    {
        $url = self::GEOLOCATION_URL . '?' . http_build_query(['key' => API_KEY]);

        curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($request));
        curl_setopt($this->ch, CURLOPT_URL, $url);

        $response = curl_exec($this->ch);

        return GeolocationResponse::fromApiResponse($response);
    }

}

/**
 * Class GeolocationRequest
 *
 * @todo make cellTower definition as an object and sanitize data
 * @link https://developers.google.com/maps/documentation/geolocation/intro#body
 * @package BitbarPlugins\Travel
 */
final class GeolocationRequest implements \JsonSerializable
{
    const DEFINITION_MMC = "homeMobileCountryCode";
    const DEFINITION_MNC = "homeMobileNetworkCode";
    const DEFINITION_RADIO_TYPE = "radioType";
    const DEFINITION_CARRIER = "carrier";
    const DEFINITION_CONSIDER_IP = "considerIp";
    const DEFINITION_CELL_TOWERS = "cellTowers";
    const DEFINITION_WIFI_ACCESS_POINTS = "wifiAccessPoints";

    /**
     * The mobile country code (MCC) for the device's home network.
     * @var int
     */
    private $homeMobileCountryCode = null;

    /**
     * The mobile network code (MNC) for the device's home network.
     * @var int
     */
    private $homeMobileNetworkCode = null;

    /**
     * The mobile radio type. Supported values are lte, gsm, cdma, and wcdma. While this field is optional,
     * it should be included if a value is available, for more accurate results.
     * @var string
     */
    private $radioType = null;

    /**
     * The carrier name.
     * @var string
     */
    private $carrier = null;

    /**
     * Specifies whether to fall back to IP geolocation if wifi and cell tower signals are not available.
     * Note that the IP address in the request header may not be the IP of the device. Defaults to true.
     * Set considerIp to false to disable fall back.
     * @var boolean
     */
    private $considerIp = true;

    /**
     * An array of cell tower objects.
     *
     * @var array
     */
    private $cellTowers;

    /**
     * An array of WiFi access point objects.
     * @var WifiAccessPoints
     */
    private $wifiAccessPoints;

    function jsonSerialize()
    {
        $return = [];

        if ($this->homeMobileCountryCode !== null) {
            $return[self::DEFINITION_MMC] = $this->homeMobileCountryCode;
        }

        if ($this->homeMobileNetworkCode !== null) {
            $return[self::DEFINITION_MNC] = $this->homeMobileNetworkCode;
        }

        if (in_array($this->radioType, ['lte', 'gsm', 'cdma', 'wcdma'], true)) {
            $return[self::DEFINITION_RADIO_TYPE] = $this->radioType;
        }

        if ($this->carrier) {
            $return[self::DEFINITION_CARRIER] = $this->carrier;
        }

        if ($this->considerIp === false) {
            $return[self::DEFINITION_CONSIDER_IP] = 'false';
        }

        if (count($this->cellTowers)) {
            $return[self::DEFINITION_CELL_TOWERS] = $this->cellTowers;
        }

        if (count($this->wifiAccessPoints)) {
            $return[self::DEFINITION_WIFI_ACCESS_POINTS] = $this->wifiAccessPoints;
        }

        return $return;
    }

    /**
     * @param array $definition
     *
     * @return self
     */
    public static function fromArrayDefinition(array $definition)
    {
        $instance = new self();

        $instance->homeMobileCountryCode = self::getMMCFromDefinition($definition);
        $instance->homeMobileNetworkCode = self::getMNCFromDefinition($definition);
        $instance->radioType = self::getRadioTypeFromDefinition($definition);
        $instance->carrier = self::getCarrierFromDefinition($definition);
        $instance->considerIp = self::getConsiderIpFromDefinition($definition);
        $instance->cellTowers = self::getCellTowersFromDefinition($definition);
        $instance->wifiAccessPoints = self::getWifiAccessPointsFromDefinition($definition);

        return $instance;
    }

    private static function getMMCFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_MMC])
            && is_int($definition[self::DEFINITION_MMC]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_MMC];
    }

    private static function getMNCFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_MNC])
            && is_int($definition[self::DEFINITION_MNC]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_MNC];
    }

    private static function getRadioTypeFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_RADIO_TYPE])
            && is_string($definition[self::DEFINITION_RADIO_TYPE])
            && in_array($definition[self::DEFINITION_RADIO_TYPE], ['lte', 'gsm', 'cdma', 'wcdma'], true))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_RADIO_TYPE];
    }

    private static function getCarrierFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_CARRIER])
            && is_string($definition[self::DEFINITION_CARRIER]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_CARRIER];
    }

    private static function getConsiderIpFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_CONSIDER_IP])
            && is_bool($definition[self::DEFINITION_CONSIDER_IP]))
        ) {
            return true;
        }

        return $definition[self::DEFINITION_CONSIDER_IP];
    }

    private static function getCellTowersFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_CELL_TOWERS])
            && is_array($definition[self::DEFINITION_CELL_TOWERS]))
        ) {
            return [];
        }

        return $definition[self::DEFINITION_CELL_TOWERS];
    }

    private static function getWifiAccessPointsFromDefinition(array $definition)
    {

        if (!(isset($definition[self::DEFINITION_WIFI_ACCESS_POINTS])
            && $definition[self::DEFINITION_WIFI_ACCESS_POINTS] instanceof WifiAccessPoints
            && count($definition[self::DEFINITION_WIFI_ACCESS_POINTS]) >= 2)
        ) {
            return [];
        }

        return $definition[self::DEFINITION_WIFI_ACCESS_POINTS];
    }

    /**
     * @param string $carrier
     * @return GeolocationRequest
     */
    public function setCarrier($carrier)
    {
        $this->carrier = $carrier;
        return $this;
    }

    /**
     * @param array $cellTowers
     * @return GeolocationRequest
     */
    public function setCellTowers($cellTowers)
    {
        $this->cellTowers = $cellTowers;
        return $this;
    }

    /**
     * @param boolean $considerIp
     * @return GeolocationRequest
     */
    public function setConsiderIp($considerIp)
    {
        $this->considerIp = $considerIp;
        return $this;
    }

    /**
     * @param int $homeMobileCountryCode
     * @return GeolocationRequest
     */
    public function setHomeMobileCountryCode($homeMobileCountryCode)
    {
        $this->homeMobileCountryCode = $homeMobileCountryCode;
        return $this;
    }

    /**
     * @param int $homeMobileNetworkCode
     * @return GeolocationRequest
     */
    public function setHomeMobileNetworkCode($homeMobileNetworkCode)
    {
        $this->homeMobileNetworkCode = $homeMobileNetworkCode;
        return $this;
    }

    /**
     * @param string $radioType
     * @return GeolocationRequest
     */
    public function setRadioType($radioType)
    {
        $this->radioType = $radioType;
        return $this;
    }

    /**
     * @param WifiAccessPoints $wifiAccessPoints
     * @return GeolocationRequest
     */
    public function setWifiAccessPoints($wifiAccessPoints)
    {
        $this->wifiAccessPoints = $wifiAccessPoints;
        return $this;
    }
}

/**
 * Class GeolocationResponse
 * @package BitbarPlugins\Travel
 */
final class GeolocationResponse
{
    const DEFINITION_LOCATION = 'location';
    const DEFINITION_LATITUDE = 'lat';
    const DEFINITION_LONGITUDE = 'lng';
    const DEFINITION_ACCURACY = 'accuracy';
    const DEFINITION_ERROR = 'error';
    const DEFINITION_MESSAGE = 'message';
    const DEFINITION_CODE = 'code';

    /**
     * @var float
     */
    private $latitude = null;

    /**
     * @var float
     */
    private $longitude = null;

    /**
     * @var float
     */
    private $accuracy = null;

    /**
     * Creates an instance of GeolocationResponse from raw API response in json
     * @param $response
     * @return GeolocationResponse
     * @throws GeolocationResponseException
     */
    public static function fromApiResponse($response)
    {
        $instance = new self();

        $geolocation = json_decode($response, true);

        if ($geolocationError = self::getErrorFromResponse($geolocation)) {
            throw $geolocationError;
        }

        $instance->latitude = self::getLatitudeFromResponse($geolocation);
        $instance->longitude = self::getLongitudeFromResponse($geolocation);
        $instance->accuracy = self::getAccuracyFromResponse($geolocation);

        return $instance;
    }

    private static function getErrorFromResponse(array $geolocation)
    {
        if (!(is_array($geolocation)
            && array_key_exists(self::DEFINITION_ERROR, $geolocation)
            && array_key_exists(self::DEFINITION_MESSAGE, $geolocation[self::DEFINITION_ERROR])
            && array_key_exists(self::DEFINITION_CODE, $geolocation[self::DEFINITION_ERROR])
            && is_string($geolocation[self::DEFINITION_ERROR][self::DEFINITION_MESSAGE])
            && is_int($geolocation[self::DEFINITION_ERROR][self::DEFINITION_CODE]))
        ) {
            return null;
        }

        return new GeolocationResponseException(
            $geolocation[self::DEFINITION_ERROR][self::DEFINITION_MESSAGE],
            $geolocation[self::DEFINITION_ERROR][self::DEFINITION_CODE]
        );
    }

    private static function getLatitudeFromResponse(array $geolocation)
    {
        if (!(is_array($geolocation)
            && array_key_exists(self::DEFINITION_LOCATION, $geolocation)
            && array_key_exists(self::DEFINITION_LATITUDE, $geolocation[self::DEFINITION_LOCATION])
            && is_float($geolocation[self::DEFINITION_LOCATION][self::DEFINITION_LATITUDE]))
        ) {
            return null;
        }

        return $geolocation[self::DEFINITION_LOCATION][self::DEFINITION_LATITUDE];
    }

    private static function getLongitudeFromResponse(array $geolocation)
    {
        if (!(is_array($geolocation)
            && array_key_exists(self::DEFINITION_LOCATION, $geolocation)
            && array_key_exists(self::DEFINITION_LONGITUDE, $geolocation[self::DEFINITION_LOCATION])
            && is_float($geolocation[self::DEFINITION_LOCATION][self::DEFINITION_LONGITUDE]))
        ) {
            return null;
        }

        return $geolocation[self::DEFINITION_LOCATION][self::DEFINITION_LONGITUDE];
    }

    private static function getAccuracyFromResponse(array $geolocation)
    {
        if (!(is_array($geolocation)
            && array_key_exists(self::DEFINITION_ACCURACY, $geolocation)
            && is_float($geolocation[self::DEFINITION_ACCURACY]))
        ) {
            return null;
        }

        return $geolocation[self::DEFINITION_ACCURACY];
    }

    /**
     * @return float
     */
    public function getAccuracy()
    {
        return $this->accuracy;
    }

    /**
     * @return float
     */
    public function getLatitude()
    {
        return $this->latitude;
    }

    /**
     * @return float
     */
    public function getLongitude()
    {
        return $this->longitude;
    }
}

final class GeolocationResponseException extends \Exception
{
    /**
     * @var int
     */
    private $statusCode;

    public function __construct($message, $code)
    {
        $this->statusCode = $code;
        $this->message = $message;
    }

    /**
     * @return int
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }
}

/**
 * Class DistanceMatrixAPI
 *
 * @link https://developers.google.com/maps/documentation/distance-matrix/
 * @package BitbarPlugins\Travel
 */
final class DistanceMatrixAPI
{
    const DISTANCE_MATRIX_URL = "https://maps.googleapis.com/maps/api/distancematrix/json";

    /**
     * @var resource curl handler
     */
    private $ch;

    private $headers = [
        "Accept: application/json",
        "Cache-Control: no-cache",
        "Pragma: no-cache",
    ];

    public function __construct()
    {
        $this->initCurl();
    }

    private function initCurl()
    {
        $this->ch = curl_init();

        curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->headers);
    }

    /**
     * Send a DistanceMatrix Request
     *
     * @param DistanceMatrixRequest $request
     * @return DistanceMatrixResponse
     * @throws DistanceMatrixResponseException
     */
    public function call(DistanceMatrixRequest $request)
    {
        $url = self::DISTANCE_MATRIX_URL . '?' . $request->toQueryParameters();
        curl_setopt($this->ch, CURLOPT_URL, $url);
        $response = curl_exec($this->ch);

        return DistanceMatrixResponse::fromApiResponse($response);
    }

}

/**
 * Class DistanceMatrixRequest
 * @todo handle departure_time with unix timestamps
 * @package BitbarPlugins\Travel
 */
final class DistanceMatrixRequest
{
    const DEFINITION_ORIGINS = "origins";
    const DEFINITION_DESTINATIONS = "destinations";
    const DEFINITION_KEY = "key";
    const DEFINITION_MODE = "mode";
    const DEFINITION_LANGUAGE = "language";
    const DEFINITION_UNITS = "units";
    const DEFINITION_DEPARTURE_TIME = "departure_time";

    const PATTERN_LATITUDE_LONGITUDE = '/^\-?\d+(?:\.\d+)?,\-?\d+(?:\.\d+)?$/';

    /**
     * One or more addresses and/or textual latitude/longitude values, separated with the pipe (|) character,
     * from which to calculate distance and time.
     * @var string
     */
    private $origins = null;

    /**
     * One or more addresses and/or textual latitude/longitude values, separated with the pipe (|) character,
     * to which to calculate distance and time.
     * @var string
     */
    private $destinations = null;

    /**
     * Your application's API key. This key identifies your application for purposes of quota management.
     * @var string
     */
    private $key = null;

    /**
     * Optional
     * Specifies the mode of transport to use when calculating distance.
     * @var string
     */
    private $mode = 'driving'; // 'driving', 'walking', 'cycling', 'transit'

    /**
     * Optional
     * The language in which to return results.
     * @var string
     */
    private $language = 'en';

    /**
     * Optional
     * Specifies the unit system to use when expressing distance as text.
     * @var string
     */
    private $units = 'metric'; // 'metric', 'imperial'

    /**
     * Optional
     * The desired time of departure. You can specify the time as an integer in seconds since midnight,
     * January 1, 1970 UTC. Alternatively, you can specify a value of now
     * @var string
     */
    private $departureTime = 'now';

    /**
     * Convert Request Object to URL-encoded query string
     *
     * @return string
     */
    public function toQueryParameters()
    {
        return http_build_query([
            self::DEFINITION_ORIGINS => $this->origins,
            self::DEFINITION_DESTINATIONS => $this->destinations,
            self::DEFINITION_KEY => $this->key,
            self::DEFINITION_MODE => $this->mode,
            self::DEFINITION_LANGUAGE => $this->language,
            self::DEFINITION_UNITS => $this->units,
            self::DEFINITION_DEPARTURE_TIME => $this->departureTime,
        ]);
    }

    /**
     * @param array $definition
     *
     * @return self
     */
    public static function fromArrayDefinition(array $definition)
    {
        $instance = new self();

        $instance->origins = self::getOriginsFromDefinition($definition);
        $instance->destinations = self::getDestinationsFromDefinition($definition);
        $instance->key = self::getKeyFromDefinition($definition);
        $instance->mode = self::getModeFromDefinition($definition);
        $instance->language = self::getLanguageFromDefinition($definition);
        $instance->units = self::getUnitsFromDefinition($definition);
        $instance->departureTime = self::getDepartureTimeFromDefinition($definition);

        return $instance;
    }

    private static function getOriginsFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_ORIGINS])
            && is_string($definition[self::DEFINITION_ORIGINS])
            && preg_match(self::PATTERN_LATITUDE_LONGITUDE, $definition[self::DEFINITION_ORIGINS]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_ORIGINS];
    }

    private static function getDestinationsFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_DESTINATIONS])
            && is_string($definition[self::DEFINITION_DESTINATIONS]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_DESTINATIONS];
    }

    private static function getKeyFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_KEY])
            && is_string($definition[self::DEFINITION_KEY]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_KEY];
    }

    private static function getModeFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_MODE])
            && is_string($definition[self::DEFINITION_MODE])
            && in_array($definition[self::DEFINITION_MODE], ['driving', 'walking', 'cycling', 'transit']))
        ) {
            return 'driving';
        }

        return $definition[self::DEFINITION_MODE];
    }

    private static function getLanguageFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_LANGUAGE])
            && is_string($definition[self::DEFINITION_LANGUAGE])
            && in_array($definition[self::DEFINITION_LANGUAGE], ['ar', 'kn', 'bg', 'ko', 'bn', 'lt', 'ca', 'lv', 'cs',
                'ml', 'da', 'mr', 'de', 'nl', 'el', 'no', 'en', 'pl', 'en-AU', 'pt', 'en-GB', 'pt-BR', 'es', 'pt-PT',
                'eu', 'ro', 'eu', 'ru', 'fa', 'sk', 'fi', 'sl', 'fil', 'sr', 'fr', 'sv', 'gl', 'ta', 'gu', 'te', 'hi',
                'th', 'hr', 'tl', 'hu', 'tr', 'id', 'uk', 'it', 'vi', 'iw', 'zh-CN', 'ja', 'zh-TW',]))
        ) {
            return 'en';
        }

        return $definition[self::DEFINITION_LANGUAGE];
    }

    private static function getUnitsFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_UNITS])
            && is_string($definition[self::DEFINITION_UNITS])
            && in_array($definition[self::DEFINITION_UNITS], ['metric', 'imperial']))
        ) {
            return 'metric';
        }

        return $definition[self::DEFINITION_UNITS];
    }

    private static function getDepartureTimeFromDefinition(array $definition)
    {
        if (!(isset($definition[self::DEFINITION_DEPARTURE_TIME])
            && is_string($definition[self::DEFINITION_DEPARTURE_TIME])
            && $definition[self::DEFINITION_DEPARTURE_TIME] === 'now')
        ) {
            return 'now';
        }

        return $definition[self::DEFINITION_DEPARTURE_TIME];
    }
}

/**
 * Class DistanceMatrixResponse
 * @package BitbarPlugins\Travel
 */
final class DistanceMatrixResponse
{
    const DEFINITION_STATUS = 'status';
    const DEFINITION_ORIGIN_ADDRESSES = 'origin_addresses';
    const DEFINITION_DESTINATION_ADDRESSES = 'destination_addresses';
    const DEFINITION_ROWS = 'rows';

    const STATUS_CODE_OK = 'OK';
    const STATUS_CODE_INVALID_REQUEST = 'INVALID_REQUEST';
    const STATUS_CODE_MAX_ELEMENTS_EXCEEDED = 'MAX_ELEMENTS_EXCEEDED';
    const STATUS_CODE_OVER_QUERY_LIMIT = 'OVER_QUERY_LIMIT';
    const STATUS_CODE_REQUEST_DENIED = 'REQUEST_DENIED';
    const STATUS_CODE_UNKNOWN_ERROR = 'UNKNOWN_ERROR';

    /**
     * Contains metadata on the request.
     * @var string
     */
    private $status;

    /**
     * Contains an array of addresses as returned by the API from your original request.
     * These are formatted by the geocoder and localized according to the language parameter passed with the request.
     * @var string
     */
    private $originAddresses;

    /**
     * Contains an array of addresses as returned by the API from your original request.
     * As with origin_addresses, these are localized if appropriate.
     * @var string
     */
    private $destinationAddresses;

    /**
     * Contains an array of elements
     * @var DistanceMatrixResponseElement
     */
    private $rows;

    /**
     * Creates an instance of DistanceMatrixResponse from raw API response in json
     * @param $response
     * @return DistanceMatrixResponse
     * @throws DistanceMatrixResponseException
     */
    public static function fromApiResponse($response)
    {
        $instance = new self();

        $distanceMatrix = json_decode($response, true);

        if ($distanceMatrixError = self::getErrorFromResponse($distanceMatrix)) {
            throw $distanceMatrixError;
        }

        $instance->status = self::getStatusFromResponse($distanceMatrix);
        $instance->originAddresses = self::getOriginAddressesFromResponse($distanceMatrix);
        $instance->destinationAddresses = self::getDestinationAddressesFromResponse($distanceMatrix);
        $instance->rows = DistanceMatrixResponseElement::fromArrayDefinition(self::getRowsFromResponse($distanceMatrix));

        return $instance;
    }

    private static function getErrorFromResponse(array $distanceMatrix)
    {
        if (!(is_array($distanceMatrix)
            && array_key_exists(self::DEFINITION_STATUS, $distanceMatrix)
            && $distanceMatrix[self::DEFINITION_STATUS] === self::STATUS_CODE_OK)
        ) {
            return new DistanceMatrixResponseException($distanceMatrix[self::DEFINITION_STATUS]);
        }

        return null;
    }

    private static function getStatusFromResponse(array $distanceMatrix)
    {
        if (!(is_array($distanceMatrix)
            && array_key_exists(self::DEFINITION_STATUS, $distanceMatrix)
            && is_string($distanceMatrix[self::DEFINITION_STATUS]))
        ) {
            return null;
        }

        return $distanceMatrix[self::DEFINITION_STATUS];
    }

    private static function getOriginAddressesFromResponse(array $distanceMatrix)
    {
        if (!(is_array($distanceMatrix)
            && array_key_exists(self::DEFINITION_ORIGIN_ADDRESSES, $distanceMatrix)
            && is_array($distanceMatrix[self::DEFINITION_ORIGIN_ADDRESSES])
            && count($distanceMatrix[self::DEFINITION_ORIGIN_ADDRESSES]) === 1)
        ) {
            return null;
        }

        return $distanceMatrix[self::DEFINITION_ORIGIN_ADDRESSES][0];
    }

    private static function getDestinationAddressesFromResponse(array $distanceMatrix)
    {
        if (!(is_array($distanceMatrix)
            && array_key_exists(self::DEFINITION_DESTINATION_ADDRESSES, $distanceMatrix)
            && is_array($distanceMatrix[self::DEFINITION_DESTINATION_ADDRESSES])
            && count($distanceMatrix[self::DEFINITION_DESTINATION_ADDRESSES]) === 1)
        ) {
            return null;
        }

        return $distanceMatrix[self::DEFINITION_DESTINATION_ADDRESSES][0];
    }

    private static function getRowsFromResponse(array $distanceMatrix)
    {
        if (!(is_array($distanceMatrix)
            && array_key_exists(self::DEFINITION_ROWS, $distanceMatrix)
            && is_array($distanceMatrix[self::DEFINITION_ROWS])
            && count($distanceMatrix[self::DEFINITION_ROWS]) === 1)
        ) {
            return null;
        }

        return $distanceMatrix[self::DEFINITION_ROWS][0];
    }

    /**
     * @return string
     */
    public function getDestinationAddresses()
    {
        return $this->destinationAddresses;
    }

    /**
     * @return string
     */
    public function getOriginAddresses()
    {
        return $this->originAddresses;
    }

    /**
     * @return DistanceMatrixResponseElement
     */
    public function getRows()
    {
        return $this->rows;
    }

    /**
     * @return string
     */
    public function getStatus()
    {
        return $this->status;
    }

}

final class DistanceMatrixResponseElement
{
    const DEFINITION_ELEMENTS = 'elements';
    const DEFINITION_STATUS = 'status';
    const DEFINITION_DURATION = 'duration';
    const DEFINITION_DISTANCE = 'distance';
    const DEFINITION_DURATION_IN_TRAFFIC = 'duration_in_traffic';

    const STATUS_CODE_OK = 'OK';
    const STATUS_CODE_NOT_FOUND = 'NOT_FOUND';
    const STATUS_CODE_ZERO_RESULTS = 'ZERO_RESULTS';

    /**
     * Element level status of the request
     * @var string
     */
    private $status = null;

    /**
     * The length of time it takes to travel this route, expressed in seconds (the value field) and as text.
     * The textual representation is localized according to the query's language parameter.
     * @var array
     */
    private $duration = [];

    /**
     * The total distance of this route, expressed in meters (value) and as text. The textual value uses the unit
     * system specified with the unit parameter of the original request, or the origin's region.
     * @var array
     */
    private $distance = [];

    /**
     * The length of time it takes to travel this route, based on current and historical traffic conditions.
     * @var array
     */
    private $durationInTraffic = [];

    /**
     * @param array $definition
     * @return DistanceMatrixResponseElement
     * @throws DistanceMatrixResponseElementException
     */
    public static function fromArrayDefinition(array $definition)
    {
        $instance = new self();

        if ($distanceMatrixElementError = self::getErrorFromDefinition($definition)) {
            throw $distanceMatrixElementError;
        }

        $instance->status = self::getStatusFromDefinition($definition);
        $instance->duration = self::getDurationFromDefinition($definition);
        $instance->distance = self::getDistanceFromDefinition($definition);
        $instance->durationInTraffic = self::getDurationInTrafficFromDefinition($definition);

        return $instance;
    }

    public static function getErrorFromDefinition(array $definition)
    {
        if (!(is_array($definition)
            && array_key_exists(self::DEFINITION_ELEMENTS, $definition)
            && is_array($definition[self::DEFINITION_ELEMENTS])
            && count($definition[self::DEFINITION_ELEMENTS]) === 1
            && is_array($definition[self::DEFINITION_ELEMENTS][0])
            && array_key_exists(self::DEFINITION_STATUS, $definition[self::DEFINITION_ELEMENTS][0])
            && $definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_STATUS] === self::STATUS_CODE_OK)
        ) {
            return new DistanceMatrixResponseElementException($definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_STATUS]);
        }

        return null;
    }

    public static function getStatusFromDefinition(array $definition)
    {
        if (!(is_array($definition[self::DEFINITION_ELEMENTS][0])
            && array_key_exists(self::DEFINITION_STATUS, $definition[self::DEFINITION_ELEMENTS][0])
            && is_string($definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_STATUS]))
        ) {
            return null;
        }

        return $definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_STATUS];
    }

    public static function getDurationFromDefinition(array $definition)
    {
        if (!(is_array($definition[self::DEFINITION_ELEMENTS][0])
            && array_key_exists(self::DEFINITION_DURATION, $definition[self::DEFINITION_ELEMENTS][0])
            && is_array($definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_DURATION]))
        ) {
            return [];
        }

        return $definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_DURATION];
    }

    public static function getDistanceFromDefinition(array $definition)
    {
        if (!(is_array($definition[self::DEFINITION_ELEMENTS][0])
            && array_key_exists(self::DEFINITION_DISTANCE, $definition[self::DEFINITION_ELEMENTS][0])
            && is_array($definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_DISTANCE]))
        ) {
            return [];
        }

        return $definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_DISTANCE];
    }

    public static function getDurationInTrafficFromDefinition(array $definition)
    {
        if (!(is_array($definition[self::DEFINITION_ELEMENTS][0])
            && array_key_exists(self::DEFINITION_DURATION_IN_TRAFFIC, $definition[self::DEFINITION_ELEMENTS][0])
            && is_array($definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_DURATION_IN_TRAFFIC]))
        ) {
            return [];
        }

        return $definition[self::DEFINITION_ELEMENTS][0][self::DEFINITION_DURATION_IN_TRAFFIC];
    }

    /**
     * @return array
     */
    public function getDistance()
    {
        return $this->distance;
    }

    /**
     * @return array
     */
    public function getDuration()
    {
        return $this->duration;
    }

    /**
     * @return array
     */
    public function getDurationInTraffic()
    {
        return $this->durationInTraffic;
    }

    /**
     * @return string
     */
    public function getStatus()
    {
        return $this->status;
    }
}

final class DistanceMatrixResponseException extends \Exception
{

}

final class DistanceMatrixResponseElementException extends \Exception
{

}

echo new TravelTimePlugin();