5 # Part of the ScoutLib application support library 6 # Copyright 2016 Edward Almasy and Internet Scout Research Group 7 # http://scout.wisc.edu 17 # ---- PUBLIC INTERFACE -------------------------------------------------- 28 $Trace = version_compare(PHP_VERSION,
"5.4.0",
">=")
29 ? debug_backtrace(FALSE, 2) : debug_backtrace(FALSE);
30 $FullFileName = $Trace[1][
"file"];
32 "FileName" => basename($FullFileName),
33 "RelativeFileName" => str_replace(getcwd().
"/",
"", $FullFileName),
34 "FullFileName" => $FullFileName,
35 "LineNumber" => $Trace[1][
"line"],
37 return ($Element === NULL) ? $Info : $Info[$Element];
46 $Trace = version_compare(PHP_VERSION,
"5.4.0",
">=")
47 ? debug_backtrace(FALSE, 2) : debug_backtrace(FALSE);
48 $FileName = $Trace[1][
"file"];
49 $Caller = basename($FileName).
":".$Trace[1][
"line"];
69 public static function CheckMyCaller($DesiredCaller, $ExceptionMsg = NULL)
71 # retrieve caller info 72 $Trace = version_compare(PHP_VERSION,
"5.4.0",
">=")
73 ? debug_backtrace(FALSE, 3) : debug_backtrace(FALSE);
74 $FullFile = $Trace[1][
"file"];
75 $File = basename($FullFile);
76 $Line = $Trace[1][
"line"];
77 $Class = isset($Trace[2][
"class"]) ? $Trace[2][
"class"] :
"";
78 $Function = isset($Trace[2][
"function"]) ? $Trace[2][
"function"] :
"";
80 # if caller does not match desired caller 81 if (($DesiredCaller != $Class)
82 && ($DesiredCaller != $Class.
"::".$Function)
83 && ($DesiredCaller != $Class.$Function)
84 && ($DesiredCaller != $File)
85 && ($DesiredCaller != $File.
":".$Line))
87 # if exception message supplied 88 if ($ExceptionMsg !== NULL)
90 # make any needed substitutions in exception message 105 $Class.
"::".$Function),
109 throw new Exception($Msg);
113 # report to our caller that their caller was not the desired one 118 # report to our caller that their caller was not the desired one 132 $TraceOpts = $IncludeArgs ? 0 : DEBUG_BACKTRACE_IGNORE_ARGS;
133 debug_print_backtrace($TraceOpts);
134 $TraceString = ob_get_contents();
137 # remove this function from backtrace entries 138 $TraceString = preg_replace(
139 "/^#0\s+".__METHOD__.
"[^\n]*\n/",
"", $TraceString, 1);
141 # renumber backtrace entries 142 $TraceString = preg_replace_callback(
"/^#(\d+)/m",
function($Matches)
144 return "#".($Matches[1] - 1);
148 # strip leading path off files names 149 $HomeDir = dirname($_SERVER[
"SCRIPT_FILENAME"]);
150 $TraceString = preg_replace(
"%".preg_quote($HomeDir,
"%").
"/%",
153 # return backtrace string to caller 164 # return word unchanged if singular and plural are the same 165 if (in_array(strtolower($Word), self::$UncountableWords))
170 # check for irregular singular forms 171 foreach (self::$IrregularWords as $Pattern => $Result)
173 $Pattern =
'/'.$Pattern.
'$/i';
174 if (preg_match($Pattern, $Word))
176 return preg_replace($Pattern, $Result, $Word);
180 # check for matches using regular expressions 181 foreach (self::$PluralizePatterns as $Pattern => $Result)
183 if (preg_match($Pattern, $Word))
185 return preg_replace($Pattern, $Result, $Word);
189 # return word unchanged if we could not process it 200 # return word unchanged if singular and plural are the same 201 if (in_array(strtolower($Word), self::$UncountableWords))
206 # check for irregular plural forms 207 foreach (self::$IrregularWords as $Result => $Pattern)
209 $Pattern =
'/'.$Pattern.
'$/i';
210 if (preg_match($Pattern, $Word))
212 return preg_replace($Pattern, $Result, $Word);
216 # check for matches using regular expressions 217 foreach (self::$SingularizePatterns as $Pattern => $Result)
219 if (preg_match($Pattern, $Word))
221 return preg_replace($Pattern, $Result, $Word);
225 # return word unchanged if we could not process it 239 $TagStrippedString = strip_tags(html_entity_decode($String));
241 # if the string contained no HTML tags, we can just treat it as text 242 if ($String == $TagStrippedString)
244 $Length = self::strlen($String);
246 # if string was short enough, we need not do anything 247 if ($Length <= $MaxLength)
252 # if BreakAnywhere is set, just chop at the max length 255 $BreakPos = $MaxLength;
257 # otherwise look for an acceptable breakpoint 260 $BreakPos = self::strrpos($String,
" ", -($Length - $MaxLength));
262 # if we couldn't find a breakpoint, just chop at max length 263 if ($BreakPos === FALSE)
265 $BreakPos = $MaxLength;
269 $Result = self::substr($String, 0, $BreakPos);
271 # tack on the ellipsis 274 # otherwise, we're in an HTML string and we have to account for 275 # how many characters are actually displayed when the string will be 279 # if there aren't enough non-whitespace displayed characters to 280 # exceed the max length, bail because we don't need to do 282 if (self::strlen(trim($TagStrippedString)) <= $MaxLength)
287 # okay, the hard way -- we have to do some limited parsing 288 # of html and attempt to count the number of printing characters 289 # as we're doing that. to accomplish this, we'll build a 290 # little state machine and iterate over the characters one at a 293 # split the string into characters (annoyingly, split()/mb_split() 294 # cannot do this, so we have to use preg_split() in unicode mode) 295 $Tokens = preg_split(
'%%u', $String, -1, PREG_SPLIT_NO_EMPTY);
304 # max length of an HTML Entity 305 $MaxEntityLength = 8;
307 # track how much we have displayed 308 $DisplayedLength = 0;
310 $Buffer =
""; #
for characters we
're not sure about 311 $BufLen = 0; # count of buffered characters 312 $Result = ""; # for characters we've included
313 $QuoteChar =
""; # quote character in use
315 # start in the 'text state' 318 # iterate over all our tokens 319 foreach ($Tokens as $Token)
323 # text state handles words that will be displayed 327 # look for characters that can end a word 331 # if we've buffered up a word 334 # and if displaying that word exceeds 335 # our length, then we're done 336 if ($DisplayedLength + $BufLen > $MaxLength)
341 # otherwise, add the buffered word to our display 343 $DisplayedLength += $BufLen;
346 # buffer this character 350 # if it could have been the start of a tag or an entity, 351 # change state appropriately 354 $State = ($Token ==
"<") ? $S_MaybeTag :
359 # for characters that can't break a word, just buffer them 367 # MaybeTag state checks if a < began a tag or not 369 # tags start with alpha characters (like <b>) 370 # or a slash (like </b>) 371 if (ctype_alpha($Token) || $Token ==
"/")
373 # if this was a tag, output it, output it, 374 # clear our buffer, and move to the Tag state 375 $Result .= $Buffer.$Token;
382 # otherwise, check if displaying this character would 383 # exceed our length. bail if so 384 if ($DisplayedLength + 1 > $MaxLength)
388 # if not, output the characters, clear our buffer, 389 # move to the Text state 390 $Result .= $Buffer.$Token;
398 # Tag state processes the contents of a tag 400 # always output tag contents 403 # check if this is the beginning of a quoted string, 404 # changing state appropriately if so 405 if ($Token ==
"\"" || $Token ==
"'")
410 # if this is the end of the tag, go back to Text state 411 elseif ($Token ==
">")
417 # Quote state processes quoted attributes 419 # always output quote contents 422 # if we've found the matching quote character, 423 # return to the Tag state 424 if ($Token == $QuoteChar)
430 # MaybeEntity decides if we're enjoying an HTML entity 431 # or just an ampersand 437 # if it was a space, then we're not in an entity 438 # as they cannot contain spaces 441 # check if we should be fone 442 if ($DisplayedLength + $BufLen > $MaxLength)
446 # if not, output the buffer, clear it, and return to Text 448 $DisplayedLength += $BufLen;
453 # if we have &xxxx; then count that as a single character entity, 454 # output it, clear the buffer, and return to Text 455 elseif ($Token ==
";")
463 # if this has been too many characters without a ; 464 # for it to be an entity, return to text 474 # tack on the ellipsis 477 # if our string contained HTML tags that we may need to close 478 if (preg_match_all(
"%<(/?[a-z]+)[^>]*>%", $Result, $Matches))
480 # pull out matches for the names of tags 481 $Matches = $Matches[1];
483 # build up an array of open tags 485 while ( ($Tag = array_shift($Matches)) !== NULL )
487 # if this was not a close tag, prepend it to our array 488 if (self::substr($Tag, 0, 1) !=
"/")
490 array_unshift($Tags, $Tag);
492 # if this tag is not self-closing, append it to our list of open tags 493 elseif (self::substr($Tag, -1) !=
"/")
495 # if this was a close tag, look to see if this tag was open 496 $Tgt = array_search(self::substr($Tag, 1), $Tags);
499 # if so, remove this tag from our list 505 # iterate over open tags, closing them as we go 506 while ( ($Tag = array_shift($Tags)) !== NULL)
508 $Result .=
"</".$Tag.
">";
522 return self::CallMbStringFuncIfAvailable(__FUNCTION__, func_get_args(), 3);
531 return self::CallMbStringFuncIfAvailable(__FUNCTION__, func_get_args(), 3);
540 return self::CallMbStringFuncIfAvailable(__FUNCTION__, func_get_args(), 3);
549 return self::CallMbStringFuncIfAvailable(__FUNCTION__, func_get_args(), 1);
567 return ($A < $B) ? -1 : 1;
585 static $ZipCache = array();
587 # if we don't have a cached value for this zip, look one up 588 if (!isset($ZipCache[$Zip]))
590 # try to open our zip code database 591 $FHandle = fopen(dirname(__FILE__).
"/StdLib--ZipCodeCoords.txt",
"r");
593 # if we couldn't open the file, we can't look up a value 594 if ($FHandle === FALSE)
596 throw new Exception(
"Unable to open zip code coordinates file");
599 # iterate over our database until we find the desired zip 600 # or run out of database 601 while (($Line = fgetcsv($FHandle, 0,
"\t")) !== FALSE)
603 if ($Line[0] == $Zip)
605 $ZipCache[$Zip] = array(
606 "Lat" => $Line[1],
"Lng" => $Line[2]);
611 # if we've scanned the entire file and have no coords for 612 # this zip, cache a failure 613 if (!isset($ZipCache[$Zip]))
615 $ZipCache[$Zip] = FALSE;
619 # hand back cached value 620 return $ZipCache[$Zip];
633 $FirstPoint = self::GetLatLngForZipCode($ZipA);
634 $SecondPoint = self::GetLatLngForZipCode($ZipB);
636 # if we scanned the whole file and lack data for either of our 637 # points, return NULL 638 if ($FirstPoint === FALSE || $SecondPoint === FALSE)
643 return self::ComputeGreatCircleDistance(
644 $FirstPoint[
"Lat"], $FirstPoint[
"Lng"],
645 $SecondPoint[
"Lat"], $SecondPoint[
"Lng"]);
660 # See http://en.wikipedia.org/wiki/Great-circle_distance 662 # Convert it all to Radians 663 $Ps = deg2rad($LatSrc);
664 $Ls = deg2rad($LonSrc);
665 $Pf = deg2rad($LatDst);
666 $Lf = deg2rad($LonDst);
668 # Compute the central angle 669 return 3958.756 * atan2(
670 sqrt( pow(cos($Pf)*sin($Lf-$Ls), 2) +
671 pow(cos($Ps)*sin($Pf) -
672 sin($Ps)*cos($Pf)*cos($Lf-$Ls), 2)),
673 sin($Ps)*sin($Pf)+cos($Ps)*cos($Pf)*cos($Lf-$Ls));
689 # See http://mathforum.org/library/drmath/view/55417.html 691 # Convert angles to radians 692 $Ps = deg2rad($LatSrc);
693 $Ls = deg2rad($LonSrc);
694 $Pf = deg2rad($LatDst);
695 $Lf = deg2rad($LonDst);
697 return rad2deg(atan2(sin($Lf-$Ls)*cos($Pf),
698 cos($Ps)*sin($Pf)-sin($Ps)*cos($Pf)*cos($Lf-$Ls)));
712 $Result = array($Perms);
717 for ($Index = count(
$Items) - 1; $Index >= 0; --$Index)
721 list($Segment) = array_splice($NewItems, $Index, 1);
722 array_unshift($NewPerms, $Segment);
723 $Result = array_merge($Result,
724 self::ArrayPermutations($NewItems, $NewPerms));
743 "CA" =>
"California",
745 "CT" =>
"Connecticut",
747 "DC" =>
"District of Columbia",
760 "MA" =>
"Massachusetts",
763 "MS" =>
"Mississippi",
768 "NH" =>
"New Hampshire",
769 "NJ" =>
"New Jersey",
770 "NM" =>
"New Mexico",
772 "NC" =>
"North Carolina",
773 "ND" =>
"North Dakota",
777 "PA" =>
"Pennsylvania",
778 "RI" =>
"Rhode Island",
779 "SC" =>
"South Carolina",
780 "SD" =>
"South Dakota",
786 "WA" =>
"Washington",
787 "WV" =>
"West Virginia",
797 # ---- PRIVATE INTERFACE ------------------------------------------------- 799 private static $PluralizePatterns = array(
800 '/(quiz)$/i' =>
"$1zes",
801 '/^(ox)$/i' =>
"$1en",
802 '/([m|l])ouse$/i' =>
"$1ice",
803 '/(matr|vert|ind)ix|ex$/i' =>
"$1ices",
804 '/(x|ch|ss|sh)$/i' =>
"$1es",
805 '/([^aeiouy]|qu)y$/i' =>
"$1ies",
806 '/(hive)$/i' =>
"$1s",
807 '/(?:([^f])fe|([lr])f)$/i' =>
"$1$2ves",
808 '/(shea|lea|loa|thie)f$/i' =>
"$1ves",
810 '/([ti])um$/i' =>
"$1a",
811 '/(tomat|potat|ech|her|vet)o$/i'=>
"$1oes",
812 '/(bu)s$/i' =>
"$1ses",
813 '/(alias)$/i' =>
"$1es",
814 '/(octop)us$/i' =>
"$1i",
815 '/(ax|test)is$/i' =>
"$1es",
816 '/(us)$/i' =>
"$1es",
820 private static $SingularizePatterns = array(
821 '/(quiz)zes$/i' =>
"$1",
822 '/(matr)ices$/i' =>
"$1ix",
823 '/(vert|ind)ices$/i' =>
"$1ex",
824 '/^(ox)en$/i' =>
"$1",
825 '/(alias)es$/i' =>
"$1",
826 '/(octop|vir)i$/i' =>
"$1us",
827 '/(cris|ax|test)es$/i' =>
"$1is",
828 '/(shoe)s$/i' =>
"$1",
830 '/(bus)es$/i' =>
"$1",
831 '/([m|l])ice$/i' =>
"$1ouse",
832 '/(x|ch|ss|sh)es$/i' =>
"$1",
833 '/(m)ovies$/i' =>
"$1ovie",
834 '/(s)eries$/i' =>
"$1eries",
835 '/([^aeiouy]|qu)ies$/i' =>
"$1y",
836 '/([lr])ves$/i' =>
"$1f",
837 '/(tive)s$/i' =>
"$1",
838 '/(hive)s$/i' =>
"$1",
839 '/(li|wi|kni)ves$/i' =>
"$1fe",
840 '/(shea|loa|lea|thie)ves$/i'=>
"$1f",
841 '/(^analy)ses$/i' =>
"$1sis",
842 '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' =>
"$1$2sis",
843 '/([ti])a$/i' =>
"$1um",
844 '/(n)ews$/i' =>
"$1ews",
845 '/(h|bl)ouses$/i' =>
"$1ouse",
846 '/(corpse)s$/i' =>
"$1",
847 '/(us)es$/i' =>
"$1",
850 private static $IrregularWords = array(
855 'child' =>
'children',
860 private static $UncountableWords = array(
879 private static function CallMbStringFuncIfAvailable($Func, $Args, $NumPlainArgs)
881 if (function_exists(
"mb_".$Func))
883 $FuncToCall =
"mb_".$Func;
887 if (count($Args) > $NumPlainArgs)
890 "Function mb_".$Func.
"() required but not available.");
894 return call_user_func_array($FuncToCall, $Args);
static strrpos()
Multibyte-aware (if supported in PHP) version of strrpos().
static CheckMyCaller($DesiredCaller, $ExceptionMsg=NULL)
Check the caller of the current function.
static SortCompare($A, $B)
Perform compare and return value appropriate for sort function callbacks.
static ZipCodeDistance($ZipA, $ZipB)
Compute the distance between two US ZIP codes.
static strpos()
Multibyte-aware (if supported in PHP) version of strpos().
static GetMyCaller()
Get string with file and line number for call to current function.
static Pluralize($Word)
Pluralize an English word.
static GetLatLngForZipCode($Zip)
Look up the GPS coordinates for a US ZIP code.
Standard utility library.
static ArrayPermutations($Items, $Perms=array())
Return all possible permutations of a given array.
static substr()
Multibyte-aware (if supported in PHP) version of substr().
static GetBacktraceAsString($IncludeArgs=TRUE)
Get backtrace as a string.
static ComputeGreatCircleDistance($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the distance in kilometers between two points, assuming a spherical earth.
static ComputeBearing($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the initial angle on a course connecting two points, assuming a spherical earth...
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
static NeatlyTruncateString($String, $MaxLength, $BreakAnywhere=FALSE)
Attempt to truncate a string as neatly as possible with respect to word breaks, punctuation, and HTML tags.
static strlen()
Multibyte-aware (if supported in PHP) version of strlen().
static Singularize($Word)
Singularize an English word.
static GetUsStatesList()
Get an array of US state names with their two-letter abbreviations as the index.
static GetCallerInfo($Element=NULL)
Get info about call to current function.