# 2024-01-01 initial version
+
*/
//=================================================================================================
#
// Initialise constants
const NL = "\n";
const BR = '
' . NL;
const GPXSTATNAME = 'GpxStat';
if (substr(__NAMESPACE__, -3) === 'new') {
$GpxStatNew = 'new'; # empty string for production version, 'new' for development version
}
const EARTH_RADIUS = 6371000; # metres
const AVGWINDOW = 8; # number of points averaged to omit outliers
#
# set debug flag
\SDV($GpxStatDebug, false); # set default debug setting if not defined in an configuration file
$gpxstat_debugon = boolval ($GpxStatDebug); # if on writes input and output to web page
# Version date
$RecipeInfo[GPXSTATNAME] ['Version'] = '2024-01-01' . $GpxStatNew; # PmWiki version numbering is done by date
# recipe version page variable
$FmtPV['$GpxStatVersion'] = "'" . __NAMESPACE__ . " version {$RecipeInfo[GPXSTATNAME] ['Version']}'"; // return version as a custom page variable
# set default formatting for recipe classes
\SDV($HTMLStylesFmt[__NAMESPACE__], NL .
'.gpxstat {font-size: smaller; font-family: monospace;}' . NL .
'.gpxstat table, .gpxstat tbody, .gpxstat tr, .gpxstat td, .gpxstat th {vertical-align: top; border-collapse: collapse;}' . NL .
'.gpxstat .tdr {border: dotted lightgrey; border-width:1px 1px 1px 0px;}' . NL .
'.gpxstat .tdl {border: dotted lightgrey; border-width:1px 0px 1px 1px;}' . NL .
'.gpxstat .tdb {border: dotted lightgrey; border-width:1px 1px 1px 1px;}' . NL
);
#
# if ($gpxstat_debugon)
# dmsg('
' . __FILE__, $RecipeInfo[GPXSTATNAME]['Version']);
#
if (!function_exists('dmsg')) {
function dmsg (string $smsgprefix, $smsgdata) { # local instance
global $MessagesFmt;
$MessagesFmt['funtionName'] [] = $smsgprefix . ': ' . PHSC (strval($smsgdata));
}
}
# 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
$thresholdSpeed = floatval ($GpxStatThresholdSpeed);
$thresholdSpeedMS = $thresholdSpeed * 1000 / 3600; # convert to m/s
\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; # import variables
global $timeFmt, $dateFmt, $thresholdSpeed, $thresholdSpeedMS, $vnamepagefile;
global $GpxStatTimezone, $timezone;
global $retVal, $analyseVal, $showVals; # for debugging
$args = \ParseArgs($m[1]); # contains all text within directive
$retVal = '<:block>' . NL; # break out of paragraph;
$analyseVal = '';
//
if ($gpxstat_debugon) { # display inputs and outputs to wiki page
#dmsg ('
', GPXSTATNAME);
#dmsg ('m[]', $m);
#dmsg ('args[]', $args);
} # end gpxstat_debugon
$dispArray = array_key_exists ('display', $args) ? explode(',', $args ['display']) : ['default']; # defaults to default
$dispArray = array_map('strtolower', $dispArray); // Convert all values to lowercase
$debugopt = $args['debug'] === 'true';
if ($debugopt) $gpxstat_debugon = true; # set on
#
switch (true) {
case array_key_exists ('gpx', $args):
if (empty ($args['gpx'])) {
return GPXSTATNAME . ': gpx filename is missing ' . strval($args['gpx']);
}
$pmatch = preg_match ('/^' . $vnamepagefile . '/i', strval($args['gpx']));
if ($pmatch === false) return GPXSTATNAME . ': gpx does not match a filename pattern "' . $args['gpx'] . '" failed (' . $vnamepagefile . ')';
if (1 == $pmatch) { # pattern matches subject
list ($fileexists, $wikifilefqdn) = CheckWikiFile ($args['gpx']);
//if ($gpxstat_debugon) dmsg ('gpxfile', '"' . $args['gpx'] . '", "' . $wikifilefqdn . '"');
if ($fileexists) break;
return $wikifilefqdn; # return file upload code to PmWiki (don't use Keep)
}
# pattern does not match subject
return GPXSTATNAME . ': "' . strval ($args['gpx']) . '" not a valid filename';
default:
return GPXSTATNAME . ': parameter "gpx=filename.gpx" is required';
} # end switch
// 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;
/*if (!($gpxXml instanceof SimpleXMLElement)) {
$retVal .= 'GpxStat_Directive: Invalid parameter. Expected a SimpleXMLElement object. "' . get_class($gpxXml) . '"' . BR;
return $retVal;
} # check is broken */
// Get the namespace of the gpx data
$trknamespaces = $gpxXml->getNamespaces(true);
#
$tzCalc = ''; # global message
list ($timezone, $tzCalc) = checkTimezone ($GpxStatTimezone, $gpxXml); # requires gpx file
#
if (array_key_exists ('timezone', $args)) {
list ($timezone, $tzCalc) = checkTimezone (strval($args['timezone']), $gpxXml); # requires gpx file
}
// Initialise variables
$trackName = []; # multiple values are possible
$trackDesc = []; # multiple values are possible
$pointsCount = 0;
$segmentsCount = 0;
$tracksCount = 0;
$metresAscend = 0;
$metresDescend = 0;
$startTime = null;
$endTime = null;
$previousTime = null;
$ascendingTime = 0;
$descendingTime = 0;
$firstElev = null;
$previousElev = null;
$movingTime = 0;
$trackDate = null;
$speedCalc = '';
$distVals = []; # distance values
$speedVals = []; # speed values
$elevVals = []; # elevation values
$showVals = $gpxstat_debugon or boolval(in_array('showvals', $dispArray));
// Extract values from the gpx root
if (isset($gpxXml->name)) { # some gpx files incorrectly have a name here
$trackName[] .= strval($gpxXml->name);
}
if (isset($gpxXml->desc)) { # some gpx files incorrectly have a desc here
$trackDesc[] .= strval($gpxXml->desc);
}
// Extract values from the metadata
if (isset($gpxXml->metadata->time)) {
$trackDate = date_create(strval($gpxXml->metadata->time));
}
if (isset($gpxXml->metadata->name)) { # some gpx file have a name here
$trackName[] .= strval($gpxXml->metadata->name);
}
if (isset($gpxXml->metadata->desc)) { # some gpx file have a desc here
$trackDesc[] .= strval($gpxXml->metadata->desc);
}
// Loop through each "trk" element in the XML
$distVals [0] = 0; # initialse first value
foreach ($gpxXml->trk as $trk) {
$tracksCount++;
$trackName[] = strval($trk->name);
$trackDesc[] = strval($trk->desc);
// Loop through each "trkseg" element
foreach ($trk->trkseg as $trkseg) {
$segmentsCount++;
// Loop through each "trkpt" element
foreach ($trkseg->trkpt as $trkpt) {
// Increment the count of points for each "trkpt" element
$pointsCount++;
$pointTime = strtotime(strval($trkpt->time));
$pointElev = floatval($trkpt->ele);
$elevVals [$pointsCount] = $pointElev;
if (empty($firstElev)) {
$firstElev = $pointElev; # save first elevation in track
}
// Calculate distance using the Haversine formula
if (isset($previousTrkpt)) {
$distPoint = haversineGreatCircleDistance(floatval($previousTrkpt['lat']), floatval($previousTrkpt['lon']), floatval($trkpt['lat']), floatval($trkpt['lon']));
$distVals [$pointsCount] = $distPoint;
// Calculate speed if not available
switch (true) {
case (isset($trkpt->speed)):
break; # speed tag exists and is not empty
case (isset($trknamespaces['gte'])):
// Register the namespace with the SimpleXMLElement
$trkpt->registerXPathNamespace('gte', $trknamespaces['gte']);
// Check if the extensions and gps nodes exist
if (isset($trkpt->extensions) && isset($trkpt->extensions->children($trknamespaces['gte'])->gps)) {
// Extract the speed
$speed = $trkpt->extensions->children($trknamespaces['gte'])->gps->attributes()->speed;
} else {$speed = null;}
if (isset($speed) && !empty($speed)) {
$trkpt->speed = strval($speed);
$speedCalc = '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;
$trkpt->speed = strval($speed);
$speedCalc = 'speeds calculated';
} # end switch
} # end if
$pointSpeed = floatval($trkpt->speed);
$speedVals [$pointsCount] = $pointSpeed;
// Calculate metres climbed and descended
if (isset($previousTrkpt)) {
$pointElevChange = $pointElev - $previousElev;
if ($pointElevChange > 0) {
$metresAscend += $pointElevChange;
} else {
$metresDescend += abs($pointElevChange);
}
}
// Set start time and end time
if (empty($startTime)) {
$startTime = $pointTime;
if (empty($trackDate)) {
$trackDate = $pointTime;
}
}
$endTime = $pointTime;
// Calculate moving times
if (isset($previousTrkpt)) {
if ($pointSpeed > $thresholdSpeedMS) { # moving
if ($previousTime !== null) {
$intervalTime = abs($pointTime - $previousTime);
$movingTime += $intervalTime;
if ($previousElev !== null) {
if ($pointElev > $previousElev) {
$ascendingTime += $intervalTime; // Add to ascending time
} elseif ($pointElev < $previousElev) {
$descendingTime += $intervalTime; // Add to descending time
}
}
}
}
}
$previousTime = $pointTime;
$previousElev = $pointElev;
$previousTrkpt = $trkpt;
} # end foreach trkseg
} # end foreach trk
} # end foreach gpxxml
$finalElev = $pointElev;
define ('TS', '');
define ('TR', '');
define ('TD', '');
define ('TDL', ' | ');
define ('TDR', ' | ');
define ('TDB', ' | ');
define ('TD2', ' | ');
define ('SS', '');
define ('SE', '');
define ('MTR', ' m ');
define ('KM', ' km ');
define ('KMH', ' km/h ');
define ('VAL', 'val');
define ('TXT', 'txt');
define ('TDAT', 'trkDate');
define ('TZDB', 'geoTimeZone'); # IANA/Olson
define ('TRTZ', 'trkDateTimezone'); # track official timezones
define ('TNAM', 'trkName');
define ('TDES', 'trkDscr');
define ('DURN', 'duration');
define ('DIST', 'distance');
define ('ASCT', 'ascent');
define ('DSCT', 'descent');
define ('ASTM', 'ascTime');
define ('DSTM', 'dscTime');
define ('STRT', 'startTime');
define ('ENDT', 'endTime');
define ('MNEL', 'minElev');
define ('MXEL', 'maxElev');
define ('CHEL', 'chgElev');
define ('STEL', 'startElev');
define ('ENEL', 'endElev');
define ('MXSP', 'maxSpeed');
define ('THSP', 'thresholdSpeed');
define ('DURM', 'durMov');
define ('DURS', 'durStp');
define ('AVSP', 'avgSpd');
define ('AVMS', 'avgMovSpd');
define ('NRPT', 'nrPoints');
define ('NRSG', 'nrSegments');
define ('NRTK', 'nrTracks');
define ('FLDS', 'fileDesc');
define ('FNAM', 'fileName');
$analyseVal .= TDR . ' Speed' . KMH . ': ' . NL;
list ($speedSmooth, $speedOutliers) = removeOutliers($speedVals, $threshold = null, $windowSize = AVGWINDOW);
$analyseVal .= TR . TD . TDR . ' Elevation' . MTR . ': ' . NL;
list ($elevSmooth, $elevOutliers) = removeOutliers($elevVals, $threshold = null, $windowSize = AVGWINDOW);
$analyseVal .= TR . TD . TDR . ' Distance' . MTR . ': ' . NL;
list ($distSmooth, $distOutliers) = removeOutliers($distVals, $threshold = null, $windowSize = AVGWINDOW);
if ((! empty($tzCalc)) or (! empty ($timezone))) {
$outVals[TZDB][TXT] = 'Time zone: ';
$outVals[TZDB][VAL] = strval($timezone);
}
$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', $trackDate);
$outVals[TNAM][TXT] = 'Track name: ';
$outVals[TNAM][VAL] = implode (BR, $trackName);
$outVals[TDES][TXT] = 'Track desc: ';
$outVals[TDES][VAL] = implode (BR, $trackDesc);
$outVals[TDAT][TXT] = 'Track date ('. $outVals[TRTZ][VAL] . '): ';
$outVals[TDAT][VAL] = date($dateFmt, $trackDate);
$outVals[DURN][TXT] = makeabbr('Duration: ', $outVals[TDAT][TXT] . date($dateFmt, $trackDate));
$duration = $endTime - $startTime;
$outVals[DURN][VAL] = gmdate($timeFmt, $duration);
$outVals[DIST][TXT] = makeabbr('Distance: ', $outVals[FNAM][VAL]);
$outVals[MXSP][TXT] = 'Max speed: ';
$outVals[ASCT][TXT] = 'Ascent: ';
$outVals[ASCT][VAL] = number_format($metresAscend) . MTR;
$outVals[DSCT][TXT] = 'Descent: ';
$outVals[DSCT][VAL] = number_format($metresDescend) . MTR;
$outVals[STRT][TXT] = makeabbr ('Start time: ', $timezone);
$outVals[STRT][VAL] = (date('Y-m-d', $startTime) == date('Y-m-d', $endTime)) ? date($timeFmt, $startTime) : date($dateFmt, $startTime) . ' ' . date($timeFmt, $startTime);
$tzid = (empty ($timezone)) ? '' : get_timezone_abbreviation($timezone);
$outVals[ENDT][TXT] = makeabbr ('End time: ', $tzid);
$outVals[ENDT][VAL] = (date('Y-m-d', $startTime) == date('Y-m-d', $endTime)) ? date($timeFmt, $endTime) : date($dateFmt, $endTime) . ' ' . date($timeFmt, $endTime);
$outVals[ASTM][TXT] = 'Duration ascending: ';
$outVals[ASTM][VAL] = gmdate($timeFmt, $ascendingTime);
$outVals[DSTM][TXT] = 'Duration descending: ';
$outVals[DSTM][VAL] = gmdate($timeFmt, $descendingTime);
$outVals[STEL][TXT] = 'Start elevation: ';
$outVals[STEL][VAL] = number_format($firstElev) . MTR;
$outVals[ENEL][TXT] = 'End elevation: ';
$outVals[ENEL][VAL] = number_format($finalElev) . MTR;
$outVals[MNEL][TXT] = 'Min elevation: ';
$outVals[MNEL][VAL] = number_format(min($elevSmooth)) . MTR;
$outVals[MXEL][TXT] = 'Max elevation: ';
$outVals[MXEL][VAL] = number_format(max($elevSmooth)) . MTR;
$outVals[CHEL][TXT] = 'Elevation ' . (($pointElev > $firstElev) ? 'gain' : 'loss') . ': ';
$outVals[CHEL][VAL] = number_format($pointElev - $firstElev) . MTR;
$outVals[DURM][TXT] = 'Duration moving: ';
$outVals[DURM][VAL] = gmdate($timeFmt, $movingTime);
$stoppedTime = $duration - $movingTime;
$outVals[DURS][TXT] = 'Duration stopped: ';
$outVals[DURS][VAL] = gmdate($timeFmt, $stoppedTime);
$distanceTotal = array_sum ($distSmooth);
$avgSpeed = ($duration > 0) ? $distanceTotal / $duration : 0;
$outVals[AVSP][TXT] = 'Avg speed: ';
$places = ($avgSpeed < 10) ? 1 : 0;
$outVals[AVSP][VAL] = number_format($avgSpeed * 3.6, $places) . KMH;
$outVals[AVMS][TXT] = 'Avg moving speed: ';
$avgMovingSpeed = ($movingTime > 0) ? $distanceTotal / $movingTime : 0;
$places = ($avgMovingSpeed < 10) ? 1 : 0;
$outVals[AVMS][VAL] = number_format($avgMovingSpeed * 3.6, $places) . KMH;
$outVals[THSP][TXT] = 'Threshold speed: ';
$outVals[THSP][VAL] = number_format($thresholdSpeed, 1) . KMH;
$outVals[NRPT][TXT] = 'Points count: ';
$outVals[NRPT][VAL] = number_format($pointsCount);
$outVals[NRSG][TXT] = 'Segments count: ';
$outVals[NRSG][VAL] = number_format($segmentsCount);
$outVals[NRTK][TXT] = 'Tracks count: ';
$outVals[NRTK][VAL] = number_format($tracksCount);
// Convert distance from meters to kilometers and format to 1 decimal place
$places = ($distanceTotal < 100000) ? 1 : 0; # metres
$outVals[DIST][VAL] = number_format($distanceTotal / 1000, $places) . KM;
// Format to 1 decimal place
$maxSpeed = max($speedSmooth);
$places = ($maxSpeed < 10) ? 1 : 0;
$outVals[MXSP][VAL] = number_format($maxSpeed , $places) . KMH; # km/h
// Display the information
foreach ($dispArray as $layout) {
switch ($layout) {
case 'table':
$retVal .= TS . NL;
$retVal .= TR . TDL . $outVals[TNAM][TXT] . ' | ' . $outVals[TNAM][VAL]. ' |
' . NL;
$retVal .= TR . TDL . $outVals[TDES][TXT] . '' . $outVals[TDES][VAL] . ' | ' . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[DIST][TXT] . TDR . $outVals[DIST][VAL] . NL;
$retVal .= TDL . $outVals[AVSP][TXT] . TDR . $outVals[AVSP][VAL] . NL;
$retVal .= TD2 . $speedCalc . NL;
$retVal .= TDL . $outVals[STEL][TXT] . TDR . $outVals[STEL][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[ASCT][TXT] . TDR . $outVals[ASCT][VAL] . NL;
$retVal .= TDL . $outVals[AVMS][TXT] . TDR . $outVals[AVMS][VAL] . NL;
$retVal .= TDL . $outVals[MNEL][TXT] . TDR . $outVals[MNEL][VAL] . NL;
$retVal .= TDL . $outVals[ENEL][TXT] . TDR . $outVals[ENEL][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[DSCT][TXT] . TDR . $outVals[DSCT][VAL] . NL;
$retVal .= TDL . $outVals[MXSP][TXT] . TDR . $outVals[MXSP][VAL] . NL;
$retVal .= TDL . $outVals[MXEL][TXT] . TDR . $outVals[MXEL][VAL] . NL;
$retVal .= TDL . $outVals[CHEL][TXT] . TDR . $outVals[CHEL][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[DURN][TXT] . TDR . $outVals[DURN][VAL] . NL;
$retVal .= TDL . $outVals[DURM][TXT] . TDR . $outVals[DURM][VAL] . NL;
$retVal .= TDL . $outVals[DURS][TXT] . TDR . $outVals[DURS][VAL] . NL;
$retVal .= TD2 . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[STRT][TXT] . TDR . $outVals[STRT][VAL] . NL;
$retVal .= TDL . $outVals[ENDT][TXT] . TDR . $outVals[ENDT][VAL] . NL;
$retVal .= TDL . $outVals[TDAT][TXT] . TDR . $outVals[TDAT][VAL] . NL;
$retVal .= TDL . $outVals[TZDB][TXT] . TDR . $outVals[TZDB][VAL] . ' ' . $tzCalc . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[NRPT][TXT] . TDR . $outVals[NRPT][VAL] . NL;
if ($segmentsCount > 1) {
$retVal .= TDL . $outVals[NRSG][TXT] . TDR . $outVals[NRSG][VAL] . NL;
} else {$retVal .= TD2 . NL;
}
if ($tracksCount > 1) {
$retVal .= TDL . $outVals[NRTK][TXT] . TDR . $outVals[NRTK][VAL] . NL;
} else {$retVal .= TD2 . NL;
}
$retVal .= TDL . $outVals[THSP][TXT] . TDR . $outVals[THSP][VAL] . NL;
$retVal .= TE . NL;
$retVal .= '' . $outVals[FLDS][TXT] . $outVals[FLDS][VAL] . '' . BR;
break;
case 'ski':
$retVal .= TS . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[STRT][TXT] . TDR . $outVals[STRT][VAL] . NL;
$retVal .= TDL . $outVals[DIST][TXT] . TDR . $outVals[DIST][VAL] . NL;
$retVal .= TDL . $outVals[MXEL][TXT] . TDR . $outVals[MXEL][VAL] . NL;
$retVal .= TDL . $outVals[DURM][TXT] . TDR . $outVals[DURM][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[ENDT][TXT] . TDR . $outVals[ENDT][VAL] . NL;
$retVal .= TDL . $outVals[MXSP][TXT] . TDR . $outVals[MXSP][VAL] . NL;
$retVal .= TDL . $outVals[DSCT][TXT] . TDR . $outVals[DSCT][VAL] . NL;
$retVal .= TDL . $outVals[DSTM][TXT] . TDR . $outVals[DSTM][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[DURN][TXT] . TDR . $outVals[DURN][VAL] . NL;
$retVal .= TDL . $outVals[AVMS][TXT] . TDR . $outVals[AVMS][VAL] . NL;
$retVal .= TDL . $outVals[ASCT][TXT] . TDR . $outVals[ASCT][VAL] . NL;
$retVal .= TDL . $outVals[ASTM][TXT] . TDR . $outVals[ASTM][VAL] . NL;
$retVal .= TE . NL;
break;
case 'walk':
case 'tramp':
$retVal .= TS . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[DIST][TXT] . TDR . $outVals[DIST][VAL] . NL;
$retVal .= TDL . $outVals[ASCT][TXT] . TDR . $outVals[ASCT][VAL] . NL;
$retVal .= TDL . $outVals[DSCT][TXT] . TDR . $outVals[DSCT][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[MXEL][TXT] . TDR . $outVals[MXEL][VAL] . NL;
$retVal .= TDL . $outVals[AVMS][TXT] . TDR . $outVals[AVMS][VAL] . NL;
$retVal .= TDL . $outVals[DURM][TXT] . TDR . $outVals[DURM][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[STRT][TXT] . TDR . $outVals[STRT][VAL] . NL;
$retVal .= TDL . $outVals[ENDT][TXT] . TDR . $outVals[ENDT][VAL] . NL;
$retVal .= TDL . $outVals[DURN][TXT] . TDR . $outVals[DURN][VAL] . NL;
$retVal .= TE . NL;
break;
case 'drive':
$retVal .= TS . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[DIST][TXT] . TDR . $outVals[DIST][VAL] . NL;
$retVal .= TDL . $outVals[DURN][TXT] . TDR . $outVals[DURN][VAL] . NL;
$retVal .= TDL . $outVals[DURM][TXT] . TDR . $outVals[DURM][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[AVMS][TXT] . TDR . $outVals[AVMS][VAL] . NL;
$retVal .= TD2 . NL;
$retVal .= TDL . $outVals[MXEL][TXT] . TDR . $outVals[MXEL][VAL] . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[STRT][TXT] . TDR . $outVals[STRT][VAL] . NL;
$retVal .= TDL . $outVals[ENDT][TXT] . TDR . $outVals[ENDT][VAL] . NL;
$retVal .= TDL . $outVals[MNEL][TXT] . TDR . $outVals[MNEL][VAL] . NL;
$retVal .= TE . NL;
break;
case 'default': # show everything by default
$retVal .= '' . NL;
$retVal .= SS . $outVals[DIST][TXT] . SE . $outVals[DIST][VAL] . BR;
$retVal .= SS . $outVals[MXSP][TXT] . SE . $outVals[MXSP][VAL] . ' ' . $speedCalc . 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[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 ($segmentsCount > 1) {
$retVal .= SS . $outVals[NRSG][TXT] . SE . $outVals[NRSG][VAL] . BR;
}
if ($tracksCount > 1) {
$retVal .= SS . $outVals[NRTK][TXT] . SE . $outVals[NRTK][VAL] . BR;
}
$retVal .= SS . $outVals[TZDB][TXT] . SE . $outVals[TZDB][VAL] . ' ' . $tzCalc . 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
if (in_array('analyse', $dispArray)) {
$retVal .= '
' . TS . NL;
$retVal .= TR . TDL . 'Analyse' . TDR . $speedCalc . NL;
$retVal .= TDL . $outVals[TZDB][TXT] . TDR . $outVals[TZDB][VAL] . ' ' . $tzCalc . NL;
$retVal .= TDL . $outVals[THSP][TXT] . TDR . $outVals[THSP][VAL] . NL;
$retVal .= TR . TDL . 'Differences: ' . $analyseVal . NL;
$retVal .= TR;
$retVal .= TDL . $outVals[NRPT][TXT] . TDR . $outVals[NRPT][VAL] . NL;
if ($segmentsCount > 1) {
$retVal .= TDL . $outVals[NRSG][TXT] . TDR . $outVals[NRSG][VAL] . NL;
} else {$retVal .= TD2 . NL;
}
if ($tracksCount > 1) {
$retVal .= TDL . $outVals[NRTK][TXT] . TDR . $outVals[NRTK][VAL] . NL;
} else {$retVal .= TD2 . NL;
}
$retVal .= TR . TDL . $outVals[TNAM][TXT] . '' . $outVals[TNAM][VAL] . ' | ' . NL;
$retVal .= TR . TDL . $outVals[TDES][TXT] . '' . $outVals[TDES][VAL] . ' | ' . NL;
$retVal .= TR . TDL . $outVals[TDAT][TXT] . '' . $outVals[TDAT][VAL] . NL;
$retVal .= TE . NL;
$retVal .= '' . $outVals[FLDS][TXT] . $outVals[FLDS][VAL] . '' . BR;
$retVal .= TS . TR . NL;
// Get the maximum speeds
$retVal .= TDL;
arsort($speedVals); // 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) / AVGWINDOW, 1) . KMH . BR;
// Get the maximum elevations
$retVal .= TDL;
arsort($elevVals); // Sort elevation in descending order while maintaining index association
$maxElevs = array_slice($elevVals, 0, AVGWINDOW, true);
$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) / AVGWINDOW, 1) . MTR . BR;
// Get the minimum elevations
$retVal .= TDL;
$minElevs = array_slice($elevVals, -intval(AVGWINDOW), AVGWINDOW, true);
$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) / AVGWINDOW, 1) . MTR . BR;
// Get the maximum distances
$retVal .= TDL;
arsort($distVals);
$maxDists = array_slice($distVals, 0, AVGWINDOW, true);
$retVal .= 'Max Distances' . BR . 'Record#: Speed' . BR;
foreach ($maxDists as $recordNumber => $maxdist) {
$retVal .= '#' . number_format($recordNumber) . ': ' . number_format($maxdist, 1) . KMH . BR;
} # end foreach
$retVal .= 'Avg Max dist: ' . number_format (array_sum($maxDists) / AVGWINDOW, 1) . KMH . BR;
// Display speed outliers
$retVal .= TDL;
$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 .= TDL;
$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 .= TDL;
$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 .= TE . NL;
} # end if
return Keep ($retVal);
} #
function removeOutliers(array $dataIn, $stdDev = null, 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.
* @param 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 $analyseVal, $showVals; # for use with analyse
$blendingFactor = 1.0; # set to igore 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 ($avgDiff, $maxDiff, $stdDevDiff) = calcDifference($dataIn); # find average, maximum, and std deviation of the differences between points
if (empty($stdDev)) { # set default std dev if not supplied
$stdDev = 2.5; # 2.5 std devs ~= 99% of data, empirically this works well
}
$threshold = $stdDevDiff * $stdDev;; # default threshold set to percentage
$analyseVal .= TDR . ' Avg diff: ' . number_format($avgDiff, 2) .
TDR . ' Std dev diff: ' . number_format($stdDevDiff, 2) .
TDR . ' Max diff: ' . number_format($maxDiff, 2) .
TDR . ' Calc Thrh: ' . number_format($threshold, 2);
if ($showVals) {
$analyseVal .= TR . TDL . 'Diff vals: ' . ' | ' . TDR . '#std dev: ' . $stdDev . NL ;
}
// Loop over the data array starting from the x-th element.
for ($indx = $windowSize; $indx < $sizeData; $indx++) {
// Compute the average delta vector over the last windowSize points.
$slidingWindow = array_slice($dataIn, $indx - $windowSize, $windowSize);
list ($avgDelta, , ) = calcDifference($slidingWindow);
// Extrapolate a new point by adding the average delta vector to the last point.
$extrapolatedPoint = floatval ($dataIn[$indx - 1] + $avgDelta);
$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) {
$analyseVal .= TR . TDL . 'Line: ' . number_format($indx - 1) . TDL . ' Avg Delta: ' . number_format($avgDelta, 2) .
TDL . ' pt:' . number_format($dataIn[$indx-1], 2) . TDL . '>pt+: ' . number_format($dataIn[$indx], 2) .
TDL . ' (expt:' . number_format($extrapolatedPoint, 2) . ')' .
TDB . ' var:' . number_format($pointVariance, 2) . NL;
}
} # end switch
} # end for
// Return the resulting data array and the outliers.
return [$smoothData, $outliers];
} # 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 = [];
$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];
}
$maxDiff = max(array_map('abs', $pointDifferences)); # absolute value
$totalDiff = array_sum ($pointDifferences);
$avgDiff = floatval ($totalDiff / ($nrItems - 1));
$squares = [];
foreach($pointDifferences as $pointVal) {
$squares[] = pow($pointVal - $avgDiff, 2);
}
$stdDev = floatval (sqrt(array_sum($squares) / count($squares)));
return [$avgDiff, $maxDiff, $stdDev];
}
#
// 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];
$gpxstatTimezone = $gpxTimezone;
if (strtolower($gpxstatTimezone) == 'detect') { # try to calculate timezone for position
list ($cur_lat, $cur_long) = get_first_lat_long_from_gpx($gpxXml);
$gpxstatTimezone = get_nearest_timezone($cur_lat, $cur_long, $country_code = null);
if (empty($gpxstatTimezone)) return [null, $tzCalc];
$tzCalc = 'TZ calculated';
}
// search IANA / Olson timezone database
if (in_array(strval($gpxstatTimezone), timezone_identifiers_list())) {
$timezone = strval ($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];
}
}
$timezone = GPXSTATNAME . ': Unrecognised: "' . strval ($gpxstatTimezone) . '"';
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];
}
// calculate distance between two points on the earth's surface
function haversineGreatCircleDistance(
float $latitudeFrom, float $longitudeFrom, float $latitudeTo, float $longitudeTo)
{
// 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 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 |