Paekākāriki escarpment 9.20 1.1 Speed can also be recorded as an extension Speed is calculated if it is not available from either of the two tags in the file. See https://www.pmwiki.org/wiki/Cookbook/GpxStat and https://kiwiwiki.nz/pmwiki/pmwiki.php/Cookbook/GpxStat + Copyright 2024-present Simon Davis This software is written for PmWiki; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. See pmwiki.php for full details and lack of warranty. + Revision History # 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 ('TE', '
'); 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