From 532fbf11fc0c0c975147b03bc1905d1745f147e0 Mon Sep 17 00:00:00 2001 From: Edward Gilbert Date: Tue, 3 Sep 2024 12:28:44 -0700 Subject: [PATCH] Api occurrence 2024 08 (#1688) - Occurrence API endpoint development -- Add collector, collector last name, and collector number as searchable functions -- Develop write functions (insert, update, delete), but not yet activated for public use -- Add new endpoint for loading and processing skeletal occurrence data -- Add swagger documentation for skeletal record processing - Taxonomy API endpoint development -- Refactor to use query builder for defining search terms (bring code in sync NEON taxonomy developments) - Mics -- Add helper functions to Lumen API --- api/app/Helpers/GPoint.php | 560 +++++++++ api/app/Helpers/Helper.php | 53 + api/app/Helpers/OccurrenceHelper.php | 1103 +++++++++++++++++ api/app/Helpers/TaxonomyHelper.php | 367 ++++++ api/app/Http/Controllers/MediaController.php | 2 +- .../Http/Controllers/OccurrenceController.php | 431 ++++++- .../Http/Controllers/TaxonomyController.php | 33 +- api/app/Models/Occurrence.php | 34 +- api/composer.json | 5 +- api/routes/web.php | 1 + api/storage/api-docs/api-docs.json | 212 +++- api/vendor/composer/autoload_classmap.php | 6 + api/vendor/composer/autoload_files.php | 11 +- api/vendor/composer/autoload_psr4.php | 2 +- api/vendor/composer/autoload_static.php | 219 ++-- imagelib/imgdetails.php | 23 +- 16 files changed, 2831 insertions(+), 231 deletions(-) create mode 100644 api/app/Helpers/GPoint.php create mode 100644 api/app/Helpers/Helper.php create mode 100644 api/app/Helpers/OccurrenceHelper.php create mode 100644 api/app/Helpers/TaxonomyHelper.php diff --git a/api/app/Helpers/GPoint.php b/api/app/Helpers/GPoint.php new file mode 100644 index 0000000000..a3eb4df06a --- /dev/null +++ b/api/app/Helpers/GPoint.php @@ -0,0 +1,560 @@ + was +** missing in front of a couple of variables. Thanks to Bob +** Robins of Maryland for catching the bugs. +** 1.2 05/18/2007 Added default of NULL to $LongOrigin arguement in convertTMtoLL() +** and convertLLtoTM() to eliminate warning messages when the +** methods are called without a value for $LongOrigin. +** 1.3 02/21/2008 Fixed a bug in the distanceFrom method, where the input parameters +** were not being converted to radians prior to calculating the +** distance. Thanks to Enrico Benco for finding pointing it out. +*/ +define ("meter2nm", (1/1852)); +define ("nm2meter", 1852); + +/*------------------------------------------------------------------------------ +** class gPoint ... for Geographic Point +** +** This class encapsulates the methods for representing a geographic point on the +** earth in three different coordinate systema. Lat/Long, UTM and Lambert Conic +** Conformal. +*/ +class GPoint +{ +/* Reference ellipsoids derived from Peter H. Dana's website- +** http://www.colorado.edu/geography/gcraft/notes/datum/datum_f.html +** email: pdana@pdana.com, web page: www.pdana.com +** +** Source: +** Defense Mapping Agency. 1987b. DMA Technical Report: Supplement to Department +** of Defense World Geodetic System 1984 Technical Report. Part I and II. +** Washington, DC: Defense Mapping Agency +*/ + var $ellipsoid = array(//Ellipsoid name, Equatorial Radius, square of eccentricity + "Airy" =>array (6377563, 0.00667054), + "Australian National" =>array (6378160, 0.006694542), + "Bessel 1841" =>array (6377397, 0.006674372), + "Bessel 1841 Nambia" =>array (6377484, 0.006674372), + "Clarke 1866" =>array (6378206, 0.006768658), //NAD 27 + "Clarke 1880" =>array (6378249, 0.006803511), + "Everest" =>array (6377276, 0.006637847), + "Fischer 1960 Mercury" =>array (6378166, 0.006693422), + "Fischer 1968" =>array (6378150, 0.006693422), + "GRS 1967" =>array (6378160, 0.006694605), + "GRS 1980" =>array (6378137, 0.00669438), //NAD 83 + "Helmert 1906" =>array (6378200, 0.006693422), + "Hough" =>array (6378270, 0.00672267), + "International" =>array (6378388, 0.00672267), + "Krassovsky" =>array (6378245, 0.006693422), + "Modified Airy" =>array (6377340, 0.00667054), + "Modified Everest" =>array (6377304, 0.006637847), + "Modified Fischer 1960" =>array (6378155, 0.006693422), + "South American 1969" =>array (6378160, 0.006694542), + "WGS 60" =>array (6378165, 0.006693422), + "WGS 66" =>array (6378145, 0.006694542), + "WGS 72" =>array (6378135, 0.006694318), + "WGS 84" =>array (6378137, 0.00669438)); + + // Properties + var $a; // Equatorial Radius + var $e2; // Square of eccentricity + var $datum; // Selected datum + var $Xp, $Yp; // X,Y pixel location + var $lat, $long; // Latitude & Longitude of the point + var $utmNorthing, $utmEasting, $utmZone; // UTM Coordinates of the point + var $lccNorthing, $lccEasting; // Lambert coordinates of the point + var $falseNorthing, $falseEasting; // Origin coordinates for Lambert Projection + var $latOfOrigin; // For Lambert Projection + var $longOfOrigin; // For Lambert Projection + var $firstStdParallel; // For lambert Projection + var $secondStdParallel; // For lambert Projection + + // constructor + function __construct($datum='WGS 84') // Default datum is WGS 84 + { + $this->setDatum($datum); + } + + function setDatum($datum){ + if(preg_match('/nad\s*83/i',$datum)){ + //NAD 83 + $datum = "GRS 1980"; + }elseif(preg_match('/nad\s*27/i',$datum)){ + //NAD 27 + $datum = "Clarke 1866"; + } + else{ + $datum = 'WGS 84'; + } + $this->a = $this->ellipsoid[$datum][0]; // Set datum Equatorial Radius + $this->e2 = $this->ellipsoid[$datum][1]; // Set datum Square of eccentricity + $this->datum = $datum; // Save the datum + } + // + // Set/Get X & Y pixel of the point (used if it is being drawn on an image) + // + function setXY($x, $y) + { + $this->Xp = $x; $this->Yp = $y; + } + function Xp() { return $this->Xp; } + function Yp() { return $this->Yp; } + // + // Set/Get/Output Longitude & Latitude of the point + // + function setLongLat($long, $lat) + { + $this->long = $long; $this->lat = $lat; + } + function Lat() { return $this->lat; } + function Long() { return $this->long; } + function printLatLong() { printf("Latitude: %1.5f Longitude: %1.5f",$this->lat, $this->long); } + // + // Set/Get/Output Universal Transverse Mercator Coordinates + // + function setUTM($easting, $northing, $zone='') // Zone is optional + { + $this->utmNorthing = (int)$northing; + $this->utmEasting = (int)$easting; + $this->utmZone = $zone; + } + function N() { return $this->utmNorthing; } + function E() { return $this->utmEasting; } + function Z() { return $this->utmZone; } + function printUTM() { print( "Northing: ".$this->utmNorthing.", Easting: ".$this->utmEasting.", Zone: ".$this->utmZone); } + // + // Set/Get/Output Lambert Conic Conformal Coordinates + // + function setLambert($easting, $northing) + { + $this->lccNorthing = $northing; + $this->lccEasting = $easting; + } + function lccN() { return $this->lccNorthing; } + function lccE() { return $this->lccEasting; } + function printLambert() { print( "Northing: ".(int)$this->lccNorthing.", Easting: ".(int)$this->lccEasting); } + +//------------------------------------------------------------------------------ +// +// Convert Longitude/Latitude to UTM +// +// Equations from USGS Bulletin 1532 +// East Longitudes are positive, West longitudes are negative. +// North latitudes are positive, South latitudes are negative +// Lat and Long are in decimal degrees +// Written by Chuck Gantz- chuck dot gantz at globalstar dot com, converted to PHP by +// Brenor Brophy, brenor dot brophy at gmail dot com +// +// UTM coordinates are useful when dealing with paper maps. Basically the +// map will can cover a single UTM zone which is 6 degrees on longitude. +// So you really don't care about an object crossing two zones. You just get a +// second map of the other zone. However, if you happen to live in a place that +// straddles two zones (For example the Santa Babara area in CA straddles zone 10 +// and zone 11) Then it can become a real pain having to have two maps all the time. +// So relatively small parts of the world (like say California) create their own +// version of UTM coordinates that are adjusted to conver the whole area of interest +// on a single map. These are called state grids. The projection system is the +// usually same as UTM (i.e. Transverse Mercator), but the central meridian +// aka Longitude of Origin is selected to suit the logitude of the area being +// mapped (like being moved to the central meridian of the area) and the grid +// may cover more than the 6 degrees of lingitude found on a UTM map. Areas +// that are wide rather than long - think Montana as an example. May still +// have to have a couple of maps to cover the whole state because TM projection +// looses accuracy as you move further away from the Longitude of Origin, 15 degrees +// is usually the limit. +// +// Now, in the case where we want to generate electronic maps that may be +// placed pretty much anywhere on the globe we really don't to deal with the +// issue of UTM zones in our coordinate system. We would really just like a +// grid that is fully contigious over the area of the map we are drawing. Similiar +// to the state grid, but local to the area we are interested in. I call this +// Local Transverse Mercator and I have modified the function below to also +// make this conversion. If you pass a Longitude value to the function as $LongOrigin +// then that is the Longitude of Origin that will be used for the projection. +// Easting coordinates will be returned (in meters) relative to that line of +// longitude - So an Easting coordinate for a point located East of the longitude +// of origin will be a positive value in meters, an Easting coordinate for a point +// West of the longitude of Origin will have a negative value in meters. Northings +// will always be returned in meters from the equator same as the UTM system. The +// UTMZone value will be valid for Long/Lat given - thought it is not meaningful +// in the context of Local TM. If a NULL value is passed for $LongOrigin +// then the standard UTM coordinates are calculated. +// + function convertLLtoTM($LongOrigin = NULL) + { + $k0 = 0.9996; + $falseEasting = 0.0; + + //Make sure the longitude is between -180.00 .. 179.9 + $LongTemp = ($this->long+180)-(integer)(($this->long+180)/360)*360-180; // -180.00 .. 179.9; + $LatRad = deg2rad($this->lat); + $LongRad = deg2rad($LongTemp); + + if (!$LongOrigin) + { // Do a standard UTM conversion - so findout what zone the point is in + $ZoneNumber = (integer)(($LongTemp + 180)/6) + 1; + // Special zone for South Norway + if( $this->lat >= 56.0 && $this->lat < 64.0 && $LongTemp >= 3.0 && $LongTemp < 12.0 ) // Fixed 1.1 + $ZoneNumber = 32; + // Special zones for Svalbard + if( $this->lat >= 72.0 && $this->lat < 84.0 ) + { + if( $LongTemp >= 0.0 && $LongTemp < 9.0 ) $ZoneNumber = 31; + else if( $LongTemp >= 9.0 && $LongTemp < 21.0 ) $ZoneNumber = 33; + else if( $LongTemp >= 21.0 && $LongTemp < 33.0 ) $ZoneNumber = 35; + else if( $LongTemp >= 33.0 && $LongTemp < 42.0 ) $ZoneNumber = 37; + } + $LongOrigin = ($ZoneNumber - 1)*6 - 180 + 3; //+3 puts origin in middle of zone + //compute the UTM Zone from the latitude and longitude + $this->utmZone = sprintf("%d%s", $ZoneNumber, $this->UTMLetterDesignator()); + // We also need to set the false Easting value adjust the UTM easting coordinate + $falseEasting = 500000.0; + } + $LongOriginRad = deg2rad($LongOrigin); + + $eccPrimeSquared = ($this->e2)/(1-$this->e2); + + $N = $this->a/sqrt(1-$this->e2*sin($LatRad)*sin($LatRad)); + $T = tan($LatRad)*tan($LatRad); + $C = $eccPrimeSquared*cos($LatRad)*cos($LatRad); + $A = cos($LatRad)*($LongRad-$LongOriginRad); + + $M = $this->a*((1 - $this->e2/4 - 3*$this->e2*$this->e2/64 - 5*$this->e2*$this->e2*$this->e2/256)*$LatRad + - (3*$this->e2/8 + 3*$this->e2*$this->e2/32 + 45*$this->e2*$this->e2*$this->e2/1024)*sin(2*$LatRad) + + (15*$this->e2*$this->e2/256 + 45*$this->e2*$this->e2*$this->e2/1024)*sin(4*$LatRad) + - (35*$this->e2*$this->e2*$this->e2/3072)*sin(6*$LatRad)); + + $this->utmEasting = ($k0*$N*($A+(1-$T+$C)*$A*$A*$A/6 + + (5-18*$T+$T*$T+72*$C-58*$eccPrimeSquared)*$A*$A*$A*$A*$A/120) + + $falseEasting); + + $this->utmNorthing = ($k0*($M+$N*tan($LatRad)*($A*$A/2+(5-$T+9*$C+4*$C*$C)*$A*$A*$A*$A/24 + + (61-58*$T+$T*$T+600*$C-330*$eccPrimeSquared)*$A*$A*$A*$A*$A*$A/720))); + if($this->lat < 0) + $this->utmNorthing += 10000000.0; //10000000 meter offset for southern hemisphere + } +// +// This routine determines the correct UTM letter designator for the given latitude +// returns 'Z' if latitude is outside the UTM limits of 84N to 80S +// Written by Chuck Gantz- chuck dot gantz at globalstar dot com, converted to PHP by +// Brenor Brophy, brenor dot brophy at gmail dot com +// + function UTMLetterDesignator() + { + if((84 >= $this->lat) && ($this->lat >= 72)) $LetterDesignator = 'X'; + else if((72 > $this->lat) && ($this->lat >= 64)) $LetterDesignator = 'W'; + else if((64 > $this->lat) && ($this->lat >= 56)) $LetterDesignator = 'V'; + else if((56 > $this->lat) && ($this->lat >= 48)) $LetterDesignator = 'U'; + else if((48 > $this->lat) && ($this->lat >= 40)) $LetterDesignator = 'T'; + else if((40 > $this->lat) && ($this->lat >= 32)) $LetterDesignator = 'S'; + else if((32 > $this->lat) && ($this->lat >= 24)) $LetterDesignator = 'R'; + else if((24 > $this->lat) && ($this->lat >= 16)) $LetterDesignator = 'Q'; + else if((16 > $this->lat) && ($this->lat >= 8)) $LetterDesignator = 'P'; + else if(( 8 > $this->lat) && ($this->lat >= 0)) $LetterDesignator = 'N'; + else if(( 0 > $this->lat) && ($this->lat >= -8)) $LetterDesignator = 'M'; + else if((-8 > $this->lat) && ($this->lat >= -16)) $LetterDesignator = 'L'; + else if((-16 > $this->lat) && ($this->lat >= -24)) $LetterDesignator = 'K'; + else if((-24 > $this->lat) && ($this->lat >= -32)) $LetterDesignator = 'J'; + else if((-32 > $this->lat) && ($this->lat >= -40)) $LetterDesignator = 'H'; + else if((-40 > $this->lat) && ($this->lat >= -48)) $LetterDesignator = 'G'; + else if((-48 > $this->lat) && ($this->lat >= -56)) $LetterDesignator = 'F'; + else if((-56 > $this->lat) && ($this->lat >= -64)) $LetterDesignator = 'E'; + else if((-64 > $this->lat) && ($this->lat >= -72)) $LetterDesignator = 'D'; + else if((-72 > $this->lat) && ($this->lat >= -80)) $LetterDesignator = 'C'; + else $LetterDesignator = 'Z'; //This is here as an error flag to show that the Latitude is outside the UTM limits + + return($LetterDesignator); + } + +//------------------------------------------------------------------------------ +// +// Convert UTM to Longitude/Latitude +// +// Equations from USGS Bulletin 1532 +// East Longitudes are positive, West longitudes are negative. +// North latitudes are positive, South latitudes are negative +// Lat and Long are in decimal degrees. +// Written by Chuck Gantz- chuck dot gantz at globalstar dot com, converted to PHP by +// Brenor Brophy, brenor dot brophy at gmail dot com +// +// If a value is passed for $LongOrigin then the function assumes that +// a Local (to the Longitude of Origin passed in) Transverse Mercator +// coordinates is to be converted - not a UTM coordinate. This is the +// complementary function to the previous one. The function cannot +// tell if a set of Northing/Easting coordinates are in the North +// or South hemesphere - they just give distance from the equator not +// direction - so only northern hemesphere lat/long coordinates are returned. +// If you live south of the equator there is a note later in the code +// explaining how to have it just return southern hemesphere lat/longs. +// + function convertTMtoLL($LongOrigin = NULL) + { + $k0 = 0.9996; + $e1 = (1-sqrt(1-$this->e2))/(1+sqrt(1-$this->e2)); + $falseEasting = 0.0; + $y = $this->utmNorthing; + + if (!$LongOrigin) + { // It is a UTM coordinate we want to convert + sscanf($this->utmZone,"%d%s",$ZoneNumber,$ZoneLetter); + $NorthernHemisphere = 1; //point is in northern hemisphere + $isSouthern = false; + if($ZoneLetter){ + if(strtoupper($ZoneLetter) < 'N'){ + $isSouthern = true; + } + if(strtoupper($ZoneLetter) == 'S'){ + if(($ZoneNumber > 18 && $ZoneNumber < 23) || $y < 3540000 || $y > 4420000){ + //Is not MGRS grid zone S within the northern hemisphere + $isSouthern = true; + } + } + } + if($isSouthern){ + $NorthernHemisphere = 0;//point is in southern hemisphere + $y -= 10000000.0; //remove 10,000,000 meter offset used for southern hemisphere + } + $LongOrigin = ($ZoneNumber - 1)*6 - 180 + 3; //+3 puts origin in middle of zone + $falseEasting = 500000.0; + } + +// $y -= 10000000.0; // Uncomment line to make LOCAL coordinates return southern hemesphere Lat/Long + $x = $this->utmEasting - $falseEasting; //remove 500,000 meter offset for longitude + + $eccPrimeSquared = ($this->e2)/(1-$this->e2); + + $M = $y / $k0; + $mu = $M/($this->a*(1-$this->e2/4-3*$this->e2*$this->e2/64-5*$this->e2*$this->e2*$this->e2/256)); + + $phi1Rad = $mu + (3*$e1/2-27*$e1*$e1*$e1/32)*sin(2*$mu) + + (21*$e1*$e1/16-55*$e1*$e1*$e1*$e1/32)*sin(4*$mu) + +(151*$e1*$e1*$e1/96)*sin(6*$mu); + $phi1 = rad2deg($phi1Rad); + + $N1 = $this->a/sqrt(1-$this->e2*sin($phi1Rad)*sin($phi1Rad)); + $T1 = tan($phi1Rad)*tan($phi1Rad); + $C1 = $eccPrimeSquared*cos($phi1Rad)*cos($phi1Rad); + $R1 = $this->a*(1-$this->e2)/pow(1-$this->e2*sin($phi1Rad)*sin($phi1Rad), 1.5); + $D = $x/($N1*$k0); + + $tlat = $phi1Rad - ($N1*tan($phi1Rad)/$R1)*($D*$D/2-(5+3*$T1+10*$C1-4*$C1*$C1-9*$eccPrimeSquared)*$D*$D*$D*$D/24 + +(61+90*$T1+298*$C1+45*$T1*$T1-252*$eccPrimeSquared-3*$C1*$C1)*$D*$D*$D*$D*$D*$D/720); // fixed in 1.1 + $this->lat = rad2deg($tlat); + + $tlong = ($D-(1+2*$T1+$C1)*$D*$D*$D/6+(5-2*$C1+28*$T1-3*$C1*$C1+8*$eccPrimeSquared+24*$T1*$T1) + *$D*$D*$D*$D*$D/120)/cos($phi1Rad); + $this->long = $LongOrigin + rad2deg($tlong); + } + +//------------------------------------------------------------------------------ +// Configure a Lambert Conic Conformal Projection +// +// falseEasting & falseNorthing are just an offset in meters added to the final +// coordinate calculated. +// +// longOfOrigin & LatOfOrigin are the "center" latitiude and longitude of the +// area being projected. All coordinates will be calculated in meters relative +// to this point on the earth. +// +// firstStdParallel & secondStdParallel are the two lines of longitude (that +// is they run east-west) that define where the "cone" intersects the earth. +// Simply put they should bracket the area being projected. +// +// google is your friend to find out more +// + function configLambertProjection ($falseEasting, $falseNorthing, + $longOfOrigin, $latOfOrigin, + $firstStdParallel, $secondStdParallel) + { + $this->falseEasting = $falseEasting; + $this->falseNorthing = $falseNorthing; + $this->longOfOrigin = $longOfOrigin; + $this->latOfOrigin = $latOfOrigin; + $this->firstStdParallel = $firstStdParallel; + $this->secondStdParallel = $secondStdParallel; + } + +//------------------------------------------------------------------------------ +// +// Convert Longitude/Latitude to Lambert Conic Easting/Northing +// +// This routine will convert a Latitude/Longitude coordinate to an Northing/ +// Easting coordinate on a Lambert Conic Projection. The configLambertProjection() +// function should have been called prior to this one to setup the specific +// parameters for the projection. The Northing/Easting parameters calculated are +// in meters (because the datum used is in meters) and are relative to the +// falseNorthing/falseEasting coordinate. Which in turn is relative to the +// Lat/Long of origin The formula were obtained from URL: +// http://www.ihsenergy.com/epsg/guid7_2.html. +// Code was written by Brenor Brophy, brenor dot brophy at gmail dot com +// + function convertLLtoLCC() + { + $e = sqrt($this->e2); + + $phi = deg2rad($this->lat); // Latitude to convert + $phi1 = deg2rad($this->firstStdParallel); // Latitude of 1st std parallel + $phi2 = deg2rad($this->secondStdParallel); // Latitude of 2nd std parallel + $lamda = deg2rad($this->long); // Lonitude to convert + $phio = deg2rad($this->latOfOrigin); // Latitude of Origin + $lamdao = deg2rad($this->longOfOrigin); // Longitude of Origin + + $m1 = cos($phi1) / sqrt(( 1 - $this->e2*sin($phi1)*sin($phi1))); + $m2 = cos($phi2) / sqrt(( 1 - $this->e2*sin($phi2)*sin($phi2))); + $t1 = tan((pi()/4)-($phi1/2)) / pow(( ( 1 - $e*sin($phi1) ) / ( 1 + $e*sin($phi1) )),$e/2); + $t2 = tan((pi()/4)-($phi2/2)) / pow(( ( 1 - $e*sin($phi2) ) / ( 1 + $e*sin($phi2) )),$e/2); + $to = tan((pi()/4)-($phio/2)) / pow(( ( 1 - $e*sin($phio) ) / ( 1 + $e*sin($phio) )),$e/2); + $t = tan((pi()/4)-($phi /2)) / pow(( ( 1 - $e*sin($phi ) ) / ( 1 + $e*sin($phi ) )),$e/2); + $n = (log($m1)-log($m2)) / (log($t1)-log($t2)); + $F = $m1/($n*pow($t1,$n)); + $rf = $this->a*$F*pow($to,$n); + $r = $this->a*$F*pow($t,$n); + $theta = $n*($lamda - $lamdao); + + $this->lccEasting = $this->falseEasting + $r*sin($theta); + $this->lccNorthing = $this->falseNorthing + $rf - $r*cos($theta); + } +//------------------------------------------------------------------------------ +// +// Convert Easting/Northing on a Lambert Conic projection to Longitude/Latitude +// +// This routine will convert a Lambert Northing/Easting coordinate to an +// Latitude/Longitude coordinate. The configLambertProjection() function should +// have been called prior to this one to setup the specific parameters for the +// projection. The Northing/Easting parameters are in meters (because the datum +// used is in meters) and are relative to the falseNorthing/falseEasting +// coordinate. Which in turn is relative to the Lat/Long of origin The formula +// were obtained from URL http://www.ihsenergy.com/epsg/guid7_2.html. Code +// was written by Brenor Brophy, brenor dot brophy at gmail dot com +// + function convertLCCtoLL() + { + $e = sqrt($this->e2); + + $phi1 = deg2rad($this->firstStdParallel); // Latitude of 1st std parallel + $phi2 = deg2rad($this->secondStdParallel); // Latitude of 2nd std parallel + $phio = deg2rad($this->latOfOrigin); // Latitude of Origin + $lamdao = deg2rad($this->longOfOrigin); // Longitude of Origin + $E = $this->lccEasting; + $N = $this->lccNorthing; + $Ef = $this->falseEasting; + $Nf = $this->falseNorthing; + + $m1 = cos($phi1) / sqrt(( 1 - $this->e2*sin($phi1)*sin($phi1))); + $m2 = cos($phi2) / sqrt(( 1 - $this->e2*sin($phi2)*sin($phi2))); + $t1 = tan((pi()/4)-($phi1/2)) / pow(( ( 1 - $e*sin($phi1) ) / ( 1 + $e*sin($phi1) )),$e/2); + $t2 = tan((pi()/4)-($phi2/2)) / pow(( ( 1 - $e*sin($phi2) ) / ( 1 + $e*sin($phi2) )),$e/2); + $to = tan((pi()/4)-($phio/2)) / pow(( ( 1 - $e*sin($phio) ) / ( 1 + $e*sin($phio) )),$e/2); + $n = (log($m1)-log($m2)) / (log($t1)-log($t2)); + $F = $m1/($n*pow($t1,$n)); + $rf = $this->a*$F*pow($to,$n); + $r_ = sqrt( pow(($E-$Ef),2) + pow(($rf-($N-$Nf)),2) ); + $t_ = pow($r_/($this->a*$F),(1/$n)); + $theta_ = atan(($E-$Ef)/($rf-($N-$Nf))); + + $lamda = $theta_/$n + $lamdao; + $phi0 = (pi()/2) - 2*atan($t_); + $phi1 = (pi()/2) - 2*atan($t_*pow(((1-$e*sin($phi0))/(1+$e*sin($phi0))),$e/2)); + $phi2 = (pi()/2) - 2*atan($t_*pow(((1-$e*sin($phi1))/(1+$e*sin($phi1))),$e/2)); + $phi = (pi()/2) - 2*atan($t_*pow(((1-$e*sin($phi2))/(1+$e*sin($phi2))),$e/2)); + + $this->lat = rad2deg($phi); + $this->long = rad2deg($lamda); + } + +//------------------------------------------------------------------------------ +// This is a useful function that returns the Great Circle distance from the +// gPoint to another Long/Lat coordinate +// +// Result is returned as meters +// + function distanceFrom($lon1, $lat1) + { + $lon1 = deg2rad($lon1); $lat1 = deg2rad($lat1); // Added in 1.3 + $lon2 = deg2rad($this->Long()); $lat2 = deg2rad($this->Lat()); + + $theta = $lon2 - $lon1; + $dist = acos(sin($lat1) * sin($lat2) + cos($lat1) * cos($lat2) * cos($theta)); + +// Alternative formula supposed to be more accurate for short distances +// $dist = 2*asin(sqrt( pow(sin(($lat1-$lat2)/2),2) + cos($lat1)*cos($lat2)*pow(sin(($lon1-$lon2)/2),2))); + return ( $dist * 6366710 ); // from http://williams.best.vwh.net/avform.htm#GCF + } + +//------------------------------------------------------------------------------ +// This function also calculates the distance between two points. In this case +// it just uses Pythagoras's theorm using TM coordinates. +// + function distanceFromTM(&$pt) + { + $E1 = $pt->E(); $N1 = $pt->N(); + $E2 = $this->E(); $N2 = $this->N(); + + $dist = sqrt(pow(($E1-$E2),2)+pow(($N1-$N2),2)); + return $dist; + } + +//------------------------------------------------------------------------------ +// This function geo-references a geoPoint to a given map. This means that it +// calculates the x,y pixel coordinate that coresponds to the Lat/Long value of +// the geoPoint. The calculation is done using the Transverse Mercator(TM) +// coordinates of the gPoint with respect to the TM coordinates of the center +// point of the map. So this only makes sense if you are using Local TM +// projection. +// +// $rX & $rY are the pixel coordinates that corespond to the Northing/Easting +// ($rE/$rN) coordinate it is to this coordinate that the point will be +// geo-referenced. The $LongOrigin is needed to make sure the Easting/Northing +// coordinates of the point are correctly converted. +// + function gRef($rX, $rY, $rE, $rN, $Scale, $LongOrigin) + { + $this->convertLLtoTM($LongOrigin); + $x = (($this->E() - $rE) / $Scale) // The easting in meters times the scale to get pixels + // is relative to the center of the image so adjust to + + ($rX); // the left coordinate. + $y = $rY - // Adjust to bottom coordinate. + (($rN - $this->N()) / $Scale); // The northing in meters + // relative to the equator. Subtract center point northing + // to get relative to image center and convert meters to pixels + $this->setXY((int)$x,(int)$y); // Save the geo-referenced result. + } +} // end of class gPoint + +?> \ No newline at end of file diff --git a/api/app/Helpers/Helper.php b/api/app/Helpers/Helper.php new file mode 100644 index 0000000000..8fdc3de06d --- /dev/null +++ b/api/app/Helpers/Helper.php @@ -0,0 +1,53 @@ + 'by-nd.png', + '/by-sa/' => 'by-sa.png', + '/by-nc-sa/' => 'by-nc-sa.png', + '/by/' => 'by.png', + '/zero/' => 'cc-zero.png' + ); + foreach($ccBadgeArr as $fragment => $fileName){ + if(strpos($inputStr, $fragment)){ + $rightsOutput = ''; + } + } + //If input is a URL, make output a clickable link + if(substr($inputStr, 0, 4) == 'http'){ + $rightsOutput = '' . $rightsOutput . ''; + } + } + $rightsOutput = '' . $rightsOutput . ''; + return $rightsOutput; + } +} diff --git a/api/app/Helpers/OccurrenceHelper.php b/api/app/Helpers/OccurrenceHelper.php new file mode 100644 index 0000000000..f73364fdd6 --- /dev/null +++ b/api/app/Helpers/OccurrenceHelper.php @@ -0,0 +1,1103 @@ +'01','II'=>'02','III'=>'03','IV'=>'04','V'=>'05','VI'=>'06','VII'=>'07','VIII'=>'08','IX'=>'09','X'=>'10','XI'=>'11','XII'=>'12'); + static $monthNames = array('jan'=>'01','ene'=>'01','feb'=>'02','mar'=>'03','abr'=>'04','apr'=>'04','may'=>'05','jun'=>'06','jul'=>'07','ago'=>'08', + 'aug'=>'08','sep'=>'09','oct'=>'10','nov'=>'11','dec'=>'12','dic'=>'12'); + + // Current version for associatedOccurrences JSON + // TODO: is this the best place for it? No other obvious landing spot. + public static $assocOccurVersion = '1.0'; + + public function __construct(){ + } + + public function __destruct(){ + } + + /* + * INPUT: String representing a verbatim date + * OUTPUT: String representing the date in MySQL format (YYYY-MM-DD) + * Time is appended to end if present + * + */ + public static function formatDate($inStr){ + $retDate = ''; + $dateStr = trim($inStr,'.,; '); + if(!$dateStr) return; + $t = ''; + $y = ''; + $m = '00'; + $d = '00'; + //Remove time portion if it exists + if(preg_match('/\d{2}:\d{2}:\d{2}/',$dateStr,$match)){ + $t = $match[0]; + } + //Parse + if(preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})/',$dateStr,$match)){ + //Format: yyyy-m-d or yyyy-mm-dd + $y = $match[1]; + $m = $match[2]; + $d = $match[3]; + } + elseif(preg_match('/^(\d{4})-(\d{1,2})/',$dateStr,$match)){ + //Format: yyyy-m or yyyy-mm + $y = $match[1]; + $m = $match[2]; + } + elseif(preg_match('/^([\d-]{1,5})\.{1}([IVX]{1,4})\.{1}(\d{2,4})/i',$dateStr,$match)){ + //Roman numerial format: dd.IV.yyyy, dd.IV.yy, dd-IV-yyyy, dd-IV-yy + $d = $match[1]; + if(!is_numeric($d)) $d = '00'; + $mStr = strtoupper($match[2]); + $y = $match[3]; + if(array_key_exists($mStr,self::$monthRoman)){ + $m = self::$monthRoman[$mStr]; + } + } + elseif(preg_match('/^(\d{1,2})[\s\/-]{1}(\D{3,})\.*[\s\/-]{1}(\d{2,4})/',$dateStr,$match)){ + //Format: dd mmm yyyy, d mmm yy, dd-mmm-yyyy, dd-mmm-yy + $d = $match[1]; + $mStr = $match[2]; + $y = $match[3]; + $mStr = strtolower(substr($mStr,0,3)); + if(array_key_exists($mStr,self::$monthNames)){ + $m = self::$monthNames[$mStr]; + } + } + elseif(preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/',$dateStr,$match)){ + //Format: mm/dd/yyyy, m/d/yy + $m = $match[1]; + $d = $match[2]; + $y = $match[3]; + } + elseif(preg_match('/^(\D{3,})\.*\s{0,2}(\d{1,2})[,\s]+([1,2]{1}[0,5-9]{1}\d{2})$/',$dateStr,$match)){ + //Format: mmm dd, yyyy + $mStr = $match[1]; + $d = $match[2]; + $y = $match[3]; + $mStr = strtolower(substr($mStr,0,3)); + if(array_key_exists($mStr,self::$monthNames)) $m = self::$monthNames[$mStr]; + } + elseif(preg_match('/^(\d{1,2})-(\d{1,2})-(\d{2,4})/',$dateStr,$match)){ + //Format: mm-dd-yyyy, mm-dd-yy + $m = $match[1]; + $d = $match[2]; + $y = $match[3]; + } + elseif(preg_match('/^(\D{3,})\.*\s+([1,2]{1}[0,5-9]{1}\d{2})/',$dateStr,$match)){ + //Format: mmm yyyy + $mStr = strtolower(substr($match[1],0,3)); + if(array_key_exists($mStr,self::$monthNames)) $m = self::$monthNames[$mStr]; + else $m = '00'; + $y = $match[2]; + } + else{ + if(preg_match('/(1[5-9]{1}\d{2}|20\d{2})/',$dateStr,$match)) $y = $match[1]; + if(preg_match_all('/([a-z]+)/i',$dateStr,$match)){ + foreach($match[1] as $test){ + $subStr = strtolower(substr($test,0,3)); + if(array_key_exists($subStr, self::$monthNames)){ + $m = self::$monthNames[$subStr]; + break; + } + } + } + if(!(int)$m){ + if(preg_match('/([IVX]{1,4})/',$dateStr,$match)){ + $mStr = $match[1]; + if(array_key_exists($mStr,self::$monthRoman)) $m = self::$monthRoman[$mStr]; + } + } + if(!(int)$m){ + if(preg_match_all('/(\d+)/',$dateStr,$match)){ + foreach($match[1] as $test){ + if($test < 13){ + $m = $test; + break; + } + } + } + } + } + //Clean, configure, return + if(!is_numeric($y)) $y = 0; + if(!is_numeric($m)) $m = '00'; + if(!is_numeric($d)) $d = '00'; + if($y){ + if(strlen($m) == 1) $m = '0'.$m; + if(strlen($d) == 1) $d = '0'.$d; + //Check to see if month is valid + if($m > 12){ + $m = '00'; + $d = '00'; + } + //check to see if day is valid for month + if($m == 2 && $d == 29){ + //Test leap date + if(!checkdate($m,$d,$y)) $d = '00'; + } + elseif($d > 31 || $m == 2 && $d > 29 || (in_array($m, array(4,6,9,11)) && $d > 30)){ + $d = '00'; + } + //Do some cleaning + if(strlen($y) == 2){ + if($y <= date('y')) $y = '20'.$y; + else $y = '19'.$y; + } + //Build + $retDate = $y.'-'.$m.'-'.$d; + } + elseif(($timestamp = strtotime($retDate)) !== false){ + $retDate = date('Y-m-d', $timestamp); + } + if($t){ + $retDate .= ' '.$t; + } + return $retDate; + } + + /* + * INPUT: String representing a verbatim scientific name + * Name may have imbedded authors, cf, aff, hybrid + * OUTPUT: Array containing parsed values + * Keys: unitind1, unitname1, unitind2, unitname2, unitind3, unitname3, author, identificationqualifier + */ + public static function parseScientificName($inStr, $conn = null, $rankId = 0){ + $taxonArr = Taxonomy::parseScientificName($inStr, $conn, $rankId); + if(array_key_exists('unitind1',$taxonArr)){ + $taxonArr['unitname1'] = $taxonArr['unitind1'].' '.$taxonArr['unitname1']; + unset($taxonArr['unitind1']); + } + if(array_key_exists('unitind2',$taxonArr)){ + $taxonArr['unitname2'] = $taxonArr['unitind2'].' '.$taxonArr['unitname2']; + unset($taxonArr['unitind2']); + } + return $taxonArr; + } + + /* + * INPUT: String representing verbatim elevation + * Verbatim string represent feet or meters + * OUTPUT: Array containing minimum and maximun elevation in meters + * Keys: minelev, maxelev + */ + public static function parseVerbatimElevation($inStr){ + $retArr = array(); + //Start parsing + if(preg_match('/([\.\d]+)\s*-\s*([\.\d]+)\s*meter/i',$inStr,$m)){ + $retArr['minelev'] = $m[1]; + $retArr['maxelev'] = $m[2]; + } + elseif(preg_match('/([\.\d]+)\s*-\s*([\.\d]+)\s*m./i',$inStr,$m)){ + $retArr['minelev'] = $m[1]; + $retArr['maxelev'] = $m[2]; + } + elseif(preg_match('/([\.\d]+)\s*-\s*([\.\d]+)\s*m$/i',$inStr,$m)){ + $retArr['minelev'] = $m[1]; + $retArr['maxelev'] = $m[2]; + } + elseif(preg_match('/([\.\d]+)\s*meter/i',$inStr,$m)){ + $retArr['minelev'] = $m[1]; + } + elseif(preg_match('/([\.\d]+)\s*m./i',$inStr,$m)){ + $retArr['minelev'] = $m[1]; + } + elseif(preg_match('/([\.\d]+)\s*m$/i',$inStr,$m)){ + $retArr['minelev'] = $m[1]; + } + elseif(preg_match('/([\.\d]+)[fet\']{,4}\s*-\s*([\.\d]+)\s{,1}[f\']{1}/i',$inStr,$m)){ + if(is_numeric($m[1])) $retArr['minelev'] = (round($m[1]*.3048)); + if(is_numeric($m[2])) $retArr['maxelev'] = (round($m[2]*.3048)); + } + elseif(preg_match('/([\.\d]+)\s*[f\']{1}/i',$inStr,$m)){ + if(is_numeric($m[1])) $retArr['minelev'] = (round($m[1]*.3048)); + } + //Clean + if($retArr){ + if(array_key_exists('minelev',$retArr) && ($retArr['minelev'] > 8000 || $retArr['minelev'] < 0)) unset($retArr['minelev']); + if(array_key_exists('maxelev',$retArr) && ($retArr['maxelev'] > 8000 || $retArr['maxelev'] < 0)) unset($retArr['maxelev']); + } + return $retArr; + } + + /* + * INPUT: String representing verbatim coordinates + * Verbatim string can be UTM, DMS + * OUTPUT: Array containing decimal values of latitude and longitude + * Keys: lat, lng + */ + public static function parseVerbatimCoordinates($inStr,$target=''){ + $retArr = array(); + if(strpos($inStr,' to ')) return $retArr; + if(strpos($inStr,' betw ')) return $retArr; + + //Try to parse lat/lng + $latDeg = 'null';$latMin = 0;$latSec = 0;$latNS = 'N'; + $lngDeg = 'null';$lngMin = 0;$lngSec = 0;$lngEW = 'W'; + //Grab lat deg and min + if(!$target || $target == 'LL'){ + if(preg_match('/([\sNSns]{0,1})(-?\d{1,2}\.{1}\d+)\D{0,1}\s{0,1}([NSns]{0,1})\D{0,1}([\sEWew]{1})(-?\d{1,4}\.{1}\d+)\D{0,1}\s{0,1}([EWew]{0,1})\D*/',$inStr,$m)){ + //Decimal degree format + $retArr['lat'] = $m[2]; + $retArr['lng'] = $m[5]; + $latDir = $m[3]; + if(!$latDir && $m[1]) $latDir = trim($m[1]); + if($retArr['lat'] > 0 && $latDir && ($latDir == 'S' || $latDir == 's')) $retArr['lat'] = -1*$retArr['lat']; + $lngDir = $m[6]; + if(!$lngDir && $m[4]) $lngDir = trim($m[4]); + if($retArr['lng'] > 0 && $latDir && ($lngDir == 'W' || $lngDir == 'w')) $retArr['lng'] = -1*$retArr['lng']; + } + elseif(preg_match('/(\d{1,2})[^\d]{1,3}\s{0,2}(\d{1,2}\.{0,1}\d*)[\']{1}(.*)/i',$inStr,$m)){ + //DMS format + $latDeg = $m[1]; + $latMin = $m[2]; + $leftOver = str_replace("''",'"',trim($m[3])); + //Grab lat NS and lng EW + if(stripos($inStr,'N') === false && strpos($inStr,'S') !== false){ + $latNS = 'S'; + } + if(stripos($inStr,'W') === false && stripos($inStr,'E') !== false){ + $lngEW = 'E'; + } + //Grab lat sec + if(preg_match('/^(\d{1,2}\.{0,1}\d*)["]{1}(.*)/i',$leftOver,$m)){ + $latSec = $m[1]; + if(count($m)>2){ + $leftOver = trim($m[2]); + } + } + //Grab lng deg and min + if(preg_match('/(\d{1,3})\D{1,3}\s{0,2}(\d{1,2}\.{0,1}\d*)[\']{1}(.*)/i',$leftOver,$m)){ + $lngDeg = $m[1]; + $lngMin = $m[2]; + $leftOver = trim($m[3]); + //Grab lng sec + if(preg_match('/^(\d{1,2}\.{0,1}\d*)["]{1}(.*)/i',$leftOver,$m)){ + $lngSec = $m[1]; + if(count($m)>2){ + $leftOver = trim($m[2]); + } + } + if(is_numeric($latDeg) && is_numeric($latMin) && is_numeric($lngDeg) && is_numeric($lngMin)){ + if($latDeg < 90 && $latMin < 60 && $lngDeg < 180 && $lngMin < 60){ + $latDec = $latDeg + ($latMin/60) + ($latSec/3600); + $lngDec = $lngDeg + ($lngMin/60) + ($lngSec/3600); + if($latNS == 'S'){ + $latDec = -$latDec; + } + if($lngEW == 'W'){ + $lngDec = -$lngDec; + } + $retArr['lat'] = round($latDec,6); + $retArr['lng'] = round($lngDec,6); + } + } + } + } + } + if((!$target && !$retArr) || $target == 'UTM'){ + //UTM parsing + $d = ''; + if(preg_match('/NAD\s*27/i',$inStr)) $d = 'NAD27'; + if(preg_match('/\D*(\d{1,2}\D{0,1})\s+(\d{6,7})m{0,1}E\s+(\d{7})m{0,1}N/i',$inStr,$m)){ + $z = $m[1]; + $e = $m[2]; + $n = $m[3]; + if($n && $e && $z){ + $llArr = self::convertUtmToLL($e,$n,$z,$d); + if(isset($llArr['lat'])) $retArr['lat'] = $llArr['lat']; + if(isset($llArr['lng'])) $retArr['lng'] = $llArr['lng']; + } + + } + elseif(preg_match('/UTM/',$inStr) || preg_match('/\d{1,2}[\D\s]+\d{6,7}[\D\s]+\d{6,7}/',$inStr)){ + //UTM + $z = ''; $e = ''; $n = ''; + if(preg_match('/^(\d{1,2}\D{0,1})[\s\D]+/',$inStr,$m)) $z = $m[1]; + if(!$z && preg_match('/[\s\D]+(\d{1,2}\D{0,1})$/',$inStr,$m)) $z = $m[1]; + if(!$z && preg_match('/[\s\D]+(\d{1,2}\D{0,1})[\s\D]+/',$inStr,$m)) $z = $m[1]; + if($z){ + if(preg_match('/(\d{6,7})m{0,1}E{1}[\D\s]+(\d{7})m{0,1}N{1}/i',$inStr,$m)){ + $e = $m[1]; + $n = $m[2]; + } + elseif(preg_match('/m{0,1}E{1}(\d{6,7})[\D\s]+m{0,1}N{1}(\d{7})/i',$inStr,$m)){ + $e = $m[1]; + $n = $m[2]; + } + elseif(preg_match('/(\d{7})m{0,1}N{1}[\D\s]+(\d{6,7})m{0,1}E{1}/i',$inStr,$m)){ + $e = $m[2]; + $n = $m[1]; + } + elseif(preg_match('/m{0,1}N{1}(\d{7})[\D\s]+m{0,1}E{1}(\d{6,7})/i',$inStr,$m)){ + $e = $m[2]; + $n = $m[1]; + } + elseif(preg_match('/(\d{6})[\D\s]+(\d{7})/',$inStr,$m)){ + $e = $m[1]; + $n = $m[2]; + } + elseif(preg_match('/(\d{7})[\D\s]+(\d{6})/',$inStr,$m)){ + $e = $m[2]; + $n = $m[1]; + } + if($e && $n){ + $llArr = self::convertUtmToLL($e,$n,$z,$d); + if(isset($llArr['lat'])) $retArr['lat'] = $llArr['lat']; + if(isset($llArr['lng'])) $retArr['lng'] = $llArr['lng']; + } + } + } + } + //Clean + if($retArr){ + if($retArr['lat'] < -90 || $retArr['lat'] > 90) return; + if($retArr['lng'] < -180 || $retArr['lng'] > 180) return; + } + return $retArr; + } + + public static function convertUtmToLL($e, $n, $z, $d){ + $retArr = array(); + if($e && $n && $z){ + $gPoint = new GPoint($d); + $gPoint->setUTM($e,$n,$z); + $gPoint->convertTMtoLL(); + $lat = $gPoint->Lat(); + $lng = $gPoint->Long(); + if($lat && $lng){ + $retArr['lat'] = round($lat,6); + $retArr['lng'] = round($lng,6); + } + } + return $retArr; + } + + public static function occurrenceArrayCleaning($recMap){ + //Trim all field values + foreach($recMap as $k => $v){ + $recMap[$k] = trim($v); + } + //Date cleaning + if(isset($recMap['eventdate']) && $recMap['eventdate']){ + if(!preg_match('/\d{4}-\d{2}-\d{2}/', $recMap['eventdate'])){ + if(is_numeric($recMap['eventdate'])){ + $recMap['eventdate'] = self::dateCheck($recMap['eventdate']); + } + else{ + //Make sure event date is a valid format or drop into verbatimEventDate + $dateStr = self::formatDate($recMap['eventdate']); + if($dateStr){ + if($recMap['eventdate'] != $dateStr && (!array_key_exists('verbatimeventdate',$recMap) || !$recMap['verbatimeventdate'])){ + $recMap['verbatimeventdate'] = $recMap['eventdate']; + } + $recMap['eventdate'] = $dateStr; + } + else{ + if(!array_key_exists('verbatimeventdate',$recMap) || !$recMap['verbatimeventdate']){ + $recMap['verbatimeventdate'] = $recMap['eventdate']; + } + unset($recMap['eventdate']); + } + } + } + } + if(array_key_exists('eventdate2',$recMap) && $recMap['eventdate2'] && is_numeric($recMap['eventdate2'])){ + $recMap['eventdate2'] = self::dateCheck($recMap['eventdate2']); + if($recMap['eventdate2'] == $recMap['eventdate']) unset($recMap['eventdate2']); + else $recMap['verbatimeventdate'] .= ' - '.$recMap['eventdate2']; + } + if(array_key_exists('verbatimeventdate',$recMap) && $recMap['verbatimeventdate'] && is_numeric($recMap['verbatimeventdate']) + && $recMap['verbatimeventdate'] > 2100 && $recMap['verbatimeventdate'] < 45000){ + //Date field was converted to Excel's numeric format (number of days since 01/01/1900) + $recMap['verbatimeventdate'] = date('Y-m-d', mktime(0,0,0,1,$recMap['verbatimeventdate']-1,1900)); + } + if(array_key_exists('dateidentified',$recMap) && $recMap['dateidentified'] && is_numeric($recMap['dateidentified']) + && $recMap['dateidentified'] > 2100 && $recMap['dateidentified'] < 45000){ + //Date field was converted to Excel's numeric format (number of days since 01/01/1900) + $recMap['dateidentified'] = date('Y-m-d', mktime(0,0,0,1,$recMap['dateidentified']-1,1900)); + } + //If month, day, or year are text, avoid SQL error by converting to numeric value + if(array_key_exists('year',$recMap) || array_key_exists('month',$recMap) || array_key_exists('day',$recMap)){ + $y = (array_key_exists('year',$recMap)?$recMap['year']:''); + $m = (array_key_exists('month',$recMap)?$recMap['month']:''); + $d = (array_key_exists('day',$recMap)?$recMap['day']:''); + $vDate = trim($y.'-'.$m.'-'.$d,'- '); + if(isset($recMap['day']) && $recMap['day'] && !is_numeric($recMap['day'])){ + unset($recMap['day']); + $d = '00'; + } + if(isset($recMap['year']) && !is_numeric($recMap['year'])){ + unset($recMap['year']); + } + if(isset($recMap['month']) && $recMap['month'] && !is_numeric($recMap['month'])){ + if(!is_numeric($recMap['month'])){ + $monAbbr = strtolower(substr($recMap['month'],0,3)); + if(preg_match('/^[IVX]{1-4}$/',$recMap['month'])){ + $vDate = $d.'-'.$recMap['month'].'-'.$y; + $recMap['month'] = self::$monthRoman[$recMap['month']]; + $recMap['eventdate'] = self::formatDate($y.'-'.$recMap['month'].'-'.($d?$d:'00')); + } + elseif(preg_match('/^\D{3,}$/',$recMap['month']) && array_key_exists($monAbbr,self::$monthNames)){ + $vDate = $d.' '.$recMap['month'].' '.$y; + $recMap['month'] = self::$monthNames[$monAbbr]; + $recMap['eventdate'] = self::formatDate($y.'-'.$recMap['month'].'-'.($d?$d:'00')); + } + elseif(preg_match('/^(\d{1,2})\s{0,1}-\s{0,1}(\D{3,10})$/',$recMap['month'],$m)){ + $recMap['month'] = $m[1]; + $recMap['eventdate'] = self::formatDate(trim($y.'-'.$recMap['month'].'-'.($d?$d:'00'),'- ')); + $vDate = $d.' '.$m[2].' '.$y; + } + else{ + unset($recMap['month']); + } + } + } + if(!array_key_exists('verbatimeventdate',$recMap) || !$recMap['verbatimeventdate']){ + $recMap['verbatimeventdate'] = $vDate; + } + if($vDate && (!array_key_exists('eventdate',$recMap) || !$recMap['eventdate'])){ + $recMap['eventdate'] = self::formatDate($vDate); + } + } + //eventDate IS NULL && year IS NULL && verbatimEventDate NOT NULL + if((!array_key_exists('eventdate',$recMap) || !$recMap['eventdate']) && array_key_exists('verbatimeventdate',$recMap) && $recMap['verbatimeventdate'] && (!array_key_exists('year',$recMap) || !$recMap['year'])){ + $dateStr = self::formatDate($recMap['verbatimeventdate']); + if($dateStr) $recMap['eventdate'] = $dateStr; + } + if((isset($recMap['recordnumberprefix']) && $recMap['recordnumberprefix']) || (isset($recMap['recordnumbersuffix']) && $recMap['recordnumbersuffix'])){ + $recNumber = $recMap['recordnumber']; + if(isset($recMap['recordnumberprefix']) && $recMap['recordnumberprefix']) $recNumber = $recMap['recordnumberprefix'].'-'.$recNumber; + if(isset($recMap['recordnumbersuffix']) && $recMap['recordnumbersuffix']){ + if(is_numeric($recMap['recordnumbersuffix']) && $recMap['recordnumber']) $recNumber .= '-'; + $recNumber .= $recMap['recordnumbersuffix']; + } + $recMap['recordnumber'] = $recNumber; + } + //If lat or long are not numeric, try to make them so + if(array_key_exists('decimallatitude',$recMap) || array_key_exists('decimallongitude',$recMap)){ + $latValue = (array_key_exists('decimallatitude',$recMap)?$recMap['decimallatitude']:''); + $lngValue = (array_key_exists('decimallongitude',$recMap)?$recMap['decimallongitude']:''); + if(($latValue && !is_numeric($latValue)) || ($lngValue && !is_numeric($lngValue))){ + $llArr = self::parseVerbatimCoordinates(trim($latValue.' '.$lngValue),'LL'); + if(array_key_exists('lat',$llArr) && array_key_exists('lng',$llArr)){ + $recMap['decimallatitude'] = $llArr['lat']; + $recMap['decimallongitude'] = $llArr['lng']; + } + else{ + unset($recMap['decimallatitude']); + unset($recMap['decimallongitude']); + } + $vcStr = ''; + if(array_key_exists('verbatimcoordinates',$recMap) && $recMap['verbatimcoordinates']){ + $vcStr .= $recMap['verbatimcoordinates'].'; '; + } + $vcStr .= $latValue.' '.$lngValue; + if(trim($vcStr)) $recMap['verbatimcoordinates'] = trim($vcStr); + } + } + //Transfer verbatim Lat/Long to verbatim coords + if(isset($recMap['verbatimlatitude']) || isset($recMap['verbatimlongitude'])){ + if(isset($recMap['verbatimlatitude']) && isset($recMap['verbatimlongitude'])){ + if(!isset($recMap['decimallatitude']) || !isset($recMap['decimallongitude'])){ + if((is_numeric($recMap['verbatimlatitude']) && is_numeric($recMap['verbatimlongitude']))){ + if($recMap['verbatimlatitude'] > -90 && $recMap['verbatimlatitude'] < 90 + && $recMap['verbatimlongitude'] > -180 && $recMap['verbatimlongitude'] < 180){ + $recMap['decimallatitude'] = $recMap['verbatimlatitude']; + $recMap['decimallongitude'] = $recMap['verbatimlongitude']; + } + } + else{ + //Attempt to extract decimal lat/long + $coordArr = self::parseVerbatimCoordinates($recMap['verbatimlatitude'].' '.$recMap['verbatimlongitude'],'LL'); + if($coordArr){ + if(array_key_exists('lat',$coordArr)) $recMap['decimallatitude'] = $coordArr['lat']; + if(array_key_exists('lng',$coordArr)) $recMap['decimallongitude'] = $coordArr['lng']; + } + } + } + } + //Place into verbatim coord field + $vCoord = ''; + if(isset($recMap['verbatimcoordinates']) && $recMap['verbatimcoordinates']) $vCoord = $recMap['verbatimcoordinates'].'; '; + if(isset($recMap['verbatimlatitude']) && stripos($vCoord,$recMap['verbatimlatitude']) === false) $vCoord .= $recMap['verbatimlatitude'].', '; + if(isset($recMap['verbatimlongitude']) && stripos($vCoord,$recMap['verbatimlongitude']) === false) $vCoord .= $recMap['verbatimlongitude']; + if($vCoord) $recMap['verbatimcoordinates'] = trim($vCoord,' ,;'); + } + //Transfer DMS to verbatim coords + if(isset($recMap['latdeg']) && $recMap['latdeg'] && isset($recMap['lngdeg']) && $recMap['lngdeg']){ + //Attempt to create decimal lat/long + if(is_numeric($recMap['latdeg']) && is_numeric($recMap['lngdeg']) && (!isset($recMap['decimallatitude']) || !$recMap['decimallatitude'] || !isset($recMap['decimallongitude']) || $recMap['decimallongitude'])){ + $latDec = abs($recMap['latdeg']); + if(isset($recMap['latmin']) && $recMap['latmin'] && is_numeric($recMap['latmin'])) $latDec += $recMap['latmin']/60; + if(isset($recMap['latsec']) && $recMap['latsec'] && is_numeric($recMap['latsec'])) $latDec += $recMap['latsec']/3600; + if($latDec > 0){ + if(isset($recMap['latns']) && stripos($recMap['latns'],'s') === 0) $latDec *= -1; + elseif($recMap['latdeg'] < 0) $latDec *= -1; + } + $lngDec = abs($recMap['lngdeg']); + if(isset($recMap['lngmin']) && $recMap['lngmin'] && is_numeric($recMap['lngmin'])) $lngDec += $recMap['lngmin']/60; + if(isset($recMap['lngsec']) && $recMap['lngsec'] && is_numeric($recMap['lngsec'])) $lngDec += $recMap['lngsec']/3600; + if($lngDec > 0){ + if(isset($recMap['lngew']) && stripos($recMap['lngew'],'w') === 0) $lngDec *= -1; + elseif($recMap['lngdeg'] < 0) $lngDec *= -1; + elseif(in_array(strtolower($recMap['country']), array('usa','united states','canada','mexico','panama'))) $lngDec *= -1; + } + $recMap['decimallatitude'] = round($latDec,6); + $recMap['decimallongitude'] = round($lngDec,6); + } + //Place into verbatim coord field + $vCoord = (isset($recMap['verbatimcoordinates'])?$recMap['verbatimcoordinates']:''); + if($vCoord) $vCoord .= '; '; + $vCoord .= $recMap['latdeg'].chr(176).' '; + if(isset($recMap['latmin']) && $recMap['latmin']) $vCoord .= $recMap['latmin'].'m '; + if(isset($recMap['latsec']) && $recMap['latsec']) $vCoord .= $recMap['latsec'].'s '; + if(isset($recMap['latns'])) $vCoord .= $recMap['latns'].'; '; + $vCoord .= $recMap['lngdeg'].chr(176).' '; + if(isset($recMap['lngmin']) && $recMap['lngmin']) $vCoord .= $recMap['lngmin'].'m '; + if(isset($recMap['lngsec']) && $recMap['lngsec']) $vCoord .= $recMap['lngsec'].'s '; + if(isset($recMap['lngew'])) $vCoord .= $recMap['lngew']; + $recMap['verbatimcoordinates'] = $vCoord; + } + /* + if(array_key_exists('verbatimcoordinates',$recMap) && $recMap['verbatimcoordinates'] && (!isset($recMap['decimallatitude']) || !isset($recMap['decimallongitude']))){ + $coordArr = self::parseVerbatimCoordinates($recMap['verbatimcoordinates']); + if($coordArr){ + if(array_key_exists('lat',$coordArr)) $recMap['decimallatitude'] = $coordArr['lat']; + if(array_key_exists('lng',$coordArr)) $recMap['decimallongitude'] = $coordArr['lng']; + } + } + */ + //Convert UTM to Lat/Long + if((array_key_exists('utmnorthing',$recMap) && $recMap['utmnorthing']) || (array_key_exists('utmeasting',$recMap) && $recMap['utmeasting'])){ + $no = (array_key_exists('utmnorthing',$recMap)?$recMap['utmnorthing']:''); + $ea = (array_key_exists('utmeasting',$recMap)?$recMap['utmeasting']:''); + $zo = (array_key_exists('utmzoning',$recMap)?$recMap['utmzoning']:''); + $da = (array_key_exists('geodeticdatum',$recMap)?$recMap['geodeticdatum']:''); + if(!isset($recMap['decimallatitude']) || !isset($recMap['decimallongitude'])){ + if($no && $ea && $zo){ + //Northing, easting, and zoning all had values + $llArr = self::convertUtmToLL($ea,$no,$zo,$da); + if(isset($llArr['lat'])) $recMap['decimallatitude'] = $llArr['lat']; + if(isset($llArr['lng'])) $recMap['decimallongitude'] = $llArr['lng']; + } + else{ + //UTM was a single field which was placed in UTM northing field within uploadspectemp table + $coordArr = self::parseVerbatimCoordinates(trim($zo.' '.$ea.' '.$no),'UTM'); + if($coordArr){ + if(array_key_exists('lat',$coordArr)) $recMap['decimallatitude'] = $coordArr['lat']; + if(array_key_exists('lng',$coordArr)) $recMap['decimallongitude'] = $coordArr['lng']; + } + } + } + $vCoord = (isset($recMap['verbatimcoordinates'])?$recMap['verbatimcoordinates']:''); + if(!($no && strpos($vCoord,$no))) $recMap['verbatimcoordinates'] = ($vCoord?$vCoord.'; ':'').$zo.' '.$ea.'E '.$no.'N'; + } + //Transfer TRS to verbatim coords + if(isset($recMap['trstownship']) && $recMap['trstownship'] && isset($recMap['trsrange']) && $recMap['trsrange']){ + $vCoord = (isset($recMap['verbatimcoordinates'])?$recMap['verbatimcoordinates']:''); + if($vCoord) $vCoord .= '; '; + $vCoord .= (stripos($recMap['trstownship'],'t') === false?'T':'').$recMap['trstownship'].' '; + $vCoord .= (stripos($recMap['trsrange'],'r') === false?'R':'').$recMap['trsrange'].' '; + if(isset($recMap['trssection'])) $vCoord .= (stripos($recMap['trssection'],'s') === false?'sec':'').$recMap['trssection'].' '; + if(isset($recMap['trssectiondetails'])) $vCoord .= $recMap['trssectiondetails']; + $recMap['verbatimcoordinates'] = trim($vCoord); + } + //coordinate uncertainity + $radius = ''; + $unitStr = ''; + if(isset($recMap['coordinateuncertaintyinmeters']) && $recMap['coordinateuncertaintyinmeters'] && !is_numeric($recMap['coordinateuncertaintyinmeters'])){ + if(preg_match('/([\d.-]+)\s*([a-z\']+)/i', $recMap['coordinateuncertaintyinmeters'], $m)){ + //value is not numeric only and thus probably have units imbedded + $radius = $m[1]; + $unitStr = strtolower($m[2]); + } + $recMap['coordinateuncertaintyinmeters'] = ''; + } + if(isset($recMap['coordinateuncertaintyradius']) && is_numeric($recMap['coordinateuncertaintyradius']) && isset($recMap['coordinateuncertaintyunits']) && $recMap['coordinateuncertaintyunits']){ + //uncertainity was supplied a separate values + $radius = $recMap['coordinateuncertaintyradius']; + $unitStr = strtolower($recMap['coordinateuncertaintyunits']); + } + if($radius && $unitStr && $unitStr != 'n/a' && (!isset($recMap['coordinateuncertaintyinmeters']) || !$recMap['coordinateuncertaintyinmeters'])){ + if($unitStr == 'mi' || $unitStr == 'mile' || $unitStr == 'miles') $recMap['coordinateuncertaintyinmeters'] = round($radius*1609); + elseif($unitStr == 'km' || $unitStr == 'kilometers') $recMap['coordinateuncertaintyinmeters'] = round($radius*1000); + elseif($unitStr == 'm' || $unitStr == 'meter' || $unitStr == 'meters') $recMap['coordinateuncertaintyinmeters'] = $radius; + elseif($unitStr == 'ft' || $unitStr == 'f' || $unitStr == 'feet' || $unitStr == "'") $recMap['coordinateuncertaintyinmeters'] = round($radius*0.3048); + } + if(!isset($recMap['coordinateuncertaintyinmeters'])){ + //Assume a mapping error and meters were mapped to wrong field + if(isset($recMap['coordinateuncertaintyradius']) && is_numeric($recMap['coordinateuncertaintyradius'])) $recMap['coordinateuncertaintyinmeters'] = $recMap['coordinateuncertaintyradius']; + elseif(isset($recMap['coordinateuncertaintyunits']) && is_numeric($recMap['coordinateuncertaintyunits'])) $recMap['coordinateuncertaintyinmeters'] = $recMap['coordinateuncertaintyunits']; + } + unset($recMap['coordinateuncertaintyradius']); + unset($recMap['coordinateuncertaintyunits']); + //Check to see if evelation are valid numeric values + if((isset($recMap['minimumelevationinmeters']) && $recMap['minimumelevationinmeters'] && !is_numeric($recMap['minimumelevationinmeters'])) + || (isset($recMap['maximumelevationinmeters']) && $recMap['maximumelevationinmeters'] && !is_numeric($recMap['maximumelevationinmeters']))){ + $vStr = (isset($recMap['verbatimelevation'])?$recMap['verbatimelevation']:''); + if(isset($recMap['minimumelevationinmeters']) && $recMap['minimumelevationinmeters']) $vStr .= ($vStr?'; ':'').$recMap['minimumelevationinmeters']; + if(isset($recMap['maximumelevationinmeters']) && $recMap['maximumelevationinmeters']) $vStr .= '-'.$recMap['maximumelevationinmeters']; + $recMap['verbatimelevation'] = $vStr; + $recMap['minimumelevationinmeters'] = ''; + $recMap['maximumelevationinmeters'] = ''; + } + //Verbatim elevation + if(array_key_exists('verbatimelevation',$recMap) && $recMap['verbatimelevation'] && (!array_key_exists('minimumelevationinmeters',$recMap) || !$recMap['minimumelevationinmeters'])){ + $eArr = self::parseVerbatimElevation($recMap['verbatimelevation']); + if($eArr){ + if(array_key_exists('minelev',$eArr)){ + $recMap['minimumelevationinmeters'] = $eArr['minelev']; + if(array_key_exists('maxelev',$eArr)) $recMap['maximumelevationinmeters'] = $eArr['maxelev']; + } + } + } + //Deal with elevation when in two fields (number and units) + if(isset($recMap['elevationnumber']) && $recMap['elevationnumber']){ + $elevStr = $recMap['elevationnumber'].$recMap['elevationunits']; + //Try to extract meters + $eArr = self::parseVerbatimElevation($elevStr); + if($eArr){ + if(array_key_exists('minelev',$eArr)){ + $recMap['minimumelevationinmeters'] = $eArr['minelev']; + if(array_key_exists('maxelev',$eArr)) $recMap['maximumelevationinmeters'] = $eArr['maxelev']; + } + } + if(!$eArr || !stripos($elevStr,'m')){ + $vElev = (isset($recMap['verbatimelevation'])?$recMap['verbatimelevation']:''); + if($vElev) $vElev .= '; '; + $recMap['verbatimelevation'] = $vElev.$elevStr; + } + } + //Concatenate collectorfamilyname and collectorinitials into recordedby + if(isset($recMap['collectorfamilyname']) && $recMap['collectorfamilyname'] && (!isset($recMap['recordedby']) || !$recMap['recordedby'])){ + $recordedBy = $recMap['collectorfamilyname']; + if(isset($recMap['collectorinitials']) && $recMap['collectorinitials']) $recordedBy .= ', '.$recMap['collectorinitials']; + $recMap['recordedby'] = $recordedBy; + //Need to add code that maps to collector table + + } + + if(array_key_exists("specificepithet",$recMap)){ + if($recMap["specificepithet"] == 'sp.' || $recMap["specificepithet"] == 'sp') $recMap["specificepithet"] = ''; + } + if(array_key_exists("taxonrank",$recMap)){ + $tr = strtolower($recMap["taxonrank"]); + if($tr == 'species' || !isset($recMap["specificepithet"]) || !$recMap["specificepithet"]) $recMap["taxonrank"] = ''; + if($tr == 'subspecies') $recMap["taxonrank"] = 'subsp.'; + if($tr == 'variety') $recMap["taxonrank"] = 'var.'; + if($tr == 'forma') $recMap["taxonrank"] = 'f.'; + } + + //Populate sciname if null + if(array_key_exists('sciname',$recMap) && $recMap['sciname']){ + if(substr($recMap['sciname'],-4) == ' sp.') $recMap['sciname'] = substr($recMap['sciname'],0,-4); + if(substr($recMap['sciname'],-3) == ' sp') $recMap['sciname'] = substr($recMap['sciname'],0,-3); + + $recMap['sciname'] = str_replace(array(' ssp. ',' ssp '),' subsp. ',$recMap['sciname']); + $recMap['sciname'] = str_replace(' var ',' var. ',$recMap['sciname']); + + $pattern = '/\b(cf\.|cf|aff\.|aff)\s{1}/'; + if(preg_match($pattern,$recMap['sciname'],$m)){ + $recMap['identificationqualifier'] = $m[1]; + $recMap['sciname'] = preg_replace($pattern,'',$recMap['sciname']); + } + } + else{ + if(array_key_exists('genus',$recMap) && array_key_exists('specificepithet',$recMap)){ + //Build sciname from individual units supplied by source + $sciName = trim($recMap['genus'].' '.$recMap['specificepithet']); + if(array_key_exists('infraspecificepithet',$recMap)){ + if(array_key_exists('taxonrank',$recMap)) $sciName .= ' '.$recMap['taxonrank']; + $sciName .= ' '.$recMap['infraspecificepithet']; + } + $recMap['sciname'] = trim($sciName); + } + elseif(array_key_exists('scientificname',$recMap)){ + //Clean and parse scientific name + $parsedArr = Taxonomy::parseScientificName($recMap['scientificname']); + $scinameStr = ''; + if(array_key_exists('unitind1', $parsedArr)){ + $scinameStr .= $parsedArr['unitind1']; + if($parsedArr['unitind1'] != '×' || $parsedArr['unitind1'] != '†') $scinameStr .= ' '; + } + if(array_key_exists('unitname1',$parsedArr)){ + $scinameStr = $parsedArr['unitname1'].' '; + if(!array_key_exists('genus',$recMap) || $recMap['genus']){ + $recMap['genus'] = $parsedArr['unitname1']; + } + } + if(array_key_exists('unitind2', $parsedArr)){ + $scinameStr .= $parsedArr['unitind2']; + if($parsedArr['unitind2'] != '×') $scinameStr .= ' '; + } + if(array_key_exists('unitname2',$parsedArr)){ + $scinameStr .= $parsedArr['unitname2'].' '; + if(!array_key_exists('specificepithet',$recMap) || !$recMap['specificepithet']){ + $recMap['specificepithet'] = $parsedArr['unitname2']; + } + } + if(array_key_exists('unitind3',$parsedArr)){ + $scinameStr .= $parsedArr['unitind3'].' '; + if((!array_key_exists('taxonrank',$recMap) || !$recMap['taxonrank'])){ + $recMap['taxonrank'] = $parsedArr['unitind3']; + } + } + if(array_key_exists('unitname3',$parsedArr)){ + $scinameStr .= $parsedArr['unitname3']; + if(!array_key_exists('infraspecificepithet',$recMap) || !$recMap['infraspecificepithet']){ + $recMap['infraspecificepithet'] = $parsedArr['unitname3']; + } + } + if(array_key_exists('author',$parsedArr)){ + if(!array_key_exists('scientificnameauthorship',$recMap) || !$recMap['scientificnameauthorship']){ + $recMap['scientificnameauthorship'] = $parsedArr['author']; + } + } + $recMap['sciname'] = trim($scinameStr); + } + } + if(isset($recMap['authorinfraspecific']) && $recMap['authorinfraspecific']){ + $recMap['scientificnameauthorship'] = $recMap['authorinfraspecific']; + } + elseif(isset($recMap['authorspecies']) && $recMap['authorspecies']){ + $recMap['scientificnameauthorship'] = $recMap['authorspecies']; + } + unset($recMap['authorinfraspecific']); + unset($recMap['authorspecies']); + + // Deal with associatedOccurrence fields, if they exist + // Check if they exist, and then check if any of the fields are non-empty + if (count($check = preg_grep('/^associatedOccurrence:.*/', array_keys($recMap))) && + strlen(implode(array_intersect_key($recMap, array_flip($check))))) { + + // Check if there is already data in the associatedOccurrences field. + if (isset($recMap['associatedOccurrences']) && $recMap['associatedOccurrences']) { + + // Check if the contents of the field already is JSON + if ($assocOccArr = json_decode($recMap['associatedOccurrences'], true)) { + + // There's already JSON here + // TODO: What should we do? Perform some checks? + // Currently, it will just append any data specified by associatedOccurrence:* fields + } else { + + // no JSON content, store what is already in associatedOccurrences in verbatimText array + $verbatimText = array(); + + // Add whatever text is mapped to associatedOccurrences to verbatimText + $verbatimText['type'] = 'verbatimText'; + $verbatimText['verbatimText'] = $recMap['associatedOccurrences']; + + } + } + + // No associatedOccurrences array exists, so build one + if (!isset($assocOccArr)) { + $assocOccArr = array(); + + $symbiotaAssociations = array(); + $symbiotaAssociations['type'] = 'symbiotaAssociations'; + $symbiotaAssociations['version'] = self::$assocOccurVersion; + $symbiotaAssociations['associations'] = array(); + + // Add the symbiotaAssociations array + array_push($assocOccArr, $symbiotaAssociations); + + // Add the verbatimText array, if it exists + if (isset($verbatimText)) array_push($assocOccArr, $verbatimText); + } + + // Create associated occurrence array + $assocArr = array(); + + // Establish the associated occurrence type, check first to see if it's already set (and valid) + if (isset($recMap['associatedOccurrence:type'])) { + + // Valid type, so use it + if ($recMap['associatedOccurrence:type'] == 'internalOccurrence' || + $recMap['associatedOccurrence:type'] == 'externalOccurrence' || + $recMap['associatedOccurrence:type'] == 'genericObservation') { + $assocArr['type'] = $recMap['associatedOccurrence:type']; + } else { + + // Invalid type, fall back to genericObservation + $assocArr['type'] = 'genericObservation'; + } + } + + // No association type set, try to guess + if (!isset($assocArr['type'])) { + + if (isset($recMap['associatedOccurrence:occidAssociate']) && $recMap['associatedOccurrence:occidAssociate']) { + $assocArr['type'] = 'internalOccurrence'; + } else if ((isset($recMap['associatedOccurrence:identifier']) && $recMap['associatedOccurrence:identifier']) || + (isset($recMap['associatedOccurrence:resourceUrl']) && $recMap['associatedOccurrence:resourceUrl'])) { + $assocArr['type'] = 'externalOccurrence'; + } else if (isset($recMap['associatedOccurrence:verbatimSciname']) && $recMap['associatedOccurrence:verbatimSciname']) { + $assocArr['type'] = 'genericObservation'; + } else { + // Should not happen, but if so, this seems to be the best fit + $assocArr['type'] = 'genericObservation'; + } + } + + // Finished with the type field, so unset it + unset($recMap['associatedOccurrence:type']); + + // TODO: restrict fields to controlled vocab for relationship and subtype? + + // Iterate through each remaining associated occurrence field + foreach (preg_grep('/^associatedOccurrence:.*/', array_keys($recMap)) as $field) { + + // Save it to the associated occurrence array if it's not empty + if ($recMap[$field]) $assocArr[str_replace('associatedOccurrence:', '', $field)] = $recMap[$field]; + + // Unset the temporary field as the data is saved in the array + unset($recMap[$field]); + } + + // Add associated occurrence array to the full associatedOccurrences array + array_push($assocOccArr[0]['associations'], $assocArr); + + // Convert to JSON and store in the associatedOccurrences field + $recMap['associatedOccurrences'] = json_encode($assocOccArr, JSON_UNESCAPED_SLASHES); + + } else { + + // Unset any remaining associatedOccurrence fields, they are empty + $recMap = array_diff_key($recMap, array_flip(preg_grep('/^associatedOccurrence:.*/', array_keys($recMap)))); + + } + + //Deal with Specify specific fields + if(isset($recMap['specify:forma']) && $recMap['specify:forma']){ + $recMap['taxonrank'] = 'f.'; + $recMap['infraspecificepithet'] = $recMap['specify:forma']; + if(isset($recMap['specify:forma_author']) && $recMap['specify:forma_author']){ + $recMap['scientificnameauthorship'] = $recMap['specify:forma_author']; + } + } + elseif(isset($recMap['specify:variety']) && $recMap['specify:variety']){ + $recMap['taxonrank'] = 'var.'; + $recMap['infraspecificepithet'] = $recMap['specify:variety']; + if(isset($recMap['specify:variety_author']) && $recMap['specify:variety_author']){ + $recMap['scientificnameauthorship'] = $recMap['specify:variety_author']; + } + } + elseif(isset($recMap['specify:subspecies']) && $recMap['specify:subspecies']){ + $recMap['taxonrank'] = 'subsp.'; + $recMap['infraspecificepithet'] = $recMap['specify:subspecies']; + if(isset($recMap['specify:subspecies_author']) && $recMap['specify:subspecies_author']){ + $recMap['scientificnameauthorship'] = $recMap['specify:subspecies_author']; + } + } + unset($recMap['specify:forma']); + unset($recMap['specify:forma_author']); + unset($recMap['specify:variety']); + unset($recMap['specify:variety_author']); + unset($recMap['specify:subspecies']); + unset($recMap['specify:subspecies_author']); + if(isset($recMap['specify:collector_last_name']) && $recMap['specify:collector_last_name']){ + $recordedByStr = ''; + if(isset($recMap['specify:collector_first_name']) && $recMap['specify:collector_first_name']){ + $recordedByStr = $recMap['specify:collector_first_name']; + } + if(isset($recMap['specify:collector_middle_initial']) && $recMap['specify:collector_middle_initial']){ + $recordedByStr .= ' '.$recMap['specify:collector_middle_initial']; + } + $recordedByStr .= ' '.$recMap['specify:collector_last_name']; + if($recordedByStr) $recMap['recordedby'] = trim($recordedByStr); + } + unset($recMap['specify:collector_first_name']); + unset($recMap['specify:collector_middle_initial']); + unset($recMap['specify:collector_last_name']); + if(isset($recMap['specify:determiner_last_name']) && $recMap['specify:determiner_last_name']){ + $identifiedBy = ''; + if(isset($recMap['specify:determiner_first_name']) && $recMap['specify:determiner_first_name']){ + $identifiedBy = $recMap['specify:determiner_first_name']; + } + if(isset($recMap['specify:determiner_middle_initial']) && $recMap['specify:determiner_middle_initial']){ + $identifiedBy .= ' '.$recMap['specify:determiner_middle_initial']; + } + $identifiedBy .= ' '.$recMap['specify:determiner_last_name']; + if($identifiedBy) $recMap['identifiedby'] = trim($identifiedBy); + } + unset($recMap['specify:determiner_first_name']); + unset($recMap['specify:determiner_middle_initial']); + unset($recMap['specify:determiner_last_name']); + if(isset($recMap['specify:qualifier_position'])){ + if($recMap['specify:qualifier_position']){ + $idQualifier = ''; + if(isset($recMap['identificationqualifier'])) $idQualifier = $recMap['identificationqualifier']; + $recMap['identificationqualifier'] = trim($idQualifier.' '.$recMap['specify:qualifier_position']); + } + unset($recMap['specify:qualifier_position']); + } + if(isset($recMap['specify:latitude1']) && $recMap['specify:latitude1']){ + $verbLatLngStr = ''; + if(isset($recMap['specify:latitude2']) && $recMap['specify:latitude2'] && $recMap['specify:latitude2'] != 'null' && $recMap['specify:latitude1'] != $recMap['specify:latitude2']){ + $verbLatLngStr = $recMap['specify:latitude1'].' to '.$recMap['specify:latitude2']; + } + if(isset($recMap['specify:longitude2']) && $recMap['specify:longitude2'] && $recMap['specify:longitude2'] != 'null' && $recMap['specify:longitude1'] != $recMap['specify:longitude2']){ + $verbLatLngStr .= '; '.$recMap['specify:longitude1'].' to '.$recMap['specify:longitude2']; + //todo: populate decimal Lat/long with mid-point and radius + } + if($verbLatLngStr){ + if(isset($recMap['verbatimcoordinates']) && $recMap['verbatimcoordinates']) $recMap['verbatimcoordinates'] .= '; '.$verbLatLngStr; + else $recMap['verbatimcoordinates'] = $verbLatLngStr; + } + else{ + $recMap['decimallatitude'] = $recMap['specify:latitude1']; + $recMap['decimallongitude'] = $recMap['specify:longitude1']; + } + } + unset($recMap['specify:latitude1']); + unset($recMap['specify:latitude2']); + unset($recMap['specify:longitude1']); + unset($recMap['specify:longitude2']); + if(isset($recMap['specify:land_ownership']) && $recMap['specify:land_ownership']){ + $locStr = $recMap['specify:land_ownership']; + if(isset($recMap['locality']) && $recMap['locality']){ + if(stripos($recMap['locality'], $recMap['specify:land_ownership']) === false) $locStr .= $recMap['locality']; + else $locStr = ''; + } + if($locStr) $recMap['locality'] = trim($locStr,';, '); + } + unset($recMap['specify:land_ownership']); + if(isset($recMap['specify:topo_quad']) && $recMap['specify:topo_quad']){ + $locStr = ''; + if(isset($recMap['locality']) && $recMap['locality']) $locStr = $recMap['locality']; + $recMap['locality'] = trim($locStr.'; '.$recMap['specify:topo_quad'],'; '); + } + unset($recMap['specify:topo_quad']); + if(isset($recMap['specify:georeferenced_by_last_name']) && $recMap['specify:georeferenced_by_last_name']){ + $georefBy = ''; + if(isset($recMap['specify:georeferenced_by_first_name']) && $recMap['specify:georeferenced_by_first_name']){ + $georefBy = $recMap['specify:georeferenced_by_first_name']; + } + if(isset($recMap['specify:georeferenced_by_middle_initial']) && $recMap['specify:georeferenced_by_middle_initial']){ + $georefBy .= ' '.$recMap['specify:georeferenced_by_middle_initial']; + } + $georefBy .= ' '.$recMap['specify:georeferenced_by_last_name']; + if($georefBy) $recMap['georeferencedby'] = trim($georefBy); + } + unset($recMap['specify:georeferenced_by_first_name']); + unset($recMap['specify:georeferenced_by_middle_initial']); + unset($recMap['specify:georeferenced_by_last_name']); + if(isset($recMap['specify:locality_continued'])){ + if($recMap['specify:locality_continued']) $recMap['locality'] .= trim(' '.$recMap['specify:locality_continued']); + unset($recMap['specify:locality_continued']); + } + if(isset($recMap['specify:georeferenced_date'])){ + if($recMap['specify:georeferenced_date']){ + $georefBy = ''; + if(isset($recMap['georeferencedby']) && $recMap['georeferencedby']) $georefBy = $recMap['georeferencedby']; + $recMap['georeferencedby'] = trim($georefBy.'; georef date: '.$recMap['specify:georeferenced_date'],'; '); + } + unset($recMap['specify:georeferenced_date']); + } + if(isset($recMap['specify:elevation_(ft)'])){ + if($recMap['specify:elevation_(ft)']){ + $verbElev = ''; + if(isset($recMap['verbatimelevation']) && $recMap['verbatimelevation']) $verbElev = $recMap['verbatimelevation']; + $recMap['verbatimelevation'] = trim($verbElev.'; '.$recMap['specify:elevation_(ft)'].'ft.','; '); + } + unset($recMap['specify:elevation_(ft)']); + } + if(isset($recMap['specify:preparer_last_name']) && $recMap['specify:preparer_last_name']){ + $prepStr = ''; + if(isset($recMap['preparations']) && $recMap['preparations']) $prepStr = $recMap['preparations']; + $prepBy = ''; + if(isset($recMap['specify:preparer_first_name']) && $recMap['specify:preparer_first_name']){ + $prepBy .= $recMap['specify:preparer_first_name']; + } + if(isset($recMap['specify:preparer_middle_initial']) && $recMap['specify:preparer_middle_initial']){ + $prepBy .= ' '.$recMap['specify:preparer_middle_initial']; + } + $prepBy = ' '.$recMap['specify:preparer_last_name']; + if($prepBy) $recMap['preparations'] = trim($prepStr.'; preparer: '.$prepBy); + } + unset($recMap['specify:preparer_first_name']); + unset($recMap['specify:preparer_middle_initial']); + unset($recMap['specify:preparer_last_name']); + if(isset($recMap['specify:prepared_by_date'])){ + if($recMap['specify:prepared_by_date']){ + $prepStr = ''; + if(isset($recMap['preparations']) && $recMap['preparations']) $prepStr = $recMap['preparations']; + $recMap['preparations'] = $prepStr.'; prepared by date: '.$recMap['specify:prepared_by_date']; + } + unset($recMap['specify:prepared_by_date']); + } + if(isset($recMap['specify:cataloger_last_name']) && $recMap['specify:cataloger_last_name']){ + $enteredBy = ''; + if(isset($recMap['specify:cataloger_first_name']) && $recMap['specify:cataloger_first_name']){ + $enteredBy = $recMap['specify:cataloger_first_name']; + } + if(isset($recMap['specify:cataloger_middle_initial']) && $recMap['specify:cataloger_middle_initial']){ + $enteredBy .= ' '.$recMap['specify:cataloger_middle_initial']; + } + $enteredBy .= ' '.$recMap['specify:cataloger_last_name']; + $recMap['recordenteredby'] = trim($enteredBy); + } + unset($recMap['specify:cataloger_first_name']); + unset($recMap['specify:cataloger_middle_initial']); + unset($recMap['specify:cataloger_last_name']); + if(isset($recMap['specify:cataloged_date'])){ + if($recMap['specify:cataloged_date']){ + $recBy = ''; + if(isset($recMap['recordenteredby']) && $recMap['recordenteredby']) $recBy = $recMap['recordenteredby']; + $recMap['recordenteredby'] = trim($recBy.'; cataloged date: '.$recMap['specify:cataloged_date'],'; '); + } + unset($recMap['specify:cataloged_date']); + } + return $recMap; + } + + public static function verifyUser($user, $conn){ + //If input is numberic, verify against uid, or convert username or email to uid + $uid = null; + $paramArr = array(); + $typeStr = ''; + $sql = 'SELECT uid FROM users WHERE '; + if(is_numeric($user)){ + $sql .= 'uid = ?'; + $paramArr[] = $user; + $typeStr = 'i'; + } + else{ + $sql .= 'username = ? OR email = ?'; + $paramArr[] = $user; + $paramArr[] = $user; + $typeStr = 'ss'; + } + if($stmt = $conn->prepare($sql)){ + $stmt->bind_param($typeStr, ...$paramArr); + $stmt->execute(); + $stmt->bind_result($uid); + $stmt->fetch(); + $stmt->close(); + } + return $uid; + } + + private static function dateCheck($inStr){ + $retStr = $inStr; + if($inStr > 2100 && $inStr < 45000){ + //Date field was converted to Excel's numeric format (number of days since 01/01/1900) + $retStr = date('Y-m-d', mktime(0,0,0,1,$inStr-1,1900)); + } + elseif($inStr > 2200000 && $inStr < 2500000){ + $dArr = explode('/',jdtogregorian($inStr)); + $retStr = $dArr[2].'-'.$dArr[0].'-'.$dArr[1]; + } + elseif($inStr > 19000000){ + $retStr = substr($inStr,0,4).'-'.substr($inStr,4,2).'-'.substr($inStr,6,2); + } + return $retStr; + } +} +?> diff --git a/api/app/Helpers/TaxonomyHelper.php b/api/app/Helpers/TaxonomyHelper.php new file mode 100644 index 0000000000..779ef43af5 --- /dev/null +++ b/api/app/Helpers/TaxonomyHelper.php @@ -0,0 +1,367 @@ +'','unitname2'=>'','unitind3'=>'','unitname3'=>''); + //Remove UTF-8 NO-BREAK SPACE codepoints + $inStr = trim(str_replace(chr(194).chr(160), ' ', $inStr)); + if($inStr && is_string($inStr)){ + //Remove underscores, common in NPS data + $inStr = preg_replace('/_+/',' ',$inStr); + //Replace misc + $inStr = str_replace(array('?','*'),'',$inStr); + + if(stripos($inStr,'cfr. ') !== false || stripos($inStr,' cfr ') !== false){ + $retArr['identificationqualifier'] = 'cf. '; + $inStr = str_ireplace(array(' cfr ','cfr. '),' ',$inStr); + } + elseif(stripos($inStr,'cf. ') !== false || stripos($inStr,'c.f. ') !== false || stripos($inStr,' cf ') !== false){ + $retArr['identificationqualifier'] = 'cf. '; + $inStr = str_ireplace(array(' cf ','c.f. ','cf. '),' ',$inStr); + } + elseif(stripos($inStr,'aff. ') !== false || stripos($inStr,' aff ') !== false){ + $retArr['identificationqualifier'] = 'aff.'; + $inStr = trim(str_ireplace(array(' aff ','aff. '),' ',$inStr)); + } + if(stripos($inStr,' spp.')){ + $rankId = 180; + $inStr = str_ireplace(' spp.','',$inStr); + } + if(stripos($inStr,' sp.')){ + $rankId = 180; + $inStr = str_ireplace(' sp.','',$inStr); + } + //Remove extra spaces + $inStr = preg_replace('/\s\s+/',' ',$inStr); + + $sciNameArr = explode(' ',trim($inStr)); + if(count($sciNameArr)){ + if(strtolower($sciNameArr[0]) == 'x' || $sciNameArr[0] == '×'){ + $retArr['unitind1'] = array_shift($sciNameArr); + } + elseif(mb_ord($sciNameArr[0]) == 215){ + $retArr['unitind1'] = '×'; + $unitStr = substr(array_shift($sciNameArr), 2); + if($unitStr) array_unshift($sciNameArr, $unitStr); + } + elseif($sciNameArr[0] == '†' || mb_ord($sciNameArr[0]) == 8224){ + $retArr['unitind1'] = array_shift($sciNameArr); + } + elseif(strpos($sciNameArr[0],chr(8224)) === 0 ){ + $retArr['unitind1'] = '†'; + $sciNameArr[0] = trim($sciNameArr[0],'†'); + } + //Genus + $retArr['unitname1'] = ucfirst(strtolower(array_shift($sciNameArr))); + if(count($sciNameArr)){ + if(strtolower($sciNameArr[0]) == 'x' || $sciNameArr[0] == '×'){ + $retArr['unitind2'] = array_shift($sciNameArr); + $retArr['unitname2'] = array_shift($sciNameArr); + } + elseif(mb_ord($sciNameArr[0]) == 215){ + $retArr['unitind2'] = '×'; + $unitStr = substr(array_shift($sciNameArr), 2); + if($unitStr) $retArr['unitname2'] = $unitStr; + else $retArr['unitname2'] = array_shift($sciNameArr); + } + elseif(strpos($sciNameArr[0],'.') !== false){ + //It is assumed that Author has been reached, thus stop process + $retArr['author'] = implode(' ',$sciNameArr); + unset($sciNameArr); + } + else{ + if(strpos($sciNameArr[0],'(') !== false){ + //Assumed subgenus exists, but keep a author incase an epithet does exist + $retArr['author'] = implode(' ',$sciNameArr); + array_shift($sciNameArr); + } + //Specific Epithet + $retArr['unitname2'] = array_shift($sciNameArr); + } + if($retArr['unitname2'] && !preg_match('/^[\-\'a-z]+$/',$retArr['unitname2'])){ + if(preg_match('/[A-Z]{1}[\-\'a-z]+/',$retArr['unitname2'])){ + //Check to see if is term is genus author + if($conn){ + $sql = 'SELECT tid FROM taxa WHERE unitname1 = "'.$conn->real_escape_string($retArr['unitname1']).'" AND unitname2 = "'.$conn->real_escape_string($retArr['unitname2']).'"'; + $rs = $conn->query($sql); + if($rs->num_rows){ + if(isset($retArr['author'])) unset($retArr['author']); + } + else{ + //Second word is likely author, thus assume assume author has been reach and stop process + $retArr['author'] = trim($retArr['unitname2'].' '.implode(' ', $sciNameArr)); + $retArr['unitname2'] = ''; + unset($sciNameArr); + } + $rs->free(); + } + } + if($retArr['unitname2']){ + $retArr['unitname2'] = strtolower($retArr['unitname2']); + if(!preg_match('/^[\-\'a-z]+$/',$retArr['unitname2'])){ + //Second word unlikely an epithet + $retArr['author'] = trim($retArr['unitname2'].' '.implode(' ', $sciNameArr)); + $retArr['unitname2'] = ''; + unset($sciNameArr); + } + } + } + } + } + if(isset($sciNameArr) && $sciNameArr){ + if($rankId == 220){ + $retArr['author'] = implode(' ',$sciNameArr); + } + else{ + $authorArr = array(); + //cycles through the final terms to evaluate and extract infraspecific data + while($sciStr = array_shift($sciNameArr)){ + if($testArr = self::cleanInfra($sciStr)){ + self::setInfraNode($sciStr, $sciNameArr, $retArr, $authorArr, $testArr['infra']); + } + elseif($kingdomName == 'Animalia' && !$retArr['unitname3'] && ($rankId == 230 || preg_match('/^[a-z]{3,}$/',$sciStr) || preg_match('/^[A-Z]{3,}$/',$sciStr))){ + $retArr['unitind3'] = ''; + $retArr['unitname3'] = strtolower($sciStr); + unset($authorArr); + $authorArr = array(); + } + else{ + $authorArr[] = $sciStr; + } + } + $retArr['author'] = implode(' ', $authorArr); + //Double check to see if infraSpecificEpithet is still embedded in author due initial lack of taxonRank indicator + if(!$retArr['unitname3'] && $retArr['author']){ + $arr = explode(' ',$retArr['author']); + $firstWord = array_shift($arr); + if(preg_match('/^[\-\'a-z]{2,}$/',$firstWord)){ + if($conn){ + $sql = 'SELECT unitind3 FROM taxa WHERE unitname1 = "'.$conn->real_escape_string($retArr['unitname1']).'" AND unitname2 = "'.$conn->real_escape_string($retArr['unitname2']).'" AND unitname3 = "'.$conn->real_escape_string($firstWord).'" '; + //echo $sql.'
'; + $rs = $conn->query($sql); + if($r = $rs->fetch_object()){ + $retArr['unitind3'] = $r->unitind3; + $retArr['unitname3'] = $firstWord; + $retArr['author'] = implode(' ',$arr); + } + $rs->free(); + } + } + } + } + if(isset($retArr['author']) && mb_strpos($retArr['author'], '×') !== false){ + if((!isset($retArr['unitind3']) || !$retArr['unitind3']) && (!isset($retArr['unitname3']) || !$retArr['unitname3'])){ + $retArr['unitind3'] = '×'; + $retArr['unitname3'] = substr($retArr['author'], trim(strpos($retArr['author'], '×') + 2)); + if(!isset($retArr['rankid']) || !$retArr['rankid']) $retArr['rankid'] = 220; + } + } + } + //Set taxon rankid + if($rankId && is_numeric($rankId)){ + $retArr['rankid'] = $rankId; + } + else{ + if($retArr['unitname3']){ + if($retArr['unitind3'] == 'subsp.' || !$retArr['unitind3']){ + $retArr['rankid'] = 230; + } + elseif($retArr['unitind3'] == 'var.'){ + $retArr['rankid'] = 240; + } + elseif($retArr['unitind3'] == 'f.'){ + $retArr['rankid'] = 260; + } + } + elseif($retArr['unitname2']){ + $retArr['rankid'] = 220; + } + elseif($retArr['unitname1']){ + if(substr($retArr['unitname1'],-5) == 'aceae' || substr($retArr['unitname1'],-4) == 'idae'){ + $retArr['rankid'] = 140; + } + } + } + if($kingdomName == 'Animalia'){ + if($retArr['unitind3']){ + $retArr['unitind3'] = ''; + if($retArr['rankid'] > 220) $retArr['rankid'] = 230; + } + } + //Build sciname, without author + $sciname = ''; + if(!empty($retArr['unitind1'])){ + $sciname = $retArr['unitind1']; + if($retArr['unitind1'] != '×' || $retArr['unitind1'] != '†') $sciname .= ' '; + } + $sciname .= $retArr['unitname1'].' '; + if(!empty($retArr['unitind2'])){ + $sciname .= $retArr['unitind2']; + if($retArr['unitind2'] != '×') $sciname .= ' '; + } + $sciname .= $retArr['unitname2'].' '; + $sciname .= trim($retArr['unitind3'].' '.$retArr['unitname3']); + $retArr['sciname'] = trim($sciname); + } + return $retArr; + } + + public static function cleanInfra($testStr){ + $retArr = array(); + $testStr = str_replace(array('-','_',' '),'',$testStr); + $testStr = strtolower(trim($testStr,'.')); + if($testStr == 'cultivated' || $testStr == 'cv' ){ + $retArr['infra'] = 'cv.'; + $retArr['rankid'] = 300; + } + elseif($testStr == 'subform' || $testStr == 'subforma' || $testStr == 'subf' || $testStr == 'subfo'){ + $retArr['infra'] = 'subf.'; + $retArr['rankid'] = 270; + } + elseif($testStr == 'forma' || $testStr == 'f' || $testStr == 'fo'){ + $retArr['infra'] = 'f.'; + $retArr['rankid'] = 260; + } + elseif($testStr == 'subvariety' || $testStr == 'subvar' || $testStr == 'subv' || $testStr == 'sv'){ + $retArr['infra'] = 'subvar.'; + $retArr['rankid'] = 250; + } + elseif($testStr == 'variety' || $testStr == 'var' || $testStr == 'v'){ + $retArr['infra'] = 'var.'; + $retArr['rankid'] = 240; + } + elseif($testStr == 'subspecies' || $testStr == 'ssp' || $testStr == 'subsp'){ + $retArr['infra'] = 'subsp.'; + $retArr['rankid'] = 230; + } + return $retArr; + } + + private static function setInfraNode($sciStr, &$sciNameArr, &$retArr, &$authorArr, $rankTag){ + if($sciNameArr){ + $infraStr = array_shift($sciNameArr); + if(preg_match('/^[a-z]{3,}$/', $infraStr)){ + $retArr['unitind3'] = $rankTag; + $retArr['unitname3'] = $infraStr; + unset($authorArr); + $authorArr = array(); + } + else{ + $authorArr[] = $sciStr; + $authorArr[] = $infraStr; + } + } + } + + //Taxonomic indexing functions + public static function rebuildHierarchyEnumTree($conn = null){ + $status = true; + if($conn){ + if($conn->query('DELETE FROM taxaenumtree')){ + self::buildHierarchyEnumTree($conn); + } + else{ + $status = 'ERROR deleting taxaenumtree prior to re-populating: '.$conn->error; + } + } + else $status = 'ERROR deleting taxaenumtree prior to re-populating: NULL connection object'; + return $status; + } + + public static function buildHierarchyEnumTree($conn = null, $taxAuthId = 1){ + set_time_limit(600); + $status = true; + if($conn){ + //Seed taxaenumtree table + $sql = 'INSERT INTO taxaenumtree(tid,parenttid,taxauthid) + SELECT DISTINCT ts.tid, ts.parenttid, ts.taxauthid + FROM taxstatus ts + WHERE (ts.taxauthid = '.$taxAuthId.') AND ts.tid NOT IN(SELECT tid FROM taxaenumtree WHERE taxauthid = '.$taxAuthId.')'; + if($conn->query($sql)){ + //Set direct parents for all taxa + $sql2 = 'INSERT INTO taxaenumtree(tid,parenttid,taxauthid) + SELECT DISTINCT ts.tid, ts.parenttid, ts.taxauthid + FROM taxstatus ts LEFT JOIN taxaenumtree e ON ts.tid = e.tid AND ts.parenttid = e.parenttid AND ts.taxauthid = e.taxauthid + WHERE (ts.taxauthid = '.$taxAuthId.') AND (e.tid IS NULL)'; + if(!$conn->query($sql2)) $status = 'ERROR setting direct parents within taxaenumtree: '.$conn->error; + + //Continue adding more distint parents + $sql3 = 'INSERT INTO taxaenumtree(tid,parenttid,taxauthid) + SELECT DISTINCT e.tid, ts.parenttid, ts.taxauthid + FROM taxaenumtree e INNER JOIN taxstatus ts ON e.parenttid = ts.tid AND e.taxauthid = ts.taxauthid + LEFT JOIN taxaenumtree e2 ON e.tid = e2.tid AND ts.parenttid = e2.parenttid AND e.taxauthid = e2.taxauthid + WHERE (ts.taxauthid = '.$taxAuthId.') AND (e2.tid IS NULL)'; + $cnt = 0; + do{ + if(!$conn->query($sql3)){ + $status = 'ERROR building taxaenumtree: '.$conn->error; + break; + } + if(!$conn->affected_rows) break; + $cnt++; + }while($cnt < 30); + } + else{ + $status = 'ERROR seeding taxaenumtree: '.$conn->error; + } + } + else $status = 'ERROR re-populating taxaenumtree: NULL connection object'; + return $status; + } + + /* + public static function buildHierarchyNestedTree($conn, $taxAuthId = 1){ + if($conn){ + set_time_limit(1200); + //Get root and then build down + $startIndex = 1; + $rankId = 0; + $sql = 'SELECT ts.tid, t.rankid '. + 'FROM taxstatus ts INNER JOIN taxa t ON ts.tid = t.tid '. + 'WHERE (ts.taxauthid = '.$taxAuthId.') AND (ts.parenttid IS NULL OR ts.parenttid = ts.tid) '. + 'ORDER BY t.rankid '; + if($rs = $conn->query($sql)){ + while($r = $rs->fetch_object()){ + if($rankId && $rankId <> $r->rankid) break; + $rankId = $r->rankid; + $startIndex = self::loadTaxonIntoNestedTree($conn, $r->tid, $taxAuthId, $startIndex); + } + $rs->free(); + } + } + else{ + $status = 'ERROR building hierarchy nested tree: NULL connection object'; + } + } + + private static function loadTaxonIntoNestedTree($conn, $tid, $taxAuthId, $startIndex){ + $endIndex = $startIndex + 1; + $sql = 'SELECT tid '. + 'FROM taxstatus '. + 'WHERE (taxauthid = '.$taxAuthId.') AND (parenttid = '.$tid.')'; + if($rs = $conn->query($sql)){ + while($r = $rs->fetch_object()){ + $endIndex = self::loadTaxonIntoNestedTree($conn, $r->tid, $taxAuthId, $endIndex); + } + $rs->free(); + } + //Load into taxanestedtree + $sqlInsert = 'REPLACE INTO taxanestedtree(tid,taxauthid,leftindex,rightindex) '. + 'VALUES ('.$tid.','.$taxAuthId.','.$startIndex.','.$endIndex.')'; + $conn->query($sqlInsert); + //Return endIndex plus one + $endIndex++; + return $endIndex; + } + */ +} +?> \ No newline at end of file diff --git a/api/app/Http/Controllers/MediaController.php b/api/app/Http/Controllers/MediaController.php index 5cc9c9d55d..b9e206a34c 100644 --- a/api/app/Http/Controllers/MediaController.php +++ b/api/app/Http/Controllers/MediaController.php @@ -287,7 +287,7 @@ public function insert(Request $request){ * ), * @OA\RequestBody( * required=true, - * description="Media object to create", + * description="Media object to be updated", * @OA\MediaType( * mediaType="application/json", * @OA\Schema( diff --git a/api/app/Http/Controllers/OccurrenceController.php b/api/app/Http/Controllers/OccurrenceController.php index 0b1d131053..351da748da 100644 --- a/api/app/Http/Controllers/OccurrenceController.php +++ b/api/app/Http/Controllers/OccurrenceController.php @@ -5,10 +5,13 @@ use App\Models\Occurrence; use App\Models\PortalIndex; use App\Models\PortalOccurrence; +use App\Helpers\OccurrenceHelper; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; class OccurrenceController extends Controller{ + /** * Occurrence controller instance. * @@ -23,6 +26,13 @@ public function __construct(){ * operationId="/api/v2/occurrence/search", * tags={""}, * @OA\Parameter( + * name="collid", + * in="query", + * description="collid(s) - collection identifier(s) in portal", + * required=false, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( * name="catalogNumber", * in="query", * description="catalogNumber", @@ -37,58 +47,72 @@ public function __construct(){ * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="country", + * name="family", * in="query", - * description="country", + * description="family", * required=false, * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="stateProvince", + * name="sciname", * in="query", - * description="State, Province, or second level political unit", + * description="Scientific Name - binomen only without authorship", * required=false, * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="county", + * name="recordedBy", * in="query", - * description="County, parish, or third level political unit", + * description="Collector/observer of occurrence", * required=false, * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="collid", + * name="recordedByLastName", * in="query", - * description="collid - collection identifier in portal", + * description="Last name of collector/observer of occurrence", * required=false, * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="datasetID", + * name="recordNumber", * in="query", - * description="dataset ID within portal", + * description="Personal number of the collector or observer of the occurrence", * required=false, * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="family", + * name="eventDate", * in="query", - * description="family", + * description="Date as YYYY, YYYY-MM or YYYY-MM-DD", * required=false, * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="sciname", + * name="country", * in="query", - * description="Scientific Name - binomen only without authorship", + * description="country", * required=false, * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="eventDate", + * name="stateProvince", * in="query", - * description="Date as YYYY, YYYY-MM or YYYY-MM-DD", + * description="State, Province, or second level political unit", + * required=false, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="county", + * in="query", + * description="County, parish, or third level political unit", + * required=false, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="datasetID", + * in="query", + * description="dataset ID within portal", * required=false, * @OA\Schema(type="string") * ), @@ -119,35 +143,70 @@ public function __construct(){ */ public function showAllOccurrences(Request $request){ $this->validate($request, [ + 'collid' => 'regex:/^[\d,]+?$/', 'limit' => ['integer', 'max:300'], 'offset' => 'integer' ]); $limit = $request->input('limit',100); $offset = $request->input('offset',0); - $conditions = []; - if($request->has('catalogNumber')) $conditions[] = ['catalogNumber',$request->catalogNumber]; - if($request->has('occurrenceID')) $conditions[] = ['occurrenceID',$request->occurrenceID]; - if($request->has('country')) $conditions[] = ['country',$request->country]; - if($request->has('stateProvince')) $conditions[] = ['stateProvince',$request->stateProvince]; - if($request->has('county')) $conditions[] = ['county','LIKE',$request->county.'%']; - if($request->has('collid')) $conditions[] = ['collid',$request->collid]; - if($request->has('family')) $conditions[] = ['family',$request->family]; - if($request->has('sciname')) $conditions[] = ['sciname','LIKE',$request->sciname.'%']; - if($request->has('datasetID')) $conditions[] = ['datasetID',$request->datasetID]; - if($request->has('eventDate')) $conditions[] = ['eventDate','LIKE',$request->eventDate.'%']; + $occurrenceModel = Occurrence::query(); + if($request->has('collid')){ + $occurrenceModel->whereIn('collid', explode(',', $request->collid)); + } + if($request->has('catalogNumber')){ + $occurrenceModel->where('catalogNumber', $request->catalogNumber); + } + if($request->has('occurrenceID')){ + //$occurrenceModel->where('occurrenceID', $request->occurrenceID); + $occurrenceID = $request->occurrenceID; + $occurrenceModel->where(function ($query) use ($occurrenceID) {$query->where('occurrenceID', $occurrenceID)->orWhere('recordID', $occurrenceID);}); + } + //Taxonomy + if($request->has('family')){ + $occurrenceModel->where('family', $request->family); + } + if($request->has('sciname')){ + $occurrenceModel->where('sciname', $request->sciname); + } + //Collector units + if($request->has('recordedBy')){ + $occurrenceModel->where('recordedBy', $request->recordedBy); + } + if($request->has('recordedByLastName')){ + $occurrenceModel->where('recordedBy', 'LIKE', '%' . $request->recordedByLastName . '%'); + } + if($request->has('recordNumber')){ + $occurrenceModel->where('recordNumber', $request->recordNumber); + } + if($request->has('eventDate')){ + $occurrenceModel->where('eventDate', $request->eventDate); + } + if($request->has('datasetID')){ + $occurrenceModel->where('datasetID', $request->datasetID); + } + //Locality place names + if($request->has('country')){ + $occurrenceModel->where('country', $request->country); + } + if($request->has('stateProvince')){ + $occurrenceModel->where('stateProvince', $request->stateProvince); + } + if($request->has('county')){ + $occurrenceModel->where('county', $request->county); + } - $fullCnt = Occurrence::where($conditions)->count(); - $result = Occurrence::where($conditions)->skip($offset)->take($limit)->get(); + $fullCnt = $occurrenceModel->count(); + $result = $occurrenceModel->skip($offset)->take($limit)->get(); $eor = false; $retObj = [ - "offset" => (int)$offset, - "limit" => (int)$limit, - "endOfRecords" => $eor, - "count" => $fullCnt, - "results" => $result + 'offset' => (int)$offset, + 'limit' => (int)$limit, + 'endOfRecords' => $eor, + 'count' => $fullCnt, + 'results' => $result ]; return response()->json($retObj); } @@ -263,6 +322,283 @@ public function showOneOccurrenceMedia($id, Request $request){ return response()->json($media); } + //Write funcitons + public function insert(Request $request){ + if($user = $this->authenticate($request)){ + $this->validate($request, [ + 'collid' => 'required|integer' + ]); + $collid = $request->input('collid'); + //Check to see if user has the necessary permission edit/add occurrences for target collection + if(!$this->isAuthorized($user, $collid)){ + return response()->json(['error' => 'Unauthorized to add new records to target collection (collid = ' . $collid . ')'], 401); + } + $inputArr = $request->all(); + $inputArr['recordID'] = (string) Str::uuid(); + $inputArr['dateEntered'] = date('Y-m-d H:i:s'); + + //$occurrence = Occurrence::create($inputArr); + //return response()->json($occurrence, 201); + } + return response()->json(['error' => 'Unauthorized'], 401); + } + + public function update($id, Request $request){ + if($user = $this->authenticate($request)){ + $id = $this->getOccid($id); + $occurrence = Occurrence::find($id); + if(!$occurrence){ + return response()->json(['status' => 'failure', 'error' => 'Occurrence resource not found'], 400); + } + if(!$this->isAuthorized($user, $occurrence['collid'])){ + return response()->json(['error' => 'Unauthorized to edit target collection (collid = ' . $occurrence['collid'] . ')'], 401); + } + //$occurrence->update($request->all()); + //return response()->json($occurrence, 200); + } + return response()->json(['error' => 'Unauthorized'], 401); + } + + public function delete($id, Request $request){ + if($user = $this->authenticate($request)){ + $id = $this->getOccid($id); + $occurrence = Occurrence::find($id); + if(!$occurrence){ + return response()->json(['status' => 'failure', 'error' => 'Occurrence resource not found'], 400); + } + if(!$this->isAuthorized($user, $occurrence['collid'])){ + return response()->json(['error' => 'Unauthorized to delete target collection (collid = ' . $occurrence['collid'] . ')'], 401); + } + //$occurrence->delete(); + //return response('Occurrence Deleted Successfully', 200); + } + return response()->json(['error' => 'Unauthorized'], 401); + } + + /** + * @OA\Post( + * path="/api/v2/occurrence/skeletal", + * operationId="skeletalImport", + * description="If an existing record can be located within target collection based on matching the input identifier, empty (null) target fields will be updated with Skeletal Data. + If the target field contains data, it will remain unaltered. + If multiple records are returned matching the input identifier, data will be added only to the first record. + If an identifier is not provided or a matching record can not be found, a new Skeletal record will be created and primed with input data. + Note that catalogNumber or otherCatalogNumber must be provided to create a new skeletal record. If processingStatus is not defined, new skeletal records will be set as 'unprocessed'", + * tags={""}, + * @OA\Parameter( + * name="apiToken", + * in="query", + * description="API security token to authenticate post action", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="collid", + * in="query", + * description="primary key of target collection dataset", + * required=true, + * @OA\Schema(type="integer") + * ), + * @OA\Parameter( + * name="identifier", + * in="query", + * description="catalog number, other identifiers, occurrenceID, or recordID GUID (UUID) used to locate target occurrence occurrence", + * required=false, + * @OA\Schema(type="string") + * ), + * @OA\Parameter( + * name="identifierTarget", + * in="query", + * description="Target field for matching identifier: catalog number, other identifiers (aka otherCatalogNumbers), GUID (occurrenceID or recordID), occid (primary key for occurrence). If identifier field is null, a new skeletal record will be created, given that a catalog number is provided.", + * required=false, + * @OA\Schema( + * type="string", + * default="CATALOGNUMBER", + * enum={"CATALOGNUMBER", "IDENTIFIERS", "GUID", "OCCID", "NONE"} + * ) + * ), + * @OA\RequestBody( + * required=true, + * description="Occurrence data to be inserted", + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * @OA\Property( + * property="catalogNumber", + * type="string", + * description="Primary catalog number", + * maxLength=32 + * ), + * @OA\Property( + * property="otherCatalogNumbers", + * type="string", + * description="Additional catalog numbers", + * maxLength=75 + * ), + * @OA\Property( + * property="sciname", + * type="string", + * description="Scientific name, without the author", + * maxLength=255 + * ), + * @OA\Property( + * property="scientificNameAuthorship", + * type="string", + * description="The authorship information of scientific name", + * maxLength=255 + * ), + * @OA\Property( + * property="family", + * type="string", + * description="Taxonomic family of the scientific name", + * maxLength=255 + * ), + * @OA\Property( + * property="recordedBy", + * type="string", + * description="Primary collector or observer", + * maxLength=255 + * ), + * @OA\Property( + * property="recordNumber", + * type="string", + * description="Identifier given at the time occurrence was recorded; typically the personal identifier of the primary collector or observer", + * maxLength=45 + * ), + * @OA\Property( + * property="eventDate", + * type="string", + * description="Date the occurrence was collected or observed, or earliest date if a range was provided" + * ), + * @OA\Property( + * property="eventDate2", + * type="string", + * description="Last date the occurrence was collected or observed. Used when a date range is provided" + * ), + * @OA\Property( + * property="country", + * type="string", + * description="The name of the country or major administrative unit", + * maxLength=64 + * ), + * @OA\Property( + * property="stateProvince", + * type="string", + * description="The name of the next smaller administrative region than country (state, province, canton, department, region, etc.)", + * maxLength=255 + * ), + * @OA\Property( + * property="county", + * type="string", + * description="The full, unabbreviated name of the next smaller administrative region than stateProvince (county, shire, department, etc.", + * maxLength=255 + * ), + * @OA\Property( + * property="processingStatus", + * type="string", + * description="Processing status of the specimen record", + * maxLength=45 + * ), + * ), + * ) + * ), + * @OA\Response( + * response="200", + * description="Returns full JSON object of the of media record that was edited" + * ), + * @OA\Response( + * response="400", + * description="Error: Bad request.", + * ), + * @OA\Response( + * response="401", + * description="Unauthorized", + * ), + * ) + */ + public function skeletalImport(Request $request){ + $this->validate($request, [ + 'collid' => 'required|integer', + 'eventDate' => 'date', + 'eventDate2' => 'date', + 'identifierTarget' => 'in:CATALOGNUMBER,IDENTIFIERS,GUID,OCCID,NONE', + ]); + if($user = $this->authenticate($request)){ + $collid = $request->input('collid'); + $identifier = $request->input('identifier'); + $identifierTarget = $request->input('identifierTarget', 'CATALOGNUMBER'); + + //Check to see if user has the necessary permission edit/add occurrences for target collection + if(!$this->isAuthorized($user, $collid)){ + return response()->json(['error' => 'Unauthorized to edit target collection (collid = ' . $collid . ')'], 401); + } + + //Remove fields with empty values and non-approved target fields + $updateArr = $request->all(); + $skeletalFieldsAllowed = array('catalogNumber', 'otherCatalogNumbers', 'sciname', 'scientificNameAuthorship', 'family', 'recordedBy', 'recordNumber', 'eventDate', 'eventDate2', 'country', 'stateProvince', 'county', 'processingStatus'); + foreach($updateArr as $fieldName => $fieldValue){ + if(!$fieldValue) unset($updateArr[$fieldName]); + elseif(!in_array($fieldName, $skeletalFieldsAllowed)) unset($updateArr[$fieldName]); + } + if(!$updateArr){ + return response()->json(['error' => 'Bad request: input data empty or does not contains allowed fields'], 400); + } + + //Get target record, if exists + $targetOccurrence = null; + if($identifier){ + $occurrenceModel = null; + if($identifierTarget == 'OCCID'){ + $occurrenceModel = Occurrence::where('occid', $identifier); + } + elseif($identifierTarget == 'GUID'){ + $occurrenceModel = Occurrence::where('occurrenceID', $identifier)->orWhere('recordID', $identifier); + } + elseif($identifierTarget == 'CATALOGNUMBER'){ + $occurrenceModel = Occurrence::where('catalogNumber', $identifier); + } + elseif($identifierTarget == 'IDENTIFIERS'){ + $occurrenceModel = Occurrence::where('otherCatalogNumbers', $identifier); + } + if($occurrenceModel){ + $targetOccurrence = $occurrenceModel->where('collid', $collid)->first(); + } + } + if($targetOccurrence){ + foreach($updateArr as $fieldName => $fieldValue){ + //Remove input if target field already contains data + if($targetOccurrence[$fieldName]){ + unset($updateArr[$fieldName]); + } + } + if(!empty($updateArr['eventDate'])){ + $updateArr['eventDate'] = OccurrenceHelper::formatDate($updateArr['eventDate']); + } + if(!empty($updateArr['eventDate2'])){ + $updateArr['eventDate2'] = OccurrenceHelper::formatDate($updateArr['eventDate2']); + } + $responseObj = ['number of fields affected' => count($updateArr), 'fields affected' => $updateArr]; + if($updateArr){ + $targetOccurrence->update($updateArr); + } + return response()->json($responseObj, 200); + } + else{ + //Record doesn't exist, thus create a new skeletal records, given that a catalog number exists + $updateArr['collid'] = $collid; + if(empty($updateArr['catalogNumber']) && empty($updateArr['otherCatalogNumbers'])){ + return response()->json(['error' => 'Bad request: catalogNumber or otherCatalogNumbers required when creating a new record'], 400); + } + if(empty($updateArr['processingStatus'])) $updateArr['processingStatus'] = 'unprocessed'; + $updateArr['recordID'] = (string) Str::uuid(); + $updateArr['dateEntered'] = date('Y-m-d H:i:s'); + $newOccurrence = Occurrence::create($updateArr); + return response()->json($newOccurrence, 201); + } + } + return response()->json(['error' => 'Unauthorized'], 401); + } + /** * @off_OA\Get( * path="/api/v2/occurrence/{identifier}/reharvest", @@ -351,24 +687,6 @@ public function oneOccurrenceReharvest($id, Request $request){ return response()->json($responseArr); } - //Write funcitons - public function create(Request $request){ - //$occurrence = Occurrence::create($request->all()); - //return response()->json($occurrence, 201); - } - - public function update($id, Request $request){ - $occurrence = Occurrence::findOrFail($id); - $occurrence->update($request->all()); - //if($occurrence->wasChanged()) ; - return response()->json($occurrence, 200); - } - - public function delete($id){ - //Occurrence::findOrFail($id)->delete(); - //return response('Occurrence Deleted Successfully', 200); - } - //Helper functions protected function getOccid($id){ if(!is_numeric($id)){ @@ -378,6 +696,15 @@ protected function getOccid($id){ return $id; } + private function isAuthorized($user, $collid){ + foreach($user['roles'] as $roles){ + if($roles['role'] == 'SuperAdmin') return true; + elseif($roles['role'] == 'CollAdmin' && $roles['tablePK'] == $collid) return true; + elseif($roles['role'] == 'CollEditor' && $roles['tablePK'] == $collid) return true; + } + return false; + } + protected function getAPIResponce($url, $asyc = false){ $resJson = false; $ch = curl_init(); diff --git a/api/app/Http/Controllers/TaxonomyController.php b/api/app/Http/Controllers/TaxonomyController.php index 6556c9b8f2..08e29a941f 100644 --- a/api/app/Http/Controllers/TaxonomyController.php +++ b/api/app/Http/Controllers/TaxonomyController.php @@ -81,13 +81,6 @@ public function showAllTaxa(Request $request){ * @OA\Schema(type="string") * ), * @OA\Parameter( - * name="taxon", - * in="query", - * description="Taxon searh term", - * required=true, - * @OA\Schema(type="string") - * ), - * @OA\Parameter( * name="type", * in="query", * description="Type of search", @@ -134,32 +127,26 @@ public function showAllTaxaSearch(Request $request){ $type = $request->input('type', 'EXACT'); - $fullCnt = 0; - $result = []; + $taxaModel = Taxonomy::query(); if($type == 'START'){ - $fullCnt = Taxonomy::where('sciname', 'like', $request->taxon . '%')->count(); - $result = Taxonomy::where('sciname', 'like', $request->taxon . '%')->skip($offset)->take($limit)->get(); + $taxaModel->where('sciname', 'LIKE', $request->taxon . '%'); } elseif($type == 'WILD'){ - $fullCnt = Taxonomy::where('sciname', 'like', '%' . $request->taxon . '%')->count(); - $result = Taxonomy::where('sciname', 'like', '%' . $request->taxon . '%')->skip($offset)->take($limit)->get(); + $taxaModel->where('sciname', 'LIKE', '%' . $request->taxon . '%'); } elseif($type == 'WHOLEWORD'){ - $fullCnt = Taxonomy::where('unitname1', $request->taxon) - ->orWhere('unitname2', $request->taxon) - ->orWhere('unitname3', $request->taxon) - ->count(); - $result = Taxonomy::where('unitname1', $request->taxon) - ->orWhere('unitname2', $request->taxon) - ->orWhere('unitname3', $request->taxon) - ->skip($offset)->take($limit)->get(); + $taxaModel->where('unitname1', $request->taxon) + ->orWhere('unitname2', $request->taxon) + ->orWhere('unitname3', $request->taxon); } else{ //Exact match - $fullCnt = Taxonomy::where('sciname', $request->taxon)->count(); - $result = Taxonomy::where('sciname', $request->taxon)->skip($offset)->take($limit)->get(); + $taxaModel->where('sciname', $request->taxon); } + $fullCnt = $taxaModel->count(); + $result = $taxaModel->skip($offset)->take($limit)->get(); + $eor = false; $retObj = [ 'offset' => (int)$offset, diff --git a/api/app/Models/Occurrence.php b/api/app/Models/Occurrence.php index 8aad2bedc8..ced0293f55 100644 --- a/api/app/Models/Occurrence.php +++ b/api/app/Models/Occurrence.php @@ -9,18 +9,40 @@ class Occurrence extends Model{ protected $primaryKey = 'occid'; public $timestamps = false; - protected $fillable = [ 'basisOfRecord', 'occurrenceID', 'catalogNumber', 'otherCatalogNumbers', 'family', 'scientificName', 'sciname', 'genus', 'specificEpithet', 'datasetID', 'organismID', + protected $fillable = [ 'collid', 'dbpk', 'basisOfRecord', 'occurrenceID', 'catalogNumber', 'otherCatalogNumbers', 'family', 'scientificName', 'sciname', 'genus', 'specificEpithet', 'datasetID', 'organismID', 'taxonRank', 'infraspecificEpithet', 'institutionCode', 'collectionCode', 'scientificNameAuthorship', 'taxonRemarks', 'identifiedBy', 'dateIdentified', 'identificationReferences', 'identificationRemarks', 'identificationQualifier', 'typeStatus', 'recordedBy', 'recordNumber', 'associatedCollectors', 'eventDate', 'eventDate2', - 'verbatimEventDate', 'eventTime', 'habitat', 'substrate', 'fieldNotes', 'fieldnumber', 'eventID', 'occurrenceRemarks', 'informationWithheld', 'dataGeneralizations', + 'verbatimEventDate', 'eventTime', 'habitat', 'substrate', 'fieldNotes', 'fieldNumber', 'eventID', 'occurrenceRemarks', 'informationWithheld', 'dataGeneralizations', 'associatedTaxa', 'dynamicProperties', 'verbatimAttributes', 'behavior', 'reproductiveCondition', 'cultivationStatus', 'establishmentMeans', 'lifeStage', 'sex', 'individualCount', 'samplingProtocol', 'samplingEffort', 'preparations', 'locationID', 'continent', 'parentLocationID', 'country', 'stateProvince', 'county', 'municipality', 'waterBody', 'islandGroup', 'island', 'countryCode', 'locality', 'localitySecurity', 'localitySecurityReason', 'decimalLatitude', 'decimalLongitude', 'geodeticDatum', 'coordinateUncertaintyInMeters', 'footprintWKT', 'locationRemarks', 'verbatimCoordinates', 'georeferencedBy', 'georeferencedDate', 'georeferenceProtocol', 'georeferenceSources', 'georeferenceVerificationStatus', 'georeferenceRemarks', 'minimumElevationInMeters', 'maximumElevationInMeters', 'verbatimElevation', 'minimumDepthInMeters', 'maximumDepthInMeters', - 'verbatimDepth', 'availability', 'disposition', 'storageLocation', 'modified', 'language', 'processingstatus', 'recordEnteredBy', 'duplicateQuantity', 'labelProject']; - protected $hidden = [ 'scientificName', 'recordedbyid', 'observerUid', 'labelProject', 'processingStatus', 'recordEnteredBy', 'associatedOccurrences', 'previousIdentifications', - 'verbatimCoordinateSystem', 'coordinatePrecision', 'dynamicFields', 'institutionID', 'collectionID', 'genericcolumn1', 'genericcolumn2' ]; + 'verbatimDepth', 'availability', 'disposition', 'storageLocation', 'modified', 'language', 'processingStatus', 'recordEnteredBy', 'duplicateQuantity', 'labelProject', 'recordID', 'dateEntered']; + protected $hidden = [ 'collection', 'scientificName', 'recordedbyid', 'observerUid', 'labelProject', 'recordEnteredBy', 'associatedOccurrences', 'previousIdentifications', + 'verbatimCoordinateSystem', 'coordinatePrecision', 'footprintWKT', 'dynamicFields', 'institutionID', 'collectionID', 'genericColumn1', 'genericColumn2' ]; + public static $snakeAttributes = false; + + public function getInstitutionCodeAttribute($value){ + if(!$value){ + $value = $this->collection->institutionCode; + } + return $value; + } + + public function getCollectionCodeAttribute($value){ + if(!$value){ + $value = $this->collection->collectionCode; + } + return $value; + } + + public function getOccurrenceIDAttribute($value){ + if(!$value && $this->collection->guidTarget == 'symbiotaUUID'){ + $value = $this->attributes['recordID']; + } + return $value; + } public function collection(){ return $this->belongsTo(Collection::class, 'collid', 'collid'); @@ -49,6 +71,4 @@ public function annotationInternalColl(){ public function portalPublications(){ return $this->belongsToMany(PortalPublication::class, 'portaloccurrences', 'occid', 'pubid')->withPivot('remoteOccid');; } - - } \ No newline at end of file diff --git a/api/composer.json b/api/composer.json index 4f23bce430..ed471feb4c 100644 --- a/api/composer.json +++ b/api/composer.json @@ -20,7 +20,10 @@ "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "app/Helpers/OccurrenceHelper.php" + ] }, "autoload-dev": { "classmap": [ diff --git a/api/routes/web.php b/api/routes/web.php index f05a482c32..4ac724458e 100644 --- a/api/routes/web.php +++ b/api/routes/web.php @@ -40,6 +40,7 @@ $router->get('occurrence/{id}/annotation', ['uses' => 'OccurrenceAnnotationController@showOccurrenceAnnotations']); $router->get('occurrence/{id}/reharvest', ['uses' => 'OccurrenceController@oneOccurrenceReharvest']); $router->get('occurrence/annotation/search', ['uses' => 'OccurrenceAnnotationController@showAllAnnotations']); + $router->post('occurrence/skeletal', ['uses' => 'OccurrenceController@skeletalImport']); $router->get('installation', ['uses' => 'InstallationController@showAllPortals']); $router->get('installation/ping', ['uses' => 'InstallationController@pingPortal']); diff --git a/api/storage/api-docs/api-docs.json b/api/storage/api-docs/api-docs.json index 147ba27fc9..fbd7ee7ac6 100644 --- a/api/storage/api-docs/api-docs.json +++ b/api/storage/api-docs/api-docs.json @@ -687,7 +687,7 @@ } ], "requestBody": { - "description": "Media object to create", + "description": "Media object to be updated", "required": true, "content": { "application/json": { @@ -932,6 +932,15 @@ ], "operationId": "/api/v2/occurrence/search", "parameters": [ + { + "name": "collid", + "in": "query", + "description": "collid(s) - collection identifier(s) in portal", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "catalogNumber", "in": "query", @@ -951,72 +960,90 @@ } }, { - "name": "country", + "name": "family", "in": "query", - "description": "country", + "description": "family", "required": false, "schema": { "type": "string" } }, { - "name": "stateProvince", + "name": "sciname", "in": "query", - "description": "State, Province, or second level political unit", + "description": "Scientific Name - binomen only without authorship", "required": false, "schema": { "type": "string" } }, { - "name": "county", + "name": "recordedBy", "in": "query", - "description": "County, parish, or third level political unit", + "description": "Collector/observer of occurrence", "required": false, "schema": { "type": "string" } }, { - "name": "collid", + "name": "recordedByLastName", "in": "query", - "description": "collid - collection identifier in portal", + "description": "Last name of collector/observer of occurrence", "required": false, "schema": { "type": "string" } }, { - "name": "datasetID", + "name": "recordNumber", "in": "query", - "description": "dataset ID within portal", + "description": "Personal number of the collector or observer of the occurrence", "required": false, "schema": { "type": "string" } }, { - "name": "family", + "name": "eventDate", "in": "query", - "description": "family", + "description": "Date as YYYY, YYYY-MM or YYYY-MM-DD", "required": false, "schema": { "type": "string" } }, { - "name": "sciname", + "name": "country", "in": "query", - "description": "Scientific Name - binomen only without authorship", + "description": "country", "required": false, "schema": { "type": "string" } }, { - "name": "eventDate", + "name": "stateProvince", "in": "query", - "description": "Date as YYYY, YYYY-MM or YYYY-MM-DD", + "description": "State, Province, or second level political unit", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "county", + "in": "query", + "description": "County, parish, or third level political unit", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "datasetID", + "in": "query", + "description": "dataset ID within portal", "required": false, "schema": { "type": "string" @@ -1172,6 +1199,148 @@ } } }, + "/api/v2/occurrence/skeletal": { + "post": { + "tags": [ + "" + ], + "description": "If an existing record can be located within target collection based on matching the input identifier, empty (null) target fields will be updated with Skeletal Data.\r\n\t\t\tIf the target field contains data, it will remain unaltered.\r\n\t\t\tIf multiple records are returned matching the input identifier, data will be added only to the first record.\r\n\t\t\tIf an identifier is not provided or a matching record can not be found, a new Skeletal record will be created and primed with input data.\r\n\t\t\tNote that catalogNumber or otherCatalogNumber must be provided to create a new skeletal record. If processingStatus is not defined, new skeletal records will be set as 'unprocessed'", + "operationId": "skeletalImport", + "parameters": [ + { + "name": "apiToken", + "in": "query", + "description": "API security token to authenticate post action", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "collid", + "in": "query", + "description": "primary key of target collection dataset", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "identifier", + "in": "query", + "description": "catalog number, other identifiers, occurrenceID, or recordID GUID (UUID) used to locate target occurrence occurrence", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "identifierTarget", + "in": "query", + "description": "Target field for matching identifier: catalog number, other identifiers (aka otherCatalogNumbers), GUID (occurrenceID or recordID), occid (primary key for occurrence). If identifier field is null, a new skeletal record will be created, given that a catalog number is provided.", + "required": false, + "schema": { + "type": "string", + "default": "CATALOGNUMBER", + "enum": [ + "CATALOGNUMBER", + "IDENTIFIERS", + "GUID", + "OCCID", + "NONE" + ] + } + } + ], + "requestBody": { + "description": "Occurrence data to be inserted", + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "catalogNumber": { + "description": "Primary catalog number", + "type": "string", + "maxLength": 32 + }, + "otherCatalogNumbers": { + "description": "Additional catalog numbers", + "type": "string", + "maxLength": 75 + }, + "sciname": { + "description": "Scientific name, without the author", + "type": "string", + "maxLength": 255 + }, + "scientificNameAuthorship": { + "description": "The authorship information of scientific name", + "type": "string", + "maxLength": 255 + }, + "family": { + "description": "Taxonomic family of the scientific name", + "type": "string", + "maxLength": 255 + }, + "recordedBy": { + "description": "Primary collector or observer", + "type": "string", + "maxLength": 255 + }, + "recordNumber": { + "description": "Identifier given at the time occurrence was recorded; typically the personal identifier of the primary collector or observer", + "type": "string", + "maxLength": 45 + }, + "eventDate": { + "description": "Date the occurrence was collected or observed, or earliest date if a range was provided", + "type": "string" + }, + "eventDate2": { + "description": "Last date the occurrence was collected or observed. Used when a date range is provided", + "type": "string" + }, + "country": { + "description": "The name of the country or major administrative unit", + "type": "string", + "maxLength": 64 + }, + "stateProvince": { + "description": "The name of the next smaller administrative region than country (state, province, canton, department, region, etc.)", + "type": "string", + "maxLength": 255 + }, + "county": { + "description": "The full, unabbreviated name of the next smaller administrative region than stateProvince (county, shire, department, etc.", + "type": "string", + "maxLength": 255 + }, + "processingStatus": { + "description": "Processing status of the specimen record", + "type": "string", + "maxLength": 45 + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Returns full JSON object of the of media record that was edited" + }, + "400": { + "description": "Error: Bad request." + }, + "401": { + "description": "Unauthorized" + } + } + } + }, "/api/v2/taxonomy": { "get": { "tags": [ @@ -1222,15 +1391,6 @@ ], "operationId": "/api/v2/taxonomy/search", "parameters": [ - { - "name": "taxon", - "in": "query", - "description": "Taxon searh term", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "taxon", "in": "query", diff --git a/api/vendor/composer/autoload_classmap.php b/api/vendor/composer/autoload_classmap.php index 92aefed7a4..5799e1349d 100644 --- a/api/vendor/composer/autoload_classmap.php +++ b/api/vendor/composer/autoload_classmap.php @@ -10,6 +10,10 @@ 'App\\Events\\Event' => $baseDir . '/app/Events/Event.php', 'App\\Events\\ExampleEvent' => $baseDir . '/app/Events/ExampleEvent.php', 'App\\Exceptions\\Handler' => $baseDir . '/app/Exceptions/Handler.php', + 'App\\Helpers\\GPoint' => $baseDir . '/app/Helpers/GPoint.php', + 'App\\Helpers\\Helper' => $baseDir . '/app/Helpers/Helper.php', + 'App\\Helpers\\OccurrenceHelper' => $baseDir . '/app/Helpers/OccurrenceHelper.php', + 'App\\Helpers\\TaxonomyHelper' => $baseDir . '/app/Helpers/TaxonomyHelper.php', 'App\\Http\\Controllers\\CollectionController' => $baseDir . '/app/Http/Controllers/CollectionController.php', 'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\DuplicateController' => $baseDir . '/app/Http/Controllers/DuplicateController.php', @@ -42,6 +46,8 @@ 'App\\Models\\TaxonomyDescription' => $baseDir . '/app/Models/TaxonomyDescription.php', 'App\\Models\\TaxonomyDescriptionStatement' => $baseDir . '/app/Models/TaxonomyDescriptionStatement.php', 'App\\Models\\User' => $baseDir . '/app/Models/User.php', + 'App\\Models\\UserAccessToken' => $baseDir . '/app/Models/UserAccessToken.php', + 'App\\Models\\UserRole' => $baseDir . '/app/Models/UserRole.php', 'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AuthServiceProvider' => $baseDir . '/app/Providers/AuthServiceProvider.php', 'App\\Providers\\EventServiceProvider' => $baseDir . '/app/Providers/EventServiceProvider.php', diff --git a/api/vendor/composer/autoload_files.php b/api/vendor/composer/autoload_files.php index 310a2532d2..30d4dd6853 100644 --- a/api/vendor/composer/autoload_files.php +++ b/api/vendor/composer/autoload_files.php @@ -8,15 +8,13 @@ return array( '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', - '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', 'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', - '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php', - '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', - 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', + '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '60799491728b879e74601d83e38b2cad' => $vendorDir . '/illuminate/collections/helpers.php', 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', '72579e7bd17821bb1321b87411366eae' => $vendorDir . '/illuminate/support/helpers.php', @@ -26,8 +24,11 @@ 'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php', 'ef65a1626449d89d0811cf9befce46f0' => $vendorDir . '/illuminate/events/functions.php', '253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php', - '0ccdf99b8f62f02c52cba55802e0c2e7' => $vendorDir . '/zircote/swagger-php/src/functions.php', + '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', 'bee9632da3ca00a99623b9c35d0c4f8b' => $vendorDir . '/laravel/lumen-framework/src/helpers.php', '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', + '0ccdf99b8f62f02c52cba55802e0c2e7' => $vendorDir . '/zircote/swagger-php/src/functions.php', '3585c46f9622c6b622ab0011d4d72b3a' => $vendorDir . '/darkaonline/swagger-lume/src/helpers.php', + 'e54f50f1f018f3f5bdd79170d9f301b2' => $baseDir . '/app/Helpers/OccurrenceHelper.php', ); diff --git a/api/vendor/composer/autoload_psr4.php b/api/vendor/composer/autoload_psr4.php index a498225796..db0d5537da 100644 --- a/api/vendor/composer/autoload_psr4.php +++ b/api/vendor/composer/autoload_psr4.php @@ -51,7 +51,7 @@ 'Illuminate\\Validation\\' => array($vendorDir . '/illuminate/validation'), 'Illuminate\\Translation\\' => array($vendorDir . '/illuminate/translation'), 'Illuminate\\Testing\\' => array($vendorDir . '/illuminate/testing'), - 'Illuminate\\Support\\' => array($vendorDir . '/illuminate/macroable', $vendorDir . '/illuminate/collections', $vendorDir . '/illuminate/support'), + 'Illuminate\\Support\\' => array($vendorDir . '/illuminate/collections', $vendorDir . '/illuminate/macroable', $vendorDir . '/illuminate/support'), 'Illuminate\\Session\\' => array($vendorDir . '/illuminate/session'), 'Illuminate\\Queue\\' => array($vendorDir . '/illuminate/queue'), 'Illuminate\\Pipeline\\' => array($vendorDir . '/illuminate/pipeline'), diff --git a/api/vendor/composer/autoload_static.php b/api/vendor/composer/autoload_static.php index ee91555633..a86a2ece57 100644 --- a/api/vendor/composer/autoload_static.php +++ b/api/vendor/composer/autoload_static.php @@ -9,15 +9,13 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa public static $files = array ( '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', - '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', 'ec07570ca5a812141189b1fa81503674' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert/Functions.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', - '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php', - '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', - 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', + '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '60799491728b879e74601d83e38b2cad' => __DIR__ . '/..' . '/illuminate/collections/helpers.php', 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', '72579e7bd17821bb1321b87411366eae' => __DIR__ . '/..' . '/illuminate/support/helpers.php', @@ -27,22 +25,25 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa 'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php', 'ef65a1626449d89d0811cf9befce46f0' => __DIR__ . '/..' . '/illuminate/events/functions.php', '253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php', - '0ccdf99b8f62f02c52cba55802e0c2e7' => __DIR__ . '/..' . '/zircote/swagger-php/src/functions.php', + '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', 'bee9632da3ca00a99623b9c35d0c4f8b' => __DIR__ . '/..' . '/laravel/lumen-framework/src/helpers.php', '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', + '0ccdf99b8f62f02c52cba55802e0c2e7' => __DIR__ . '/..' . '/zircote/swagger-php/src/functions.php', '3585c46f9622c6b622ab0011d4d72b3a' => __DIR__ . '/..' . '/darkaonline/swagger-lume/src/helpers.php', + 'e54f50f1f018f3f5bdd79170d9f301b2' => __DIR__ . '/../..' . '/app/Helpers/OccurrenceHelper.php', ); public static $prefixLengthsPsr4 = array ( - 'v' => + 'v' => array ( 'voku\\' => 5, ), - 'W' => + 'W' => array ( 'Webmozart\\Assert\\' => 17, ), - 'S' => + 'S' => array ( 'Symfony\\Polyfill\\Php81\\' => 23, 'Symfony\\Polyfill\\Php80\\' => 23, @@ -70,12 +71,12 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa 'Symfony\\Component\\Console\\' => 26, 'SwaggerLume\\' => 12, ), - 'R' => + 'R' => array ( 'Ramsey\\Uuid\\' => 12, 'Ramsey\\Collection\\' => 18, ), - 'P' => + 'P' => array ( 'Psr\\SimpleCache\\' => 16, 'Psr\\Log\\' => 8, @@ -85,21 +86,21 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa 'PhpParser\\' => 10, 'PhpOption\\' => 10, ), - 'O' => + 'O' => array ( 'Opis\\Closure\\' => 13, 'OpenApi\\' => 8, ), - 'M' => + 'M' => array ( 'Monolog\\' => 8, ), - 'L' => + 'L' => array ( 'Laravel\\SerializableClosure\\' => 28, 'Laravel\\Lumen\\' => 14, ), - 'I' => + 'I' => array ( 'Illuminate\\View\\' => 16, 'Illuminate\\Validation\\' => 22, @@ -126,20 +127,20 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa 'Illuminate\\Broadcasting\\' => 24, 'Illuminate\\Auth\\' => 16, ), - 'G' => + 'G' => array ( 'GrahamCampbell\\ResultType\\' => 26, ), - 'F' => + 'F' => array ( 'FastRoute\\' => 10, 'Faker\\' => 6, ), - 'E' => + 'E' => array ( 'Egulias\\EmailValidator\\' => 23, ), - 'D' => + 'D' => array ( 'Dotenv\\' => 7, 'Doctrine\\Instantiator\\' => 22, @@ -150,354 +151,354 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa 'Database\\Seeders\\' => 17, 'Database\\Factories\\' => 19, ), - 'C' => + 'C' => array ( 'Cron\\' => 5, 'Carbon\\' => 7, ), - 'B' => + 'B' => array ( 'Brick\\Math\\' => 11, ), - 'A' => + 'A' => array ( 'App\\' => 4, ), ); public static $prefixDirsPsr4 = array ( - 'voku\\' => + 'voku\\' => array ( 0 => __DIR__ . '/..' . '/voku/portable-ascii/src/voku', ), - 'Webmozart\\Assert\\' => + 'Webmozart\\Assert\\' => array ( 0 => __DIR__ . '/..' . '/webmozart/assert/src', ), - 'Symfony\\Polyfill\\Php81\\' => + 'Symfony\\Polyfill\\Php81\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-php81', ), - 'Symfony\\Polyfill\\Php80\\' => + 'Symfony\\Polyfill\\Php80\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', ), - 'Symfony\\Polyfill\\Php73\\' => + 'Symfony\\Polyfill\\Php73\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-php73', ), - 'Symfony\\Polyfill\\Php72\\' => + 'Symfony\\Polyfill\\Php72\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-php72', ), - 'Symfony\\Polyfill\\Mbstring\\' => + 'Symfony\\Polyfill\\Mbstring\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', ), - 'Symfony\\Polyfill\\Intl\\Normalizer\\' => + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer', ), - 'Symfony\\Polyfill\\Intl\\Idn\\' => + 'Symfony\\Polyfill\\Intl\\Idn\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', ), - 'Symfony\\Polyfill\\Intl\\Grapheme\\' => + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme', ), - 'Symfony\\Polyfill\\Ctype\\' => + 'Symfony\\Polyfill\\Ctype\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', ), - 'Symfony\\Contracts\\Translation\\' => + 'Symfony\\Contracts\\Translation\\' => array ( 0 => __DIR__ . '/..' . '/symfony/translation-contracts', ), - 'Symfony\\Contracts\\Service\\' => + 'Symfony\\Contracts\\Service\\' => array ( 0 => __DIR__ . '/..' . '/symfony/service-contracts', ), - 'Symfony\\Contracts\\EventDispatcher\\' => + 'Symfony\\Contracts\\EventDispatcher\\' => array ( 0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts', ), - 'Symfony\\Component\\Yaml\\' => + 'Symfony\\Component\\Yaml\\' => array ( 0 => __DIR__ . '/..' . '/symfony/yaml', ), - 'Symfony\\Component\\VarDumper\\' => + 'Symfony\\Component\\VarDumper\\' => array ( 0 => __DIR__ . '/..' . '/symfony/var-dumper', ), - 'Symfony\\Component\\Translation\\' => + 'Symfony\\Component\\Translation\\' => array ( 0 => __DIR__ . '/..' . '/symfony/translation', ), - 'Symfony\\Component\\String\\' => + 'Symfony\\Component\\String\\' => array ( 0 => __DIR__ . '/..' . '/symfony/string', ), - 'Symfony\\Component\\Process\\' => + 'Symfony\\Component\\Process\\' => array ( 0 => __DIR__ . '/..' . '/symfony/process', ), - 'Symfony\\Component\\Mime\\' => + 'Symfony\\Component\\Mime\\' => array ( 0 => __DIR__ . '/..' . '/symfony/mime', ), - 'Symfony\\Component\\HttpKernel\\' => + 'Symfony\\Component\\HttpKernel\\' => array ( 0 => __DIR__ . '/..' . '/symfony/http-kernel', ), - 'Symfony\\Component\\HttpFoundation\\' => + 'Symfony\\Component\\HttpFoundation\\' => array ( 0 => __DIR__ . '/..' . '/symfony/http-foundation', ), - 'Symfony\\Component\\Finder\\' => + 'Symfony\\Component\\Finder\\' => array ( 0 => __DIR__ . '/..' . '/symfony/finder', ), - 'Symfony\\Component\\EventDispatcher\\' => + 'Symfony\\Component\\EventDispatcher\\' => array ( 0 => __DIR__ . '/..' . '/symfony/event-dispatcher', ), - 'Symfony\\Component\\ErrorHandler\\' => + 'Symfony\\Component\\ErrorHandler\\' => array ( 0 => __DIR__ . '/..' . '/symfony/error-handler', ), - 'Symfony\\Component\\Console\\' => + 'Symfony\\Component\\Console\\' => array ( 0 => __DIR__ . '/..' . '/symfony/console', ), - 'SwaggerLume\\' => + 'SwaggerLume\\' => array ( 0 => __DIR__ . '/..' . '/darkaonline/swagger-lume/src', ), - 'Ramsey\\Uuid\\' => + 'Ramsey\\Uuid\\' => array ( 0 => __DIR__ . '/..' . '/ramsey/uuid/src', ), - 'Ramsey\\Collection\\' => + 'Ramsey\\Collection\\' => array ( 0 => __DIR__ . '/..' . '/ramsey/collection/src', ), - 'Psr\\SimpleCache\\' => + 'Psr\\SimpleCache\\' => array ( 0 => __DIR__ . '/..' . '/psr/simple-cache/src', ), - 'Psr\\Log\\' => + 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/src', ), - 'Psr\\EventDispatcher\\' => + 'Psr\\EventDispatcher\\' => array ( 0 => __DIR__ . '/..' . '/psr/event-dispatcher/src', ), - 'Psr\\Container\\' => + 'Psr\\Container\\' => array ( 0 => __DIR__ . '/..' . '/psr/container/src', ), - 'Psr\\Cache\\' => + 'Psr\\Cache\\' => array ( 0 => __DIR__ . '/..' . '/psr/cache/src', ), - 'PhpParser\\' => + 'PhpParser\\' => array ( 0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser', ), - 'PhpOption\\' => + 'PhpOption\\' => array ( 0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption', ), - 'Opis\\Closure\\' => + 'Opis\\Closure\\' => array ( 0 => __DIR__ . '/..' . '/opis/closure/src', ), - 'OpenApi\\' => + 'OpenApi\\' => array ( 0 => __DIR__ . '/..' . '/zircote/swagger-php/src', ), - 'Monolog\\' => + 'Monolog\\' => array ( 0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog', ), - 'Laravel\\SerializableClosure\\' => + 'Laravel\\SerializableClosure\\' => array ( 0 => __DIR__ . '/..' . '/laravel/serializable-closure/src', ), - 'Laravel\\Lumen\\' => + 'Laravel\\Lumen\\' => array ( 0 => __DIR__ . '/..' . '/laravel/lumen-framework/src', ), - 'Illuminate\\View\\' => + 'Illuminate\\View\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/view', ), - 'Illuminate\\Validation\\' => + 'Illuminate\\Validation\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/validation', ), - 'Illuminate\\Translation\\' => + 'Illuminate\\Translation\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/translation', ), - 'Illuminate\\Testing\\' => + 'Illuminate\\Testing\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/testing', ), - 'Illuminate\\Support\\' => + 'Illuminate\\Support\\' => array ( - 0 => __DIR__ . '/..' . '/illuminate/macroable', - 1 => __DIR__ . '/..' . '/illuminate/collections', + 0 => __DIR__ . '/..' . '/illuminate/collections', + 1 => __DIR__ . '/..' . '/illuminate/macroable', 2 => __DIR__ . '/..' . '/illuminate/support', ), - 'Illuminate\\Session\\' => + 'Illuminate\\Session\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/session', ), - 'Illuminate\\Queue\\' => + 'Illuminate\\Queue\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/queue', ), - 'Illuminate\\Pipeline\\' => + 'Illuminate\\Pipeline\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/pipeline', ), - 'Illuminate\\Pagination\\' => + 'Illuminate\\Pagination\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/pagination', ), - 'Illuminate\\Log\\' => + 'Illuminate\\Log\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/log', ), - 'Illuminate\\Http\\' => + 'Illuminate\\Http\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/http', ), - 'Illuminate\\Hashing\\' => + 'Illuminate\\Hashing\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/hashing', ), - 'Illuminate\\Filesystem\\' => + 'Illuminate\\Filesystem\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/filesystem', ), - 'Illuminate\\Events\\' => + 'Illuminate\\Events\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/events', ), - 'Illuminate\\Encryption\\' => + 'Illuminate\\Encryption\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/encryption', ), - 'Illuminate\\Database\\' => + 'Illuminate\\Database\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/database', ), - 'Illuminate\\Contracts\\' => + 'Illuminate\\Contracts\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/contracts', ), - 'Illuminate\\Container\\' => + 'Illuminate\\Container\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/container', ), - 'Illuminate\\Console\\' => + 'Illuminate\\Console\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/console', ), - 'Illuminate\\Config\\' => + 'Illuminate\\Config\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/config', ), - 'Illuminate\\Cache\\' => + 'Illuminate\\Cache\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/cache', ), - 'Illuminate\\Bus\\' => + 'Illuminate\\Bus\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/bus', ), - 'Illuminate\\Broadcasting\\' => + 'Illuminate\\Broadcasting\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/broadcasting', ), - 'Illuminate\\Auth\\' => + 'Illuminate\\Auth\\' => array ( 0 => __DIR__ . '/..' . '/illuminate/auth', ), - 'GrahamCampbell\\ResultType\\' => + 'GrahamCampbell\\ResultType\\' => array ( 0 => __DIR__ . '/..' . '/graham-campbell/result-type/src', ), - 'FastRoute\\' => + 'FastRoute\\' => array ( 0 => __DIR__ . '/..' . '/nikic/fast-route/src', ), - 'Faker\\' => + 'Faker\\' => array ( 0 => __DIR__ . '/..' . '/fakerphp/faker/src/Faker', ), - 'Egulias\\EmailValidator\\' => + 'Egulias\\EmailValidator\\' => array ( 0 => __DIR__ . '/..' . '/egulias/email-validator/src', ), - 'Dotenv\\' => + 'Dotenv\\' => array ( 0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src', ), - 'Doctrine\\Instantiator\\' => + 'Doctrine\\Instantiator\\' => array ( 0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator', ), - 'Doctrine\\Inflector\\' => + 'Doctrine\\Inflector\\' => array ( 0 => __DIR__ . '/..' . '/doctrine/inflector/lib/Doctrine/Inflector', ), - 'Doctrine\\Common\\Lexer\\' => + 'Doctrine\\Common\\Lexer\\' => array ( 0 => __DIR__ . '/..' . '/doctrine/lexer/lib/Doctrine/Common/Lexer', ), - 'Doctrine\\Common\\Annotations\\' => + 'Doctrine\\Common\\Annotations\\' => array ( 0 => __DIR__ . '/..' . '/doctrine/annotations/lib/Doctrine/Common/Annotations', ), - 'DeepCopy\\' => + 'DeepCopy\\' => array ( 0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy', ), - 'Database\\Seeders\\' => + 'Database\\Seeders\\' => array ( 0 => __DIR__ . '/../..' . '/database/seeders', ), - 'Database\\Factories\\' => + 'Database\\Factories\\' => array ( 0 => __DIR__ . '/../..' . '/database/factories', ), - 'Cron\\' => + 'Cron\\' => array ( 0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron', ), - 'Carbon\\' => + 'Carbon\\' => array ( 0 => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon', ), - 'Brick\\Math\\' => + 'Brick\\Math\\' => array ( 0 => __DIR__ . '/..' . '/brick/math/src', ), - 'App\\' => + 'App\\' => array ( 0 => __DIR__ . '/../..' . '/app', ), ); public static $prefixesPsr0 = array ( - 'M' => + 'M' => array ( - 'Mockery' => + 'Mockery' => array ( 0 => __DIR__ . '/..' . '/mockery/mockery/library', ), @@ -509,6 +510,10 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa 'App\\Events\\Event' => __DIR__ . '/../..' . '/app/Events/Event.php', 'App\\Events\\ExampleEvent' => __DIR__ . '/../..' . '/app/Events/ExampleEvent.php', 'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php', + 'App\\Helpers\\GPoint' => __DIR__ . '/../..' . '/app/Helpers/GPoint.php', + 'App\\Helpers\\Helper' => __DIR__ . '/../..' . '/app/Helpers/Helper.php', + 'App\\Helpers\\OccurrenceHelper' => __DIR__ . '/../..' . '/app/Helpers/OccurrenceHelper.php', + 'App\\Helpers\\TaxonomyHelper' => __DIR__ . '/../..' . '/app/Helpers/TaxonomyHelper.php', 'App\\Http\\Controllers\\CollectionController' => __DIR__ . '/../..' . '/app/Http/Controllers/CollectionController.php', 'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php', 'App\\Http\\Controllers\\DuplicateController' => __DIR__ . '/../..' . '/app/Http/Controllers/DuplicateController.php', @@ -541,6 +546,8 @@ class ComposerStaticInite18bd6b57182fc88f4b9d0452696caaa 'App\\Models\\TaxonomyDescription' => __DIR__ . '/../..' . '/app/Models/TaxonomyDescription.php', 'App\\Models\\TaxonomyDescriptionStatement' => __DIR__ . '/../..' . '/app/Models/TaxonomyDescriptionStatement.php', 'App\\Models\\User' => __DIR__ . '/../..' . '/app/Models/User.php', + 'App\\Models\\UserAccessToken' => __DIR__ . '/../..' . '/app/Models/UserAccessToken.php', + 'App\\Models\\UserRole' => __DIR__ . '/../..' . '/app/Models/UserRole.php', 'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AuthServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AuthServiceProvider.php', 'App\\Providers\\EventServiceProvider' => __DIR__ . '/../..' . '/app/Providers/EventServiceProvider.php', diff --git a/imagelib/imgdetails.php b/imagelib/imgdetails.php index 0d8be6c9ae..32148a1d2f 100644 --- a/imagelib/imgdetails.php +++ b/imagelib/imgdetails.php @@ -399,16 +399,21 @@ function openOccurrenceSearch(target) { if($imgArr['occid']) echo '
' . $LANG['DISPLAY_SPECIMEN_DETAILS'] . '
'; if($imgUrl) echo '
' . $LANG['OPEN_MEDIUM_SIZED_IMAGE'] . '
'; if($origUrl) echo '
' . $LANG['OPEN_LARGE_IMAGE'] . '
'; - ?> -
-
: - ' . htmlspecialchars($ADMIN_EMAIL, ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE) . ''; + $emailAddress = $ADMIN_EMAIL; + if($emailAddress){ ?> -
+
+
: + ' . $emailAddress . ''; + ?> +
+