' . __FILE__, $RecipeInfo[GPXSTATNAME]['Version']);
# declare $gpxstat for (:if enabled gpxstat:) recipe installation check
$gpxstat = true; # enabled
# acquire other configuration variables from config.php if they exist
\SDV($GpxStatTimeFmt, 'H:i'); # e.g. 'Y-m-d T H:i:s'
$timeFmt = strval($GpxStatTimeFmt);
\SDV($GpxStatDateFmt, 'Y-m-d'); # e.g. 'Y-m-d T H:i:s'
$dateFmt = strval ($GpxStatDateFmt);
\SDV($GpxStatThresholdSpeed, 0.8); # set threshold moving speed in km/hour
$thresholdSpeedKMH = floatval ($GpxStatThresholdSpeed);
\SDV ($GpxStatBanner, 'on'); # change display of banner
$bannerFlag = mb_strtolower(strval ($GpxStatBanner));
\SDV ($GpxStatBrief, 'true'); # make descriptors brief
$briefFlag = boolval ($GpxStatBrief);
\SDV ($GpxStatTimezone, null); # parameter is checked below
#
## Add a PmWiki custom markup
$vnamepagefile = "(?:[$PageNameChars]+(?:\.|\/)){0,2}" # optional group/page name and separator (/ or .) repeated
. "[$UploadNameChars]+" . '.gpx'; # file name and .gpx
# (:GpxStat Optional parameters:)
$gpxstat_markup_pattern = '/\\(:'
. mb_strtolower(GPXSTATNAME) . '(.*)?:\\)/i';
# when has to be at least fulltext
\Markup(GPXSTATNAME . $GpxStatNew, #name
'fulltext', # when, e.g. fulltext, directives
$gpxstat_markup_pattern, # pattern
__NAMESPACE__ . '\GpxStat_Directive' );
# if ($gpxstat_debugon) dmsg('GpxStat markup', $gpxstat_markup_pattern);
//
return; # completed PmWiki Info recipe setup
/*-----------------------------------------------------------------------------------------------------------*/
#
/** GPX file statistics
* /param
* gpx=groupname.pagename/filename.gpx
* display=layout
* timezone=tzname
* /return The PmWiki-formatted information as HTML.
*/
#
function GpxStat_Directive (array $m):string {
#
global $gpxstat_debugon, $RecipeInfo;# import variables
global $timeFmt, $dateFmt, $thresholdSpeedKMH, $vnamepagefile, $bannerFlag, $briefFlag;
global $GpxStatTimezone;
global $retVal, $showVals; # for debugging
$args = \ParseArgs($m[1]); # contains all text within directive
$retVal = '<:block>' . NL; # break out of paragraph;
$diffVals = [];
$dispArray = ['default']; # defaults to default
$calcVals = []; # initialise
$calcVals [SNAM] = '';
$calcVals [ENAM] = '';
//
if (array_key_exists ('debug', $args)) {
$debugopt = $args['debug'] === 'true';
if ($debugopt) $gpxstat_debugon = true; # set on inside directive
}
if ($gpxstat_debugon) { # display inputs and outputs to wiki page
#dmsg ('
', GPXSTATNAME);
#dmsg ('m[]', $m);
#dmsg ('args[]', $args);
} # end gpxstat_debugon
if (array_key_exists ('display', $args)) {
$dispArray = explode(',', $args ['display']);
$dispArray = array_map('strtolower', $dispArray); // Convert all values to lowercase
}
if (array_key_exists ('startname', $args)) {
$calcVals [SNAM] = strval ($args['startname']);
}
if (array_key_exists ('endname', $args)) {
$calcVals [ENAM] = strval ($args['endname']);
}
if (array_key_exists ('banner', $args)) {
$bannerFlag = mb_strtolower(strval ($args['banner']));
}
if (array_key_exists ('brief', $args)) {
$briefFlag = boolval ($args['brief']);
}
if (array_key_exists ('stoppedthreshold', $args)){
$thresholdSpeedKMH = floatval ($args['stoppedthreshold']); # override config or default values
}
#
switch (true) {
case (array_key_exists ('gpx', $args)):
$fileName = urldecode (strval($args['gpx']));
if (empty ($fileName)) {
return GPXSTATNAME . ': gpx filename is missing ' . strval($fileName);
}
$pmatch = preg_match ('/^' . $vnamepagefile . '/i', $fileName);
if ($pmatch === false) return GPXSTATNAME . ': gpx does not match a filename pattern "' . $fileName . '" failed (' . $vnamepagefile . ')';
if (1 == $pmatch) { # pattern matches subject
list ($fileexists, $wikifilefqdn) = CheckWikiFile ($fileName);
//if ($gpxstat_debugon) dmsg ('gpxfile', '"' . $args['gpx'] . '", "' . $wikifilefqdn . '"');
if ($fileexists) break; # exit switch, we have a file to process
return $wikifilefqdn; # return file upload code to PmWiki (don't use Keep)
}
# pattern does not match subject
return GPXSTATNAME . ': "' . $fileName . '" not a valid filename';
default:
return GPXSTATNAME . ': parameter "gpx=filename.gpx" is required';
} # end switch
$thresholdSpeedMS = $thresholdSpeedKMH * (KM_TO_M / HOUR_TO_S); # convert to m/s
// Load the XML file
libxml_use_internal_errors(true);
$gpxXml = simplexml_load_file($wikifilefqdn); # returns class object or null
#
if ($gpxXml === false) {
// Retrieve an array of errors
$errors = libxml_get_errors();
// Iterate over each error
foreach ($errors as $error) {
$retVal .= display_xml_error($error, $gpxXml);
}
// Clear the libxml error buffer
libxml_clear_errors();
// Handle the errors
return 'File "' . $wikifilefqdn . '" ' . BR . $retVal;
}
if (! ($gpxXml->getName() === 'gpx')) return 'File is not a gpx file' . BR;
// Get the namespace of the gpx data
$trknamespaces = $gpxXml->getNamespaces(true);
#
$calcVals [TZCL] = ''; # global message
list ($calcVals [TMZN], $calcVals [TZCL]) = checkTimezone ($GpxStatTimezone, $gpxXml); # requires gpx file
#
if (array_key_exists ('timezone', $args)) {
list ($calcVals [TMZN], $calcVals [TZCL]) = checkTimezone (strval($args['timezone']), $gpxXml); # requires gpx file
}
// Initialise variables
$calcVals [TNAM] = []; # multiple values are possible
$calcVals [TDES] = []; # multiple values are possible
$calcVals [NRSG] = 0; # count track segments
$calcVals [NRTK] = 0; # count number of tracks
$calcVals [ASCTRAW] = 0;
$calcVals [DSCTRAW] = 0;
$calcVals [STRT] = null;
$calcVals [ENDT] = null;
$calcVals [ASTM] = 0;
$calcVals [DSTM] = 0;
$calcVals [ASSP] = 0;
$calcVals [DSSP] = 0;
$calcVals [STEL] = null;
$calcVals [DURM] = 0;
$calcVals [DURN] = 0;
$calcVals [DURS] = 0;
$calcVals [TDAT] = null;
$calcVals [SPCL] = '';
$timeVals = []; # time values in seconds
$distVals = []; # distance values in metres
$speedVals = []; # speed values in metres per second
$elevVals = []; # elevation values in metres
$pointsNumber = -1; # point number starts from zero
$showVals = true or $gpxstat_debugon or boolval(in_array('showvals', $dispArray));
// Extract values from the gpx root
if (isset($gpxXml->name)) { # some gpx files have a name here
$calcVals [TNAM][] = strval($gpxXml->name);
}
if (isset($gpxXml->desc)) { # some gpx files have a desc here
$calcVals [TDES][] = strval($gpxXml->desc);
}
// Extract values from the metadata
if (isset($gpxXml->metadata->time)) {
$calcVals [TDAT] = date_create(strval($gpxXml->metadata->time));
}
if (isset($gpxXml->metadata->name)) { # some gpx file have a name here
$calcVals [TNAM][] = strval($gpxXml->metadata->name);
}
if (isset($gpxXml->metadata->desc)) { # some gpx file have a desc here
$calcVals [TDES][] = strval($gpxXml->metadata->desc);
}
// Loop through each "trk" element in the XML
foreach ($gpxXml->trk as $trk) {
// treat each track as separate (to avoid values from jumps from one track to the next)
$previousTrkpt = null;
$previousTime = null;
$previousElev = null;
$calcVals [NRTK]++;
if (isset($trk->name)) $calcVals [TNAM][] = strval($trk->name);
if (isset($trk->desc)) $calcVals [TDES][] = strval($trk->desc);
// Loop through each "trkseg" element
foreach ($trk->trkseg as $trkSeg) {
$calcVals [NRSG]++;
// Loop through each "trkpt" element
foreach ($trkSeg->trkpt as $trkPnt) {
// Increment the count of points for each "trkpt" element
$pointsNumber++; # note these arrays start from 0
$timeVals [$pointsNumber] = $pointTime = (isset($trkPnt->time)) ? strtotime(strval($trkPnt->time)) : 0; # UNIX timestamp
$elevVals [$pointsNumber] = $pointElev = (isset($trkPnt->ele)) ? floatval($trkPnt->ele) : 0;
if (empty($calcVals [STEL])) {
$calcVals [STEL] = $pointElev; # save first elevation in track
}
// Calculate distance using the Haversine formula
if (isset($previousTrkpt)) {
$distPoint = haversineGreatCircleDistance(floatval($previousTrkpt['lat']), floatval($previousTrkpt['lon']), floatval($trkPnt['lat']), floatval($trkPnt['lon'])); # metres
$distVals [$pointsNumber] = $distPoint; # calculated
// Calculate speed if not available
switch (true) {
case (isset($trkPnt->speed)):
break; # speed tag exists and is not empty
case (isset($trknamespaces['gte'])):
// Register the namespace with the SimpleXMLElement
$trkPnt->registerXPathNamespace('gte', $trknamespaces['gte']);
// Check if the extensions and gps nodes exist
if (isset($trkPnt->extensions) && isset($trkPnt->extensions->children($trknamespaces['gte'])->gps)) {
// Extract the speed
$speed = $trkPnt->extensions->children($trknamespaces['gte'])->gps->attributes()->speed;
} else {$speed = null;}
if (isset($speed) && !empty($speed)) {
$trkPnt->speed = strval($speed);
$calcVals [SPCL] = 'speed from extension';
break; # speed exists in extension
}
# fall through if speed not set in extension
default: // Calculate speed if not available
$timeDelta = abs($previousTime - $pointTime);
$speed = ($timeDelta > 0) ? $distPoint / $timeDelta : 0;
$trkPnt->speed = strval($speed);
$calcVals [SPCL] = 'speeds calculated';
} # end switch
} else {
$distVals [$pointsNumber] = 0; # distance of zero if no previous point
} # end if
$speedVals [$pointsNumber] = $pointSpeed = (isset($trkPnt->speed)) ? floatval($trkPnt->speed) : 0; # defaults to zero
// Calculate metres climbed and descended
if (isset($previousTrkpt)) {
$pointElevChange = $pointElev - $previousElev;
if ($pointElevChange > 0) {
$calcVals [ASCTRAW] += $pointElevChange;
} else {
$calcVals [DSCTRAW] += abs($pointElevChange);
}
}
// Set start time
if (empty($calcVals [STRT])) {
$calcVals [STRT] = $pointTime;
}
if (empty($calcVals [TDAT])) {
$calcVals [TDAT] = $pointTime;
}
$calcVals [ENDT] = $pointTime;
// Calculate moving times
if (isset($previousTrkpt)) {
if ($previousTime !== null) {
$intervalTime = abs($pointTime - $previousTime);
$calcVals [DURN] += $intervalTime; # add to track duration
if ($pointSpeed > $thresholdSpeedMS) { # moving
$calcVals [DURM] += $intervalTime;
if ($previousElev !== null) {
if ($pointElev > $previousElev) {
$calcVals [ASTM] += $intervalTime; // Add to ascending time
} elseif ($pointElev < $previousElev) {
$calcVals [DSTM] += $intervalTime; // Add to descending time
}
}
} else { # not moving
$calcVals [DURS] += $intervalTime; # stopped
}
}
}
$previousTime = $pointTime;
$previousElev = $pointElev;
$previousTrkpt = $trkPnt;
} # end foreach trkseg
} # end foreach trk
} # end foreach gpxxml
$calcVals [ENEL] = $pointElev;
$calcVals [NRPT] = $pointsNumber + 1;
list ($speedSmooth, $speedOutliers, $diffVals[MXSP]) = removeOutliers('Speed', $speedVals, NRSTDDEV, AVGWINDOW);
list ($elevSmooth, $elevOutliers, $diffVals[MXEL]) = removeOutliers('Elevation', $elevVals, NRSTDDEVELEV, AVGWINDOW);
list ($distSmooth, $distOutliers, $diffVals[DIST]) = removeOutliers('Distance', $distVals, NRSTDDEV, AVGWINDOW);
list ($calcVals [ASCT], $calcVals [DSCT]) = calcChange ($elevSmooth, $speedSmooth, $thresholdSpeedKMH); # calculate ascend and descent from smoothed values
list ($calcVals [ASSP], $calcVals [DSSP]) = calcAvgSpeed ($elevSmooth, $speedSmooth, $distSmooth, $timeVals, $thresholdSpeedKMH); # calculate average speed from smoothed values
list ($minV, $maxV) = calcMinMax ($speedSmooth);
$calcVals [MXSP] = $maxV; # calculate from smoothed values
$calcVals [DIST] = array_sum ($distSmooth);
#
$outVals[TZDB][TXT] = $outVals[TZDB][VAL] = '';
if ((!empty($calcVals [TZCL])) or (! empty ($calcVals [TMZN]))) {
$outVals[TZDB][TXT] = 'Time zone: ';
$outVals[TZDB][VAL] = strval($calcVals [TMZN]);
}
#
switch ($bannerFlag) {
case 'off' :
$outVals[GPXS][TXT] = '';
$outVals[GPXS][VAL] = '';
break;
case 'on' :
$outVals[GPXS][TXT] = makeabbr (GPXSTXT, 'Version ' . $RecipeInfo[GPXSTATNAME] ['Version']);
$outVals[GPXS][VAL] = '';
break;
default:
$retVal = 'GpxStat: Banner parameter must have values "on", "off", or "version"';
# falls through
case 'version' :
$outVals[GPXS][TXT] = GPXSTXT;
$outVals[GPXS][VAL] = 'Version ' . $RecipeInfo[GPXSTATNAME] ['Version'];
} # end switch
$outVals[FLDS][TXT] = 'File: ';
$outVals[FLDS][VAL] = '"' . urldecode ($wikifilefqdn) . '"';
$outVals[FNAM][TXT] = 'Filename: ';
$outVals[FNAM][VAL] = urldecode(basename ($wikifilefqdn, '.gpx'));
$outVals[TRTZ][TXT] = 'Track date time zone: ';
$outVals[TRTZ][VAL] = date('T', $calcVals [TDAT]);
$outVals[TNAM][TXT] = 'Track name: ';
$outVals[TNAM][VAL] = implode (BR, $calcVals [TNAM]);
$outVals[TDES][TXT] = 'Track desc: ';
$outVals[TDES][VAL] = implode (BR, $calcVals [TDES]);
$outVals[TDAT][TXT] = 'Track date ('. $outVals[TRTZ][VAL] . '): ';
$outVals[TDAT][VAL] = date($dateFmt, $calcVals [TDAT]);
$outVals[DURN][TXT] = makeabbr('Duration: ', $outVals[TDAT][TXT] . date($dateFmt, $calcVals [TDAT]));
$outVals[DURN][VAL] = displaySecsAsHoursMins($calcVals [DURN]) . HOUR;
$calcVals [ELPS] = $calcVals [ENDT] - $calcVals [STRT];
$outVals[ELPS][TXT] = 'Elapsed: ';
$outVals[ELPS][VAL] = displaySecsAsHoursMins($calcVals [ELPS]) . HOUR;
$outVals[DIST][TXT] = makeabbr('Distance: ', $outVals[FNAM][VAL]);
$outVals[MXSP][TXT] = 'Max speed: ';
$outVals[ASCT][TXT] = 'Ascent: ';
$outVals[ASCT][VAL] = number_format($calcVals [ASCT]) . MTR;
$outVals[DSCT][TXT] = 'Descent: ';
$outVals[DSCT][VAL] = number_format($calcVals [DSCT]) . MTR;
$outVals[STRT][TXT] = (empty($calcVals [SNAM])) ? 'Start: ' : makeabbr ($calcVals [SNAM], 'Start');
$outVals[STRT][VAL] = makeabbr((date('Y-m-d', $calcVals [STRT]) == date('Y-m-d', $calcVals [ENDT])) ? date($timeFmt, $calcVals [STRT]) : date($dateFmt, $calcVals [STRT]) . ' ' . date($timeFmt, $calcVals [STRT]), $calcVals [TMZN]);
$tzid = (empty ($calcVals [TMZN])) ? '' : get_timezone_abbreviation($calcVals [TMZN]);
$outVals[ENDT][TXT] = (empty($calcVals [ENAM])) ? 'End: ' : makeabbr ($calcVals [ENAM], 'End');
$outVals[ENDT][VAL] = makeabbr((date('Y-m-d', $calcVals [STRT]) == date('Y-m-d', $calcVals [ENDT])) ? date($timeFmt, $calcVals [ENDT]) : date($dateFmt, $calcVals [ENDT]) . ' ' . date($timeFmt, $calcVals [ENDT]), $tzid);
$outVals[ASTM][TXT] = 'Duration ascent: ';
$outVals[ASTM][VAL] = displaySecsAsHoursMins($calcVals [ASTM]) . HOUR;
$outVals[DSTM][TXT] = 'Duration descent: ';
$outVals[DSTM][VAL] = displaySecsAsHoursMins($calcVals [DSTM]) . HOUR;
$outVals[STEL][TXT] = ($briefFlag) ? 'Start elev: ' : 'Start elevation: ';
$outVals[STEL][VAL] = number_format($calcVals [STEL]) . MTR;
$outVals[ENEL][TXT] = ($briefFlag) ? 'End elev: ' : 'End elevation: ';
$outVals[ENEL][VAL] = number_format($calcVals [ENEL]) . MTR;
list ($minV, $maxV) = calcMinMax ($elevSmooth);
$outVals[MNEL][TXT] = ($briefFlag) ? 'Min elev: ' : 'Min elevation: ';
$outVals[MNEL][VAL] = number_format($minV) . MTR;
$outVals[MXEL][TXT] = ($briefFlag) ? 'Max elev: ' : 'Max elevation: ';
$outVals[MXEL][VAL] = number_format($maxV) . MTR;
$outVals[CHEL][TXT] = (($briefFlag) ? 'Elev ' : 'Elevation ') . (($calcVals [ENEL] >= $calcVals [STEL]) ? 'gain' : 'loss') . ': ';
$outVals[CHEL][VAL] = number_format($calcVals [ENEL] - $calcVals [STEL]) . MTR;
$outVals[DURM][TXT] = 'Duration moving: ';
$outVals[DURM][VAL] = displaySecsAsHoursMins($calcVals [DURM]) . HOUR;
$outVals[DURS][TXT] = 'Duration stopped: ';
$outVals[DURS][VAL] = displaySecsAsHoursMins($calcVals [DURS]) . HOUR;
$calcVals [AVSP] = ($calcVals [DURN] > 0) ? $calcVals [DIST] / $calcVals [DURN] : 0;
$outVals[AVSP][TXT] = 'Avg speed: ';
$decPlaces = ($calcVals [AVSP] < 10) ? 1 : 0;
$outVals[AVSP][VAL] = number_format($calcVals [AVSP] * 3.6, $decPlaces) . KMH;
$outVals[AVMS][TXT] = 'Avg moving speed: ';
$calcVals [AVMS] = ($calcVals [DURM] > 0) ? $calcVals [DIST] / $calcVals [DURM] : 0;
$decPlaces = ($calcVals [AVMS] < 10) ? 1 : 0;
$outVals[AVMS][VAL] = number_format($calcVals [AVMS] * 3.6, $decPlaces) . KMH;
$outVals[ASSP][TXT] = 'Avg asc speed: ';
$decPlaces = ($calcVals [ASSP] < 10) ? 1 : 0;
$outVals[ASSP][VAL] = number_format($calcVals [ASSP], $decPlaces) . KMH;
$outVals[DSSP][TXT] = 'Avg dsc speed: ';
$decPlaces = ($calcVals [DSSP] < 10) ? 1 : 0;
$outVals[DSSP][VAL] = number_format($calcVals [DSSP], $decPlaces) . KMH;
$outVals[THSP][TXT] = 'Threshold speed: ';
$outVals[THSP][VAL] = number_format($thresholdSpeedKMH, 1) . KMH;
$outVals[NRPT][TXT] = '# Points: ';
$outVals[NRPT][VAL] = number_format($calcVals [NRPT]);
$outVals[NRSG][TXT] = '# Segments: ';
$outVals[NRSG][VAL] = number_format($calcVals [NRSG]);
$outVals[NRTK][TXT] = '# Tracks: ';
$outVals[NRTK][VAL] = number_format($calcVals [NRTK]);
// Select number of decimal places
$decPlaces = ($calcVals [DIST] < 100000) ? 1 : 0; # metres
$outVals[DIST][VAL] = number_format($calcVals [DIST] / 1000, $decPlaces) . KM;
// Format to 1 decimal place
$decPlaces = ($calcVals [MXSP] < 10) ? 1 : 0;
$outVals[MXSP][VAL] = number_format($calcVals [MXSP], $decPlaces) . KMH; # km/h
// Display the information
if (empty (array_Filter($timeVals))) {
// GPX file contained no time values, e.g. if it was converted from a KML file
$retVal .= TBLS . NL;
$retVal .= TBLDL . $outVals[DIST][TXT] . TBLDR . $outVals[DIST][VAL] . NL;
$retVal .= TBLDL . $outVals[GPXS][TXT] . TBLDR . $outVals[GPXS][VAL] . NL;
$retVal .= TBLE . NL;
} else {
foreach ($dispArray as $layout) {
switch ($layout) {
case 'table':
$retVal .= TBLS . NL;
$retVal .= TBLR . TBLDL . $outVals[TNAM][TXT] . '' . $outVals[TNAM][VAL]. ' | ' . NL;
$retVal .= TBLR . TBLDL . $outVals[TDES][TXT] . '' . $outVals[TDES][VAL] . ' | ' . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[DIST][TXT] . TBLDR . $outVals[DIST][VAL] . NL;
$retVal .= TBLDL . $outVals[AVSP][TXT] . TBLDR . $outVals[AVSP][VAL] . NL;
$retVal .= TBLD2 . $calcVals [SPCL] . NL;
$retVal .= TBLDL . $outVals[STEL][TXT] . TBLDR . $outVals[STEL][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[ASCT][TXT] . TBLDR . $outVals[ASCT][VAL] . NL;
$retVal .= TBLDL . $outVals[AVMS][TXT] . TBLDR . $outVals[AVMS][VAL] . NL;
$retVal .= TBLDL . $outVals[MNEL][TXT] . TBLDR . $outVals[MNEL][VAL] . NL;
$retVal .= TBLDL . $outVals[ENEL][TXT] . TBLDR . $outVals[ENEL][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[DSCT][TXT] . TBLDR . $outVals[DSCT][VAL] . NL;
$retVal .= TBLDL . $outVals[MXSP][TXT] . TBLDR . $outVals[MXSP][VAL] . NL;
$retVal .= TBLDL . $outVals[MXEL][TXT] . TBLDR . $outVals[MXEL][VAL] . NL;
$retVal .= TBLDL . $outVals[CHEL][TXT] . TBLDR . $outVals[CHEL][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[DURN][TXT] . TBLDR . $outVals[DURN][VAL] . NL;
$retVal .= TBLDL . $outVals[DURM][TXT] . TBLDR . $outVals[DURM][VAL] . NL;
$retVal .= TBLDL . $outVals[DURS][TXT] . TBLDR . $outVals[DURS][VAL] . NL;
$retVal .= TBLDL . $outVals[GPXS][TXT] . TBLDR . $outVals[GPXS][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[STRT][TXT] . TBLDR . $outVals[STRT][VAL] . NL;
$retVal .= TBLDL . $outVals[ENDT][TXT] . TBLDR . $outVals[ENDT][VAL] . NL;
$retVal .= TBLDL . $outVals[TDAT][TXT] . TBLDR . $outVals[TDAT][VAL] . NL;
$retVal .= TBLDL . $outVals[TZDB][TXT] . TBLDR . $outVals[TZDB][VAL] . ' ' . $calcVals [TZCL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[NRPT][TXT] . TBLDR . $outVals[NRPT][VAL] . NL;
if ($calcVals [NRSG] > 1) {
$retVal .= TBLDL . $outVals[NRSG][TXT] . TBLDR . $outVals[NRSG][VAL] . NL;
} else {$retVal .= TBLD2 . NL;
}
if ($calcVals [NRTK] > 1) {
$retVal .= TBLDL . $outVals[NRTK][TXT] . TBLDR . $outVals[NRTK][VAL] . NL;
} else {$retVal .= TBLD2 . NL;
}
$retVal .= TBLDL . $outVals[THSP][TXT] . TBLDR . $outVals[THSP][VAL] . NL;
$retVal .= TBLR . TBLDL . $outVals[FLDS][TXT] . '' . $outVals[FLDS][VAL] . ' | ' . NL;
$retVal .= TBLE . NL;
break;
case 'ski':
$retVal .= TBLS . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[STRT][TXT] . TBLDR . $outVals[STRT][VAL] . NL;
$retVal .= TBLDL . $outVals[DIST][TXT] . TBLDR . $outVals[DIST][VAL] . NL;
$retVal .= TBLDL . $outVals[MXEL][TXT] . TBLDR . $outVals[MXEL][VAL] . NL;
$retVal .= TBLDL . $outVals[DURM][TXT] . TBLDR . $outVals[DURM][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[ENDT][TXT] . TBLDR . $outVals[ENDT][VAL] . NL;
$retVal .= TBLDL . $outVals[MXSP][TXT] . TBLDR . $outVals[MXSP][VAL] . NL;
$retVal .= TBLDL . $outVals[ASCT][TXT] . TBLDR . $outVals[ASCT][VAL] . NL;
$retVal .= TBLDL . $outVals[ASTM][TXT] . TBLDR . $outVals[ASTM][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[DURN][TXT] . TBLDR . $outVals[DURN][VAL] . NL;
$retVal .= TBLDL . $outVals[DSSP][TXT] . TBLDR . $outVals[DSSP][VAL] . NL;
$retVal .= TBLDL . $outVals[DSCT][TXT] . TBLDR . $outVals[DSCT][VAL] . NL;
$retVal .= TBLDL . $outVals[DSTM][TXT] . TBLDR . $outVals[DSTM][VAL] . NL;
$retVal .= TBLE . NL;
break;
case 'walk':
$retVal .= TBLS . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[DIST][TXT] . TBLDR . $outVals[DIST][VAL] . NL;
$retVal .= TBLDL . $outVals[ASCT][TXT] . TBLDR . $outVals[ASCT][VAL] . NL;
$retVal .= TBLDL . $outVals[DSCT][TXT] . TBLDR . $outVals[DSCT][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[MXEL][TXT] . TBLDR . $outVals[MXEL][VAL] . NL;
$retVal .= TBLDL . $outVals[AVMS][TXT] . TBLDR . $outVals[AVMS][VAL] . NL;
$retVal .= TBLDL . $outVals[DURM][TXT] . TBLDR . $outVals[DURM][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[STRT][TXT] . TBLDR . $outVals[STRT][VAL] . NL;
$retVal .= TBLDL . $outVals[ENDT][TXT] . TBLDR . $outVals[ENDT][VAL] . NL;
$retVal .= TBLDL . $outVals[DURN][TXT] . TBLDR . $outVals[DURN][VAL] . NL;
$retVal .= TBLE . NL;
break;
case 'tramp':
$retVal .= TBLS . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[STRT][TXT] . TBLDR . $outVals[STRT][VAL] . NL;
$retVal .= TBLDL . $outVals[STEL][TXT] . TBLDR . $outVals[STEL][VAL] . NL;
$retVal .= TBLDL . $outVals[MXEL][TXT] . TBLDR . $outVals[MXEL][VAL] . NL;
$retVal .= TBLDL . $outVals[DIST][TXT] . TBLDR . $outVals[DIST][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[ENDT][TXT] . TBLDR . $outVals[ENDT][VAL] . NL;
$retVal .= TBLDL . $outVals[ENEL][TXT] . TBLDR . $outVals[ENEL][VAL] . NL;
$retVal .= TBLDL . $outVals[ASCT][TXT] . TBLDR . $outVals[ASCT][VAL] . NL;
$retVal .= TBLDL . $outVals[AVMS][TXT] . TBLDR . $outVals[AVMS][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[DURN][TXT] . TBLDR . $outVals[DURN][VAL] . NL;
$retVal .= TBLDL . $outVals[CHEL][TXT] . TBLDR . $outVals[CHEL][VAL] . NL;
$retVal .= TBLDL . $outVals[DSCT][TXT] . TBLDR . $outVals[DSCT][VAL] . NL;
$retVal .= TBLDL . $outVals[DURM][TXT] . TBLDR . $outVals[DURM][VAL] . NL;
$retVal .= TBLE . NL;
break;
case 'train':
case 'fly': # add in case we want to differentiate these later
case 'boat':
case 'bus':
case 'drive':
$retVal .= TBLS . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[DIST][TXT] . TBLDR . $outVals[DIST][VAL] . NL;
$retVal .= TBLDL . $outVals[DURN][TXT] . TBLDR . $outVals[DURN][VAL] . NL;
$retVal .= TBLDL . $outVals[DURM][TXT] . TBLDR . $outVals[DURM][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[AVMS][TXT] . TBLDR . $outVals[AVMS][VAL] . NL;
if (in_array('train', $dispArray)) {
$retVal .= TBLDL . $outVals[MXSP][TXT] . TBLDR . $outVals[MXSP][VAL] . NL;
} else {
$retVal .= TBLDL . $outVals[GPXS][TXT] . TBLDR . $outVals[GPXS][VAL] . NL;
}
$retVal .= TBLDL . $outVals[MXEL][TXT] . TBLDR . $outVals[MXEL][VAL] . NL;
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[STRT][TXT] . TBLDR . $outVals[STRT][VAL] . NL;
$retVal .= TBLDL . $outVals[ENDT][TXT] . TBLDR . $outVals[ENDT][VAL] . NL;
$retVal .= TBLDL . $outVals[MNEL][TXT] . TBLDR . $outVals[MNEL][VAL] . NL;
$retVal .= TBLE . NL;
break;
case 'default': # show everything by default in a list
$retVal .= '' . NL;
$retVal .= SS . GPXSTXT . SE . $RecipeInfo[GPXSTATNAME] ['Version'] . BR;
$retVal .= SS . $outVals[DIST][TXT] . SE . $outVals[DIST][VAL] . BR;
$retVal .= SS . $outVals[MXSP][TXT] . SE . $outVals[MXSP][VAL] . ' ' . $calcVals [SPCL] . BR;
$retVal .= SS . $outVals[DSSP][TXT] . SE . $outVals[DSSP][VAL] . BR;
$retVal .= SS . $outVals[ASSP][TXT] . SE . $outVals[ASSP][VAL] . BR;
$retVal .= SS . $outVals[AVMS][TXT] . SE . $outVals[AVMS][VAL] . BR;
$retVal .= SS . $outVals[AVSP][TXT] . SE . $outVals[AVSP][VAL] . BR;
$retVal .= SS . $outVals[MNEL][TXT] . SE . $outVals[MNEL][VAL] . BR;
$retVal .= SS . $outVals[MXEL][TXT] . SE . $outVals[MXEL][VAL] . BR;
$retVal .= SS . $outVals[STEL][TXT] . SE . $outVals[STEL][VAL] . BR;
$retVal .= SS . $outVals[ENEL][TXT] . SE . $outVals[ENEL][VAL] . BR;
$retVal .= SS . $outVals[CHEL][TXT] . SE . $outVals[CHEL][VAL] . BR;
$retVal .= SS . $outVals[ASCT][TXT] . SE . $outVals[ASCT][VAL] . BR;
$retVal .= SS . $outVals[DSCT][TXT] . SE . $outVals[DSCT][VAL] . BR;
$retVal .= SS . $outVals[STRT][TXT] . SE . $outVals[STRT][VAL] . BR;
$retVal .= SS . $outVals[ENDT][TXT] . SE . $outVals[ENDT][VAL] . BR;
$retVal .= SS . $outVals[ELPS][TXT] . SE . $outVals[ELPS][VAL] . BR;
$retVal .= SS . $outVals[DURN][TXT] . SE . $outVals[DURN][VAL] . BR;
$retVal .= SS . $outVals[DURM][TXT] . SE . $outVals[DURM][VAL] . BR;
$retVal .= SS . $outVals[ASTM][TXT] . SE . $outVals[ASTM][VAL] . BR;
$retVal .= SS . $outVals[DSTM][TXT] . SE . $outVals[DSTM][VAL] . BR;
$retVal .= SS . $outVals[DURS][TXT] . SE . $outVals[DURS][VAL] . BR;
$retVal .= SS . $outVals[NRPT][TXT] . SE . $outVals[NRPT][VAL] . BR;
if ($calcVals [NRSG] > 1) {
$retVal .= SS . $outVals[NRSG][TXT] . SE . $outVals[NRSG][VAL] . BR;
}
if ($calcVals [NRTK] > 1) {
$retVal .= SS . $outVals[NRTK][TXT] . SE . $outVals[NRTK][VAL] . BR;
}
$retVal .= SS . $outVals[TZDB][TXT] . SE . $outVals[TZDB][VAL] . ' ' . $calcVals [TZCL] . BR;
$retVal .= SS . $outVals[TDAT][TXT] . SE . $outVals[TDAT][VAL] . BR;
$retVal .= SS . $outVals[TNAM][TXT] . SE . $outVals[TNAM][VAL] . BR;
$retVal .= SS . $outVals[TDES][TXT] . SE . $outVals[TDES][VAL] . BR;
$retVal .= SS . $outVals[FLDS][TXT] . SE . $outVals[FLDS][VAL] . SE . NL;
$retVal .= '
' . NL;
break;
case 'analyse': # displayed after all other display options
break;
case 'showvals' : # changes analyse output
break;
default:
$retVal .= GPXSTATNAME . ': Unknown display option: "' . implode (', ', $dispArray) . '"' . BR;
} # end switch
} # end foreach
} # end if
// Provide analysis of GPX file
if (in_array('analyse', $dispArray)) {
$retVal .= '
' . TBLS . NL;
$retVal .= TBLR . TBLDL . 'Analyse' . TBLDR . $calcVals [SPCL] . NL;
$retVal .= TBLDL . $outVals[TZDB][TXT] . TBLDR . $outVals[TZDB][VAL] . ' ' . $calcVals [TZCL] . NL;
$retVal .= TBLDL . $outVals[THSP][TXT] . TBLDR . $outVals[THSP][VAL] . NL;
$retVal .= TBLR . TBLD . 'Stopped durations: ';
// analyse the effect of a variance in stopped threshold speed
$colCount = 1;
$analyseTHSpeed = 0.2; # km/h
$thSpeed = 0.1; # km/h
for ($indx = 1; $indx <= 17; $indx++) {
if ($colCount++ > 5) {
$colCount = 1;
$retVal .= NL . TBLR . NL;
}
list ($stoppedDuration, $stoppedCount) = calcStoppedDuration ($timeVals, $speedVals, $thSpeed);
$retVal .= TBLDB . displaySecsAsHoursMins($stoppedDuration) . HOUR . number_format ($thSpeed, 1) . KMH . ' (#' . number_format ($stoppedCount) . ')' . NL;
$thSpeed += $analyseTHSpeed;
} #end for
// analyse the distribution of the data
foreach ($diffVals as $diffKey => $diffVal) {
$retVal .= TBLR . TBLDL . 'Variances: ' . TBLDR . $outVals[$diffKey][TXT] . ' ' . $outVals[$diffKey][VAL] . NL . $diffVal . NL;
}
$retVal .= TBLR;
$retVal .= TBLDL . $outVals[NRPT][TXT] . TBLDR . $outVals[NRPT][VAL] . NL;
if ($calcVals [NRSG] > 1) {
$retVal .= TBLDL . $outVals[NRSG][TXT] . TBLDR . $outVals[NRSG][VAL] . NL;
} else {$retVal .= TBLD2 . NL;
}
if ($calcVals [NRTK] > 1) {
$retVal .= TBLDL . $outVals[NRTK][TXT] . TBLDR . $outVals[NRTK][VAL] . NL;
} else {$retVal .= TBLD2 . NL;
}
$retVal .= TBLR . TBLDL . $outVals[TNAM][TXT] . '' . $outVals[TNAM][VAL] . ' | ' . NL;
$retVal .= TBLR . TBLDL . $outVals[TDES][TXT] . '' . $outVals[TDES][VAL] . ' | ' . NL;
$retVal .= TBLR . TBLDL . $outVals[TDAT][TXT] . '' . $outVals[TDAT][VAL] . NL;
$retVal .= TBLE . NL;
$retVal .= '' . $outVals[FLDS][TXT] . $outVals[FLDS][VAL] . '' . BR;
$retVal .= 'Max, Min, Outliers' . NL . TBLS . TBLR . NL;
// Get the maximum speeds for analysis
$retVal .= TBLDL;
arsort($speedVals, SORT_NUMERIC); // Sort speeds in descending order while maintaining index association
$maxSpeeds = array_slice($speedVals, 0, AVGWINDOW, true);
$retVal .= 'Max Speeds' . BR . 'Record#: Speed' . BR;
foreach ($maxSpeeds as $recordNumber => $maxspeed) {
$retVal .= '#' . number_format($recordNumber) . ': ' . number_format($maxspeed, 1) . KMH . BR;
} # end foreach
$retVal .= 'Avg Max speed: ' . number_format (array_sum($maxSpeeds) / count ($maxSpeeds), 1) . KMH . BR;
// Get the maximum elevations for analysis
$retVal .= TBLDL;
arsort($elevVals, SORT_NUMERIC); // Sort elevation in descending order while maintaining index association
$maxElevs = array_slice($elevVals, 0, AVGWINDOW);
$retVal .= 'Max Elevations' . BR . 'Record#: Elevation' . BR;
foreach ($maxElevs as $recordNumber => $maxelev) {
$retVal .= '#' . number_format($recordNumber) . ': ' . number_format($maxelev, 1) . MTR . BR;
} # end foreach
$retVal .= 'Avg Max Elev: ' . number_format (array_sum($maxElevs) / count ($maxElevs), 1) . MTR . BR;
// Get the minimum elevations for analysis
$retVal .= TBLDL;
$minElevs = array_slice($elevVals, -intval(AVGWINDOW));
$retVal .= 'Min Elevations' . BR . 'Record#: Elevation' . BR;
foreach ($minElevs as $recordNumber => $minelev) {
$retVal .= '#' . number_format($recordNumber) . ': ' . number_format($minelev, 1) . MTR . BR;
} # end foreach
$retVal .= 'Avg Min Elev: ' . number_format (array_sum($minElevs) / count ($minElevs), 1) . MTR . BR;
// Get the maximum distances for analysis
$retVal .= TBLDL;
arsort($distVals, SORT_NUMERIC);
$maxDists = array_slice($distVals, 0, AVGWINDOW, true);
$retVal .= 'Max Distances' . BR . 'Record#: Distance' . BR;
foreach ($maxDists as $recordNumber => $maxdist) {
$retVal .= '#' . number_format($recordNumber) . ': ' . number_format($maxdist, 1) . MTR . BR;
} # end foreach
$retVal .= 'Avg Max dist: ' . number_format (array_sum($maxDists) / count ($maxDists), 1) . KMH . BR;
// Display speed outliers
$retVal .= TBLDL;
$retVal .= 'Speed outliers (' . number_format(count($speedOutliers)) . ')' . BR . 'Record#: Speed' . BR;
foreach ($speedOutliers as $speedOutlier) {
$retVal .= '#' . number_format($speedOutlier['position']) . ': ' . number_format($speedOutlier['value'], 1) . KMH . BR;
} # end foreach
// Display elevation outliers
$retVal .= TBLDL;
$retVal .= 'Elevation outliers (' . number_format(count($elevOutliers)) . ')' . BR . 'Record#: Elevation' . BR;
foreach ($elevOutliers as $elevOutlier) {
$retVal .= (isset($elevOutlier)) ? '#' . number_format($elevOutlier['position']) . ': ' . number_format($elevOutlier['value'], 1) . MTR . BR : 'Elev outlier missing ' . BR;
} # end foreach
// Display distance outliers
$retVal .= TBLDL;
$retVal .= 'Distance outliers (' . number_format(count($distOutliers)) . ')' . BR . 'Record#: Distance' . BR;
foreach ($distOutliers as $distOutlier) {
$retVal .= '#' . number_format($distOutlier['position']) . ': ' . number_format($distOutlier['value'], 1) . MTR . BR;
} # end foreach
$retVal .= TBLE . NL . ' ' . NL;
} # end if
return Keep ($retVal);
} #
#
function removeOutliers(string $dataName, array $dataIn, $stdDev, int $windowSize = AVGWINDOW) {
/*
* This function removes outliers from a dataset using a simple outlier detection algorithm.
*
* @param array $dataIn The input data array.
* @param float $stdDev The stdDev value for outlier detection. See https://en.wikipedia.org/wiki/Standard_deviation#Rules_for_normally_distributed_data
1 sd ~= 68%; 2 sd ~= 95%; 2.5 sd ~= 99%; 3 sd ~= 99.7%
* @param int $windowSize The number of points to consider for the average delta vector.
* @local float $blendingFactor The blending factor for combining the extrapolated point and the actual data point.
*
* @return associative array with
* data array with outliers removed
* array of outliers along with their original positions in the input data array.
* Each outlier is represented as an associative array with `value` and `position` keys, where `value` is the outlier value and `position` is its original position in the input data array.
*
* The blending factor determines how much weight is given to the actual data point versus the extrapolated point.
* A common starting point is 0.5, which gives equal weight to both. To give more weight to the actual data points, increase $P (e.g., to 0.6 or 0.7).
* To give more weight to the extrapolated points (i.e., smooth the data more), decrease $P (e.g., to 0.4 or 0.3).
*/
global $showVals; # for use with analyse
$outlierVals = ''; # log outlier values
$blendingFactor = 1.0; # set to ignore extrapolated point if within stdDev (don't change this for this gpx application)
$smoothData = []; // Initialise an empty array to store the smoothed result.
$outliers = []; // Initialise an empty array to store the outliers.
$sizeData = count($dataIn); // Get the size of the input data array.
list ($diffAvg, $diffMax, $diffStdDev) = calcDifference($dataIn); # find average, maximum, and std deviation of the differences between points
#
$threshold = $diffStdDev * $stdDev;; # default threshold set to value
$outlierVals .= TBLDR . ' Diff avg: ' . number_format($diffAvg, 2) .
TBLDR . ' Diff std dev: ' . number_format($diffStdDev, 2) .
TBLDR . ' Diff max: ' . number_format($diffMax, 2) .
TBLDR . ' Calc Thrh: ' . number_format($threshold, 2);
if ($showVals) {
$outlierVals .= TBLR . TBLDL . $dataName . ' diff vals: ' . ' | ' . TBLDR . '#points: ' . number_format($sizeData) . TBLDR . '#std dev: ' . $stdDev . NL ;
$outlierVals .= TBLR . ' | ' . '' . $dataName . ' details' . NL .TBLS . NL;
}
// Loop over the data array starting from the x-th element.
for ($indx = 1; $indx < $sizeData; $indx++) {
// Compute the average delta vector over the last windowSize points
# start from 1 as we are calclating the difference from the previous point
$slidingWindow = array_slice($dataIn, $indx - $windowSize, $windowSize);
list ($wndwDelta, , ) = calcDifference($slidingWindow);
// Extrapolate a new point by adding the average delta vector to the last point.
$extrapolatedPoint = floatval ($dataIn[$indx - 1] + $wndwDelta);
$pointVariance = floatval ($dataIn[$indx] - $dataIn[$indx - 1]);
// If the variance between the actual data point and the previous point is less than the threshold...
switch (true) {
case (abs($pointVariance) <= $threshold):
// ...consider it a good data point and blend it with the extrapolated point.
$smoothData[] = ($blendingFactor * $dataIn[$indx]) + ((1 - $blendingFactor) * $extrapolatedPoint);
break;
default:
// If the variance exceeds the threshold, consider it an outlier and use just the extrapolated point.
$smoothData[] = $extrapolatedPoint;
// Add the outlier and its original position to the outliers array.
$outliers[] = ['value' => $dataIn[$indx], 'position' => intval($indx)];
if ($showVals) {
$outlierVals .= TBLR . TBLDL . '#: ' . number_format($indx - 1) .
TBLDL . ' Wndw delta: ' . number_format($wndwDelta, 2) .
TBLDL . ' pt:' . ((isset ($dataIn[$indx-1])) ? number_format($dataIn[$indx-1], 1) : 'null') .
TBLDL . '>pt+: ' . number_format($dataIn[$indx], 1) .
TBLDL . ' (expt:' . number_format($extrapolatedPoint, 1) . ')' .
TBLDB . ' var:' . number_format($pointVariance, 1) . NL;
}
} # end switch
} # end for
if ($showVals) {
$outlierVals .= TBLE . ' ';
}
// Return the resulting data array and the outliers.
return [$smoothData, $outliers, $outlierVals];
} # end removeOutliers
#
function calcDifference (array $dataIn) {
/* Calculate the average difference between all consecutive values in an array.
* $dataIn array The input array of numeric values.
* @return array The average difference, max diff, and standard deviation if the array has at least two elements, otherwise zero.
*/
$pointDifferences = []; # initialise
$squares = [];
$nrItems = count($dataIn);
if($nrItems <= 1) {
return [0, 0, 0];
}
// calculate the difference between every consecutive point
for($indx = 0; $indx < $nrItems - 1; $indx++) {
$pointDifferences [] = $dataIn[$indx + 1] - $dataIn[$indx];
}
$diffMax = max(array_map('abs', $pointDifferences)); # absolute value
$totalDiff = array_sum ($pointDifferences);
$diffAvg = floatval ($totalDiff / ($nrItems - 1));
$squares = [];
foreach ($pointDifferences as $pointVal) {
$squares[] = pow($pointVal - $diffAvg, 2);
}
$stdDev = floatval (sqrt(array_sum($squares) / count($squares)));
return [$diffAvg, $diffMax, $stdDev];
} # end calcDifference
#
function calcStoppedDuration (array $timeVals, array $speedVals, float $thSpeedKMH) {
// Calculate stopped times; note parameter arrays start from 1
$nrSpeedVals = count ($speedVals);
if (count ($speedVals) !== count ($timeVals)) return [$nrSpeedVals, count ($timeVals)]; # should never happen
$thresholdSpeedMS = $thSpeedKMH * (KM_TO_M / HOUR_TO_S); # convert to metres per second
$stoppedDuration = 0;
$stoppedCount = 0;
for ($indx = 1; $indx < $nrSpeedVals; $indx++) {
if ($speedVals [$indx] <= $thresholdSpeedMS) { # stopped
$intervalDuration = abs($timeVals [$indx] - $timeVals [$indx - 1]);
# this is a proxy for detecting separate tracks
# if the interval is greater than 4 hours ignore it for analysis purposes
if ($intervalDuration < 4 * HOUR_TO_S) { # four hours in seconds
$stoppedDuration += $intervalDuration;
$stoppedCount++;
}
}
}
return [$stoppedDuration, $stoppedCount]; # unix timestamp
} # end CalcStoppedTime
#
function calcChange (array $dataIn, array $speedVals, float $thSpeedKMH) {
// Calculate values increasing and descreasing when moving
$nrItems = count($dataIn);
if ($nrItems !== count ($speedVals)) return [$nrItems, count ($speedVals)]; # should never happen
$thresholdSpeedMS = $thSpeedKMH * (KM_TO_M / HOUR_TO_S); # convert to metres per second
$Increase = 0;
$Decrease = 0;
for ($indx = 2; $indx < $nrItems; $indx++) {
if ($speedVals [$indx] > $thresholdSpeedMS) { # moving
$pointChange = $dataIn [$indx] - $dataIn [$indx - 1];
if ($pointChange < 0) { # decreasing
$Decrease += abs($pointChange);
} else {
$Increase += $pointChange;
} # if
}
} # for
return [$Increase, $Decrease]; # unix timestamp
} # end calcChange
#
function calcAvgSpeed (array $elevVals, array $speedVals, array $distVals, array $timeVals, float $thSpeedKMH) {
// Calculate speeds ascending and descending when moving
$nrElevVals = count ($elevVals);
if ($nrElevVals !== count ($speedVals)) return [$nrElevVals, count ($speedVals)]; # should never happen
$thresholdSpeedMS = $thSpeedKMH * (KM_TO_M / HOUR_TO_S); # convert to metres per second
$ascDist = 0; # sum of ascending distance in metres
$ascTime = 0; # sum of time ascending in seconds
$dscDist = 0; # sum or descending distance in metres
$dscTime = 0; # sum of time descending in seconds
for ($indx = 2; $indx < $nrElevVals; $indx++) {
if ($speedVals [$indx] > $thresholdSpeedMS) { # moving
$elevChange = $elevVals [$indx] - $elevVals [$indx - 1];
if ($elevChange == 0) continue; # next
#
$distChange = $distVals [$indx]; # metres
$timeChange = $timeVals [$indx] - $timeVals [$indx - 1]; # seconds
if ($elevChange < 0) { # descending
$dscDist += $distChange;
$dscTime += $timeChange;
} else { # ascending
$ascDist += $distChange;
$ascTime += $timeChange;
} # if
}
} # for
$ascSpeed = calculateSpeedKMH ($ascDist, $ascTime);
$dscSpeed = calculateSpeedKMH ($dscDist, $dscTime);
return [$ascSpeed, $dscSpeed]; # speed KMH
} # end calcAvgSpeed
#
function calcMinMax (array $calcVals) : array {
arsort($calcVals, SORT_NUMERIC); // Sort vals in descending order while maintaining index association
$minVals = array_slice($calcVals, -intval(AVGWINDOW));
$minVal = array_sum($minVals) / count ($minVals);
$maxVals = array_slice($calcVals, 0, intval(AVGWINDOW), true);
$maxVal = array_sum($maxVals) / count ($maxVals);
return [$minVal, $maxVal];
}
#
function calculateSpeedKMH (float $distanceInMeters, float $durationInSeconds): float {
$speedInKMH = ($durationInSeconds == 0) ? 0 : $distanceInMeters / 1000 / ($durationInSeconds / HOUR_TO_S);
return $speedInKMH;
}
#
// check wiki file exists when passed groupname.pagename/filename.ext
function CheckWikiFile (string $wikifilename):array {
global $UploadDir, $FmtV, $gpxstat_debugon, $pagename;
# check if wiki file exists
# if it does exist return FQDN
# if it doesn't exist return markup to allow file to be uploaded
$wikifilefqdn = \DownloadUrl ($pagename, $wikifilename); #PmWiki function, returns the public URL of an attached file or false if it doesn't exist
//if ($gpxstat_debugon) tpmsg ('wikifile', '"' . $wikifilename . '", "' . strval ($wikifilefqdn) . '", "' . $FmtV['$LinkUpload'] . '"');
if (! $wikifilefqdn === false) { # file exists
return [true, $wikifilefqdn];
}
$wikimarkup = 'Upload: [[Attach:' . $wikifilename . ' | ' . $wikifilename . ']]' . BR; #
# note that the markup [[>>]] already seems to have been processed by the time we return this
return [false, $wikimarkup];
} # end CheckWikiFile
#
function checkTimezone ($gpxTimezone, $gpxXml) {
# seach IANA / Olson and official abbreviations to validate input
global $retVal;
$tzCalc = '';
// Check if the parameter is a SimpleXMLElement object
$gpxXml->registerXPathNamespace('gpx', 'https://www.topografix.com/GPX/1/1');
if (empty($gpxTimezone)) return [null, $tzCalc]; # no timezone to check
$gpxstatTimezone = strval ($gpxTimezone);
if (strtolower($gpxstatTimezone) == 'detect') { # try to calculate timezone for position
list ($cur_lat, $cur_long) = get_first_lat_long_from_gpx($gpxXml);
$timezone = get_nearest_timezone($cur_lat, $cur_long, $country_code = null);
if (empty($timezone)) return [null, 'TZ not detected'];
return [$timezone, 'TZ detected'];
}
// search IANA / Olson timezone database
if (in_array($gpxstatTimezone, timezone_identifiers_list())) {
$timezone = $gpxstatTimezone;
date_default_timezone_set($timezone); # side effect
return [$timezone, $tzCalc];
}
// search official timezone abbreviations
$timezone = timezone_name_from_abbr($gpxstatTimezone);
if (! empty($timezone)) {
if (in_array(strval($timezone), timezone_identifiers_list())) {
date_default_timezone_set($timezone); # side effect
return [$timezone, $tzCalc];
}
}
$tzCalc = 'Unrecognised: "' . $gpxstatTimezone . '" timezone';
$retVal .= GPXSTATNAME . ': ' . $tzCalc;
return [null, $tzCalc];
} # end checkTimezone
#
function get_nearest_timezone($cur_lat, $cur_long, $country_code = null) {
// Function to calculate the nearest timezone based on the given latitude and longitude
// This method might not be 100% accurate for countries with multiple timezones.
// see https://stackoverflow.com/questions/3126878/get-php-timezone-name-from-latitude-and-longitude
// Get all timezone identifiers, if country code is provided then get timezone identifiers of that country
$timezone_ids = (empty($country_code)) # country_code not supplied
? \DateTimeZone::listIdentifiers()
: \DateTimeZone::listIdentifiers(\DateTimeZone::PER_COUNTRY, $country_code);
// Check if timezone identifiers exist
if($timezone_ids && is_array($timezone_ids) && isset($timezone_ids[0])) {
// If only one identifier exists, set it as the timezone
if(count($timezone_ids) == 1) {
return $timezone_ids[0];
}
// Loop through all timezone identifiers
$tz_distance = PHP_INT_MAX;
$time_zone = null;
foreach($timezone_ids as $timezone_id) {
// Create a new DateTimeZone object
$timezone = new \DateTimeZone($timezone_id);
// Get the location of the timezone
$location = $timezone->getLocation();
$tz_lat = $location['latitude'];
$tz_long = $location['longitude'];
// Calculate the distance between the given location and the timezone location
$theta = floatval($cur_long) - $tz_long;
$distance = (sin(deg2rad(floatval($cur_lat))) * sin(deg2rad($tz_lat)))
+ (cos(deg2rad(floatval($cur_lat))) * cos(deg2rad($tz_lat)) * cos(deg2rad($theta)));
$distance = acos($distance);
$distance = abs(rad2deg($distance));
// If no timezone has been set or the calculated distance is less than the previous distance
// then set the current timezone as the nearest timezone
if($tz_distance > $distance) {
$time_zone = $timezone_id;
$tz_distance = $distance;
}
}
// Return the nearest timezone
return $time_zone;
}
// If no timezone identifiers exist, return message
return 'get_nearest_timezone: no timezone ids' . BR;
}
#
function get_first_lat_long_from_gpx($xml) {
/**
* Function to get the first latitude and longitude from GPX data.
*
* @param string $gpx_contents The GPX data as a string.
* @return array withth 'lat' and 'lon' , or null.
*/
global $retVal;
$xml->registerXPathNamespace('gpx', 'https://www.topografix.com/GPX/1/1');
// Find the first track point
$trackpoints = $xml->xpath('//gpx:trkpt');
if ($trackpoints === false) return null;
if (empty($trackpoints)) return null;
// Track points are found and the first one exists
// Get the first track point
$first_point = $trackpoints[0];
// Get the latitude and longitude attributes as strings
$lat = strval($first_point['lat']);
$lon = strval($first_point['lon']);
// Return the latitude and longitude as an associative array
return [$lat, $lon];
}
#
function haversineGreatCircleDistance (float $latitudeFrom, float $longitudeFrom, float $latitudeTo, float $longitudeTo): float {
// calculate distance in metres between two points on the earth's surface
// convert from degrees to radians
## see https://community.esri.com/t5/coordinate-reference-systems-blog/distance-on-a-sphere-the-haversine-formula/ba-p/902128
$latFrom = deg2rad($latitudeFrom);
$lonFrom = deg2rad($longitudeFrom);
$latTo = deg2rad($latitudeTo);
$lonTo = deg2rad($longitudeTo);
$latDelta = $latTo - $latFrom;
$lonDelta = $lonTo - $lonFrom;
$angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));
return $angle * EARTH_RADIUS; # return in units of metres
} # end haversineGreatCircleDistance
#
function get_timezone_abbreviation($timezone_id) {
$abb_list = timezone_abbreviations_list();
foreach ($abb_list as $abb_key => $abb_val) {
$key = array_search($timezone_id, array_column($abb_val, 'timezone_id'));
if ($key !== false) {
return strtoupper($abb_key);
}
}
return false;
}
#
function displaySecsAsHoursMins(int $duration, string $format = '%02d:%02d'): string {
# convert a duration in seconds to a display as hours and minutes
$hours = intval (floor($duration / HOUR_TO_S));
$minutes = intval (($duration / 60)) % 60;
return sprintf($format, $hours, $minutes);
}
#
function makeabbr (string $literal, $title): string {
# https://stackoverflow.com/questions/5362628/how-to-get-the-names-and-abbreviations-of-a-time-zone-in-php
if (empty($title)) return $literal;
return '' . $literal . '';
}
function display_xml_error($error, $gpxXml) {
# This function takes a LibXMLError object and the XML data as input,
# and returns a string that represents the error.
$return = GPXSTATNAME . ': "' . $gpxXml[$error->line - 1] . '"' . BR;
$return .= str_repeat('-', $error->column) . "^\n";
// Determine the error level and add the appropriate message to the return string
switch ($error->level) {
case LIBXML_ERR_WARNING:
$return .= "Warning $error->code: ";
break;
case LIBXML_ERR_ERROR:
$return .= "Error $error->code: ";
break;
case LIBXML_ERR_FATAL:
$return .= "Fatal Error $error->code: ";
break;
}
// Add the error message, line, and column to the return string
$return .= trim($error->message) . BR .
"Line: $error->line" . BR .
"Column: $error->column";
// If a file is associated with the error, add it to the return string
if ($error->file) {
$return .= BR . "File: $error->file";
}
return $return;
} # end display_xml_error
|