<?PHP
#
#   FILE:  StdLib.php
#
#   Part of the ScoutLib application support library
#   Copyright 2016 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu
#

/**
* Standard utility library.
* \nosubgrouping
*/
class StdLib
{

    # ---- PUBLIC INTERFACE --------------------------------------------------

    /**
    * Get string with file and line number for call to current function.
    * @return string String with caller info in the form "FILE:LINE".
    */
    public static function GetMyCaller()
    {
        $Trace = version_compare(PHP_VERSION, "5.4.0", ">=")
                ? debug_backtrace(FALSE, 2) : debug_backtrace(FALSE);
        $Caller = basename($Trace[1]["file"]).":".$Trace[1]["line"];
        return $Caller;
    }

    /**
    * Check the caller of the current function.  In the desired caller
    * parameter, if a file name is specified it should include the ".php"
    * extension but should not have a leading path.  In the exception
    * message parameter, the following strings can be used and the
    * appropriate values will be substituted in:  %FILE% (no leading path),
    * %LINE%, %FULLFILE% (includes leading path), %CLASS%, %FUNCTION%,
    * and %METHOD% (equivalent to "%CLASS%::%FUNCTION%").
    * @param string $DesiredCaller String describing desired caller, in
    *       the form "Class", "Class::Method", "Function", "File", or
    *       "File:Line".
    * @param string $ExceptionMsg If specified and the caller was not the
    *       desired caller, an exception will be thrown with this message.
    *       (OPTIONAL)
    * @return bool TRUE if caller matched desired caller, otherwise FALSE.
    */
    public static function CheckMyCaller($DesiredCaller, $ExceptionMsg = NULL)
    {
        # retrieve caller info
        $Trace = version_compare(PHP_VERSION, "5.4.0", ">=")
                ? debug_backtrace(FALSE, 3) : debug_backtrace(FALSE);
        $FullFile = $Trace[1]["file"];
        $File = basename($FullFile);
        $Line = $Trace[1]["line"];
        $Class = isset($Trace[2]["class"]) ? $Trace[2]["class"] : "";
        $Function = isset($Trace[2]["function"]) ? $Trace[2]["function"] : "";

        # if caller does not match desired caller
        if (($DesiredCaller != $Class)
                && ($DesiredCaller != $Class."::".$Function)
                && ($DesiredCaller != $Class.$Function)
                && ($DesiredCaller != $File)
                && ($DesiredCaller != $File.":".$Line))
        {
            # if exception message supplied
            if ($ExceptionMsg !== NULL)
            {
                # make any needed substitutions in exception message
                $Msg = str_replace(
                        array(
                            "%FILE%",
                            "%LINE%",
                            "%FULLFILE%",
                            "%CLASS%",
                            "%FUNCTION%",
                            "%METHOD%"),
                        array(
                            $File,
                            $Line,
                            $FullFile,
                            $Class,
                            $Function,
                            $Class."::".$Function),
                        $ExceptionMsg);

                # throw exception
                throw new Exception($Msg);
            }
            else
            {
                # report to our caller that their caller was not the desired one
                return FALSE;
            }
        }

        # report to our caller that their caller was not the desired one
        return TRUE;
    }

    /**
    * Pluralize an English word.
    * @param string $Word Word to make plural.
    * @return string Word in plural form.
    */
    public static function Pluralize($Word)
    {
        # return word unchanged if singular and plural are the same
        if (in_array(strtolower($Word), self::$UncountableWords))
        {
            return $Word;
        }

        # check for irregular singular forms
        foreach (self::$IrregularWords as $Pattern => $Result)
        {
            $Pattern = '/'.$Pattern.'$/i';
            if (preg_match($Pattern, $Word))
            {
                return preg_replace($Pattern, $Result, $Word);
            }
        }

        # check for matches using regular expressions
        foreach (self::$PluralizePatterns as $Pattern => $Result)
        {
            if (preg_match($Pattern, $Word))
            {
                return preg_replace($Pattern, $Result, $Word);
            }
        }

        # return word unchanged if we could not process it
        return $Word;
    }

    /**
    * Singularize an English word.
    * @param string $Word Word to make singular.
    * @return string Word in singular form.
    */
    public static function Singularize($Word)
    {
        # return word unchanged if singular and plural are the same
        if (in_array(strtolower($Word), self::$UncountableWords))
        {
            return $Word;
        }

        # check for irregular plural forms
        foreach (self::$IrregularWords as $Result => $Pattern)
        {
            $Pattern = '/'.$Pattern.'$/i';
            if (preg_match($Pattern, $Word))
            {
                return preg_replace($Pattern, $Result, $Word);
            }
        }

        # check for matches using regular expressions
        foreach (self::$SingularizePatterns as $Pattern => $Result)
        {
            if (preg_match($Pattern, $Word))
            {
                return preg_replace($Pattern, $Result, $Word);
            }
        }

        # return word unchanged if we could not process it
        return $Word;
    }

    /**
    * Perform compare and return value appropriate for sort function callbacks.
    * @param mixed $A First value to compare.
    * @param mixed $B Second value to compare.
    * @return int 0 if values are equal, -1 if A is less than B, or 1 if B is
    *       greater than A.
    */
    public static function SortCompare($A, $B)
    {
        if ($A == $B)
        {
            return 0;
        }
        else
        {
            return ($A < $B) ? -1 : 1;
        }
    }


    /**
    * Look up the GPS coordinates for a US ZIP code.  Database of GPS
    * coordinates used was drawn from Census 2010. See the "Zip Code
    * Tabulation Areas" section on
    * https://www.census.gov/geo/maps-data/data/gazetteer2010.html for
    * the original source file.  The version used here has been cut
    * down to columns 1, 8, and 9 from that source.
    * @param int $Zip Zip code to look up.
    * @return array Having members "Lat" and "Lng" on successful
    * lookup, FALSE otherwise
    * @throws Exception When coordinates file cannot be opened.
    */
    public static function GetLatLngForZipCode($Zip)
    {
        static $ZipCache = array();

        # if we don't have a cached value for this zip, look one up
        if (!isset($ZipCache[$Zip]))
        {
            # try to open our zip code database
            $FHandle = fopen(dirname(__FILE__)."/StdLib--ZipCodeCoords.txt", "r");

            # if we couldn't open the file, we can't look up a value
            if ($FHandle === FALSE)
            {
                throw new Exception("Unable to open zip code coordinates file");
            }

            # iterate over our database until we find the desired zip
            # or run out of database
            while (($Line = fgetcsv($FHandle, 0, "\t")) !== FALSE)
            {
                if ($Line[0] == $Zip)
                {
                    $ZipCache[$Zip] = array(
                        "Lat" => $Line[1], "Lng" => $Line[2]);
                    break;
                }
            }

            # if we've scanned the entire file and have no coords for
            # this zip, cache a failure
            if (!isset($ZipCache[$Zip]))
            {
                $ZipCache[$Zip] = FALSE;
            }
        }

        # hand back cached value
        return $ZipCache[$Zip];
    }

    /**
    * Compute the distance between two US ZIP codes.
    * @param int $ZipA First zip code.
    * @param int $ZipB Second zip code.
    * @return double Distance in Km between the two zip codes or FALSE
    *     if either zip could not be found
    */
    public static function ZipCodeDistance($ZipA, $ZipB)
    {

        $FirstPoint = self::GetLatLngForZipCode($ZipA);
        $SecondPoint = self::GetLatLngForZipCode($ZipB);

        # if we scanned the whole file and lack data for either of our
        # points, return NULL
        if ($FirstPoint === FALSE || $SecondPoint === FALSE)
        {
            return FALSE;
        }

        return self::ComputeGreatCircleDistance(
            $FirstPoint["Lat"], $FirstPoint["Lng"],
            $SecondPoint["Lat"], $SecondPoint["Lng"]);
    }

    /**
    * Computes the distance in kilometers between two points, assuming a
    * spherical earth.
    * @param int $LatSrc Latitude of the source coordinate.
    * @param int $LonSrc Longitude of the source coordinate.
    * @param int $LatDst Latitude of the destination coordinate.
    * @param int $LonDst Longitude of the destination coordinate.
    * @return distance in miles between the two points.
    */
    public static function ComputeGreatCircleDistance($LatSrc, $LonSrc,
                             $LatDst, $LonDst)
    {
        # See http://en.wikipedia.org/wiki/Great-circle_distance

        # Convert it all to Radians
        $Ps = deg2rad($LatSrc);
        $Ls = deg2rad($LonSrc);
        $Pf = deg2rad($LatDst);
        $Lf = deg2rad($LonDst);

        # Compute the central angle
        return 3958.756 * atan2(
            sqrt( pow(cos($Pf)*sin($Lf-$Ls), 2) +
                  pow(cos($Ps)*sin($Pf) -
                      sin($Ps)*cos($Pf)*cos($Lf-$Ls), 2)),
                  sin($Ps)*sin($Pf)+cos($Ps)*cos($Pf)*cos($Lf-$Ls));

    }

    /**
    * Computes the initial angle on a course connecting two points, assuming a
    * spherical earth.
    * @param int $LatSrc Latitude of the source coordinate.
    * @param int $LonSrc Longitude of the source coordinate.
    * @param int $LatDst Latitude of the destination coordinate.
    * @param int $LonDst Longitude of the destination coordinate.
    * @return initial angle on a course connecting two points.
    */
    public static function ComputeBearing($LatSrc, $LonSrc,
                                   $LatDst, $LonDst)
    {
        # See http://mathforum.org/library/drmath/view/55417.html

        # Convert angles to radians
        $Ps = deg2rad($LatSrc);
        $Ls = deg2rad($LonSrc);
        $Pf = deg2rad($LatDst);
        $Lf = deg2rad($LonDst);

        return rad2deg(atan2(sin($Lf-$Ls)*cos($Pf),
                             cos($Ps)*sin($Pf)-sin($Ps)*cos($Pf)*cos($Lf-$Ls)));
    }

    /** Format to feed to date() to get SQL-compatible date/time string. */
    const SQL_DATE_FORMAT = "Y-m-d H:i:s";


    # ---- PRIVATE INTERFACE -------------------------------------------------

    private static $PluralizePatterns = array(
        '/(quiz)$/i'               => "$1zes",
        '/^(ox)$/i'                => "$1en",
        '/([m|l])ouse$/i'          => "$1ice",
        '/(matr|vert|ind)ix|ex$/i' => "$1ices",
        '/(x|ch|ss|sh)$/i'         => "$1es",
        '/([^aeiouy]|qu)y$/i'      => "$1ies",
        '/(hive)$/i'               => "$1s",
        '/(?:([^f])fe|([lr])f)$/i' => "$1$2ves",
        '/(shea|lea|loa|thie)f$/i' => "$1ves",
        '/sis$/i'                  => "ses",
        '/([ti])um$/i'             => "$1a",
        '/(tomat|potat|ech|her|vet)o$/i'=> "$1oes",
        '/(bu)s$/i'                => "$1ses",
        '/(alias)$/i'              => "$1es",
        '/(octop)us$/i'            => "$1i",
        '/(ax|test)is$/i'          => "$1es",
        '/(us)$/i'                 => "$1es",
        '/s$/i'                    => "s",
        '/$/'                      => "s"
    );
    private static $SingularizePatterns = array(
        '/(quiz)zes$/i'             => "$1",
        '/(matr)ices$/i'            => "$1ix",
        '/(vert|ind)ices$/i'        => "$1ex",
        '/^(ox)en$/i'               => "$1",
        '/(alias)es$/i'             => "$1",
        '/(octop|vir)i$/i'          => "$1us",
        '/(cris|ax|test)es$/i'      => "$1is",
        '/(shoe)s$/i'               => "$1",
        '/(o)es$/i'                 => "$1",
        '/(bus)es$/i'               => "$1",
        '/([m|l])ice$/i'            => "$1ouse",
        '/(x|ch|ss|sh)es$/i'        => "$1",
        '/(m)ovies$/i'              => "$1ovie",
        '/(s)eries$/i'              => "$1eries",
        '/([^aeiouy]|qu)ies$/i'     => "$1y",
        '/([lr])ves$/i'             => "$1f",
        '/(tive)s$/i'               => "$1",
        '/(hive)s$/i'               => "$1",
        '/(li|wi|kni)ves$/i'        => "$1fe",
        '/(shea|loa|lea|thie)ves$/i'=> "$1f",
        '/(^analy)ses$/i'           => "$1sis",
        '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i'  => "$1$2sis",
        '/([ti])a$/i'               => "$1um",
        '/(n)ews$/i'                => "$1ews",
        '/(h|bl)ouses$/i'           => "$1ouse",
        '/(corpse)s$/i'             => "$1",
        '/(us)es$/i'                => "$1",
        '/s$/i'                     => ""
    );
    private static $IrregularWords = array(
        'move'   => 'moves',
        'foot'   => 'feet',
        'goose'  => 'geese',
        'sex'    => 'sexes',
        'child'  => 'children',
        'man'    => 'men',
        'tooth'  => 'teeth',
        'person' => 'people'
    );
    private static $UncountableWords = array(
        'sheep',
        'fish',
        'deer',
        'series',
        'species',
        'money',
        'rice',
        'information',
        'equipment'
    );
}
